owlplanner 2025.2.9__py3-none-any.whl → 2025.2.11__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/abcapi.py CHANGED
@@ -43,7 +43,7 @@ class Row(object):
43
43
  """
44
44
  Add an element at index ``ind`` of value ``val`` to the row.
45
45
  """
46
- assert 0 <= ind and ind < self.nvars, "Index %d out of range." % ind
46
+ assert 0 <= ind and ind < self.nvars, f"Index {ind} out of range."
47
47
  self.ind.append(ind)
48
48
  self.val.append(val)
49
49
 
@@ -154,7 +154,7 @@ class Bounds(object):
154
154
  self.integrality = []
155
155
 
156
156
  def setBinary(self, ii):
157
- assert 0 <= ii and ii < self.nvars, "Index %d out of range." % ii
157
+ assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
158
158
  self.ind.append(ii)
159
159
  self.lb.append(0)
160
160
  self.ub.append(1)
@@ -162,21 +162,21 @@ class Bounds(object):
162
162
  self.integrality.append(ii)
163
163
 
164
164
  def set0_Ub(self, ii, ub):
165
- assert 0 <= ii and ii < self.nvars, "Index %d out of range." % ii
165
+ assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
166
166
  self.ind.append(ii)
167
167
  self.lb.append(0)
168
168
  self.ub.append(ub)
169
169
  self.key.append("ra")
170
170
 
171
171
  def setLb_Inf(self, ii, lb):
172
- assert 0 <= ii and ii < self.nvars, "Index %d out of range." % ii
172
+ assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
173
173
  self.ind.append(ii)
174
174
  self.lb.append(lb)
175
175
  self.ub.append(np.inf)
176
176
  self.key.append("lo")
177
177
 
178
178
  def setRange(self, ii, lb, ub):
179
- assert 0 <= ii and ii < self.nvars, "Index %d out of range." % ii
179
+ assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
180
180
  self.ind.append(ii)
181
181
  self.lb.append(lb)
182
182
  self.ub.append(ub)
@@ -223,7 +223,7 @@ class Objective(object):
223
223
  self.val = []
224
224
 
225
225
  def setElem(self, ind, val):
226
- assert 0 <= ind and ind < self.nvars, "Index %d out of range." % ind
226
+ assert 0 <= ind and ind < self.nvars, f"Index {ind} out of range."
227
227
  self.ind.append(ind)
228
228
  self.val.append(val)
229
229
 
owlplanner/config.py CHANGED
@@ -42,7 +42,7 @@ def saveConfig(plan, file, mylog):
42
42
  diconf["Assets"] = {}
43
43
  for j in range(plan.N_j):
44
44
  amounts = plan.beta_ij[:, j] / 1000
45
- diconf["Assets"]["%s savings balances" % accountTypes[j]] = amounts.tolist()
45
+ diconf["Assets"][f"{accountTypes[j]} savings balances"] = amounts.tolist()
46
46
  if plan.N_i == 2:
47
47
  diconf["Assets"]["Beneficiary fractions"] = plan.phi_j.tolist()
48
48
  diconf["Assets"]["Spousal surplus deposit fraction"] = plan.eta
@@ -113,23 +113,23 @@ def saveConfig(plan, file, mylog):
113
113
  filename = filename + ".toml"
114
114
  if not filename.startswith("case_"):
115
115
  filename = "case_" + filename
116
- mylog.vprint("Saving plan case file as '%s'." % filename)
116
+ mylog.vprint(f"Saving plan case file as '{filename}'.")
117
117
 
118
118
  try:
119
119
  with open(filename, "w") as casefile:
120
120
  toml.dump(diconf, casefile, encoder=toml.TomlNumpyEncoder())
121
121
  except Exception as e:
122
- raise RuntimeError("Failed to save case file %s: %s" % (filename, e))
122
+ raise RuntimeError(f"Failed to save case file {filename}: {e}")
123
123
  elif isinstance(file, StringIO):
124
124
  try:
125
125
  string = toml.dumps(diconf, encoder=toml.TomlNumpyEncoder())
126
126
  file.write(string)
127
127
  except Exception as e:
128
- raise RuntimeError("Failed to save case to StringIO: %s", e)
128
+ raise RuntimeError(f"Failed to save case to StringIO: {e}")
129
129
  elif file is None:
130
130
  pass
131
131
  else:
132
- raise ValueError("Argument %s has unknown type" % type(file))
132
+ raise ValueError(f"Argument {type(file)} has unknown type")
133
133
 
134
134
  return diconf
135
135
 
@@ -151,27 +151,27 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
151
151
  if not filename.endswith(".toml"):
152
152
  filename = filename + ".toml"
153
153
 
154
- mylog.vprint("Reading plan from case file '%s'." % filename)
154
+ mylog.vprint(f"Reading plan from case file '{filename}'.")
155
155
 
156
156
  try:
157
157
  with open(filename, "r") as f:
158
158
  diconf = toml.load(f)
159
159
  except Exception as e:
160
- raise FileNotFoundError("File %s not found: %s" % (filename, e))
160
+ raise FileNotFoundError(f"File {filename} not found: {e}")
161
161
  elif isinstance(file, BytesIO):
162
162
  try:
163
163
  string = file.getvalue().decode("utf-8")
164
164
  diconf = toml.loads(string)
165
165
  except Exception as e:
166
- raise RuntimeError("Cannot read from BytesIO: %s" % e)
166
+ raise RuntimeError(f"Cannot read from BytesIO: {e}")
167
167
  elif isinstance(file, StringIO):
168
168
  try:
169
169
  string = file.getvalue()
170
170
  diconf = toml.loads(string)
171
171
  except Exception as e:
172
- raise RuntimeError("Cannot read from StringIO: %s" % e)
172
+ raise RuntimeError(f"Cannot read from StringIO: {e}")
173
173
  else:
174
- raise ValueError("%s not a valid type" % type(file))
174
+ raise ValueError(f"Type {type(file)} not a valid type")
175
175
 
176
176
  # Basic Info.
177
177
  name = diconf["Plan Name"]
@@ -181,14 +181,14 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
181
181
  expectancy = diconf["Basic Info"]["Life expectancy"]
182
182
  startDate = diconf["Basic Info"]["Start date"]
183
183
  icount = len(yobs)
184
-
185
- mylog.vprint("Plan for %d individual%s: %s." % (icount, ["", "s"][icount - 1], inames))
184
+ s = ["", "s"][icount - 1]
185
+ mylog.vprint(f"Plan for {icount} individual{s}: {inames}.")
186
186
  p = plan.Plan(inames, yobs, expectancy, name, startDate=startDate, verbose=True, logstreams=logstreams)
187
187
 
188
188
  # Assets.
189
189
  balances = {}
190
190
  for acc in accountTypes:
191
- balances[acc] = diconf["Assets"]["%s savings balances" % acc]
191
+ balances[acc] = diconf["Assets"][f"{acc} savings balances"]
192
192
  p.setAccountBalances(
193
193
  taxable=balances["taxable"], taxDeferred=balances["tax-deferred"], taxFree=balances["tax-free"]
194
194
  )
@@ -207,11 +207,11 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
207
207
  elif dirname != "" and os.path.exists(dirname + "/" + timeListsFileName):
208
208
  myfile = dirname + "/" + timeListsFileName
209
209
  else:
210
- raise FileNotFoundError("File '%s' not found." % timeListsFileName)
210
+ raise FileNotFoundError(f"File '{timeListsFileName}' not found.")
211
211
  p.readContributions(myfile)
212
212
  else:
213
213
  p.timeListsFileName = timeListsFileName
214
- mylog.vprint("Ignoring to read contributions file %s." % timeListsFileName)
214
+ mylog.vprint(f"Ignoring to read contributions file {timeListsFileName}.")
215
215
 
216
216
  # Fixed Income.
217
217
  ssecAmounts = np.array(diconf["Fixed Income"]["Social security amounts"], dtype=np.float32)
@@ -272,7 +272,7 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
272
272
  generic=boundsAR["generic"],
273
273
  )
274
274
  else:
275
- raise ValueError("Unknown asset allocation type %s." % allocType)
275
+ raise ValueError(f"Unknown asset allocation type {allocType}.")
276
276
 
277
277
  # Optimization Parameters.
278
278
  p.objective = diconf["Optimization Parameters"]["Objective"]
owlplanner/logging.py CHANGED
@@ -27,7 +27,7 @@ class Logger(object):
27
27
  self._logstreams = 2 * logstreams
28
28
  self.vprint("Using logstream as stream logger.")
29
29
  else:
30
- raise ValueError("Log streams %r must be a list." % logstreams)
30
+ raise ValueError(f"Log streams {logstreams} must be a list.")
31
31
 
32
32
  def setVerbose(self, verbose=True):
33
33
  """
owlplanner/plan.py CHANGED
@@ -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
 
@@ -161,7 +161,7 @@ def _checkCaseStatus(func):
161
161
  @wraps(func)
162
162
  def wrapper(self, *args, **kwargs):
163
163
  if self.caseStatus != "solved":
164
- self.mylog.vprint("Preventing to run method %s() while case is %s." % (func.__name__, self.caseStatus))
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,9 +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(
205
- "CPU time used: %dm%.1fs, Wall time: %dm%.1fs." % (int(pt / 60), pt % 60, int(rt / 60), rt % 60)
206
- )
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.")
207
205
  return result
208
206
 
209
207
  return wrapper
@@ -245,9 +243,9 @@ class Plan(object):
245
243
  self.defaultSolver = "HiGHS"
246
244
 
247
245
  self.N_i = len(yobs)
248
- assert 0 < self.N_i and self.N_i <= 2, "Cannot support %d individuals." % self.N_i
249
- assert self.N_i == len(expectancy), "Expectancy must have %d entries." % self.N_i
250
- assert self.N_i == len(inames), "Names for individuals must have %d entries." % self.N_i
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."
251
249
  assert inames[0] != "" or (self.N_i == 2 and inames[1] == ""), "Name for each individual must be provided."
252
250
 
253
251
  self.filingStatus = ["single", "married"][self.N_i - 1]
@@ -301,13 +299,11 @@ class Plan(object):
301
299
  self.prevMAGI = np.zeros((2))
302
300
 
303
301
  # Scenario starts at the beginning of this year and ends at the end of the last year.
304
- self.mylog.vprint(
305
- "Preparing scenario of %d years for %d individual%s." % (self.N_n, self.N_i, ["", "s"][self.N_i - 1])
306
- )
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}.")
307
304
  for i in range(self.N_i):
308
- self.mylog.vprint(
309
- "%14s: life horizon from %d -> %d." % (self.inames[i], thisyear, thisyear + self.horizons[i] - 1)
310
- )
305
+ endyear = thisyear + self.horizons[i] - 1
306
+ self.mylog.vprint(f"{self.inames[i]:>14}: life horizon from {thisyear} -> {endyear}.")
311
307
 
312
308
  # Prepare income tax and RMD time series.
313
309
  self.rho_in = tx.rho_in(self.yobs, self.N_n)
@@ -340,7 +336,7 @@ class Plan(object):
340
336
 
341
337
  def setLogstreams(self, verbose, logstreams):
342
338
  self.mylog = logging.Logger(verbose, logstreams)
343
- # self.mylog.vprint("Setting logstreams to %r." % logstreams)
339
+ # self.mylog.vprint(f"Setting logstreams to {logstreams}.")
344
340
 
345
341
  def logger(self):
346
342
  return self.mylog
@@ -382,7 +378,7 @@ class Plan(object):
382
378
  # Take midnight as the reference.
383
379
  self.yearFracLeft = 1 - (refdate.timetuple().tm_yday - 1) / (365 + lp)
384
380
 
385
- 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}.")
386
382
 
387
383
  return None
388
384
 
@@ -397,7 +393,7 @@ class Plan(object):
397
393
  if value in opts:
398
394
  return value
399
395
 
400
- raise ValueError("Value type must be one of: %r" % opts)
396
+ raise ValueError(f"Value type must be one of: {opts}")
401
397
 
402
398
  return None
403
399
 
@@ -407,7 +403,7 @@ class Plan(object):
407
403
  to distinguish graph outputs and as base name for
408
404
  saving configurations and workbooks.
409
405
  """
410
- self.mylog.vprint("Renaming plan %s -> %s." % (self._name, newname))
406
+ self.mylog.vprint(f"Renaming plan {self._name} -> {newname}.")
411
407
  self._name = newname
412
408
 
413
409
  def setSpousalDepositFraction(self, eta):
@@ -425,8 +421,8 @@ class Plan(object):
425
421
  self.mylog.vprint("Deposit fraction can only be 0 for single individuals.")
426
422
  eta = 0
427
423
  else:
428
- self.mylog.vprint("Setting spousal surplus deposit fraction to %.1f." % eta)
429
- self.mylog.vprint("\t%s: %.1f, %s: %.1f" % (self.inames[0], (1 - eta), self.inames[1], eta))
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}")
430
426
  self.eta = eta
431
427
 
432
428
  def setDefaultPlots(self, value):
@@ -435,7 +431,7 @@ class Plan(object):
435
431
  """
436
432
 
437
433
  self.defaultPlots = self._checkValue(value)
438
- self.mylog.vprint("Setting plots default value to %s." % value)
434
+ self.mylog.vprint(f"Setting plots default value to {value}.")
439
435
 
440
436
  def setDividendRate(self, mu):
441
437
  """
@@ -443,7 +439,7 @@ class Plan(object):
443
439
  """
444
440
  assert 0 <= mu and mu <= 100, "Rate must be between 0 and 100."
445
441
  mu /= 100
446
- 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)}.")
447
443
  self.mu = mu
448
444
  self.caseStatus = "modified"
449
445
 
@@ -453,7 +449,7 @@ class Plan(object):
453
449
  """
454
450
  assert 0 <= psi and psi <= 100, "Rate must be between 0 and 100."
455
451
  psi /= 100
456
- 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)}.")
457
453
  self.psi = psi
458
454
  self.caseStatus = "modified"
459
455
 
@@ -462,17 +458,18 @@ class Plan(object):
462
458
  Set fractions of savings accounts that is left to surviving spouse.
463
459
  Default is [1, 1, 1] for taxable, tax-deferred, adn tax-exempt accounts.
464
460
  """
465
- 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."
466
462
  for j in range(self.N_j):
467
463
  assert 0 <= phi[j] <= 1, "Fractions must be between 0 and 1."
468
464
 
469
465
  self.phi_j = np.array(phi, dtype=np.float32)
470
- self.mylog.vprint("Spousal beneficiary fractions set to", phi)
466
+ self.mylog.vprint("Spousal beneficiary fractions set to",
467
+ ["{:.2f}".format(self.phi_j[j]) for j in range(self.N_j)])
471
468
  self.caseStatus = "modified"
472
469
 
473
470
  if np.any(self.phi_j != 1):
474
471
  self.mylog.vprint("Consider changing spousal deposit fraction for better convergence.")
475
- self.mylog.vprint("\tRecommended: setSpousalDepositFraction(%d)" % self.i_d)
472
+ self.mylog.vprint(f"\tRecommended: setSpousalDepositFraction({self.i_d}.)")
476
473
 
477
474
  def setHeirsTaxRate(self, nu):
478
475
  """
@@ -481,7 +478,7 @@ class Plan(object):
481
478
  """
482
479
  assert 0 <= nu and nu <= 100, "Rate must be between 0 and 100."
483
480
  nu /= 100
484
- self.mylog.vprint("Heirs tax rate on tax-deferred portion of estate set to %s." % u.pc(nu, f=0))
481
+ self.mylog.vprint(f"Heirs tax rate on tax-deferred portion of estate set to {u.pc(nu, f=0)}.")
485
482
  self.nu = nu
486
483
  self.caseStatus = "modified"
487
484
 
@@ -490,14 +487,15 @@ class Plan(object):
490
487
  Set value of pension for each individual and commencement age.
491
488
  Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
492
489
  """
493
- assert len(amounts) == self.N_i, "Amounts must have %d entries." % self.N_i
494
- assert len(ages) == self.N_i, "Ages must have %d entries." % self.N_i
495
- assert len(indexed) >= self.N_i, "Indexed list must have at least %d entries." % self.N_i
490
+ assert len(amounts) == self.N_i, f"Amounts must have {self.N_i} entries."
491
+ assert len(ages) == self.N_i, f"Ages must have {self.N_i} entries."
492
+ assert len(indexed) >= self.N_i, f"Indexed list must have at least {self.N_i} entries."
496
493
 
497
494
  fac = u.getUnits(units)
498
495
  amounts = u.rescale(amounts, fac)
499
496
 
500
- self.mylog.vprint("Setting pension of", [u.d(amounts[i]) for i in range(self.N_i)], "at age(s)", ages)
497
+ self.mylog.vprint("Setting pension of", [u.d(amounts[i]) for i in range(self.N_i)],
498
+ "at age(s)", [int(ages[i]) for i in range(self.N_i)])
501
499
 
502
500
  thisyear = date.today().year
503
501
  # Use zero array freshly initialized.
@@ -522,17 +520,15 @@ class Plan(object):
522
520
  Set value of social security for each individual and commencement age.
523
521
  Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
524
522
  """
525
- assert len(amounts) == self.N_i, "Amounts must have %d entries." % self.N_i
526
- assert len(ages) == self.N_i, "Ages must have %d entries." % self.N_i
523
+ assert len(amounts) == self.N_i, f"Amounts must have {self.N_i} entries."
524
+ assert len(ages) == self.N_i, f"Ages must have {self.N_i} entries."
527
525
 
528
526
  fac = u.getUnits(units)
529
527
  amounts = u.rescale(amounts, fac)
530
528
 
531
529
  self.mylog.vprint(
532
- "Setting social security benefits of",
533
- [u.d(amounts[i]) for i in range(self.N_i)],
534
- "at age(s)",
535
- ages,
530
+ "Setting social security benefits of", [u.d(amounts[i]) for i in range(self.N_i)],
531
+ "at age(s)", [int(ages[i]) for i in range(self.N_i)],
536
532
  )
537
533
 
538
534
  thisyear = date.today().year
@@ -560,10 +556,10 @@ class Plan(object):
560
556
  as a second argument. Default value is 60%.
561
557
  Dip and increase are percent changes in the smile profile.
562
558
  """
563
- assert 0 <= percent and percent <= 100, "Survivor value %r outside range." % percent
564
- assert 0 <= dip and dip <= 100, "Dip value %r outside range." % dip
565
- assert -100 <= increase and increase <= 100, "Increase value %r outside range." % dip
566
- assert 0 <= delay and delay <= self.N_n - 2, "Delay value %r outside range." % delay
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."
567
563
 
568
564
  self.chi = percent / 100
569
565
 
@@ -606,13 +602,7 @@ class Plan(object):
606
602
  self.rateFrm = frm
607
603
  self.rateTo = to
608
604
  self.tau_kn = dr.genSeries(self.N_n).transpose()
609
- self.mylog.vprint(
610
- "Generating rate series of",
611
- len(self.tau_kn[0]),
612
- "years using",
613
- method,
614
- "method.",
615
- )
605
+ self.mylog.vprint(f"Generating rate series of {len(self.tau_kn[0])} years using {method} method.")
616
606
 
617
607
  # Account for how late we are now in the first year and reduce rate accordingly.
618
608
  self.tau_kn[:, 0] *= self.yearFracLeft
@@ -671,10 +661,10 @@ class Plan(object):
671
661
  each spouse. For single individuals, these lists will contain only
672
662
  one entry. Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
673
663
  """
674
- plurals = ["", "y", "ies"]
675
- assert len(taxable) == self.N_i, "taxable must have %d entr%s." % (self.N_i, plurals[self.N_i])
676
- assert len(taxDeferred) == self.N_i, "taxDeferred must have %d entr%s." % (self.N_i, plurals[self.N_i])
677
- assert len(taxFree) == self.N_i, "taxFree must have %d entr%s." % (self.N_i, plurals[self.N_i])
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}."
678
668
 
679
669
  fac = u.getUnits(units)
680
670
  taxable = u.rescale(taxable, fac)
@@ -716,12 +706,12 @@ class Plan(object):
716
706
  self.interpCenter = center
717
707
  self.interpWidth = width
718
708
  else:
719
- raise ValueError("Method %s not supported." % method)
709
+ raise ValueError(f"Method {method} not supported.")
720
710
 
721
711
  self.interpMethod = method
722
712
  self.caseStatus = "modified"
723
713
 
724
- self.mylog.vprint("Asset allocation interpolation method set to %s." % method)
714
+ self.mylog.vprint(f"Asset allocation interpolation method set to {method}.")
725
715
 
726
716
  def setAllocationRatios(self, allocType, taxable=None, taxDeferred=None, taxFree=None, generic=None):
727
717
  """
@@ -750,31 +740,19 @@ class Plan(object):
750
740
  if allocType == "account":
751
741
  # Make sure we have proper input.
752
742
  for item in [taxable, taxDeferred, taxFree]:
753
- 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."
754
744
  for i in range(self.N_i):
755
745
  # Initial and final.
756
- assert len(item[i]) == 2, "%s[%d] must have 2 lists (initial and final)." % (
757
- item,
758
- i,
759
- )
746
+ assert len(item[i]) == 2, f"{item}[{i}] must have 2 lists (initial and final)."
760
747
  for z in range(2):
761
- assert len(item[i][z]) == self.N_k, "%s[%d][%d] must have %d entries." % (
762
- item,
763
- i,
764
- z,
765
- self.N_k,
766
- )
748
+ assert len(item[i][z]) == self.N_k, f"{item}[{i}][{z}] must have {self.N_k} entries."
767
749
  assert abs(sum(item[i][z]) - 100) < 0.01, "Sum of percentages must add to 100."
768
750
 
769
751
  for i in range(self.N_i):
770
- self.mylog.vprint(
771
- self.inames[i],
772
- ": Setting gliding allocation ratios (%) to",
773
- allocType,
774
- )
775
- self.mylog.vprint(" taxable:", taxable[i][0], "->", taxable[i][1])
776
- self.mylog.vprint(" taxDeferred:", taxDeferred[i][0], "->", taxDeferred[i][1])
777
- self.mylog.vprint(" taxFree:", taxFree[i][0], "->", taxFree[i][1])
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]}")
778
756
 
779
757
  # Order in alpha is j, i, 0/1, k.
780
758
  alpha = {}
@@ -798,22 +776,14 @@ class Plan(object):
798
776
  assert len(generic) == self.N_i, "generic must have one list per individual."
799
777
  for i in range(self.N_i):
800
778
  # Initial and final.
801
- 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)."
802
780
  for z in range(2):
803
- assert len(generic[i][z]) == self.N_k, "generic[%d][%d] must have %d entries." % (
804
- i,
805
- z,
806
- self.N_k,
807
- )
781
+ assert len(generic[i][z]) == self.N_k, f"generic[{i}][{z}] must have {self.N_k} entries."
808
782
  assert abs(sum(generic[i][z]) - 100) < 0.01, "Sum of percentages must add to 100."
809
783
 
810
784
  for i in range(self.N_i):
811
- self.mylog.vprint(
812
- self.inames[i],
813
- ": Setting gliding allocation ratios (%) to",
814
- allocType,
815
- )
816
- 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]}")
817
787
 
818
788
  for i in range(self.N_i):
819
789
  Nin = self.horizons[i] + 1
@@ -829,14 +799,11 @@ class Plan(object):
829
799
  elif allocType == "spouses":
830
800
  assert len(generic) == 2, "generic must have 2 entries (initial and final)."
831
801
  for z in range(2):
832
- assert len(generic[z]) == self.N_k, "generic[%d] must have %d entries." % (
833
- z,
834
- self.N_k,
835
- )
802
+ assert len(generic[z]) == self.N_k, f"generic[{z}] must have {self.N_k} entries."
836
803
  assert abs(sum(generic[z]) - 100) < 0.01, "Sum of percentages must add to 100."
837
804
 
838
- self.mylog.vprint("Setting gliding allocation ratios (%) to", allocType)
839
- 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]}")
840
807
 
841
808
  # Use longest-lived spouse for both time scales.
842
809
  Nxn = max(self.horizons) + 1
@@ -854,7 +821,7 @@ class Plan(object):
854
821
  self.ARCoord = allocType
855
822
  self.caseStatus = "modified"
856
823
 
857
- self.mylog.vprint("Interpolating assets allocation ratios using", self.interpMethod, "method.")
824
+ self.mylog.vprint(f"Interpolating assets allocation ratios using {self.interpMethod} method.")
858
825
 
859
826
  def readContributions(self, filename):
860
827
  """
@@ -880,7 +847,7 @@ class Plan(object):
880
847
  try:
881
848
  filename, self.timeLists = timelists.read(filename, self.inames, self.horizons, self.mylog)
882
849
  except Exception as e:
883
- raise Exception("Unsuccessful read of contributions: %s" % e)
850
+ raise Exception(f"Unsuccessful read of contributions: {e}")
884
851
  return False
885
852
 
886
853
  self.timeListsFileName = filename
@@ -1045,13 +1012,7 @@ class Plan(object):
1045
1012
  self.nvars = _qC(C["z"], self.N_i, self.N_n, self.N_z)
1046
1013
 
1047
1014
  self.C = C
1048
- self.mylog.vprint(
1049
- "Problem has",
1050
- len(C),
1051
- "distinct time series forming",
1052
- self.nvars,
1053
- "decision variables.",
1054
- )
1015
+ self.mylog.vprint(f"Problem has {len(C)} distinct time series forming {self.nvars} decision variables.")
1055
1016
 
1056
1017
  return None
1057
1018
 
@@ -1132,7 +1093,7 @@ class Plan(object):
1132
1093
  # Roth conversions equalities/inequalities.
1133
1094
  if "maxRothConversion" in options:
1134
1095
  if options["maxRothConversion"] == "file":
1135
- # self.mylog.vprint('Fixing Roth conversions to those from file %s.' % self.timeListsFileName)
1096
+ # self.mylog.vprint(f"Fixing Roth conversions to those from file {self.timeListsFileName}.")
1136
1097
  for i in range(Ni):
1137
1098
  for n in range(self.horizons[i]):
1138
1099
  rhs = self.myRothX_in[i][n]
@@ -1157,7 +1118,7 @@ class Plan(object):
1157
1118
  try:
1158
1119
  i_x = self.inames.index(rhsopt)
1159
1120
  except ValueError:
1160
- raise ValueError("Unknown individual %s for noRothConversions:" % rhsopt)
1121
+ raise ValueError(f"Unknown individual {rhsopt} for noRothConversions:")
1161
1122
 
1162
1123
  for n in range(Nn):
1163
1124
  B.set0_Ub(_q2(Cx, i_x, n, Ni, Nn), zero)
@@ -1397,10 +1358,10 @@ class Plan(object):
1397
1358
  """
1398
1359
  if yend + self.N_n > self.year_n[0]:
1399
1360
  yend = self.year_n[0] - self.N_n - 1
1400
- 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}.")
1401
1362
  N = yend - ystart + 1
1402
1363
 
1403
- self.mylog.vprint("Running historical range from %d to %d." % (ystart, yend))
1364
+ self.mylog.vprint(f"Running historical range from {ystart} to {yend}.")
1404
1365
 
1405
1366
  self.mylog.setVerbose(verbose)
1406
1367
 
@@ -1409,7 +1370,7 @@ class Plan(object):
1409
1370
  elif objective == "maxBequest":
1410
1371
  columns = ["partial", "final"]
1411
1372
  else:
1412
- self.mylog.print("Invalid objective %s." % objective)
1373
+ self.mylog.print(f"Invalid objective {objective}.")
1413
1374
  return None
1414
1375
 
1415
1376
  df = pd.DataFrame(columns=columns)
@@ -1450,7 +1411,7 @@ class Plan(object):
1450
1411
  self.mylog.print("It is pointless to run Monte Carlo simulations with fixed rates.")
1451
1412
  return
1452
1413
 
1453
- self.mylog.vprint("Running %d Monte Carlo simulations." % N)
1414
+ self.mylog.vprint(f"Running {N} Monte Carlo simulations.")
1454
1415
  self.mylog.setVerbose(verbose)
1455
1416
 
1456
1417
  # Turn off Medicare by default, unless specified in options.
@@ -1465,7 +1426,7 @@ class Plan(object):
1465
1426
  elif objective == "maxBequest":
1466
1427
  columns = ["partial", "final"]
1467
1428
  else:
1468
- self.mylog.print("Invalid objective %s." % objective)
1429
+ self.mylog.print(f"Invalid objective {objective}.")
1469
1430
  return None
1470
1431
 
1471
1432
  df = pd.DataFrame(columns=columns)
@@ -1505,8 +1466,9 @@ class Plan(object):
1505
1466
 
1506
1467
  description = io.StringIO()
1507
1468
 
1508
- print("Success rate: %s on %d samples." % (u.pc(len(df) / N), N), file=description)
1509
- title = "$N$ = %d, $P$ = %s" % (N, u.pc(len(df) / N))
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}"
1510
1472
  means = df.mean(axis=0, numeric_only=True)
1511
1473
  medians = df.median(axis=0, numeric_only=True)
1512
1474
 
@@ -1518,13 +1480,14 @@ class Plan(object):
1518
1480
  # or if solution led to empty accounts at the end of first spouse's life.
1519
1481
  if np.all(self.phi_j == 1) or medians.iloc[0] < 1:
1520
1482
  if medians.iloc[0] < 1:
1521
- print("Optimized solutions all have null partial bequest in year %d." % my[0], file=description)
1483
+ print(f"Optimized solutions all have null partial bequest in year {my[0]}.", file=description)
1522
1484
  df.drop("partial", axis=1, inplace=True)
1523
1485
  means = df.mean(axis=0, numeric_only=True)
1524
1486
  medians = df.median(axis=0, numeric_only=True)
1525
1487
 
1526
1488
  df /= 1000
1527
1489
  if len(df) > 0:
1490
+ thisyear = self.year_n[0]
1528
1491
  if objective == "maxBequest":
1529
1492
  fig, axes = plt.subplots()
1530
1493
  # Show both partial and final bequests in the same histogram.
@@ -1532,37 +1495,36 @@ class Plan(object):
1532
1495
  legend = []
1533
1496
  # Don't know why but legend is reversed from df.
1534
1497
  for q in range(len(means) - 1, -1, -1):
1535
- legend.append(
1536
- "%d: $M$: %s, $\\bar{x}$: %s"
1537
- % (my[q], u.d(medians.iloc[q], latex=True), u.d(means.iloc[q], latex=True))
1538
- )
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}")
1539
1501
  plt.legend(legend, shadow=True)
1540
- plt.xlabel("%d $k" % self.year_n[0])
1502
+ plt.xlabel(f"{thisyear} $k")
1541
1503
  plt.title(objective)
1542
- leads = ["partial %d" % my[0], " final %d" % my[1]]
1504
+ leads = [f"partial {my[0]}", f" final {my[1]}"]
1543
1505
  elif len(means) == 2:
1544
1506
  # Show partial bequest and net spending as two separate histograms.
1545
1507
  fig, axes = plt.subplots(1, 2, figsize=(10, 5))
1546
1508
  cols = ["partial", objective]
1547
- leads = ["partial %d" % my[0], objective]
1509
+ leads = [f"partial {my[0]}", objective]
1548
1510
  for q in range(2):
1549
1511
  sbn.histplot(df[cols[q]], kde=True, ax=axes[q])
1550
- legend = [
1551
- ("$M$: %s, $\\bar{x}$: %s" % (u.d(medians.iloc[q], latex=True), u.d(means.iloc[q], latex=True)))
1552
- ]
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}"]
1553
1515
  axes[q].set_label(legend)
1554
1516
  axes[q].legend(labels=legend)
1555
1517
  axes[q].set_title(leads[q])
1556
- axes[q].set_xlabel("%d $k" % self.year_n[0])
1518
+ axes[q].set_xlabel(f"{thisyear} $k")
1557
1519
  else:
1558
1520
  # Show net spending as single histogram.
1559
1521
  fig, axes = plt.subplots()
1560
1522
  sbn.histplot(df[objective], kde=True, ax=axes)
1561
- legend = [
1562
- ("$M$: %s, $\\bar{x}$: %s" % (u.d(medians.iloc[0], latex=True), u.d(means.iloc[0], latex=True)))
1563
- ]
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}"]
1564
1526
  plt.legend(legend, shadow=True)
1565
- plt.xlabel("%d $k" % self.year_n[0])
1527
+ plt.xlabel(f"{thisyear} $k")
1566
1528
  plt.title(objective)
1567
1529
  leads = [objective]
1568
1530
 
@@ -1570,15 +1532,13 @@ class Plan(object):
1570
1532
  # plt.show()
1571
1533
 
1572
1534
  for q in range(len(means)):
1573
- print("%12s: Median (%d $): %s" % (leads[q], self.year_n[0], u.d(medians.iloc[q])), file=description)
1574
- print("%12s: Mean (%d $): %s" % (leads[q], self.year_n[0], u.d(means.iloc[q])), file=description)
1575
- print(
1576
- "%12s: Range: %s - %s"
1577
- % (leads[q], u.d(1000 * df.iloc[:, q].min()), u.d(1000 * df.iloc[:, q].max())),
1578
- file=description,
1579
- )
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)
1580
1540
  nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
1581
- print("%12s: N zero solns: %d" % (leads[q], nzeros), file=description)
1541
+ print(f"{leads[q]:>12}: N zero solns: {nzeros}", file=description)
1582
1542
 
1583
1543
  return fig, description
1584
1544
 
@@ -1637,13 +1597,13 @@ class Plan(object):
1637
1597
 
1638
1598
  for opt in myoptions:
1639
1599
  if opt not in knownOptions:
1640
- raise ValueError("Option %s is not one of %r." % (opt, knownOptions))
1600
+ raise ValueError(f"Option {opt} is not one of {knownOptions}.")
1641
1601
 
1642
1602
  if objective not in knownObjectives:
1643
- raise ValueError("Objective %s is not one of %r." % (objective, knownObjectives))
1603
+ raise ValueError(f"Objective {objective} is not one of {knownObjectives}.")
1644
1604
 
1645
1605
  if objective == "maxBequest" and "netSpending" not in myoptions:
1646
- raise RuntimeError("Objective %s needs netSpending option." % objective)
1606
+ raise RuntimeError(f"Objective {objective} needs netSpending option.")
1647
1607
 
1648
1608
  if objective == "maxBequest" and "bequest" in myoptions:
1649
1609
  self.mylog.vprint("Ignoring bequest option provided.")
@@ -1672,7 +1632,7 @@ class Plan(object):
1672
1632
  if "solver" in options:
1673
1633
  solver = myoptions["solver"]
1674
1634
  if solver not in knownSolvers:
1675
- raise ValueError("Unknown solver %s." % solver)
1635
+ raise ValueError(f"Unknown solver {solver}.")
1676
1636
  else:
1677
1637
  solver = self.defaultSolver
1678
1638
 
@@ -1735,7 +1695,7 @@ class Plan(object):
1735
1695
 
1736
1696
  self._estimateMedicare(solution.x)
1737
1697
 
1738
- 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)}")
1739
1699
 
1740
1700
  delta = solution.x - old_x
1741
1701
  absdiff = np.sum(np.abs(delta), axis=0)
@@ -1757,9 +1717,9 @@ class Plan(object):
1757
1717
  old_x = solution.x
1758
1718
 
1759
1719
  if solution.success:
1760
- 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.")
1761
1721
  self.mylog.vprint(solution.message)
1762
- self.mylog.vprint("Objective:", u.d(solution.fun * objFac))
1722
+ self.mylog.vprint(f"Objective: {u.d(solution.fun * objFac)}")
1763
1723
  # self.mylog.vprint('Upper bound:', u.d(-solution.mip_dual_bound))
1764
1724
  self._aggregateResults(solution.x)
1765
1725
  self._timestamp = datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
@@ -1871,7 +1831,7 @@ class Plan(object):
1871
1831
  task.set_Stream(mosek.streamtype.wrn, _streamPrinter)
1872
1832
  # task.writedata(self._name+'.ptf')
1873
1833
  if solsta == mosek.solsta.integer_optimal:
1874
- 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.")
1875
1835
  task.solutionsummary(mosek.streamtype.msg)
1876
1836
  self.mylog.vprint("Objective:", u.d(solution * objFac))
1877
1837
  self.caseStatus = "solved"
@@ -2052,7 +2012,7 @@ class Plan(object):
2052
2012
  """
2053
2013
  _estate = np.sum(self.b_ijn[:, :, :, self.N_n], axis=(0, 2))
2054
2014
  _estate[1] *= 1 - self.nu
2055
- 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]}.")
2056
2016
 
2057
2017
  return None
2058
2018
 
@@ -2104,67 +2064,63 @@ class Plan(object):
2104
2064
 
2105
2065
  totIncome = np.sum(self.g_n, axis=0)
2106
2066
  totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
2107
- dic[f"Total net spending in {now}$"] = "%s (%s nominal)" % (u.d(totIncomeNow), u.d(totIncome))
2067
+ dic[f"Total net spending in {now}$"] = f"{u.d(totIncomeNow)} ({u.d(totIncome)} nominal)"
2108
2068
 
2109
2069
  totRoth = np.sum(self.x_in, axis=(0, 1))
2110
2070
  totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
2111
- dic[f"Total Roth conversions in {now}$"] = "%s (%s nominal)" % (u.d(totRothNow), u.d(totRoth))
2071
+ dic[f"Total Roth conversions in {now}$"] = f"{u.d(totRothNow)} ({u.d(totRoth)} nominal)"
2112
2072
 
2113
2073
  taxPaid = np.sum(self.T_n, axis=0)
2114
2074
  taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
2115
- dic[f"Total income tax paid on ordinary income in {now}$"] = "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2075
+ dic[f"Total income tax paid on ordinary income in {now}$"] = f"{u.d(taxPaidNow)} ({u.d(taxPaid)} nominal)"
2116
2076
 
2117
2077
  taxPaid = np.sum(self.U_n, axis=0)
2118
2078
  taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
2119
- dic[f"Total tax paid on gains and dividends in {now}$"] = "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2079
+ dic[f"Total tax paid on gains and dividends in {now}$"] = f"{u.d(taxPaidNow)} ({u.d(taxPaid)} nominal)"
2120
2080
 
2121
2081
  taxPaid = np.sum(self.M_n, axis=0)
2122
2082
  taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
2123
- dic[f"Total Medicare premiums paid in {now}$"] = "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2083
+ dic[f"Total Medicare premiums paid in {now}$"] = f"{u.d(taxPaidNow)} ({u.d(taxPaid)} nominal)"
2124
2084
 
2125
2085
  if self.N_i == 2 and self.n_d < self.N_n:
2126
2086
  p_j = self.partialEstate_j * (1 - self.phi_j)
2127
2087
  p_j[1] *= 1 - self.nu
2128
2088
  nx = self.n_d - 1
2089
+ ynx = self.year_n[nx]
2129
2090
  totOthers = np.sum(p_j)
2130
2091
  totOthersNow = totOthers / self.gamma_n[nx + 1]
2131
2092
  q_j = self.partialEstate_j * self.phi_j
2132
2093
  totSpousal = np.sum(q_j)
2133
2094
  totSpousalNow = totSpousal / self.gamma_n[nx + 1]
2134
- dic[
2135
- "Spousal wealth transfer from %s to %s in year %d (nominal)"
2136
- % (self.inames[self.i_d], self.inames[self.i_s], self.year_n[nx])
2137
- ] = "taxable: %s tax-def: %s tax-free: %s" % (u.d(q_j[0]), u.d(q_j[1]), u.d(q_j[2]))
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])}")
2138
2099
 
2139
- dic["Sum of spousal bequests to %s in year %d in %d$" % (self.inames[self.i_s], self.year_n[nx], now)] = (
2140
- "%s (%s nominal)" % (u.d(totSpousalNow), u.d(totSpousal))
2141
- )
2100
+ dic[f"Sum of spousal bequests to {iname_s} in year {ynx} in {now}$"] = (
2101
+ f"{u.d(totSpousalNow)} ({u.d(totSpousal)} nominal)")
2142
2102
  dic[
2143
- "Post-tax non-spousal bequests from %s in year %d (nominal)" % (self.inames[self.i_d], self.year_n[nx])
2144
- ] = "taxable: %s tax-def: %s tax-free: %s" % (u.d(p_j[0]), u.d(p_j[1]), u.d(p_j[2]))
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])}")
2145
2105
  dic[
2146
- "Sum of post-tax non-spousal bequests from %s in year %d in %d$"
2147
- % (self.inames[self.i_d], self.year_n[nx], now)
2148
- ] = "%s (%s nominal)" % (u.d(totOthersNow), u.d(totOthers))
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"] = "%.2f" % (self.gamma_n[-1])
2120
+ dic["Cumulative inflation factor from start date to end of plan"] = f"{self.gamma_n[-1]:.2f}"
2163
2121
  for i in range(self.N_i):
2164
- dic["%12s's %02d-year life horizon" % (self.inames[i], self.horizons[i])] = "%d -> %d" % (
2165
- now,
2166
- now + self.horizons[i] - 1,
2167
- )
2122
+ dic[f"{self.inames[i]:>12}'s {self.horizons[i]:02}-year life horizon"] = (
2123
+ f"{now} -> {now + self.horizons[i] - 1}")
2168
2124
 
2169
2125
  dic["Plan name"] = self._name
2170
2126
  dic["Number of decision variables"] = str(self.A.nvars)
@@ -2182,7 +2138,7 @@ class Plan(object):
2182
2138
  import seaborn as sbn
2183
2139
 
2184
2140
  if self.rateMethod in [None, "user", "historical average", "conservative"]:
2185
- self.mylog.vprint("Warning: Cannot plot correlations for %s rate method." % self.rateMethod)
2141
+ self.mylog.vprint(f"Warning: Cannot plot correlations for {self.rateMethod} rate method.")
2186
2142
  return None
2187
2143
 
2188
2144
  rateNames = [
@@ -2218,7 +2174,7 @@ class Plan(object):
2218
2174
  # plt.subplots_adjust(wspace=0.3, hspace=0.3)
2219
2175
 
2220
2176
  title = self._name + "\n"
2221
- title += "Rates Correlations (N=%d) %s" % (self.N_n, self.rateMethod)
2177
+ title += f"Rates Correlations (N={self.N_n}) {self.rateMethod}"
2222
2178
  if self.rateMethod in ["historical", "histochastic"]:
2223
2179
  title += " (" + str(self.rateFrm) + "-" + str(self.rateTo) + ")"
2224
2180
 
@@ -2418,7 +2374,7 @@ class Plan(object):
2418
2374
  elif self.ARCoord == "account":
2419
2375
  acList = ["taxable", "tax-deferred", "tax-free"]
2420
2376
  else:
2421
- raise ValueError("Unknown coordination %s" % self.ARCoord)
2377
+ raise ValueError(f"Unknown coordination {self.ARCoord}.")
2422
2378
 
2423
2379
  figures = []
2424
2380
  assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
@@ -2876,12 +2832,12 @@ class Plan(object):
2876
2832
  df.to_csv(fname)
2877
2833
  break
2878
2834
  except PermissionError:
2879
- self.mylog.print('Failed to save "%s": %s.' % (fname, "Permission denied"))
2835
+ self.mylog.print(f'Failed to save "{fname}": Permission denied.')
2880
2836
  key = input("Close file and try again? [Yn] ")
2881
2837
  if key == "n":
2882
2838
  break
2883
2839
  except Exception as e:
2884
- raise Exception("Unanticipated exception %r." % e)
2840
+ raise Exception(f"Unanticipated exception: {e}.")
2885
2841
 
2886
2842
  return None
2887
2843
 
@@ -2945,7 +2901,7 @@ def _stackPlot(x, inames, title, irange, series, snames, location, yformat="\\$k
2945
2901
  ax.set_ylabel("%")
2946
2902
  ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x), ",")))
2947
2903
  else:
2948
- raise RuntimeError("Unknown yformat: %s." % yformat)
2904
+ raise RuntimeError(f"Unknown yformat: {yformat}.")
2949
2905
 
2950
2906
  return fig, ax
2951
2907
 
@@ -2963,7 +2919,7 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
2963
2919
  fname = basename
2964
2920
 
2965
2921
  if overwrite is False and isfile(fname):
2966
- mylog.print('File "%s" already exists.' % fname)
2922
+ mylog.print(f'File "{fname}" already exists.')
2967
2923
  key = input("Overwrite? [Ny] ")
2968
2924
  if key != "y":
2969
2925
  mylog.vprint("Skipping save and returning.")
@@ -2971,16 +2927,16 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
2971
2927
 
2972
2928
  while True:
2973
2929
  try:
2974
- mylog.vprint('Saving plan as "%s".' % fname)
2930
+ mylog.vprint(f'Saving plan as "{fname}".')
2975
2931
  wb.save(fname)
2976
2932
  break
2977
2933
  except PermissionError:
2978
- mylog.print('Failed to save "%s": %s.' % (fname, "Permission denied"))
2934
+ mylog.print(f'Failed to save "{fname}": Permission denied.')
2979
2935
  key = input("Close file and try again? [Yn] ")
2980
2936
  if key == "n":
2981
2937
  break
2982
2938
  except Exception as e:
2983
- raise Exception("Unanticipated exception %r." % e)
2939
+ raise Exception(f"Unanticipated exception {e}.")
2984
2940
 
2985
2941
  return None
2986
2942
 
@@ -3004,7 +2960,7 @@ def _formatSpreadsheet(ws, ftype):
3004
2960
  ws.column_dimensions[column].width = width
3005
2961
  return None
3006
2962
  else:
3007
- raise RuntimeError("Unknown format: %s." % ftype)
2963
+ raise RuntimeError(f"Unknown format: {ftype}.")
3008
2964
 
3009
2965
  for cell in ws[1] + ws["A"]:
3010
2966
  cell.style = "Pandas"
owlplanner/progress.py CHANGED
@@ -14,7 +14,7 @@ class Progress(object):
14
14
  self.mylog.print("|--- progress ---|")
15
15
 
16
16
  def show(self, x):
17
- self.mylog.print("\r\r%s" % u.pc(x, f=0), end="")
17
+ self.mylog.print(f"\r\r{u.pc(x, f=0)}", end="")
18
18
 
19
19
  def finish(self):
20
20
  self.mylog.print()
owlplanner/rates.py CHANGED
@@ -121,9 +121,9 @@ def historicalValue(amount, year):
121
121
  valued at the beginning of the year specified.
122
122
  """
123
123
  thisyear = date.today().year
124
- assert TO == thisyear - 1, "Rates file needs to be updated to be current to %d." % thisyear
125
- assert year >= FROM, "Only data from %d is available." % FROM
126
- assert year <= thisyear, "Year must be < %d for historical data." % thisyear
124
+ assert TO == thisyear - 1, f"Rates file needs to be updated to be current to {thisyear}."
125
+ assert year >= FROM, f"Only data from {FROM} is available."
126
+ assert year <= thisyear, f"Year must be < {thisyear} for historical data."
127
127
 
128
128
  span = thisyear - year
129
129
  ub = len(Inflation)
@@ -203,7 +203,7 @@ class Rates(object):
203
203
  "stochastic",
204
204
  "histochastic",
205
205
  ]:
206
- raise ValueError("Unknown rate selection method %s." % method)
206
+ raise ValueError(f"Unknown rate selection method {method}.")
207
207
 
208
208
  Nk = len(self._defRates)
209
209
  # First process fixed methods relying on values.
@@ -221,7 +221,7 @@ class Rates(object):
221
221
  self._setFixedRates(self._conservRates)
222
222
  elif method == "user":
223
223
  assert values is not None, "Fixed values must be provided with the user option."
224
- assert len(values) == Nk, "values must have %d items." % Nk
224
+ assert len(values) == Nk, f"Values must have {Nk} items."
225
225
  self.means = np.array(values, dtype=float)
226
226
  # Convert percent to decimal for storing.
227
227
  self.means /= 100.0
@@ -230,8 +230,8 @@ class Rates(object):
230
230
  elif method == "stochastic":
231
231
  assert values is not None, "Mean values must be provided with the stochastic option."
232
232
  assert stdev is not None, "Standard deviations must be provided with the stochastic option."
233
- assert len(values) == Nk, "values must have %d items." % Nk
234
- assert len(stdev) == Nk, "stdev must have %d items." % Nk
233
+ assert len(values) == Nk, f"Values must have {Nk} items."
234
+ assert len(stdev) == Nk, f"stdev must have {Nk} items."
235
235
  self.means = np.array(values, dtype=float)
236
236
  self.stdev = np.array(stdev, dtype=float)
237
237
  # Convert percent to decimal for storing.
@@ -256,7 +256,7 @@ class Rates(object):
256
256
  x += 1
257
257
  corrarr = newcorr
258
258
  else:
259
- raise RuntimeError("Unable to process correlation shape of %s." % corrarr.shape)
259
+ raise RuntimeError(f"Unable to process correlation shape of {corrarr.shape}.")
260
260
 
261
261
  self.corr = corrarr
262
262
  assert np.array_equal(self.corr, self.corr.T), "Correlation matrix must be symmetric."
@@ -271,25 +271,25 @@ class Rates(object):
271
271
  else:
272
272
  # Then methods relying on historical data range.
273
273
  assert frm is not None, "From year must be provided with this option."
274
- assert FROM <= frm and frm <= TO, 'Lower range "frm=%d" out of bounds.' % frm
275
- assert FROM <= to and to <= TO, 'Upper range "to=%d" out of bounds.' % to
274
+ assert FROM <= frm and frm <= TO, f"Lower range 'frm={frm}' out of bounds."
275
+ assert FROM <= to and to <= TO, f"Upper range 'to={to}' out of bounds."
276
276
  assert frm < to, "Unacceptable range."
277
277
  self.frm = frm
278
278
  self.to = to
279
279
 
280
280
  if method == "historical":
281
- self.mylog.vprint("Using historical rates representing data from %d to %d." % (frm, to))
281
+ self.mylog.vprint(f"Using historical rates representing data from {frm} to {to}.")
282
282
  self._rateMethod = self._histRates
283
283
  elif method == "historical average" or method == "means":
284
- self.mylog.vprint("Using average of rates from %d to %d." % (frm, to))
284
+ self.mylog.vprint(f"Using average of rates from {frm} to {to}.")
285
285
  self.means, self.stdev, self.corr, self.covar = getRatesDistributions(frm, to, self.mylog)
286
286
  self._setFixedRates(self.means)
287
287
  elif method == "histochastic":
288
- self.mylog.vprint("Using histochastic rates derived from years %d to %d." % (frm, to))
288
+ self.mylog.vprint(f"Using histochastic rates derived from years {frm} to {to}.")
289
289
  self._rateMethod = self._stochRates
290
290
  self.means, self.stdev, self.corr, self.covar = getRatesDistributions(frm, to, self.mylog)
291
291
  else:
292
- raise ValueError("Method $s not supported." % method)
292
+ raise ValueError(f"Method {method} not supported.")
293
293
 
294
294
  self.method = method
295
295
 
@@ -297,7 +297,7 @@ class Rates(object):
297
297
 
298
298
  def _setFixedRates(self, rates):
299
299
  Nk = len(self._defRates)
300
- assert len(rates) == Nk, "Rate list provided must have %d entries." % Nk
300
+ assert len(rates) == Nk, f"Rate list provided must have {Nk} entries."
301
301
  self._myRates = np.array(rates)
302
302
  self._rateMethod = self._fixedRates
303
303
 
owlplanner/tax2025.py CHANGED
@@ -154,7 +154,7 @@ def taxBrackets(N_i, n_d, N_n):
154
154
  Return dictionary containing future tax brackets
155
155
  unadjusted for inflation for plotting.
156
156
  """
157
- assert 0 < N_i and N_i <= 2, "Cannot process %d individuals." % N_i
157
+ assert 0 < N_i and N_i <= 2, f"Cannot process {N_i} individuals."
158
158
  # This 1 is the number of years left in TCJA from 2025.
159
159
  ytc = 1
160
160
  status = N_i - 1
owlplanner/timelists.py CHANGED
@@ -55,12 +55,12 @@ def read(finput, inames, horizons, mylog):
55
55
  try:
56
56
  dfDict = pd.read_excel(finput, sheet_name=None)
57
57
  except Exception as e:
58
- raise Exception("Could not read file %r: %s." % (finput, e))
59
- streamName = "file '%s'" % finput
58
+ raise Exception(f"Could not read file {finput}: {e}.")
59
+ streamName = f"file '{finput}'"
60
60
 
61
61
  timeLists = condition(dfDict, inames, horizons, mylog)
62
62
 
63
- mylog.vprint("Successfully read time horizons from %s." % streamName)
63
+ mylog.vprint(f"Successfully read time horizons from {streamName}.")
64
64
 
65
65
  return finput, timeLists
66
66
 
@@ -101,10 +101,10 @@ def condition(dfDict, inames, horizons, mylog):
101
101
  else:
102
102
  for item in timeHorizonItems:
103
103
  if item != "big-ticket items" and df[item].iloc[n] < 0:
104
- raise ValueError("Item %s for %s in year %d is < 0." % (item, iname, df["year"].iloc[n]))
104
+ raise ValueError(f"Item {item} for {iname} in year {df['year'].iloc[n]} is < 0.")
105
105
 
106
106
  if len(missing) > 0:
107
- mylog.vprint("Adding %d missing years for %s: %r." % (len(missing), iname, missing))
107
+ mylog.vprint(f"Adding {len(missing)} missing years for {iname}: {missing}.")
108
108
 
109
109
  df.sort_values("year", inplace=True)
110
110
  # Replace empty (NaN) cells with 0 value.
owlplanner/utils.py CHANGED
@@ -65,7 +65,7 @@ def getUnits(units) -> int:
65
65
  elif units in {"m", "M"}:
66
66
  fac = 1000000
67
67
  else:
68
- raise ValueError("Unknown units %r." % units)
68
+ raise ValueError(f"Unknown units {units}.")
69
69
 
70
70
  return fac
71
71
 
owlplanner/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.02.09"
1
+ __version__ = "2025.02.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.2.9
3
+ Version: 2025.2.11
4
4
  Summary: Owl: Retirement planner with great wisdom
5
5
  Project-URL: HomePage, https://github.com/mdlacasse/owl
6
6
  Project-URL: Repository, https://github.com/mdlacasse/owl
@@ -0,0 +1,17 @@
1
+ owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
2
+ owlplanner/abcapi.py,sha256=LbzW_KcNy0IeHp42MUHwGu_H67B2h_e1_vu-c2ACTkQ,6646
3
+ owlplanner/config.py,sha256=XFVcXFVpEuWXzybaijNGSTt72py3cYJ3oq0S1ujivl0,11702
4
+ owlplanner/logging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
5
+ owlplanner/plan.py,sha256=f_F7sIka4c_91dV7X5XHJkVvztXbtS_cqi24ncBI9gY,113084
6
+ owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
7
+ owlplanner/rates.py,sha256=TN407qU4n-bac1oymkQ_n2QKEPwFQxy6JZVGwgIkLQU,15585
8
+ owlplanner/tax2025.py,sha256=PVteko6G9gjAT247GnTzAPUe_RaLnZUArFtdzf1dF3M,7014
9
+ owlplanner/timelists.py,sha256=tYieZU67FT6TCcQQis36JaXGI7dT6NqD7RvdEjgJL4M,4026
10
+ owlplanner/utils.py,sha256=HM70W60qB41zfnbl2LltNwAuLYHyy5XYbwnbNcaa6FE,2351
11
+ owlplanner/version.py,sha256=n8NJ4iSncRuwW7_28_WeKgNkjAmkCzQOs0fAbkRFU5E,28
12
+ owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
14
+ owlplanner-2025.2.11.dist-info/METADATA,sha256=VkjJTA1BGuWJ1HnFS4tGWYrPvA1s_iV0YyIrnxYN2yk,63947
15
+ owlplanner-2025.2.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ owlplanner-2025.2.11.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
17
+ owlplanner-2025.2.11.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
2
- owlplanner/abcapi.py,sha256=J2xlj6LXwIblot0g4UeO5BC-vIYXsrI72duH82H6Di8,6658
3
- owlplanner/config.py,sha256=tKqqDxzygC283UPyZwa47oq0PBH0uEv_1bwiwAdRm4Q,11731
4
- owlplanner/logging.py,sha256=0y6Mhj56vmilgnDvw0pA8ur15oz5O3IN7qNCaf8yUIk,2532
5
- owlplanner/plan.py,sha256=5biGxkIfilBHOfoROpDs7m_BmzywJ6qldm_8aGAXbXU,114092
6
- owlplanner/progress.py,sha256=uyCUmuvZzvg2UPonjmXmUsWnnP55Mm6n19Xg3WcjwHU,386
7
- owlplanner/rates.py,sha256=CRQBaAXIB7hkxkLyDxxptBGuwnXwoIvK9_j4qTAc23Y,15627
8
- owlplanner/tax2025.py,sha256=VNRo1CiskHLEY_m5-wPVBVK-eFoFIaDF2lFRW-bHdc4,7016
9
- owlplanner/timelists.py,sha256=NgyPifalMBLfKi-2mtkAems4DnOJSkPtPPXR7LmbGTU,4052
10
- owlplanner/utils.py,sha256=Wy3ngYuVmWB2CeJx-v6vdhlSeSszQTjtuPtbc7hTb_A,2353
11
- owlplanner/version.py,sha256=WQPMjK8ZbcCw8owMuvICW0qcZSL2-1yIsSwBODMumNs,28
12
- owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
14
- owlplanner-2025.2.9.dist-info/METADATA,sha256=cxhHp7ve0Rpmdr2pChd-y21DmNOejUweN_x8tR7N9K8,63946
15
- owlplanner-2025.2.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- owlplanner-2025.2.9.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
17
- owlplanner-2025.2.9.dist-info/RECORD,,