owlplanner 2025.12.5__py3-none-any.whl → 2025.12.20__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.
@@ -18,8 +18,23 @@ import numpy as np
18
18
 
19
19
  def getFRAs(yobs):
20
20
  """
21
- Return full retirement age based on birth year.
22
- Returns an array of fractional age.
21
+ Return full retirement age (FRA) based on birth year.
22
+
23
+ The FRA is determined by birth year according to Social Security rules:
24
+ - Birth year >= 1960: FRA is 67
25
+ - Birth year < 1960: FRA increases by 2 months for each year after 1954
26
+
27
+ Parameters
28
+ ----------
29
+ yobs : array-like
30
+ Array of birth years, one for each individual.
31
+
32
+ Returns
33
+ -------
34
+ numpy.ndarray
35
+ Array of FRA values in fractional years (1/12 increments), one for each individual.
36
+ Ages are returned in Social Security age format. Comparisons to FRA should be
37
+ done using Social Security age (which accounts for birthday-on-first adjustments).
23
38
  """
24
39
  fras = np.zeros(len(yobs))
25
40
 
@@ -35,7 +50,32 @@ def getFRAs(yobs):
35
50
 
36
51
  def getSpousalBenefits(pias):
37
52
  """
38
- Compute spousal benefit. Returns an array.
53
+ Compute the maximum spousal benefit amount for each individual.
54
+
55
+ The spousal benefit is calculated as 50% of the spouse's Primary Insurance Amount (PIA),
56
+ minus the individual's own PIA. The result is the additional benefit the individual
57
+ would receive as a spouse, which cannot be negative.
58
+
59
+ Note: This calculation is not affected by which day of the month is the birthday.
60
+
61
+ Parameters
62
+ ----------
63
+ pias : array-like
64
+ Array of Primary Insurance Amounts (monthly benefit at FRA), one for each individual.
65
+ Must have exactly 1 or 2 entries.
66
+
67
+ Returns
68
+ -------
69
+ numpy.ndarray
70
+ Array of spousal benefit amounts (monthly), one for each individual.
71
+ For a single individual, returns [0].
72
+ For two individuals, returns the additional spousal benefit each would receive
73
+ (which is max(0, 0.5 * spouse_PIA - own_PIA)).
74
+
75
+ Raises
76
+ ------
77
+ ValueError
78
+ If the pias array does not have exactly 1 or 2 entries.
39
79
  """
40
80
  icount = len(pias)
41
81
  benefits = np.zeros(icount)
@@ -51,15 +91,51 @@ def getSpousalBenefits(pias):
51
91
  return benefits
52
92
 
53
93
 
54
- def getSelfFactor(fra, age):
94
+ def getSelfFactor(fra, convage, bornOnFirst):
55
95
  """
56
- Return factor to multiply PIA given the age when SS starts.
57
- Year of FRA and age can be fractional.
96
+ Return the reduction/increase factor to multiply PIA based on claiming age.
97
+
98
+ This function calculates the adjustment factor for self benefits based on when
99
+ Social Security benefits start relative to Full Retirement Age (FRA):
100
+ - Before FRA: Benefits are reduced (minimum 70% at age 62)
101
+ - At FRA: Full benefit (100% of PIA)
102
+ - After FRA: Benefits are increased by 8% per year (up to 132% at age 70)
103
+
104
+ The function automatically adjusts for Social Security age if the birthday is on
105
+ the first day of the month (adds 1/12 year to conventional age).
106
+
107
+ Parameters
108
+ ----------
109
+ fra : float
110
+ Full Retirement Age in years (can be fractional with 1/12 increments).
111
+ convage : float
112
+ Conventional age when benefits start, in years (can be fractional with 1/12 increments).
113
+ Must be between 62 and 70 inclusive.
114
+ bornOnFirst : bool
115
+ True if birthday is on the first day of the month, False otherwise.
116
+ If True, the function adds 1/12 year to convert to Social Security age.
117
+
118
+ Returns
119
+ -------
120
+ float
121
+ Factor to multiply PIA. Examples:
122
+ - 0.75 = 75% of PIA (claiming at 62 with FRA of 66)
123
+ - 1.0 = 100% of PIA (claiming at FRA)
124
+ - 1.32 = 132% of PIA (claiming at 70 with FRA of 66)
125
+
126
+ Raises
127
+ ------
128
+ ValueError
129
+ If convage is less than 62 or greater than 70.
58
130
  """
59
- if age < 62 or age > 70:
60
- raise ValueError(f"Age {age} out of range.")
131
+ if convage < 62 or convage > 70:
132
+ raise ValueError(f"Age {convage} out of range.")
61
133
 
62
- diff = fra - age
134
+ # Add a month to conventional age if born on the first.
135
+ offset = 0 if not bornOnFirst else 1/12
136
+ ssage = convage + offset
137
+
138
+ diff = fra - ssage
63
139
  if diff <= 0:
64
140
  return 1. - .08 * diff
65
141
  elif diff <= 3:
@@ -70,15 +146,50 @@ def getSelfFactor(fra, age):
70
146
  return .8 - 0.05 * (diff - 3)
71
147
 
72
148
 
73
- def getSpousalFactor(fra, age):
149
+ def getSpousalFactor(fra, convage, bornOnFirst):
74
150
  """
75
- Return factor to multiply spousal benefit given the age when benefit starts.
76
- Year of FRA and age can be fractional.
151
+ Return the reduction factor to multiply spousal benefits based on claiming age.
152
+
153
+ This function calculates the adjustment factor for spousal benefits based on when
154
+ benefits start relative to Full Retirement Age (FRA):
155
+ - Before FRA: Benefits are reduced (minimum 32.5% at age 62)
156
+ - At or after FRA: Full spousal benefit (50% of spouse's PIA, no increase for delay)
157
+
158
+ The function automatically adjusts for Social Security age if the birthday is on
159
+ the first day of the month (adds 1/12 year to conventional age).
160
+
161
+ Parameters
162
+ ----------
163
+ fra : float
164
+ Full Retirement Age in years (can be fractional with 1/12 increments).
165
+ convage : float
166
+ Conventional age when benefits start, in years (can be fractional with 1/12 increments).
167
+ Must be at least 62 (no maximum, but no increase beyond FRA).
168
+ bornOnFirst : bool
169
+ True if birthday is on the first day of the month, False otherwise.
170
+ If True, the function adds 1/12 year to convert to Social Security age.
171
+
172
+ Returns
173
+ -------
174
+ float
175
+ Factor to multiply spousal benefit. Examples:
176
+ - 0.325 = 32.5% reduction factor (claiming at 62 with FRA of 66)
177
+ - 1.0 = 100% of spousal benefit (claiming at or after FRA)
178
+ Note: Unlike self benefits, spousal benefits do not increase beyond FRA.
179
+
180
+ Raises
181
+ ------
182
+ ValueError
183
+ If convage is less than 62.
77
184
  """
78
- if age < 62:
79
- raise ValueError(f"Age {age} out of range.")
185
+ if convage < 62:
186
+ raise ValueError(f"Age {convage} out of range.")
187
+
188
+ # Add a month to conventional age if born on the first.
189
+ offset = 0 if not bornOnFirst else 1/12
190
+ ssage = convage + offset
80
191
 
81
- diff = fra - age
192
+ diff = fra - ssage
82
193
  if diff <= 0:
83
194
  return 1.
84
195
  elif diff <= 3:
owlplanner/tax2025.py CHANGED
@@ -273,6 +273,26 @@ def taxBrackets(N_i, n_d, N_n, yOBBBA=2099):
273
273
  return data
274
274
 
275
275
 
276
+ def computeNIIT(N_i, MAGI_n, I_n, Q_n, n_d, N_n):
277
+ """
278
+ Compute ACA tax on Dividends (Q) and Interests (I).
279
+ For accounting for rent and/or trust income, one can easily add a column
280
+ to the Wages and Contributions file and add yearly amount to Q_n + I_n below.
281
+ """
282
+ J_n = np.zeros(N_n)
283
+ status = N_i - 1
284
+
285
+ for n in range(N_n):
286
+ if status and n == n_d:
287
+ status -= 1
288
+
289
+ Gmax = niitThreshold[status]
290
+ if MAGI_n[n] > Gmax:
291
+ J_n[n] = niitRate * min(MAGI_n[n] - Gmax, I_n[n] + Q_n[n])
292
+
293
+ return J_n
294
+
295
+
276
296
  def rho_in(yobs, N_n):
277
297
  """
278
298
  Return Required Minimum Distribution fractions for each individual.
owlplanner/tax2026.py CHANGED
@@ -20,20 +20,12 @@ import numpy as np
20
20
  from datetime import date
21
21
 
22
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
23
  ###############################################################################
32
24
  # Start of section where rates need to be actualized every year.
33
25
  ###############################################################################
34
26
  # Single [0] and married filing jointly [1].
35
27
 
36
- # These are 2025 current.
28
+ # These are current for 2026, i.e., applying to tax year 2025.
37
29
  taxBrackets_OBBBA = np.array(
38
30
  [
39
31
  [12400, 50400, 105700, 201775, 256225, 640600, 9999999],
@@ -41,17 +33,20 @@ taxBrackets_OBBBA = np.array(
41
33
  ]
42
34
  )
43
35
 
36
+ # These are current for 2026 (2025TY).
44
37
  irmaaBrackets = np.array(
45
38
  [
46
- [0, 106000, 133000, 167000, 200000, 500000],
47
- [0, 212000, 266000, 334000, 400000, 750000],
39
+ [0, 109000, 137000, 171000, 205000, 500000],
40
+ [0, 218000, 274000, 342000, 410000, 750000],
48
41
  ]
49
42
  )
50
43
 
51
- # Index [0] stores the standard Medicare part B premium.
44
+ # These are current for 2026 (2025TY).
45
+ # Index [0] stores the standard Medicare part B basic premium.
52
46
  # 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])
47
+ irmaaFees = 12 * np.array([202.90, 81.20, 121.70, 121.70, 121.70, 40.70])
54
48
 
49
+ #########################################################################
55
50
  # Make projection for pre-TCJA using 2017 to current year.
56
51
  # taxBrackets_2017 = np.array(
57
52
  # [ [9325, 37950, 91900, 191650, 416700, 418400, 9999999],
@@ -60,41 +55,60 @@ irmaaFees = 12 * np.array([185.00, 74.00, 111.00, 110.90, 111.00, 37.00])
60
55
  #
61
56
  # stdDeduction_2017 = [6350, 12700]
62
57
  #
63
- # For 2025, I used a 30.5% adjustment from 2017, rounded to closest 50.
58
+ # COLA from 2017: [2.0, 2.8, 1.6, 1.3, 5.9, 8.7, 3.2, 2.5, 2.8]
59
+ # For 2026, I used a 35.1% adjustment from 2017, rounded to closest 10.
64
60
  #
65
61
  # These are speculated.
66
62
  taxBrackets_preTCJA = np.array(
67
63
  [
68
- [12150, 49550, 119950, 250200, 544000, 546200, 9999999], # Single
69
- [24350, 99100, 199850, 304600, 543950, 614450, 9999999], # MFJ
64
+ [12600, 51270, 124160, 258920, 562960, 565260, 9999999], # Single
65
+ [25200, 102540, 206840, 315260, 562960, 635920, 9999999], # MFJ
70
66
  ]
71
67
  )
72
68
 
73
- # These are 2025 current.
69
+ # These are speculated (adjusted for inflation to 2026).
70
+ stdDeduction_preTCJA = np.array([8580, 17160]) # Single, MFJ
71
+ #########################################################################
72
+
73
+ # These are current for 2026 (2025TY).
74
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
75
 
78
- # These are current (adjusted for inflation) per individual.
79
- extra65Deduction = np.array([2000, 1600]) # Single, MFJ
76
+ # These are current for 2026 (2025TY) per individual.
77
+ extra65Deduction = np.array([2000, 1600]) # Single, MFJ
80
78
 
81
- # Thresholds setting capital gains brackets 0%, 15%, 20% (adjusted for inflation).
79
+ # These are current for 2026 (2025TY).
80
+ # Thresholds setting capital gains brackets 0%, 15%, 20%.
82
81
  capGainRates = np.array(
83
82
  [
84
- [48350, 533400],
85
- [96700, 600050],
83
+ [49450, 545500],
84
+ [98900, 613700],
86
85
  ]
87
86
  )
88
87
 
88
+ ###############################################################################
89
+ # End of section where rates need to be actualized every year.
90
+ ###############################################################################
91
+
92
+ ###############################################################################
93
+ # Data that is unlikely to change.
94
+ ###############################################################################
95
+
89
96
  # Thresholds for net investment income tax (not adjusted for inflation).
90
97
  niitThreshold = np.array([200000, 250000])
91
98
  niitRate = 0.038
92
99
 
93
- # Thresholds for 65+ bonus for circumventing tax on social security.
100
+ # Thresholds for 65+ bonus of $6k per individual for circumventing tax
101
+ # on social security for low-income households. This expires in 2029.
102
+ # These numbers are hard-coded below as the tax code will likely change
103
+ # the rules for eligibility and will require a code review.
104
+ # Bonus decreases linearly above threshold by 1% / $1k over threshold.
94
105
  bonusThreshold = np.array([75000, 150000])
95
106
 
96
- ###############################################################################
97
- # End of section where rates need to be actualized every year.
107
+ taxBracketNames = ["10%", "12/15%", "22/25%", "24/28%", "32/33%", "35%", "37/40%"]
108
+
109
+ rates_OBBBA = np.array([0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.370])
110
+ rates_preTCJA = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
111
+
98
112
  ###############################################################################
99
113
 
100
114
 
@@ -273,6 +287,26 @@ def taxBrackets(N_i, n_d, N_n, yOBBBA=2099):
273
287
  return data
274
288
 
275
289
 
290
+ def computeNIIT(N_i, MAGI_n, I_n, Q_n, n_d, N_n):
291
+ """
292
+ Compute ACA tax on Dividends (Q) and Interests (I).
293
+ For accounting for rent and/or trust income, one can easily add a column
294
+ to the Wages and Contributions file and add yearly amount to Q_n + I_n below.
295
+ """
296
+ J_n = np.zeros(N_n)
297
+ status = N_i - 1
298
+
299
+ for n in range(N_n):
300
+ if status and n == n_d:
301
+ status -= 1
302
+
303
+ Gmax = niitThreshold[status]
304
+ if MAGI_n[n] > Gmax:
305
+ J_n[n] = niitRate * min(MAGI_n[n] - Gmax, I_n[n] + Q_n[n])
306
+
307
+ return J_n
308
+
309
+
276
310
  def rho_in(yobs, N_n):
277
311
  """
278
312
  Return Required Minimum Distribution fractions for each individual.
owlplanner/timelists.py CHANGED
@@ -34,6 +34,43 @@ _timeHorizonItems = [
34
34
  ]
35
35
 
36
36
 
37
+ _debtItems = [
38
+ "name",
39
+ "type",
40
+ "year",
41
+ "term",
42
+ "amount",
43
+ "rate",
44
+ ]
45
+
46
+
47
+ _debtTypes = [
48
+ "loan",
49
+ "mortgage",
50
+ ]
51
+
52
+
53
+ _fixedAssetItems = [
54
+ "name",
55
+ "type",
56
+ "basis",
57
+ "value",
58
+ "rate",
59
+ "yod",
60
+ "commission",
61
+ ]
62
+
63
+
64
+ _fixedAssetTypes = [
65
+ "collectibles",
66
+ "fixed annuity",
67
+ "precious metals",
68
+ "real estate",
69
+ "residence",
70
+ "stocks",
71
+ ]
72
+
73
+
37
74
  def read(finput, inames, horizons, mylog):
38
75
  """
39
76
  Read listed parameters from an excel spreadsheet or through
@@ -54,19 +91,40 @@ def read(finput, inames, horizons, mylog):
54
91
  else:
55
92
  # Read all worksheets in memory but only process those with proper names.
56
93
  try:
57
- dfDict = pd.read_excel(finput, sheet_name=None, usecols=_timeHorizonItems)
94
+ # dfDict = pd.read_excel(finput, sheet_name=None, usecols=_timeHorizonItems)
95
+ dfDict = pd.read_excel(finput, sheet_name=None)
58
96
  except Exception as e:
59
97
  raise Exception(f"Could not read file {finput}: {e}.") from e
60
98
  streamName = f"file '{finput}'"
61
99
 
62
- timeLists = _condition(dfDict, inames, horizons, mylog)
63
-
100
+ timeLists = _conditionTimetables(dfDict, inames, horizons, mylog)
64
101
  mylog.vprint(f"Successfully read time horizons from {streamName}.")
65
102
 
66
- return finput, timeLists
103
+ houseLists = _conditionHouseTables(dfDict, mylog)
104
+ mylog.vprint(f"Successfully read household tables from {streamName}.")
105
+
106
+ return finput, timeLists, houseLists
67
107
 
68
108
 
69
- def _condition(dfDict, inames, horizons, mylog):
109
+ def _checkColumns(df, iname, colList):
110
+ """
111
+ Ensure all columns in colList are present. Remove others.
112
+ """
113
+ # Drop all columns not in the list.
114
+ df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
115
+ for col in df.columns:
116
+ if col == "" or col not in colList:
117
+ df.drop(col, axis=1, inplace=True)
118
+
119
+ # Check that all columns in the list are present.
120
+ for item in colList:
121
+ if item not in df.columns:
122
+ raise ValueError(f"Column {item} not found for {iname}.")
123
+
124
+ return df
125
+
126
+
127
+ def _conditionTimetables(dfDict, inames, horizons, mylog):
70
128
  """
71
129
  Make sure that time horizons contain all years up to life expectancy,
72
130
  and that values are positive (except big-ticket items).
@@ -81,14 +139,7 @@ def _condition(dfDict, inames, horizons, mylog):
81
139
 
82
140
  df = dfDict[iname]
83
141
 
84
- df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
85
- for col in df.columns:
86
- if col == "" or col not in _timeHorizonItems:
87
- df.drop(col, axis=1, inplace=True)
88
-
89
- for item in _timeHorizonItems:
90
- if item not in df.columns:
91
- raise ValueError(f"Item {item} not found for {iname}.")
142
+ df = _checkColumns(df, iname, _timeHorizonItems)
92
143
 
93
144
  # Only consider lines in proper year range. Go back 5 years for Roth maturation.
94
145
  df = df[df["year"] >= (thisyear - 5)]
@@ -97,13 +148,14 @@ def _condition(dfDict, inames, horizons, mylog):
97
148
  missing = []
98
149
  for n in range(-5, horizons[i]):
99
150
  year = thisyear + n
100
- if not (df[df["year"] == year]).any(axis=None):
151
+ year_rows = df[df["year"] == year]
152
+ if year_rows.empty:
101
153
  df.loc[len(df)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
102
154
  missing.append(year)
103
155
  else:
104
156
  for item in _timeHorizonItems:
105
- if item != "big-ticket items" and df[item].iloc[n] < 0:
106
- raise ValueError(f"Item {item} for {iname} in year {df['year'].iloc[n]} is < 0.")
157
+ if item != "big-ticket items" and year_rows[item].iloc[0] < 0:
158
+ raise ValueError(f"Item {item} for {iname} in year {year} is < 0.")
107
159
 
108
160
  if len(missing) > 0:
109
161
  mylog.vprint(f"Adding {len(missing)} missing years for {iname}: {missing}.")
@@ -115,8 +167,64 @@ def _condition(dfDict, inames, horizons, mylog):
115
167
  timeLists[iname] = df
116
168
 
117
169
  if df["year"].iloc[-1] != endyear - 1:
118
- raise ValueError(
119
- f"Time horizon for {iname} too short.\n\tIt should end in {endyear}, not {df['year'].iloc[-1]}"
120
- )
170
+ raise ValueError(f"""Time horizon for {iname} too short.\n\t
171
+ It should end in {endyear}, not {df['year'].iloc[-1]}""")
121
172
 
122
173
  return timeLists
174
+
175
+
176
+ def _conditionHouseTables(dfDict, mylog):
177
+ """
178
+ Read debts and fixed assets from Household Financial Profile workbook.
179
+ """
180
+ houseDic = {}
181
+
182
+ items = {"Debts" : _debtItems, "Fixed Assets": _fixedAssetItems}
183
+ types = {"Debts" : _debtTypes, "Fixed Assets": _fixedAssetTypes}
184
+ for page in items.keys():
185
+ if page in dfDict:
186
+ df = dfDict[page]
187
+ df = _checkColumns(df, page, items[page])
188
+ # Check categorical variables.
189
+ isInList = df["type"].isin(types[page])
190
+ df = df[isInList]
191
+
192
+ # Convert percentage columns from decimal to percentage if needed
193
+ # UI uses 0-100 range for percentages (e.g., 4.5 = 4.5%)
194
+ # If Excel read percentage-formatted cells, values might be decimals (0.045)
195
+ # Convert values < 1.0 to percentage format (multiply by 100)
196
+ if page == "Debts" and "rate" in df.columns:
197
+ # If rate values are less than 1, assume they're decimals (0.045 = 4.5%)
198
+ # and convert to percentages (4.5) to match UI format (0-100 range)
199
+ mask = (df["rate"] < 1.0) & (df["rate"] > 0)
200
+ if mask.any():
201
+ df.loc[mask, "rate"] = df.loc[mask, "rate"] * 100.0
202
+ mylog.vprint(f"Converted {mask.sum()} rate value(s) from decimal to percentage in Debts table.")
203
+
204
+ elif page == "Fixed Assets":
205
+ # Convert rate and commission if they're decimals
206
+ # Both should be in 0-100 range to match UI format
207
+ if "rate" in df.columns:
208
+ mask = (df["rate"] < 1.0) & (df["rate"] > 0)
209
+ if mask.any():
210
+ df.loc[mask, "rate"] = df.loc[mask, "rate"] * 100.0
211
+ mylog.vprint(
212
+ f"Converted {mask.sum()} rate value(s) from decimal "
213
+ f"to percentage in Fixed Assets table."
214
+ )
215
+ if "commission" in df.columns:
216
+ mask = (df["commission"] < 1.0) & (df["commission"] > 0)
217
+ if mask.any():
218
+ df.loc[mask, "commission"] = df.loc[mask, "commission"] * 100.0
219
+ mylog.vprint(
220
+ f"Converted {mask.sum()} commission value(s) from decimal "
221
+ f"to percentage in Fixed Assets table."
222
+ )
223
+
224
+ houseDic[page] = df
225
+ mylog.vprint(f"Found {len(df)} valid row(s) in {page} table.")
226
+ else:
227
+ houseDic[page] = pd.DataFrame(columns=items[page])
228
+ mylog.vprint(f"Table for {page} not found. Assuming empty table.")
229
+
230
+ return houseDic
owlplanner/utils.py CHANGED
@@ -70,7 +70,7 @@ def getUnits(units) -> int:
70
70
  return fac
71
71
 
72
72
 
73
- # Next two functins could be a one-line lambda functions.
73
+ # Next two functions could be a one-line lambda functions.
74
74
  # e.g., krond = lambda a, b: 1 if a == b else 0
75
75
  def krond(a, b) -> int:
76
76
  """
@@ -101,3 +101,27 @@ def roundCents(values, decimals=2):
101
101
  arr = np.where((-0.009 < arr) & (arr <= 0), 0, arr)
102
102
 
103
103
  return arr
104
+
105
+
106
+ def parseDobs(dobs):
107
+ """
108
+ Parse a list of dates and return int32 arrays of year, months, days.
109
+ """
110
+ icount = len(dobs)
111
+ yobs = []
112
+ mobs = []
113
+ tobs = []
114
+ for i in range(icount):
115
+ ls = dobs[i].split("-")
116
+ if len(ls) != 3:
117
+ raise ValueError(f"Date {dobs[i]} not in ISO format.")
118
+ if not 1 <= int(ls[1]) <= 12:
119
+ raise ValueError(f"Month in date {dobs[i]} not valid.")
120
+ if not 1 <= int(ls[2]) <= 31:
121
+ raise ValueError(f"Day in date {dobs[i]} not valid.")
122
+
123
+ yobs.append(ls[0])
124
+ mobs.append(ls[1])
125
+ tobs.append(ls[2])
126
+
127
+ return np.array(yobs, dtype=np.int32), np.array(mobs, dtype=np.int32), np.array(tobs, dtype=np.int32)
owlplanner/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.12.05"
1
+ __version__ = "2025.12.20"