owlplanner 2025.2.25__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.25 → owlplanner-2025.2.27}/PKG-INFO +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/case_jack+jill.toml +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/case_john+sally.toml +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/case_jon+jane.toml +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/pyproject.toml +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/plan.py +40 -24
- owlplanner-2025.2.27/src/owlplanner/version.py +1 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Asset_Allocation.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Assets.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Create_Case.py +18 -7
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Documentation.py +9 -7
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Fixed_Income.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Graphs.py +3 -3
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Historical_Range.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Logs.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Monte_Carlo.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Optimization_Parameters.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Output_Files.py +10 -7
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Quick_Start.py +4 -3
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Rates_Selection.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Wages_And_Contributions.py +9 -7
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Worksheets.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/owlbridge.py +10 -11
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/plots.py +2 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/requirements.txt +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/sskeys.py +63 -27
- owlplanner-2025.2.27/ui/tomlexamples.py +16 -0
- owlplanner-2025.2.25/src/owlplanner/version.py +0 -1
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/.flake8 +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/.gitattributes +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/.gitignore +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/INSTALL.md +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/LICENSE +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/README.md +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/USER_GUIDE.md +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docker/Dockerfile +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docker/README.md +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docker/docker-compose.yml +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docker/fastentrypoint.sh +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/allocations.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/owl.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/profile.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/owl.pdf +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/docs/owl.tex +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/case_joe.toml +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/joe.xlsx +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/jon+jane.xlsx +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/examples/template.xlsx +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/owlplanner.cmd +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/owlplanner.sh +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/requirements.txt +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/config.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/logging.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/rates.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/tax2025.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/timelists.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/tests/test_logger.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/tests/test_regressions.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/tests/test_repro.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/tests/test_units.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ttt.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/About_Owl.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/README.md +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/Settings.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/main+fonts.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/main.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.2.27}/ui/progress.py +0 -0
- {owlplanner-2025.2.25 → 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" = "john+sally"
|
|
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
|
|
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 = "This case
|
|
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"
|
|
@@ -2062,6 +2062,12 @@ class Plan(object):
|
|
|
2062
2062
|
|
|
2063
2063
|
return mylist
|
|
2064
2064
|
|
|
2065
|
+
def summaryDf(self):
|
|
2066
|
+
"""
|
|
2067
|
+
Return summary as a dataframe.
|
|
2068
|
+
"""
|
|
2069
|
+
return pd.DataFrame(self.summaryDic(), index=[self._name])
|
|
2070
|
+
|
|
2065
2071
|
def summaryString(self):
|
|
2066
2072
|
"""
|
|
2067
2073
|
Return summary as a string.
|
|
@@ -2081,29 +2087,34 @@ class Plan(object):
|
|
|
2081
2087
|
dic = {}
|
|
2082
2088
|
# Results
|
|
2083
2089
|
dic["Plan name"] = self._name
|
|
2084
|
-
dic[
|
|
2085
|
-
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)
|
|
2086
2092
|
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
|
|
2087
2093
|
|
|
2088
2094
|
totIncome = np.sum(self.g_n, axis=0)
|
|
2089
2095
|
totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
|
|
2090
|
-
dic[
|
|
2096
|
+
dic["Total net spending"] = f"{u.d(totIncomeNow)}"
|
|
2097
|
+
dic["- Total net spending (nominal)"] = f"{u.d(totIncome)}"
|
|
2091
2098
|
|
|
2092
2099
|
totRoth = np.sum(self.x_in, axis=(0, 1))
|
|
2093
2100
|
totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
|
|
2094
|
-
dic[
|
|
2101
|
+
dic["Total Roth conversions"] = f"{u.d(totRothNow)}"
|
|
2102
|
+
dic["- Total Roth conversions (nominal)"] = f"{u.d(totRoth)}"
|
|
2095
2103
|
|
|
2096
2104
|
taxPaid = np.sum(self.T_n, axis=0)
|
|
2097
2105
|
taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
|
|
2098
|
-
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)}"
|
|
2099
2108
|
|
|
2100
2109
|
taxPaid = np.sum(self.U_n, axis=0)
|
|
2101
2110
|
taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
|
|
2102
|
-
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)}"
|
|
2103
2113
|
|
|
2104
2114
|
taxPaid = np.sum(self.M_n, axis=0)
|
|
2105
2115
|
taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
|
|
2106
|
-
dic[
|
|
2116
|
+
dic["Total Medicare premiums paid"] = f"{u.d(taxPaidNow)}"
|
|
2117
|
+
dic["- Total Medicare premiums paid (nominal)"] = f"{u.d(taxPaid)}"
|
|
2107
2118
|
|
|
2108
2119
|
if self.N_i == 2 and self.n_d < self.N_n:
|
|
2109
2120
|
p_j = self.partialEstate_j * (1 - self.phi_j)
|
|
@@ -2117,30 +2128,35 @@ class Plan(object):
|
|
|
2117
2128
|
totSpousalNow = totSpousal / self.gamma_n[nx + 1]
|
|
2118
2129
|
iname_s = self.inames[self.i_s]
|
|
2119
2130
|
iname_d = self.inames[self.i_d]
|
|
2120
|
-
dic[f"
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
dic[f"
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
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])}")
|
|
2131
2146
|
|
|
2132
2147
|
estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
|
|
2133
2148
|
estate[1] *= 1 - self.nu
|
|
2134
2149
|
lastyear = self.year_n[-1]
|
|
2135
|
-
dic[f"Post-tax account values at the end of final plan year {lastyear} (nominal)"] = (
|
|
2136
|
-
f"taxable: {u.d(estate[0])} tax-def: {u.d(estate[1])} tax-free: {u.d(estate[2])}")
|
|
2137
|
-
|
|
2138
2150
|
totEstate = np.sum(estate)
|
|
2139
2151
|
totEstateNow = totEstate / self.gamma_n[-1]
|
|
2140
|
-
dic[f"Total estate value at the end of
|
|
2141
|
-
|
|
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
|
+
|
|
2142
2158
|
dic["Plan starting date"] = str(self.startDate)
|
|
2143
|
-
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}"
|
|
2144
2160
|
for i in range(self.N_i):
|
|
2145
2161
|
dic[f"{self.inames[i]:>12}'s {self.horizons[i]:02}-year life horizon"] = (
|
|
2146
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),
|
|
@@ -307,13 +307,15 @@ The first line of the *Sources* worksheets are the most important
|
|
|
307
307
|
as these lines are the only ones that are actionable.
|
|
308
308
|
|
|
309
309
|
#### Output Files
|
|
310
|
-
This page allow to save
|
|
310
|
+
This page allow to compare cases and save files for future use.
|
|
311
311
|
First it shows a synopsis of the computed scenario by
|
|
312
312
|
displaying sums of income, bequest, and spending values over the duration of the plan.
|
|
313
|
-
|
|
314
|
-
|
|
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.
|
|
315
317
|
|
|
316
|
-
|
|
318
|
+
Similarly, parameters used to generate the case are collected in *toml* format and displayed.
|
|
317
319
|
The `Download case file...` button allows to save the parameters used to generate the
|
|
318
320
|
outcome of this case to a *case* file.
|
|
319
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.25"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|