owlplanner 2025.2.24__tar.gz → 2025.2.27__tar.gz
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-2025.2.24 → owlplanner-2025.2.27}/PKG-INFO +1 -1
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/case_jack+jill.toml +1 -1
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/case_joe.toml +1 -1
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/case_john+sally.toml +1 -1
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/case_jon+jane.toml +1 -1
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/case_kim+sam-bequest.toml +1 -1
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/case_kim+sam-spending.toml +1 -1
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/pyproject.toml +1 -1
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/plan.py +44 -24
- owlplanner-2025.2.27/src/owlplanner/version.py +1 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Asset_Allocation.py +1 -2
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Assets.py +1 -2
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Create_Case.py +18 -7
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Documentation.py +20 -11
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Fixed_Income.py +1 -2
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Graphs.py +3 -3
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Historical_Range.py +1 -2
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Logs.py +1 -2
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Monte_Carlo.py +1 -2
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Optimization_Parameters.py +1 -2
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Output_Files.py +10 -7
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Quick_Start.py +4 -3
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Rates_Selection.py +1 -2
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Wages_And_Contributions.py +9 -7
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Worksheets.py +1 -2
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/owlbridge.py +10 -11
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/plots.py +2 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/requirements.txt +1 -1
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/sskeys.py +63 -27
- owlplanner-2025.2.27/ui/tomlexamples.py +16 -0
- owlplanner-2025.2.24/src/owlplanner/version.py +0 -1
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/.flake8 +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/.gitattributes +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/.gitignore +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/INSTALL.md +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/LICENSE +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/README.md +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/USER_GUIDE.md +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docker/Dockerfile +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docker/README.md +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docker/docker-compose.yml +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docker/fastentrypoint.sh +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/allocations.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/owl.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/profile.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/owl.pdf +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/docs/owl.tex +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/joe.xlsx +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/jon+jane.xlsx +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/examples/template.xlsx +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/owlplanner.cmd +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/owlplanner.sh +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/requirements.txt +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/config.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/logging.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/rates.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/tax2025.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/timelists.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/tests/test_logger.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/tests/test_regressions.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/tests/test_repro.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/tests/test_units.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ttt.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/About_Owl.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/README.md +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/Settings.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/main+fonts.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/main.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/progress.py +0 -0
- {owlplanner-2025.2.24 → owlplanner-2025.2.27}/ui/style.css +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"Plan Name" = "jack+jill"
|
|
2
|
-
Description = "This example
|
|
2
|
+
Description = "This example aims to demonstrate some of Owl's capabilities. Jack and Jill are a married couple a few years from retirement. A wages and contributions file called 'jack+jill.xlsx' is associated with this case. This case uses the historical rate sequence of 1969 as a test case for guiding spending amounts from a near worst-case historical scenario. This case also demonstrates that the optimal strategy for Roth conversions does not necessarily involve surfing a tax bracket."
|
|
3
3
|
|
|
4
4
|
["Basic Info"]
|
|
5
5
|
Status = "married"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"Plan Name" = "joe"
|
|
2
|
-
Description = "This example
|
|
2
|
+
Description = "This is an example of a case involving a single individual. Joe is single and will retire in a few years. His wages and contributions are contained in the 'joe.xlsx' spreadsheet."
|
|
3
3
|
|
|
4
4
|
["Basic Info"]
|
|
5
5
|
Status = "single"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"Plan Name" = "john+sally"
|
|
2
|
-
Description = "This example reproduces the case of John and Sally, discussed by Eric Sajdak."
|
|
2
|
+
Description = "This example reproduces the case of John and Sally, discussed by Eric Sajdak. This case can be used to compare the heuristic strategy of surfing a tax bracket for performing Roth conversions to a solution optimized by linear programming. The former is a good approach when one assumes fixed rates. When rates are varying, and the market drops, an optimized solution shows that it is sometime advantageous to convert above the target tax bracket. File 'john+sally.xlsx' contains wages and contributions associated with this case. Run this case with different rates to see the effects on Roth conversions."
|
|
3
3
|
|
|
4
4
|
["Basic Info"]
|
|
5
5
|
Status = "married"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"Plan Name" = "Jon+Jane"
|
|
2
|
-
Description = "
|
|
2
|
+
Description = "This case reproduces a similar case discussed a while back on i-orp. It involves Jon and Jane close to retirement, and assumes a base case of optimistic returns of 10% with an inflation of 3.5%. A wages and contributions file called 'jon+jane.xlsx' is associated with this case."
|
|
3
3
|
|
|
4
4
|
["Basic Info"]
|
|
5
5
|
Status = "married"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"Plan Name" = "kim+sam-bequest"
|
|
2
|
-
Description = "Kim and Sam
|
|
2
|
+
Description = "This is the case of Kim and Sam used as an example case for optimizing bequest and Roth conversions. No wages and contributions file is associated with this case."
|
|
3
3
|
|
|
4
4
|
["Basic Info"]
|
|
5
5
|
Status = "married"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"Plan Name" = "kim+sam-spending"
|
|
2
|
-
Description = "Kim and Sam
|
|
2
|
+
Description = "This is the case of Kim and Sam used as an example case for optimizing spending and Roth conversions. No wages and contributions is associated with this case."
|
|
3
3
|
|
|
4
4
|
["Basic Info"]
|
|
5
5
|
Status = "married"
|
|
@@ -34,6 +34,10 @@ from owlplanner import logging
|
|
|
34
34
|
from owlplanner import progress
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
# This makes all graphs appear have the same height.
|
|
38
|
+
plt.rcParams.update({'figure.autolayout': True})
|
|
39
|
+
|
|
40
|
+
|
|
37
41
|
def _genGamma_n(tau):
|
|
38
42
|
"""
|
|
39
43
|
Utility function to generate a cumulative inflation multiplier
|
|
@@ -2058,6 +2062,12 @@ class Plan(object):
|
|
|
2058
2062
|
|
|
2059
2063
|
return mylist
|
|
2060
2064
|
|
|
2065
|
+
def summaryDf(self):
|
|
2066
|
+
"""
|
|
2067
|
+
Return summary as a dataframe.
|
|
2068
|
+
"""
|
|
2069
|
+
return pd.DataFrame(self.summaryDic(), index=[self._name])
|
|
2070
|
+
|
|
2061
2071
|
def summaryString(self):
|
|
2062
2072
|
"""
|
|
2063
2073
|
Return summary as a string.
|
|
@@ -2077,29 +2087,34 @@ class Plan(object):
|
|
|
2077
2087
|
dic = {}
|
|
2078
2088
|
# Results
|
|
2079
2089
|
dic["Plan name"] = self._name
|
|
2080
|
-
dic[
|
|
2081
|
-
dic[f"Net
|
|
2090
|
+
dic["Net yearly spending basis"] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2091
|
+
dic[f"Net spending for year {now}"] = u.d(self.g_n[0] / self.yearFracLeft)
|
|
2082
2092
|
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
|
|
2083
2093
|
|
|
2084
2094
|
totIncome = np.sum(self.g_n, axis=0)
|
|
2085
2095
|
totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
|
|
2086
|
-
dic[
|
|
2096
|
+
dic["Total net spending"] = f"{u.d(totIncomeNow)}"
|
|
2097
|
+
dic["- Total net spending (nominal)"] = f"{u.d(totIncome)}"
|
|
2087
2098
|
|
|
2088
2099
|
totRoth = np.sum(self.x_in, axis=(0, 1))
|
|
2089
2100
|
totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
|
|
2090
|
-
dic[
|
|
2101
|
+
dic["Total Roth conversions"] = f"{u.d(totRothNow)}"
|
|
2102
|
+
dic["- Total Roth conversions (nominal)"] = f"{u.d(totRoth)}"
|
|
2091
2103
|
|
|
2092
2104
|
taxPaid = np.sum(self.T_n, axis=0)
|
|
2093
2105
|
taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
|
|
2094
|
-
dic[
|
|
2106
|
+
dic["Total income tax paid on ordinary income"] = f"{u.d(taxPaidNow)}"
|
|
2107
|
+
dic["- Total income tax paid on ordinary income (nominal)"] = f"{u.d(taxPaid)}"
|
|
2095
2108
|
|
|
2096
2109
|
taxPaid = np.sum(self.U_n, axis=0)
|
|
2097
2110
|
taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
|
|
2098
|
-
dic[
|
|
2111
|
+
dic["Total tax paid on gains and dividends"] = f"{u.d(taxPaidNow)}"
|
|
2112
|
+
dic["- Total tax paid on gains and dividends (nominal)"] = f"{u.d(taxPaid)}"
|
|
2099
2113
|
|
|
2100
2114
|
taxPaid = np.sum(self.M_n, axis=0)
|
|
2101
2115
|
taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
|
|
2102
|
-
dic[
|
|
2116
|
+
dic["Total Medicare premiums paid"] = f"{u.d(taxPaidNow)}"
|
|
2117
|
+
dic["- Total Medicare premiums paid (nominal)"] = f"{u.d(taxPaid)}"
|
|
2103
2118
|
|
|
2104
2119
|
if self.N_i == 2 and self.n_d < self.N_n:
|
|
2105
2120
|
p_j = self.partialEstate_j * (1 - self.phi_j)
|
|
@@ -2113,30 +2128,35 @@ class Plan(object):
|
|
|
2113
2128
|
totSpousalNow = totSpousal / self.gamma_n[nx + 1]
|
|
2114
2129
|
iname_s = self.inames[self.i_s]
|
|
2115
2130
|
iname_d = self.inames[self.i_d]
|
|
2116
|
-
dic[f"
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
dic[f"
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
f"{u.d(
|
|
2131
|
+
dic[f"Sum of spousal transfer to {iname_s} in year {ynx}"] = (f"{u.d(totSpousalNow)}")
|
|
2132
|
+
dic[f"- Sum of spousal transfer to {iname_s} in year {ynx} (nominal)"] = (f"{u.d(totSpousal)}")
|
|
2133
|
+
dic[f"-- Spousal transfer to {iname_s} in year {ynx} - taxable (nominal)"] = (f"{u.d(q_j[0])}")
|
|
2134
|
+
dic[f"-- Spousal transfer to {iname_s} in year {ynx} - tax-def (nominal)"] = (f"{u.d(q_j[1])}")
|
|
2135
|
+
dic[f"-- Spousal transfer to {iname_s} in year {ynx} - tax-free (nominal)"] = (f"{u.d(q_j[2])}")
|
|
2136
|
+
|
|
2137
|
+
dic[f"Sum of post-tax non-spousal bequests from {iname_d} in year {ynx}"] = (f"{u.d(totOthersNow)}")
|
|
2138
|
+
dic[f"- Sum of post-tax non-spousal bequests from {iname_d} in year {ynx} (nominal)"] = (
|
|
2139
|
+
f"{u.d(totOthers)}")
|
|
2140
|
+
dic[f"-- Post-tax non-spousal bequests from {iname_d} in year {ynx} - taxable (nominal)"] = (
|
|
2141
|
+
f"{u.d(p_j[0])}")
|
|
2142
|
+
dic[f"-- Post-tax non-spousal bequests from {iname_d} in year {ynx} - tax-def (nominal)"] = (
|
|
2143
|
+
f"{u.d(p_j[1])}")
|
|
2144
|
+
dic[f"-- Post-tax non-spousal bequests from {iname_d} in year {ynx} - tax-free (nominal)"] = (
|
|
2145
|
+
f"{u.d(p_j[2])}")
|
|
2127
2146
|
|
|
2128
2147
|
estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
|
|
2129
2148
|
estate[1] *= 1 - self.nu
|
|
2130
2149
|
lastyear = self.year_n[-1]
|
|
2131
|
-
dic[f"Post-tax account values at the end of final plan year {lastyear} (nominal)"] = (
|
|
2132
|
-
f"taxable: {u.d(estate[0])} tax-def: {u.d(estate[1])} tax-free: {u.d(estate[2])}")
|
|
2133
|
-
|
|
2134
2150
|
totEstate = np.sum(estate)
|
|
2135
2151
|
totEstateNow = totEstate / self.gamma_n[-1]
|
|
2136
|
-
dic[f"Total estate value at the end of
|
|
2137
|
-
|
|
2152
|
+
dic[f"Total estate value at the end of {lastyear}"] = (f"{u.d(totEstateNow)}")
|
|
2153
|
+
dic[f"- Total estate value at the end of {lastyear} (nominal)"] = (f"{u.d(totEstate)}")
|
|
2154
|
+
dic[f"-- Post-tax account value at the end of {lastyear} - taxable (nominal)"] = (f"{u.d(estate[0])}")
|
|
2155
|
+
dic[f"-- Post-tax account value at the end of {lastyear} - tax-def (nominal)"] = (f"{u.d(estate[1])}")
|
|
2156
|
+
dic[f"-- Post-tax account value at the end of {lastyear} - tax-free (nominal)"] = (f"{u.d(estate[2])}")
|
|
2157
|
+
|
|
2138
2158
|
dic["Plan starting date"] = str(self.startDate)
|
|
2139
|
-
dic["Cumulative inflation factor from start date to end of
|
|
2159
|
+
dic[f"Cumulative inflation factor from start date to end of {lastyear}"] = f"{self.gamma_n[-1]:.2f}"
|
|
2140
2160
|
for i in range(self.N_i):
|
|
2141
2161
|
dic[f"{self.inames[i]:>12}'s {self.horizons[i]:02}-year life horizon"] = (
|
|
2142
2162
|
f"{now} -> {now + self.horizons[i] - 1}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2025.02.27"
|
|
@@ -81,8 +81,7 @@ def checkAllAllocs():
|
|
|
81
81
|
return result
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
ret = kz.titleBar("
|
|
85
|
-
kz.caseHeader("Asset Allocation")
|
|
84
|
+
ret = kz.titleBar("Asset Allocation")
|
|
86
85
|
|
|
87
86
|
if ret is None or kz.caseHasNoPlan():
|
|
88
87
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from datetime import date
|
|
2
|
+
from io import StringIO
|
|
2
3
|
import streamlit as st
|
|
3
4
|
|
|
4
5
|
import sskeys as kz
|
|
5
6
|
import owlbridge as owb
|
|
7
|
+
import tomlexamples as tomlex
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
caseChoices = kz.allCaseNames()
|
|
9
|
-
ret = kz.titleBar("
|
|
10
|
-
kz.caseHeader("Create Case")
|
|
11
|
+
ret = kz.titleBar("Create Case", caseChoices)
|
|
11
12
|
|
|
12
13
|
if ret == kz.newCase:
|
|
13
14
|
st.info("#### Starting a new case from scratch.\n\n" "A name for the scenario must first be provided.")
|
|
@@ -23,12 +24,22 @@ elif ret == kz.loadCaseFile:
|
|
|
23
24
|
# "<a href="Documentation" target="_self">Documentation</a>", unsafe_allow_html=True)
|
|
24
25
|
st.info(
|
|
25
26
|
"#### Starting a case from a *case* parameter file.\n\n"
|
|
26
|
-
"
|
|
27
|
-
"Alternatively, select `New Case...` to start a case from scratch
|
|
27
|
+
"Upload your own case or select one from multiple examples."
|
|
28
|
+
" Alternatively, you can select `New Case...` in the margin selector box to start a case from scratch.\n\n"
|
|
29
|
+
"Look at the :material/help: [Documentation](Documentation) for more details."
|
|
28
30
|
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
st.write("#### Upload your own case file")
|
|
32
|
+
file = st.file_uploader("Upload *case* parameter file...", key="_confile", type=["toml"])
|
|
33
|
+
if file is not None:
|
|
34
|
+
mystringio = StringIO(file.read().decode("utf-8"))
|
|
35
|
+
if kz.createCaseFromFile(mystringio):
|
|
36
|
+
st.rerun()
|
|
37
|
+
|
|
38
|
+
st.write("#### Load a case example")
|
|
39
|
+
case = st.selectbox("Examples available from GitHub", tomlex.cases, index=None, placeholder="Select a case")
|
|
40
|
+
if case is not None:
|
|
41
|
+
mystringio = tomlex.loadExample(case)
|
|
42
|
+
if kz.createCaseFromFile(mystringio):
|
|
32
43
|
st.rerun()
|
|
33
44
|
else:
|
|
34
45
|
helpmsg = "Case name can be changed by editing it directly."
|
|
@@ -36,7 +36,7 @@ formulation of the optimization problem can be found
|
|
|
36
36
|
### Getting started with the user interface
|
|
37
37
|
Functions of each page are described below in the same order as they appear in the sidebar.
|
|
38
38
|
Typically, pages would be accessed in order, starting from the top.
|
|
39
|
-
The `
|
|
39
|
+
The `Case selector` box at the top of the page allows to select an existing case
|
|
40
40
|
or create a new one from scratch, or from a *case* parameter file, which
|
|
41
41
|
would then populate all parameter values.
|
|
42
42
|
This box is present in all pages except those in the **Resources** section
|
|
@@ -51,7 +51,7 @@ This section contains the steps for creating and configuring case scenarios.
|
|
|
51
51
|
|
|
52
52
|
#### Create Case
|
|
53
53
|
This page is where every new scenario begins.
|
|
54
|
-
It controls the creation of scenarios as the `
|
|
54
|
+
It controls the creation of scenarios as the `Case selector` drop-down menu contains
|
|
55
55
|
two additional items when this page is open:
|
|
56
56
|
one to create new cases, and the other to create cases from a *case* parameter file.
|
|
57
57
|
This page also allows you to duplicate and/or rename scenarios, as well as deleting them.
|
|
@@ -65,7 +65,7 @@ When duplicating a scenario, make sure to visit all pages in the **Case Setup**
|
|
|
65
65
|
and verify that all parameters are as intended.
|
|
66
66
|
|
|
67
67
|
##### Initializing the life parameters for the realization
|
|
68
|
-
Start with the `
|
|
68
|
+
Start with the `Case selector` box and choose one of `New case...` or `Upload case file...`.
|
|
69
69
|
|
|
70
70
|
If `Upload case file...` is selected, a *case* file must be uploaded.
|
|
71
71
|
These files end with the *.toml* extension, are human readable (and therefore editable),
|
|
@@ -135,14 +135,21 @@ The wages and contributions data contains 9 columns titled as follows:
|
|
|
135
135
|
| ... | | | | | | | | |
|
|
136
136
|
|20XX | | | | | | | | |
|
|
137
137
|
|
|
138
|
-
Here, 20XX represents the last row which could be the last year based on the life
|
|
138
|
+
Here, 20XX represents the last row which could be the last year based on the life
|
|
139
|
+
expectancy values provided.
|
|
139
140
|
While loading an Excel workbook, missing years or empty cells will be filled with zero values,
|
|
140
141
|
while years outside the time span of the plan will be ignored.
|
|
141
142
|
For the columns, *anticipated wages* is the annual amount
|
|
142
|
-
(gross minus tax-deferred contributions) that you anticipate to receive from employment
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
(gross minus tax-deferred contributions) that you anticipate to receive from employment
|
|
144
|
+
or other sources (e.g. rentals).
|
|
145
|
+
This column does not include dividends from your taxable investment accounts,
|
|
146
|
+
as they will be calculated based on your return rate assumptions.
|
|
147
|
+
|
|
148
|
+
Note that column names are case sensitive and all entries must be in lower case.
|
|
149
|
+
The easiest way to complete the process of filling this file is either to start from the template
|
|
145
150
|
file provided [here](https://raw.github.com/mdlacasse/Owl/main/examples/template.xlsx).
|
|
151
|
+
Values can also be filled in the user interface, but this approach does not have
|
|
152
|
+
Excel capabilities for cross-column calculations.
|
|
146
153
|
|
|
147
154
|
For the purpose of planning, there is no clear definition of retirement age. There will be a year,
|
|
148
155
|
however, from which you will stop having anticipated income, or diminished income due to decreasing your
|
|
@@ -300,13 +307,15 @@ The first line of the *Sources* worksheets are the most important
|
|
|
300
307
|
as these lines are the only ones that are actionable.
|
|
301
308
|
|
|
302
309
|
#### Output Files
|
|
303
|
-
This page allow to save
|
|
310
|
+
This page allow to compare cases and save files for future use.
|
|
304
311
|
First it shows a synopsis of the computed scenario by
|
|
305
312
|
displaying sums of income, bequest, and spending values over the duration of the plan.
|
|
306
|
-
|
|
307
|
-
|
|
313
|
+
If more than one case was run, they will be compared provided they were made
|
|
314
|
+
for the same individuals.
|
|
315
|
+
The contents of the synopsis can be downloaded as a plain text file by
|
|
316
|
+
clicking the button below it.
|
|
308
317
|
|
|
309
|
-
|
|
318
|
+
Similarly, parameters used to generate the case are collected in *toml* format and displayed.
|
|
310
319
|
The `Download case file...` button allows to save the parameters used to generate the
|
|
311
320
|
outcome of this case to a *case* file.
|
|
312
321
|
|
|
@@ -36,8 +36,7 @@ def getToggleInput(i, key, thing):
|
|
|
36
36
|
st.toggle(thing, on_change=kz.setpull, value=defval, args=[nkey], key="_" + nkey)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
ret = kz.titleBar("
|
|
40
|
-
kz.caseHeader("Fixed Income")
|
|
39
|
+
ret = kz.titleBar("Fixed Income")
|
|
41
40
|
|
|
42
41
|
if ret is None or kz.caseHasNoPlan():
|
|
43
42
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -4,8 +4,7 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
ret = kz.titleBar("
|
|
8
|
-
kz.caseHeader("Graphs")
|
|
7
|
+
ret = kz.titleBar("Graphs")
|
|
9
8
|
|
|
10
9
|
if ret is None or kz.caseHasNoPlan():
|
|
11
10
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -21,9 +20,10 @@ else:
|
|
|
21
20
|
ret = kz.getRadio("Dollar amounts in plots", choices, "plots", callback=owb.setDefaultPlots)
|
|
22
21
|
|
|
23
22
|
with col2:
|
|
23
|
+
helpmsg = "Click on button if graphs are not all showing."
|
|
24
24
|
st.button(
|
|
25
25
|
"Re-run single case",
|
|
26
|
-
help=
|
|
26
|
+
help=helpmsg,
|
|
27
27
|
on_click=owb.runPlan,
|
|
28
28
|
disabled=kz.caseIsNotRunReady(),
|
|
29
29
|
)
|
|
@@ -4,8 +4,7 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
ret = kz.titleBar("
|
|
8
|
-
kz.caseHeader("Historical Range")
|
|
7
|
+
ret = kz.titleBar("Historical Range")
|
|
9
8
|
|
|
10
9
|
if ret is None or kz.caseHasNoPlan():
|
|
11
10
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -16,8 +16,7 @@ def initProfile():
|
|
|
16
16
|
owb.setProfile(profileChoices[0], False)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
ret = kz.titleBar("
|
|
20
|
-
kz.caseHeader("Optimization Parameters")
|
|
19
|
+
ret = kz.titleBar("Optimization Parameters")
|
|
21
20
|
|
|
22
21
|
if ret is None or kz.caseHasNoPlan():
|
|
23
22
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -3,8 +3,7 @@ import streamlit as st
|
|
|
3
3
|
import sskeys as kz
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
|
-
ret = kz.titleBar("
|
|
7
|
-
kz.caseHeader("Output Files")
|
|
6
|
+
ret = kz.titleBar("Output Files")
|
|
8
7
|
|
|
9
8
|
if ret is None or kz.caseHasNoPlan():
|
|
10
9
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -16,12 +15,15 @@ else:
|
|
|
16
15
|
st.info("Case status is currently '%s'." % kz.getKey("caseStatus"))
|
|
17
16
|
else:
|
|
18
17
|
caseName = kz.getKey("name")
|
|
19
|
-
|
|
20
|
-
if
|
|
18
|
+
df = kz.compareSummaries()
|
|
19
|
+
if df is not None:
|
|
21
20
|
st.write("#### Synopsis")
|
|
22
|
-
st.code(lines, language=None)
|
|
21
|
+
# st.code(lines, language=None)
|
|
22
|
+
# st.markdown(df.to_markdown())
|
|
23
|
+
st.dataframe(df[1:], use_container_width=True)
|
|
23
24
|
st.download_button(
|
|
24
|
-
"Download synopsis", data=
|
|
25
|
+
"Download synopsis", data=df[1:].to_string(), file_name=f"Synopsis_{caseName}.txt",
|
|
26
|
+
mime="text/plain;charset=UTF-8"
|
|
25
27
|
)
|
|
26
28
|
|
|
27
29
|
st.divider()
|
|
@@ -54,5 +56,6 @@ else:
|
|
|
54
56
|
st.code(lines, language="toml")
|
|
55
57
|
|
|
56
58
|
st.download_button(
|
|
57
|
-
"Download case parameter file", data=lines,
|
|
59
|
+
"Download case parameter file", data=lines,
|
|
60
|
+
file_name=f"case_{caseName}.toml", mime="application/toml"
|
|
58
61
|
)
|
|
@@ -36,7 +36,8 @@ of Jack and Jill provided here as an example:
|
|
|
36
36
|
- Wages and contributions file named
|
|
37
37
|
[jack+jill.xlsx](https://raw.github.com/mdlacasse/Owl/main/examples/jack+jill.xlsx)
|
|
38
38
|
in Excel format.
|
|
39
|
-
1) Navigate to the **Create Case** page and
|
|
39
|
+
1) Navigate to the **Create Case** page and select the case of *jack+jill* among the GitHub examples.
|
|
40
|
+
Alternatively, you can drag and drop the case parameter file
|
|
40
41
|
you just downloaded (*case_jack+jill.toml*).
|
|
41
42
|
1) Navigate to the **Wages and Contributions** page and
|
|
42
43
|
drag and drop the wages and contributions file you downloaded (*jack+jill.xlsx*).
|
|
@@ -54,8 +55,8 @@ its parameters can be saved by using the `Download case file...` button on the `
|
|
|
54
55
|
Alternatively, you can duplicate any existing case by using
|
|
55
56
|
the `Duplicate case` button, and then edit its values to fit your situation.
|
|
56
57
|
|
|
57
|
-
Multiple cases can coexist and can be called and compared using the `
|
|
58
|
-
at the
|
|
58
|
+
Multiple cases can coexist and can be called and compared using the `Case selector` box
|
|
59
|
+
at the top of the page.
|
|
59
60
|
|
|
60
61
|
More information can be found on the :material/help: **[Documentation](Documentation)**
|
|
61
62
|
page located in the **Resources** section.
|
|
@@ -47,8 +47,7 @@ kz.initKey("rateType", rateChoices[0])
|
|
|
47
47
|
kz.initKey("fixedType", fixedChoices[0])
|
|
48
48
|
kz.initKey("varyingType", varyingChoices[0])
|
|
49
49
|
|
|
50
|
-
ret = kz.titleBar("
|
|
51
|
-
kz.caseHeader("Rates Selection")
|
|
50
|
+
ret = kz.titleBar("Rates Selection")
|
|
52
51
|
|
|
53
52
|
if ret is None or kz.caseHasNoPlan():
|
|
54
53
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -4,15 +4,15 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
# Refresh Wages and Contributions tables as time window likely to change.
|
|
7
8
|
def resetTimeLists():
|
|
8
|
-
if not kz.getKey("duplicate"):
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# if not kz.getKey("duplicate"):
|
|
10
|
+
tlists = owb.resetContributions()
|
|
11
|
+
for i, iname in enumerate(tlists):
|
|
12
|
+
kz.setKey("timeList" + str(i), tlists[iname])
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
ret = kz.titleBar("
|
|
15
|
-
kz.caseHeader("Wages and Contributions")
|
|
15
|
+
ret = kz.titleBar("Wages and Contributions")
|
|
16
16
|
|
|
17
17
|
if ret is None or kz.caseHasNoPlan():
|
|
18
18
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -38,11 +38,12 @@ else:
|
|
|
38
38
|
" that has not yet been uploaded."
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
+
st.write("#### Upload a *Wages and Contributions* file")
|
|
41
42
|
kz.initKey("_xlsx", 0)
|
|
42
43
|
stTimeLists = st.file_uploader(
|
|
43
44
|
"Upload values from a wages and contributions file...",
|
|
44
45
|
key="_stTimeLists" + str(kz.getKey("_xlsx")),
|
|
45
|
-
type=["xlsx"],
|
|
46
|
+
type=["xlsx", "ods"],
|
|
46
47
|
)
|
|
47
48
|
if stTimeLists is not None:
|
|
48
49
|
if owb.readContributions(stTimeLists):
|
|
@@ -51,6 +52,7 @@ else:
|
|
|
51
52
|
kz.storeKey("_xlsx", kz.getKey("_xlsx") + 1)
|
|
52
53
|
st.rerun()
|
|
53
54
|
|
|
55
|
+
st.divider()
|
|
54
56
|
for i in range(n):
|
|
55
57
|
st.write("##### " + kz.getKey("iname" + str(i)) + "'s timetable")
|
|
56
58
|
colfor = {"year": st.column_config.NumberColumn(None, format="%d")}
|
|
@@ -3,8 +3,7 @@ import streamlit as st
|
|
|
3
3
|
import sskeys as kz
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
|
-
ret = kz.titleBar("
|
|
7
|
-
kz.caseHeader("Worksheets")
|
|
6
|
+
ret = kz.titleBar("Worksheets")
|
|
8
7
|
|
|
9
8
|
if ret is None or kz.caseHasNoPlan():
|
|
10
9
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -122,15 +122,15 @@ def runPlan(plan):
|
|
|
122
122
|
except Exception as e:
|
|
123
123
|
st.error(f"Solution failed: {e}")
|
|
124
124
|
kz.storeKey("caseStatus", "exception")
|
|
125
|
-
kz.storeKey("
|
|
125
|
+
kz.storeKey("summaryDf", None)
|
|
126
126
|
return
|
|
127
127
|
|
|
128
128
|
kz.storeKey("caseStatus", plan.caseStatus)
|
|
129
129
|
if plan.caseStatus == "solved":
|
|
130
|
-
kz.storeKey("
|
|
130
|
+
kz.storeKey("summaryDf", plan.summaryDf())
|
|
131
131
|
kz.storeKey("casetoml", getCaseString().getvalue())
|
|
132
132
|
else:
|
|
133
|
-
kz.storeKey("
|
|
133
|
+
kz.storeKey("summaryDf", None)
|
|
134
134
|
kz.storeKey("casetoml", "")
|
|
135
135
|
|
|
136
136
|
|
|
@@ -381,7 +381,7 @@ def _setAllocationRatios(plan):
|
|
|
381
381
|
def plotSingleResults(plan):
|
|
382
382
|
c = 0
|
|
383
383
|
n = 3
|
|
384
|
-
cols = st.columns(n, gap="
|
|
384
|
+
cols = st.columns(n, gap="small")
|
|
385
385
|
fig = plan.showRates(figure=True)
|
|
386
386
|
if fig:
|
|
387
387
|
cols[c].write("##### Annual Rates")
|
|
@@ -400,7 +400,7 @@ def plotSingleResults(plan):
|
|
|
400
400
|
cols[c].pyplot(fig)
|
|
401
401
|
c = (c + 1) % n
|
|
402
402
|
|
|
403
|
-
cols = st.columns(n, gap="
|
|
403
|
+
cols = st.columns(n, gap="small")
|
|
404
404
|
fig = plan.showSources(figure=True)
|
|
405
405
|
if fig:
|
|
406
406
|
cols[c].write("##### Raw Income Sources")
|
|
@@ -544,17 +544,16 @@ def saveCaseFile(plan):
|
|
|
544
544
|
return BytesIO(encoded_data)
|
|
545
545
|
|
|
546
546
|
|
|
547
|
-
def createCaseFromFile(
|
|
548
|
-
|
|
547
|
+
def createCaseFromFile(strio):
|
|
548
|
+
logstrio = StringIO()
|
|
549
549
|
try:
|
|
550
|
-
|
|
551
|
-
plan = owl.readConfig(mystringio, logstreams=[strio], readContributions=False)
|
|
550
|
+
plan = owl.readConfig(strio, logstreams=[logstrio], readContributions=False)
|
|
552
551
|
except Exception as e:
|
|
553
552
|
st.error(f"Failed to parse case file: {e}")
|
|
554
553
|
return "", {}
|
|
555
554
|
|
|
556
555
|
name, mydic = genDic(plan)
|
|
557
|
-
mydic["logs"] =
|
|
556
|
+
mydic["logs"] = logstrio
|
|
558
557
|
|
|
559
558
|
return name, mydic
|
|
560
559
|
|
|
@@ -565,7 +564,7 @@ def genDic(plan):
|
|
|
565
564
|
dic["plan"] = plan
|
|
566
565
|
dic["name"] = plan._name
|
|
567
566
|
dic["description"] = plan._description
|
|
568
|
-
dic["
|
|
567
|
+
dic["summaryDf"] = None
|
|
569
568
|
dic["casetoml"] = ""
|
|
570
569
|
dic["caseStatus"] = "new"
|
|
571
570
|
dic["status"] = ["unknown", "single", "married"][plan.N_i]
|
|
@@ -3,6 +3,7 @@ Module for storing keys in Streamlit session state.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import streamlit as st
|
|
6
|
+
import pandas as pd
|
|
6
7
|
import copy
|
|
7
8
|
import re
|
|
8
9
|
|
|
@@ -22,8 +23,8 @@ def init():
|
|
|
22
23
|
ss = st.session_state
|
|
23
24
|
if "cases" not in ss:
|
|
24
25
|
ss.cases = {
|
|
25
|
-
newCase: {"iname0": "", "status": "unkown", "caseStatus": "new"
|
|
26
|
-
loadCaseFile: {"iname0": "", "status": "unkown", "caseStatus": "new"
|
|
26
|
+
newCase: {"iname0": "", "status": "unkown", "caseStatus": "new"},
|
|
27
|
+
loadCaseFile: {"iname0": "", "status": "unkown", "caseStatus": "new"},
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
# Variable for storing name of current case.
|
|
@@ -151,21 +152,6 @@ def caseIsNotMCReady():
|
|
|
151
152
|
return caseIsNotRunReady() or getKey("rateType") != "varying" or "tochastic" not in getKey("varyingType")
|
|
152
153
|
|
|
153
154
|
|
|
154
|
-
def titleBar(nkey, choices=None):
|
|
155
|
-
if choices is None:
|
|
156
|
-
choices = onlyCaseNames()
|
|
157
|
-
helpmsg = "Select an existing case, or create a new one from scratch or from a *case* parameter file."
|
|
158
|
-
return st.sidebar.selectbox(
|
|
159
|
-
"Select case",
|
|
160
|
-
choices,
|
|
161
|
-
help=helpmsg,
|
|
162
|
-
index=getIndex(currentCaseName(), choices),
|
|
163
|
-
key="_" + nkey,
|
|
164
|
-
on_change=switchToCase,
|
|
165
|
-
args=[nkey],
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
155
|
def currentCaseDic() -> dict:
|
|
170
156
|
return ss.cases[ss.currentCase]
|
|
171
157
|
|
|
@@ -192,16 +178,17 @@ def duplicateCase():
|
|
|
192
178
|
ss.cases[dupname] = copy.deepcopy(ss.cases[ss.currentCase])
|
|
193
179
|
ss.cases[ss.currentCase]["plan"] = currentPlan
|
|
194
180
|
ss.cases[dupname]["name"] = dupname
|
|
195
|
-
ss.cases[dupname]["
|
|
181
|
+
ss.cases[dupname]["summaryDf"] = None
|
|
196
182
|
ss.cases[dupname]["duplicate"] = True
|
|
197
183
|
refreshCase(ss.cases[dupname])
|
|
198
184
|
ss.currentCase = dupname
|
|
185
|
+
st.toast("Case duplicated except for Wages and Contributions tables.")
|
|
199
186
|
|
|
200
187
|
|
|
201
|
-
def createCaseFromFile(
|
|
188
|
+
def createCaseFromFile(strio):
|
|
202
189
|
import owlbridge as owb
|
|
203
190
|
|
|
204
|
-
name, dic = owb.createCaseFromFile(
|
|
191
|
+
name, dic = owb.createCaseFromFile(strio)
|
|
205
192
|
if name == "":
|
|
206
193
|
return False
|
|
207
194
|
elif name in ss.cases:
|
|
@@ -228,7 +215,7 @@ def createNewCase(case):
|
|
|
228
215
|
st.error(f"Case name '{casename}' already exists.")
|
|
229
216
|
return
|
|
230
217
|
|
|
231
|
-
ss.cases[casename] = {"name": casename, "caseStatus": "unknown", "
|
|
218
|
+
ss.cases[casename] = {"name": casename, "caseStatus": "unknown", "logs": None}
|
|
232
219
|
setCurrentCase(ss._newcase)
|
|
233
220
|
|
|
234
221
|
|
|
@@ -237,10 +224,11 @@ def renameCase(key):
|
|
|
237
224
|
return
|
|
238
225
|
newname = ss["_" + key]
|
|
239
226
|
plan = getKey("plan")
|
|
240
|
-
if plan:
|
|
241
|
-
plan.rename(newname)
|
|
242
227
|
ss.cases[newname] = ss.cases.pop(ss.currentCase)
|
|
243
228
|
ss.cases[newname]["name"] = newname
|
|
229
|
+
if plan:
|
|
230
|
+
plan.rename(newname)
|
|
231
|
+
ss.cases[newname]["caseStatus"] = "modified"
|
|
244
232
|
setCurrentCase(newname)
|
|
245
233
|
|
|
246
234
|
|
|
@@ -325,6 +313,34 @@ def getAccountBalances(ni):
|
|
|
325
313
|
return bal
|
|
326
314
|
|
|
327
315
|
|
|
316
|
+
def compareSummaries():
|
|
317
|
+
df = getKey("summaryDf")
|
|
318
|
+
if df is None:
|
|
319
|
+
return None
|
|
320
|
+
for case in onlyCaseNames():
|
|
321
|
+
if case == currentCaseName():
|
|
322
|
+
continue
|
|
323
|
+
odf = ss.cases[case]["summaryDf"]
|
|
324
|
+
if odf is None or set(odf.columns) != set(df.columns):
|
|
325
|
+
continue
|
|
326
|
+
df = pd.concat([df, odf])
|
|
327
|
+
|
|
328
|
+
if df.shape[0] > 1:
|
|
329
|
+
# Unroll to subtract strings representations of numbers.
|
|
330
|
+
for col in range(1, df.shape[1] - 5):
|
|
331
|
+
strval = df.iloc[0, col]
|
|
332
|
+
if isinstance(strval, str) and strval[0] == "$":
|
|
333
|
+
f0val = float(strval[1:].replace(",", ""))
|
|
334
|
+
for row in range(1, df.shape[0]):
|
|
335
|
+
fnval = float(df.iloc[row, col][1:].replace(",", ""))
|
|
336
|
+
diff = fnval - f0val
|
|
337
|
+
sign = "+" if diff >= 0 else "-"
|
|
338
|
+
sign = "" if diff == 0 else sign
|
|
339
|
+
df.iloc[row, col] = f"{sign}${abs(diff):,.0f}"
|
|
340
|
+
|
|
341
|
+
return df.transpose()
|
|
342
|
+
|
|
343
|
+
|
|
328
344
|
def getSolveParameters():
|
|
329
345
|
maximize = getKey("objective")
|
|
330
346
|
if maximize is None:
|
|
@@ -488,8 +504,28 @@ def orangeDivider():
|
|
|
488
504
|
st.html("<style> hr {border-color: orange;}</style><hr>")
|
|
489
505
|
|
|
490
506
|
|
|
491
|
-
def
|
|
492
|
-
# st.html("<div style=
|
|
493
|
-
|
|
494
|
-
|
|
507
|
+
def titleBar(txt, choices=None):
|
|
508
|
+
# st.html(f"<div style='text-align: left;color: orange;font-style: italic;'>{currentCaseName()}</div>")
|
|
509
|
+
if choices is None:
|
|
510
|
+
choices = onlyCaseNames()
|
|
511
|
+
helpmsg = "Select an existing case."
|
|
512
|
+
else:
|
|
513
|
+
helpmsg = "Select an existing case, or create a new one from scratch or from a *case* parameter file."
|
|
514
|
+
|
|
515
|
+
col1, col2 = st.columns(2, gap="large")
|
|
516
|
+
with col1:
|
|
517
|
+
st.write("## " + txt)
|
|
518
|
+
with col2:
|
|
519
|
+
nkey = txt
|
|
520
|
+
ret = st.selectbox(
|
|
521
|
+
"Case selector",
|
|
522
|
+
choices,
|
|
523
|
+
help=helpmsg,
|
|
524
|
+
index=getIndex(currentCaseName(), choices),
|
|
525
|
+
key="_" + nkey,
|
|
526
|
+
on_change=switchToCase,
|
|
527
|
+
args=[nkey],
|
|
528
|
+
)
|
|
529
|
+
|
|
495
530
|
orangeDivider()
|
|
531
|
+
return ret
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import streamlit as st
|
|
3
|
+
from io import StringIO
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
cases = ["jack+jill", "joe", "john+sally", "jon+jane", "kim+sam-bequest", "kim+sam-spending"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def loadExample(case):
|
|
10
|
+
url = f"https://raw.github.com/mdlacasse/owl/main/examples/case_{case}.toml"
|
|
11
|
+
response = requests.get(url)
|
|
12
|
+
if response.status_code == 200:
|
|
13
|
+
return StringIO(response.text)
|
|
14
|
+
else:
|
|
15
|
+
st.error(f"Failed to load case parameter file from GitHub: {response.status_code}.")
|
|
16
|
+
return None
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2025.02.23"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|