Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .streamlit/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[server]
runOnSave = true
8 changes: 7 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
"PYTHONPATH": "${workspaceFolder}"
}
},

{
"name": "Streamlit: Current File",
"type": "debugpy",
"request": "launch",
"module": "streamlit",
"args": ["run", "${file}", "--server.port", "2000"]
},
{
"name": "Python:Streamlit",
"type": "debugpy",
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ COPY . .
RUN uv pip install -e .

# Expose port
EXPOSE 8501
EXPOSE 80

# Start the server
CMD ["uv", "run", "streamlit", "run", "hydroshift/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.headless=true"]
CMD ["uv", "run", "streamlit", "run", "hydroshift/streamlit_app.py", "--server.port=80", "--server.address=0.0.0.0", "--server.headless=true"]
14 changes: 14 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
client:
container_name: client
init: true
image: ${ECR_AWS_ACCOUNT_ID}.dkr.ecr.${ECR_AWS_REGION}.amazonaws.com/${ECR_CLIENT_IMAGE_TAG}
networks:
- hydroshift
restart: unless-stopped
mem_limit: 4GB
mem_reservation: 2GB

networks:
hydroshift:
driver: bridge
2 changes: 1 addition & 1 deletion hydroshift/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.1"
__version__ = "0.1.2"
2 changes: 1 addition & 1 deletion hydroshift/_pages/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from hydroshift._pages.changepoint import changepoint
from hydroshift._pages.homepage import homepage
from hydroshift._pages.homepage import homepage, reset_homepage
from hydroshift._pages.summary import summary
64 changes: 31 additions & 33 deletions hydroshift/_pages/changepoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
CP_F2_CAPTION,
CP_T1_CAPTION,
CP_T2_CAPTION,
MAX_CACHE_ENTRIES,
METRICS,
VALID_ARL0S,
)
from hydroshift.errors import GageNotFoundException
from hydroshift.utils.changepoint import cp_pvalue_batch, cpm_process_stream
from hydroshift.utils.common import num_2_word
from hydroshift.utils.data_retrieval import Gage
Expand Down Expand Up @@ -170,7 +172,7 @@ def results_text(self) -> str:
evidence = "moderate"
elif self.pval_df.isna().all().all():
evidence = "no"
elif min_p < 1:
elif min_p <= 1:
evidence = "minor"
groups, _ = self.get_change_windows()

Expand Down Expand Up @@ -267,33 +269,27 @@ def validate_data(self):

# Validate
if data is None:
st.session_state.valid_data = False
st.session_state.data_comment = "Unable to retrieve data."
return False, "Unable to retrieve data."
elif len(data) < st.session_state.burn_in:
st.session_state.valid_data = False
st.session_state.data_comment = "Not enough peaks available for analysis. {} peaks found, but burn-in length was {}".format(
len(data["peaks"]), st.session_state.burn_in
)
return False, "Not enough peaks available for analysis. {} peaks found, but burn-in length was {}".format(len(data["peak_va"]), st.session_state.burn_in)
else:
st.session_state.valid_data = True
return True, None


def define_variables():
"""Set up page state and get default variables."""
# Ensure gage valid
if st.session_state.gage_id is None:
st.session_state.valid_data = False
st.session_state.data_comment = "Invalid USGS gage provided."
return
if not st.session_state.gage_id.isnumeric():
st.session_state.valid_data = False
st.session_state.data_comment = "Invalid USGS gage provided."
return

if st.session_state["gage"] is None:
return False, "USGS gage not found"
if not Gage(st.session_state["gage_id"]).ams_valid:
return False, "USGS gage has invalid AMS data."
# Instantiate analysis class and get data
if "changepoint" not in st.session_state:
st.session_state.changepoint = ChangePointAnalysis(st.session_state.gage)
st.session_state.changepoint.validate_data()
elif st.session_state.changepoint.gage.gage_id != st.session_state.gage_id:
st.session_state.changepoint = ChangePointAnalysis(st.session_state.gage)
return st.session_state.changepoint.validate_data()


def refresh_data_editor():
Expand All @@ -305,12 +301,16 @@ def make_sidebar():
"""User control for analysis."""
with st.sidebar:
st.title("Settings")
st.session_state["gage_id"] = st.text_input(
"Enter USGS Gage Number:",
st.session_state["gage_id"],
on_change=refresh_data_editor,
)
st.session_state.gage = Gage(st.session_state["gage_id"])
try:
st.session_state["gage_id"] = st.text_input(
"Enter USGS Gage Number:",
st.session_state["gage_id"],
on_change=refresh_data_editor,
)
st.session_state.gage = Gage(st.session_state["gage_id"])
except GageNotFoundException:
st.session_state.gage = None
return
with st.form("changepoint_params"):
st.select_slider(
"False Positive Rate (1 in #)",
Expand Down Expand Up @@ -351,7 +351,6 @@ def make_sidebar():
st.divider()
write_template("data_sources_side_bar.html")


def run_analysis():
"""Run the change point model analysis."""
cpa = st.session_state.changepoint
Expand All @@ -361,7 +360,7 @@ def run_analysis():
)


@st.cache_data
@st.cache_data(max_entries=MAX_CACHE_ENTRIES)
def get_pvalues(data: pd.DataFrame) -> pd.DataFrame:
"""Get pvalue df associated with changepoint analysis."""
ts = data["peak_va"].values
Expand All @@ -371,7 +370,7 @@ def get_pvalues(data: pd.DataFrame) -> pd.DataFrame:
return pval_df


@st.cache_data
@st.cache_data(max_entries=MAX_CACHE_ENTRIES)
def get_changepoints(data: pd.DataFrame, arl0: int, burn_in: int) -> dict:
"""Run the process stream analysis and return changepoints identified."""
ts = data["peak_va"].values
Expand All @@ -385,7 +384,7 @@ def get_changepoints(data: pd.DataFrame, arl0: int, burn_in: int) -> dict:
return cp_dict


@st.cache_data
@st.cache_data(max_entries=MAX_CACHE_ENTRIES)
def ffa_analysis(data: pd.DataFrame, regimes: list):
"""Run multiple flood frequency analyses for different regimes."""
ffas = []
Expand Down Expand Up @@ -415,8 +414,7 @@ def ffa_analysis(data: pd.DataFrame, regimes: list):

def make_body():
"""Assemble main app body."""
left_col, right_col = st.columns([2, 1]) # Formatting
with left_col:
with st.container(width=850):
cpa: ChangePointAnalysis = st.session_state.changepoint
st.title(cpa.title)
warnings()
Expand All @@ -432,8 +430,8 @@ def make_body():
st.header("Changepoint detection results")
st.markdown(cpa.results_text)
if len(cpa.cp_dict) > 1:
st.table(cpa.cp_df)
st.markdown(CP_T1_CAPTION)
st.table(cpa.cp_df)

st.header("Modified flood frequency analysis")
if len(st.session_state[st.session_state.data_editor_key]["added_rows"]) > 0:
Expand Down Expand Up @@ -484,8 +482,8 @@ def changepoint():
"""Outline the page."""
st.set_page_config(page_title="USGS Gage Changepoint Analysis", layout="wide")
make_sidebar()
define_variables()
if st.session_state.valid_data:
valid, msg = define_variables()
if valid:
try:
run_analysis()
except Exception as e:
Expand All @@ -494,4 +492,4 @@ def changepoint():
else:
make_body()
else:
st.error(st.session_state.data_comment)
st.error(msg)
79 changes: 57 additions & 22 deletions hydroshift/_pages/homepage.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,75 @@
import streamlit as st

from hydroshift._pages import summary
from hydroshift.consts import DEFAULT_GAGE
from hydroshift.utils.jinja import write_template


def homepage():
"""Landing page for app."""
st.set_page_config(page_title="HydroShift", layout="wide")
# st.session_state["gage_id"] = None
st.set_page_config(layout="centered", initial_sidebar_state ="collapsed")

left_col, right_col, _ = st.columns([2, 1.5, 0.2], gap="large")
st.markdown(
"""
<style>
.stApp {
background: linear-gradient(
#f5f5f5 0%,
#f5f5f5 45%,
#b3c7e8 100%
);
}
.stAppDeployButton {display:none;}
.stAppHeader {display:none;}
.block-container {
text-align: center;
}
div.stButton > button {
border-radius: 12px;
font-weight: 600;
background: linear-gradient(135deg, #00c6ff, #4287f5);
border: none;
color: white;
transition: 0.2s ease-in-out;
}
div.stButton > button:hover {
background: linear-gradient(135deg, #ff7e33, #f5a742);
transform: translateY(-2px);
box-shadow: 0px 4px 12px rgba(0,0,0,0.25);
color: black;
}
</style>
""",
unsafe_allow_html=True,
)

with left_col:
# --- Centered content ---
with st.container(horizontal_alignment ="center"):
st.title("HydroShift")

with st.container(horizontal=True, horizontal_alignment ="center"):
st.image("hydroshift/images/logo_base.png", width=400)

st.subheader("USGS Streamflow Change Detection Tool")
write_template("app_description.md")

st.write("") # blank line for more space
gage_input = st.text_input("Enter a USGS Gage Number to begin:")
col1, col2 = st.columns([1, 8])
with col1:
gage_input = st.text_input("Enter a USGS Gage Number:", placeholder="e.g., 01646500")

with st.container(horizontal=True, horizontal_alignment ="center"):
submit = st.button("Submit")
with col2:
demo = st.button("Use Demo Data")

if submit and gage_input:
st.session_state["gage_id"] = gage_input
if demo:
st.session_state["gage_id"] = DEFAULT_GAGE
if st.session_state["gage_id"] is not None:
# try:
# st.session_state["site_data"] = load_site_data(st.session_state["gage_id"])
# except ValueError:
# st.error(f"Data not found for gage: {st.session_state['gage_id']}")
st.rerun()
with right_col:
st.title("")
st.image("hydroshift/images/logo_base.png")
st.container(border=False, height=50)

if submit and gage_input:
st.session_state["gage_id"] = gage_input
if demo:
st.session_state["gage_id"] = DEFAULT_GAGE
if st.session_state.get("gage_id") is not None:
st.switch_page(st.Page(summary, title="Gage Summary"))

write_template("footer.html")

def reset_homepage():
st.session_state["gage_id"] = None
st.rerun()
Loading