owlplanner 2025.9.15__py3-none-any.whl → 2025.11.2__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
@@ -1223,8 +1223,8 @@ class Plan(object):
1223
1223
 
1224
1224
  for i in range(self.N_i):
1225
1225
  for j in range(self.N_j):
1226
- backTau = 1 - yearSpent * np.sum(self.tau_kn[:, 0] * self.alpha_ijkn[i, j, :, 0])
1227
- rhs = self.beta_ij[i, j] * backTau
1226
+ backTau = 1 + yearSpent * np.sum(self.tau_kn[:, 0] * self.alpha_ijkn[i, j, :, 0])
1227
+ rhs = self.beta_ij[i, j] / backTau
1228
1228
  self.B.setRange(_q3(self.C["b"], i, j, 0, self.N_i, self.N_j, self.N_n + 1), rhs, rhs)
1229
1229
 
1230
1230
  def _add_surplus_deposit_linking(self):
owlplanner/tax2025.py CHANGED
@@ -70,7 +70,7 @@ taxBrackets_preTCJA = np.array(
70
70
  ]
71
71
  )
72
72
 
73
- # These are 2025 current (adjusted for inflation).
73
+ # These are 2025 current.
74
74
  stdDeduction_OBBBA = np.array([15750, 31500]) # Single, MFJ
75
75
  # These are speculated (adjusted for inflation).
76
76
  stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
@@ -78,7 +78,7 @@ stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
78
78
  # These are current (adjusted for inflation) per individual.
79
79
  extra65Deduction = np.array([2000, 1600]) # Single, MFJ
80
80
 
81
- # Thresholds for capital gains (adjusted for inflation).
81
+ # Thresholds setting capital gains brackets 0%, 15%, 20% (adjusted for inflation).
82
82
  capGainRates = np.array(
83
83
  [
84
84
  [48350, 533400],
owlplanner/tax2026.py ADDED
@@ -0,0 +1,339 @@
1
+ """
2
+
3
+ Owl/tax2026
4
+ ---
5
+
6
+ A retirement planner using linear programming optimization.
7
+
8
+ See companion document for a complete explanation and description
9
+ of all variables and parameters.
10
+
11
+ Module to handle all tax calculations.
12
+
13
+ Copyright © 2026 - Martin-D. Lacasse
14
+
15
+ Disclaimers: This code is for educational purposes only and does not constitute financial advice.
16
+
17
+ """
18
+
19
+ import numpy as np
20
+ from datetime import date
21
+
22
+
23
+ ##############################################################################
24
+ # Prepare the data.
25
+
26
+ taxBracketNames = ["10%", "12/15%", "22/25%", "24/28%", "32/33%", "35%", "37/40%"]
27
+
28
+ rates_OBBBA = np.array([0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.370])
29
+ rates_preTCJA = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
30
+
31
+ ###############################################################################
32
+ # Start of section where rates need to be actualized every year.
33
+ ###############################################################################
34
+ # Single [0] and married filing jointly [1].
35
+
36
+ # These are 2025 current.
37
+ taxBrackets_OBBBA = np.array(
38
+ [
39
+ [12400, 50400, 105700, 201775, 256225, 640600, 9999999],
40
+ [24800, 100800, 211400, 403550, 512450, 768700, 9999999],
41
+ ]
42
+ )
43
+
44
+ irmaaBrackets = np.array(
45
+ [
46
+ [0, 106000, 133000, 167000, 200000, 500000],
47
+ [0, 212000, 266000, 334000, 400000, 750000],
48
+ ]
49
+ )
50
+
51
+ # Index [0] stores the standard Medicare part B premium.
52
+ # Following values are incremental IRMAA part B monthly fees.
53
+ irmaaFees = 12 * np.array([185.00, 74.00, 111.00, 110.90, 111.00, 37.00])
54
+
55
+ # Make projection for pre-TCJA using 2017 to current year.
56
+ # taxBrackets_2017 = np.array(
57
+ # [ [9325, 37950, 91900, 191650, 416700, 418400, 9999999],
58
+ # [18650, 75900, 153100, 233350, 416700, 470700, 9999999],
59
+ # ])
60
+ #
61
+ # stdDeduction_2017 = [6350, 12700]
62
+ #
63
+ # For 2025, I used a 30.5% adjustment from 2017, rounded to closest 50.
64
+ #
65
+ # These are speculated.
66
+ taxBrackets_preTCJA = np.array(
67
+ [
68
+ [12150, 49550, 119950, 250200, 544000, 546200, 9999999], # Single
69
+ [24350, 99100, 199850, 304600, 543950, 614450, 9999999], # MFJ
70
+ ]
71
+ )
72
+
73
+ # These are 2025 current.
74
+ stdDeduction_OBBBA = np.array([16100, 32200]) # Single, MFJ
75
+ # These are speculated (adjusted for inflation to 2026). TODO
76
+ stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
77
+
78
+ # These are current (adjusted for inflation) per individual.
79
+ extra65Deduction = np.array([2000, 1600]) # Single, MFJ
80
+
81
+ # Thresholds setting capital gains brackets 0%, 15%, 20% (adjusted for inflation).
82
+ capGainRates = np.array(
83
+ [
84
+ [48350, 533400],
85
+ [96700, 600050],
86
+ ]
87
+ )
88
+
89
+ # Thresholds for net investment income tax (not adjusted for inflation).
90
+ niitThreshold = np.array([200000, 250000])
91
+ niitRate = 0.038
92
+
93
+ # Thresholds for 65+ bonus for circumventing tax on social security.
94
+ bonusThreshold = np.array([75000, 150000])
95
+
96
+ ###############################################################################
97
+ # End of section where rates need to be actualized every year.
98
+ ###############################################################################
99
+
100
+
101
+ def mediVals(yobs, horizons, gamma_n, Nn, Nq):
102
+ """
103
+ Return tuple (nm, L, C) of year index when Medicare starts and vectors L, and C
104
+ defining end points of constant piecewise linear functions representing IRMAA fees.
105
+ """
106
+ thisyear = date.today().year
107
+ assert Nq == len(irmaaFees), f"Inconsistent value of Nq: {Nq}."
108
+ assert Nq == len(irmaaBrackets[0]), "Inconsistent IRMAA brackets array."
109
+ Ni = len(yobs)
110
+ # What index year will Medicare start? 65 - age.
111
+ nm = 65 - (thisyear - yobs)
112
+ nm = np.min(nm)
113
+ # Has it already started?
114
+ nm = max(0, nm)
115
+ Nmed = Nn - nm
116
+
117
+ L = np.zeros((Nmed, Nq-1))
118
+ C = np.zeros((Nmed, Nq))
119
+
120
+ # Year starts at offset nm in the plan.
121
+ for nn in range(Nmed):
122
+ imed = 0
123
+ n = nm + nn
124
+ if thisyear + n - yobs[0] >= 65 and n < horizons[0]:
125
+ imed += 1
126
+ if Ni == 2 and thisyear + n - yobs[1] >= 65 and n < horizons[1]:
127
+ imed += 1
128
+ if imed:
129
+ status = 0 if Ni == 1 else 1 if n < horizons[0] and n < horizons[1] else 0
130
+ L[nn] = gamma_n[n] * irmaaBrackets[status][1:]
131
+ C[nn] = imed * gamma_n[n] * irmaaFees
132
+ else:
133
+ raise RuntimeError("mediVals: This should never happen.")
134
+
135
+ return nm, L, C
136
+
137
+
138
+ def capitalGainTaxRate(Ni, magi_n, gamma_n, nd, Nn):
139
+ """
140
+ Return an array of decimal rates for capital gains.
141
+ Parameter nd is the index year of first passing of a spouse, if applicable,
142
+ nd == Nn for single individuals.
143
+ """
144
+ status = Ni - 1
145
+ cgRate_n = np.zeros(Nn)
146
+
147
+ for n in range(Nn):
148
+ if status and n == nd:
149
+ status -= 1
150
+
151
+ if magi_n[n] > gamma_n[n] * capGainRates[status][1]:
152
+ cgRate_n[n] = 0.20
153
+ elif magi_n[n] > gamma_n[n] * capGainRates[status][0]:
154
+ cgRate_n[n] = 0.15
155
+
156
+ return cgRate_n
157
+
158
+
159
+ def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
160
+ """
161
+ Compute Medicare costs directly.
162
+ """
163
+ thisyear = date.today().year
164
+ Ni = len(yobs)
165
+ costs = np.zeros(Nn)
166
+ for n in range(Nn):
167
+ status = 0 if Ni == 1 else 1 if n < horizons[0] and n < horizons[1] else 0
168
+ for i in range(Ni):
169
+ if thisyear + n - yobs[i] >= 65 and n < horizons[i]:
170
+ # Start with the (inflation-adjusted) basic Medicare part B premium.
171
+ costs[n] += gamma_n[n] * irmaaFees[0]
172
+ if n < 2:
173
+ mymagi = prevmagi[n]
174
+ else:
175
+ mymagi = magi[n - 2]
176
+ for q in range(1, 6):
177
+ if mymagi > gamma_n[n] * irmaaBrackets[status][q]:
178
+ costs[n] += gamma_n[n] * irmaaFees[q]
179
+
180
+ return costs
181
+
182
+
183
+ def taxParams(yobs, i_d, n_d, N_n, gamma_n, MAGI_n, yOBBBA=2099):
184
+ """
185
+ Input is year of birth, index of shortest-lived individual,
186
+ lifespan of shortest-lived individual, total number of years
187
+ in the plan, and the year that preTCJA rates might come back.
188
+
189
+ It returns 3 time series:
190
+ 1) Standard deductions at year n (sigma_n).
191
+ 2) Tax rate in year n (theta_tn)
192
+ 3) Delta from top to bottom of tax brackets (Delta_tn)
193
+ This is pure speculation on future values.
194
+ Returned values are not indexed for inflation.
195
+ """
196
+ # Compute the deltas in-place between brackets, starting from the end.
197
+ deltaBrackets_OBBBA = np.array(taxBrackets_OBBBA)
198
+ deltaBrackets_preTCJA = np.array(taxBrackets_preTCJA)
199
+ for t in range(6, 0, -1):
200
+ for i in range(2):
201
+ deltaBrackets_OBBBA[i, t] -= deltaBrackets_OBBBA[i, t - 1]
202
+ deltaBrackets_preTCJA[i, t] -= deltaBrackets_preTCJA[i, t - 1]
203
+
204
+ # Prepare the 3 arrays to return - use transpose for easy slicing.
205
+ sigmaBar = np.zeros((N_n))
206
+ Delta = np.zeros((N_n, 7))
207
+ theta = np.zeros((N_n, 7))
208
+
209
+ filingStatus = len(yobs) - 1
210
+ souls = list(range(len(yobs)))
211
+ thisyear = date.today().year
212
+
213
+ for n in range(N_n):
214
+ # First check if shortest-lived individual is still with us.
215
+ if n == n_d:
216
+ souls.remove(i_d)
217
+ filingStatus -= 1
218
+
219
+ if thisyear + n < yOBBBA:
220
+ sigmaBar[n] = stdDeduction_OBBBA[filingStatus] * gamma_n[n]
221
+ Delta[n, :] = deltaBrackets_OBBBA[filingStatus, :]
222
+ else:
223
+ sigmaBar[n] = stdDeduction_preTCJA[filingStatus] * gamma_n[n]
224
+ Delta[n, :] = deltaBrackets_preTCJA[filingStatus, :]
225
+
226
+ # Add 65+ additional exemption(s) and "bonus" phasing out.
227
+ for i in souls:
228
+ if thisyear + n - yobs[i] >= 65:
229
+ sigmaBar[n] += extra65Deduction[filingStatus] * gamma_n[n]
230
+ if thisyear + n <= 2028:
231
+ sigmaBar[n] += 6000 * max(0, 1 - 0.06*max(0, MAGI_n[n] - bonusThreshold[filingStatus]))
232
+
233
+ # Fill in future tax rates for year n.
234
+ if thisyear + n < yOBBBA:
235
+ theta[n, :] = rates_OBBBA[:]
236
+ else:
237
+ theta[n, :] = rates_preTCJA[:]
238
+
239
+ Delta = Delta.transpose()
240
+ theta = theta.transpose()
241
+
242
+ # Return series unadjusted for inflation, except for sigmaBar, in STD order.
243
+ return sigmaBar, theta, Delta
244
+
245
+
246
+ def taxBrackets(N_i, n_d, N_n, yOBBBA=2099):
247
+ """
248
+ Return dictionary containing future tax brackets
249
+ unadjusted for inflation for plotting.
250
+ """
251
+ if not (0 < N_i <= 2):
252
+ raise ValueError(f"Cannot process {N_i} individuals.")
253
+
254
+ n_d = min(n_d, N_n)
255
+ status = N_i - 1
256
+
257
+ # Number of years left in OBBBA from this year.
258
+ thisyear = date.today().year
259
+ if yOBBBA < thisyear:
260
+ raise ValueError(f"Expiration year {yOBBBA} cannot be in the past.")
261
+
262
+ ytc = yOBBBA - thisyear
263
+
264
+ data = {}
265
+ for t in range(len(taxBracketNames) - 1):
266
+ array = np.zeros(N_n)
267
+ for n in range(N_n):
268
+ stat = status if n < n_d else 0
269
+ array[n] = taxBrackets_OBBBA[stat][t] if n < ytc else taxBrackets_preTCJA[stat][t]
270
+
271
+ data[taxBracketNames[t]] = array
272
+
273
+ return data
274
+
275
+
276
+ def rho_in(yobs, N_n):
277
+ """
278
+ Return Required Minimum Distribution fractions for each individual.
279
+ This implementation does not support spouses with more than
280
+ 10-year difference.
281
+ It starts at age 73 until it goes to 75 in 2033.
282
+ """
283
+ # Notice that table starts at age 72.
284
+ rmdTable = [
285
+ 27.4,
286
+ 26.5,
287
+ 25.5,
288
+ 24.6,
289
+ 23.7,
290
+ 22.9,
291
+ 22.0,
292
+ 21.1,
293
+ 20.2,
294
+ 19.4,
295
+ 18.5,
296
+ 17.7,
297
+ 16.8,
298
+ 16.0,
299
+ 15.2,
300
+ 14.4,
301
+ 13.7,
302
+ 12.9,
303
+ 12.2,
304
+ 11.5,
305
+ 10.8,
306
+ 10.1,
307
+ 9.5,
308
+ 8.9,
309
+ 8.4,
310
+ 7.8,
311
+ 7.3,
312
+ 6.8,
313
+ 6.4,
314
+ 6.0,
315
+ 5.6,
316
+ 5.2,
317
+ 4.9,
318
+ 4.6,
319
+ ]
320
+
321
+ N_i = len(yobs)
322
+ if N_i == 2 and abs(yobs[0] - yobs[1]) > 10:
323
+ raise RuntimeError("RMD: Unsupported age difference of more than 10 years.")
324
+
325
+ rho = np.zeros((N_i, N_n))
326
+ thisyear = date.today().year
327
+ for i in range(N_i):
328
+ agenow = thisyear - yobs[i]
329
+ # Account for increase of RMD age between 2023 and 2032.
330
+ yrmd = 70 if yobs[i] < 1949 else 72 if 1949 <= yobs[i] <= 1950 else 73 if 1951 <= yobs[i] <= 1959 else 75
331
+ for n in range(N_n):
332
+ yage = agenow + n
333
+
334
+ if yage < yrmd:
335
+ pass # rho[i][n] = 0
336
+ else:
337
+ rho[i][n] = 1.0 / rmdTable[yage - 72]
338
+
339
+ return rho
owlplanner/timelists.py CHANGED
@@ -54,7 +54,7 @@ def read(finput, inames, horizons, mylog):
54
54
  else:
55
55
  # Read all worksheets in memory but only process those with proper names.
56
56
  try:
57
- dfDict = pd.read_excel(finput, sheet_name=None)
57
+ dfDict = pd.read_excel(finput, sheet_name=None, usecols=_timeHorizonItems)
58
58
  except Exception as e:
59
59
  raise Exception(f"Could not read file {finput}: {e}.") from e
60
60
  streamName = f"file '{finput}'"
owlplanner/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.09.15"
1
+ __version__ = "2025.11.02"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.9.15
3
+ Version: 2025.11.2
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
@@ -709,7 +709,7 @@ Description-Content-Type: text/markdown
709
709
 
710
710
  ## A retirement exploration tool based on linear programming
711
711
 
712
- <img align=right src="https://raw.github.com/mdlacasse/Owl/main/docs/images/owl.png" width="250">
712
+ <img align=right src="https://github.com/mdlacasse/Owl/blob/main/docs/images/owl.png?raw=true" width="250">
713
713
 
714
714
  -------------------------------------------------------------------------------------
715
715
 
@@ -739,7 +739,7 @@ Strictly speaking, it is not a planning tool, but more an environment for explor
739
739
  It provides different realizations of a financial strategy through the rigorous
740
740
  mathematical optimization of relevant decision variables. Two major objective goals can be set: either
741
741
  maximize net spending, or after-tax bequest under various constraints.
742
- Look at *Basic capabilities* below for more detail.
742
+ Look at the *Capabilities* section below for more detail.
743
743
 
744
744
  One can certainly have a savings plan, but due to the volatility of financial investments,
745
745
  it is impossible to have a certain asset earnings plan. This does not mean one cannot make decisions.
@@ -758,7 +758,7 @@ collecting your data, or academic papers that share the results without really s
758
758
  the underlying mathematical models.
759
759
  The algorithms in Owl rely on the open-source HiGHS linear programming solver. The complete formulation and
760
760
  detailed description of the underlying
761
- mathematical model can be found [here](https://raw.github.com/mdlacasse/Owl/main/docs/owl.pdf).
761
+ mathematical model can be found [here](https://github.com/mdlacasse/Owl/blob/main/docs/owl.pdf).
762
762
 
763
763
  It is anticipated that most end users will use Owl through the graphical interface
764
764
  either at [owlplanner.streamlit.app](https://owlplanner.streamlit.app)
@@ -768,7 +768,7 @@ as described [here](USER_GUIDE.md).
768
768
 
769
769
  Not every retirement decision strategy can be framed as an easy-to-solve optimization problem.
770
770
  In particular, if one is interested in comparing different withdrawal strategies,
771
- [FI Calc](ficalc.app) is an elegant application that addresses this need.
771
+ [FI Calc](https://ficalc.app) is an elegant application that addresses this need.
772
772
  If, however, you also want to optimize spending, bequest, and Roth conversions, with
773
773
  an approach also considering Medicare and federal income tax over the next few years,
774
774
  then Owl is definitely a tool that can help guide your decisions.
@@ -840,7 +840,9 @@ Tax status covers married filing jointly and single, depending on the number of
840
840
  Maturation rules for Roth contributions and conversions are implemented as constraints
841
841
  limiting withdrawal amounts to cover Roth account balances for 5 years after the events.
842
842
  Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints.
843
- Future values are simple projections of current values with the assumed inflation rates.
843
+ They can also be optimized explicitly as an option, but this choice can lead to longer calculations
844
+ due to the use of the many additional binary variables required by the formulation.
845
+ Future Medicare and IRMAA values are simple projections of current values with the assumed inflation rates.
844
846
 
845
847
  ### Limitations
846
848
  Owl is work in progress. At the current time:
@@ -851,15 +853,16 @@ These cases are detected and will generate an error message.
851
853
  - Social security rule for surviving spouse assumes that benefits were taken at full retirement age.
852
854
  - Current version has no optimization of asset allocations between individuals and/or types of savings accounts.
853
855
  If there is interest, that could be added in the future.
854
- - In the current implementation, social securiy is always taxed at 85%.
855
- - Medicare calculations are done through a self-consistent loop.
856
- This means that the Medicare premiums are calculated after an initial solution is generated,
856
+ - In the current implementation, social securiy is always taxed at 85%, assuming that your taxable income will be larger than 34 k$ (single) or 44 k$ (married filing jointly).
857
+ - When Medicare calculations are done through a self-consistent loop,
858
+ the Medicare premiums are calculated after an initial solution is generated,
857
859
  and then a new solution is re-generated with these premiums as a constraint.
858
860
  In some situations, when the income (MAGI) is near an IRMAA bracket, oscillatory solutions can arise.
859
861
  While the solutions generated are very close to one another, Owl will pick the smallest solution
860
- for being conservative.
861
- - Part D is not included in the IRMAA calculations. Being considerably more significant,
862
- only Part B is taken into account.
862
+ for being conservative. While sometimes computationally costly,
863
+ a comparison with a full Medicare optimization should always be performed.
864
+ - Part D is not included in the IRMAA calculations. Only Part B is taken into account,
865
+ which is considerably more significant.
863
866
  - Future tax brackets are pure speculations derived from the little we know now and projected to the next 30 years.
864
867
  Your guesses are as good as mine.
865
868
 
@@ -886,8 +889,11 @@ assets to support, even with no estate being left.
886
889
  - Optimization solver from [HiGHS](https://highs.dev)
887
890
  - Streamlit Community Cloud [Streamlit](https://streamlit.io)
888
891
  - Contributors: Josh (noimjosh@gmail.com) for Docker image code,
892
+ kg333 for fixing an error in Docker's instructions,
889
893
  Dale Seng (sengsational) for great insights and suggestions,
890
- Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions, Clark Jefcoat (hubcity) for fruitful interactions.
894
+ Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions,
895
+ Clark Jefcoat (hubcity) for fruitful interactions,
896
+ Benjamin Quinn (blquinn) and Gene Wood (gene1wood) for improvements and bug fixes.
891
897
 
892
898
  ---------------------------------------------------------------------
893
899
 
@@ -2,13 +2,14 @@ owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
2
2
  owlplanner/abcapi.py,sha256=rtg7d0UbftinokR9VlB49VUjDjzUq3ONnJbhMXVIrgo,6879
3
3
  owlplanner/config.py,sha256=UF2Dy6E9PiX6Ua8B1R0aYCNUoIYmY46up8awf_36B_Q,12615
4
4
  owlplanner/mylogging.py,sha256=OVGeDFO7LIZG91R6HMpZBzjno-B8PH8Fo00Jw2Pdgqw,2558
5
- owlplanner/plan.py,sha256=pIKULy5GFo_8xOZByZ9_CXrHx9TcXQpaob8cblK46N8,115180
5
+ owlplanner/plan.py,sha256=NDdV0Eri76JMpiNPeuMzLYGX-lDDBEbjXd6Qg6k_CIw,115180
6
6
  owlplanner/progress.py,sha256=dUUlFmSAKUei36rUj2BINRY10f_YEUo_e23d0es6nrc,530
7
7
  owlplanner/rates.py,sha256=9Nmo8AKsyi5PoCUrzhr06phkSlNTv-TXzj5iYFU76AY,14113
8
- owlplanner/tax2025.py,sha256=2Jb_UbPT6ye-znRjA0nSaF8T8M17QW4MoRPDoW9XJ8s,10833
9
- owlplanner/timelists.py,sha256=Q4kBt9kKAa5qxsvOe9wfyUtCQVgiwPmJXTwXUPRBBv8,4066
8
+ owlplanner/tax2025.py,sha256=4KYaT6TO6WU7wDjgdRW48lqfwvVCtaXs9tcw1nleKhg,10834
9
+ owlplanner/tax2026.py,sha256=hgCiCJWVzJITk0cA8W-zxl-a0kObijPZ1yXc0F6MAwk,10848
10
+ owlplanner/timelists.py,sha256=UdzH6A_-w4REn4A1po7yndSiy1R8_R-i_C-94re4JYY,4093
10
11
  owlplanner/utils.py,sha256=afAjeO6Msf6Rn4jwz_7Ody9rHGWlBR7iQFqe1xzLNQc,2513
11
- owlplanner/version.py,sha256=zy-IhWuHFT4sXG7yuvz7mQvMfgQ6BwWFA7KrAtVvwYg,28
12
+ owlplanner/version.py,sha256=7KKWNrt_Dn84IwHPr2R5et9VpN8p8GhpeUnuavkZnVA,28
12
13
  owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
14
  owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
14
15
  owlplanner/plotting/__init__.py,sha256=uhxqtUi0OI-QWNOO2LkXgQViW_9yM3rYb-204Wit974,250
@@ -16,7 +17,7 @@ owlplanner/plotting/base.py,sha256=UimGKpMTV-dVm3BX5Apr_Ltorc7dlDLCRPRQ3RF_v7c,2
16
17
  owlplanner/plotting/factory.py,sha256=EDopIAPQr9zHRgemObko18FlCeRNhNCoLNNFAOq-X6s,1030
17
18
  owlplanner/plotting/matplotlib_backend.py,sha256=AOEkapD94U5hGNoS0EdbRoe8mgdMHH4oOvkXADZS914,17957
18
19
  owlplanner/plotting/plotly_backend.py,sha256=AO33GxBHGYG5osir_H1iRRtGxdhs4AjfLV2d_xm35nY,33138
19
- owlplanner-2025.9.15.dist-info/METADATA,sha256=yacvHpuuPAtZ4fvLHH2UcbIdtkWwgVc0zwIvtOBNw64,54045
20
- owlplanner-2025.9.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
- owlplanner-2025.9.15.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
22
- owlplanner-2025.9.15.dist-info/RECORD,,
20
+ owlplanner-2025.11.2.dist-info/METADATA,sha256=MIS1cRREb2Fe56Qu8BJ_9tF5YXB3gFL2Ldaufm2k0Ak,54622
21
+ owlplanner-2025.11.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
+ owlplanner-2025.11.2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
23
+ owlplanner-2025.11.2.dist-info/RECORD,,