From c29310a97490b5ff379e42901a9882d84c17b6e5 Mon Sep 17 00:00:00 2001 From: joshdpaul Date: Tue, 26 May 2026 08:49:47 -0800 Subject: [PATCH 01/10] update arctic rivers stats coverage ID --- routes/arctic_hydrology.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/routes/arctic_hydrology.py b/routes/arctic_hydrology.py index b58f09ce..22db978d 100644 --- a/routes/arctic_hydrology.py +++ b/routes/arctic_hydrology.py @@ -26,7 +26,7 @@ from . import routes coverages = { - "stats": ["ak_hydro_segments_stats_combined"], + "stats": ["ak_hydro_segments_mhit_stats_combined"], "doy_climatology": ["ak_hydro_segments_doy_climatology"], } @@ -672,7 +672,9 @@ def run_get_arctic_hydrology_hydroviz(stream_id): )[0] pgw_ds = asyncio.run( fetch_hydro_data( - coverages["stats"], stream_id, source=stat_source_encodings["original_gcm"] + coverages["stats"], + stream_id, + source=stat_source_encodings["original_gcm"], ) )[0] for dim, mapping in stats_decode_dict.items(): From e2806003af102a81ed1b1ac0dde4672e1de470ad Mon Sep 17 00:00:00 2001 From: joshdpaul Date: Fri, 29 May 2026 07:54:35 -0800 Subject: [PATCH 02/10] update arctic segments url --- generate_urls.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/generate_urls.py b/generate_urls.py index b2810824..52164dc8 100644 --- a/generate_urls.py +++ b/generate_urls.py @@ -133,17 +133,18 @@ def generate_wfs_conus_hydrology_url(stream_id): def generate_wfs_arctic_hydrology_url(stream_id): """ Generate a WFS URL for fetching arctic hydrology data for a given stream ID. Returns both attributes and geometry for a single stream ID. - If the stream ID is an empty string, returns only attributes for all streams.""" + If the stream ID is an empty string, returns only COMID attribute for all streams. + """ if stream_id == "": wfs_url = ( GS_BASE_URL - + "wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=hydrology:arctic_segments&propertyName=(COMID)&outputFormat=application/json" + + "wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=hydrology:arctic_rivers_segments_joined_3338&propertyName=(COMID)&outputFormat=application/json" ) return wfs_url else: wfs_url = ( GS_BASE_URL - + f"wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=hydrology:arctic_segments&propertyName=(COMID,the_geom)&outputFormat=application/json&cql_filter=(COMID={stream_id})" + + f"wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=hydrology:arctic_rivers_segments_joined_3338&propertyName=(COMID,the_geom,Gauge_ID,ID_1,ID_2,Name,outlet)&outputFormat=application/json&cql_filter=(COMID={stream_id})" ) return wfs_url From e439dfc722da0a5163fa08404122caf84e8aacfe Mon Sep 17 00:00:00 2001 From: joshdpaul Date: Fri, 29 May 2026 08:04:21 -0800 Subject: [PATCH 03/10] pull new attributes from GS layer --- routes/arctic_hydrology.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/routes/arctic_hydrology.py b/routes/arctic_hydrology.py index 22db978d..195701fd 100644 --- a/routes/arctic_hydrology.py +++ b/routes/arctic_hydrology.py @@ -36,7 +36,7 @@ "gcm_diff_applied_to_cheng": 2, } -# TODO: populate this with actual values from the GS layer after computing via MHIT +# TODO: populate this with actual values from the GS layer after computing via MHIT. _SUMMARY_STUB = { "ma99_hist": { "value": None, @@ -185,7 +185,7 @@ async def get_features(stream_id): async with ClientSession() as session: layer_data = await fetch_layer_data(url, session) - gdf = gpd.GeoDataFrame.from_features(layer_data["features"], crs="EPSG:4326") + gdf = gpd.GeoDataFrame.from_features(layer_data["features"], crs="EPSG:3338") gdf["geometry"] = gdf["geometry"].make_valid() return gdf @@ -462,11 +462,17 @@ def populate_feature_attributes(data_dict, gdf): Returns: Data dictionary with the vector attributes populated.""" - data_dict["name"] = "" - data_dict["huc8"] = None - data_dict["huc8_outlet"] = None - data_dict["latitude"] = round(gdf.loc[0].geometry.representative_point().y, 4) - data_dict["longitude"] = round(gdf.loc[0].geometry.representative_point().x, 4) + # data_dict["name"] = "" # arctic rivers segments do not have stream names associated + + # the watershed ID matches the GVV code for HUC8 in Alaska or Yukon watershed in Canada + # all Yukon watersheds begin with "YTHYDRO" while HUC8s are just numeric + data_dict["watershed"] = gdf.loc[0].get("ID_1", None) + data_dict["watershed_outlet"] = gdf.loc[0].get("outlet", None) + + # copy and convert gdf to WGS84 for lat/lon extraction + gdf_4326 = gdf.to_crs("EPSG:4326") + data_dict["latitude"] = round(gdf_4326.loc[0].geometry.representative_point().y, 4) + data_dict["longitude"] = round(gdf_4326.loc[0].geometry.representative_point().x, 4) return data_dict From f68a13500ac73e66aebb94a6de4d8dba08ba2ead Mon Sep 17 00:00:00 2001 From: joshdpaul Date: Fri, 29 May 2026 08:32:30 -0800 Subject: [PATCH 04/10] pull summary stats from GS layer --- generate_urls.py | 11 ++ routes/arctic_hydrology.py | 336 +++++++++++++++++++++++++++---------- 2 files changed, 255 insertions(+), 92 deletions(-) diff --git a/generate_urls.py b/generate_urls.py index 52164dc8..bc56740b 100644 --- a/generate_urls.py +++ b/generate_urls.py @@ -149,6 +149,17 @@ def generate_wfs_arctic_hydrology_url(stream_id): return wfs_url +def generate_wfs_arctic_hydrology_stats_url(stream_id): + """ + Generate a WFS URL for fetching arctic hydrology summary stats for a given stream ID + from the arctic_rivers_segments_stats_simplified layer. + """ + return ( + GS_BASE_URL + + f"wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=hydrology:arctic_rivers_segments_stats_simplified&outputFormat=application/json&cql_filter=(COMID={stream_id})" + ) + + def generate_usgs_gauge_daily_streamflow_data_url(gauge_id, start_date, end_date): """ Generate a USGS OGC API URL for fetching daily streamflow data for a given gauge ID and date range. diff --git a/routes/arctic_hydrology.py b/routes/arctic_hydrology.py index 195701fd..4f012e62 100644 --- a/routes/arctic_hydrology.py +++ b/routes/arctic_hydrology.py @@ -17,7 +17,7 @@ ) from generate_requests import generate_conus_hydrology_wcs_str -from generate_urls import generate_wfs_arctic_hydrology_url +from generate_urls import generate_wfs_arctic_hydrology_url, generate_wfs_arctic_hydrology_stats_url from fetch_data import fetch_data, fetch_layer_data, describe_via_wcps from validate_request import get_axis_encodings from postprocessing import prune_nulls_with_max_intensity @@ -36,94 +36,6 @@ "gcm_diff_applied_to_cheng": 2, } -# TODO: populate this with actual values from the GS layer after computing via MHIT. -_SUMMARY_STUB = { - "ma99_hist": { - "value": None, - "range_low": None, - "range_high": None, - "units": "cfs", - "description": "historical mean annual flow", - }, - "ma99_delta": { - "value": None, - "range_low": None, - "range_high": None, - "units": "percent", - "description": "projected change in mean annual flow", - }, - "dh1_delta": { - "value": None, - "range_low": None, - "range_high": None, - "units": "percent", - "description": "projected change in maximum 1-day flow", - }, - "dl1_delta": { - "value": None, - "range_low": None, - "range_high": None, - "units": "percent", - "description": "projected change in minimum 1-day flow", - }, - "dh15_hist": { - "value": None, - "range_low": None, - "range_high": None, - "units": "days", - "description": "historical high flow pulse duration", - }, - "dh15_delta": { - "value": None, - "range_low": None, - "range_high": None, - "units": "days", - "description": "projected change in high flow pulse duration", - }, - "dl16_hist": { - "value": None, - "range_low": None, - "range_high": None, - "units": "days", - "description": "historical low flow pulse duration", - }, - "dl16_delta": { - "value": None, - "range_low": None, - "range_high": None, - "units": "days", - "description": "projected change in low flow pulse duration", - }, - "fh1_hist": { - "value": None, - "range_low": None, - "range_high": None, - "units": "events", - "description": "historical high flood pulse count", - }, - "fh1_delta": { - "value": None, - "range_low": None, - "range_high": None, - "units": "events", - "description": "projected change in high flood pulse count", - }, - "fl1_hist": { - "value": None, - "range_low": None, - "range_high": None, - "units": "events", - "description": "historical low flood pulse count", - }, - "fl1_delta": { - "value": None, - "range_low": None, - "range_high": None, - "units": "events", - "description": "projected change in low flood pulse count", - }, -} - async def get_decode_dicts_from_axis_attributes(cov_ids): """ @@ -193,6 +105,245 @@ async def get_features(stream_id): return render_template("400/bad_request.html"), 400 +async def get_stats_features(stream_id): + """Function to fetch summary stat attributes from the WFS stats layer for a given stream ID. + Args: + stream_id (str): Stream ID for the hydrology data + Returns: + GeoDataFrame with stat attributes, or None if unavailable.""" + try: + url = generate_wfs_arctic_hydrology_stats_url(stream_id) + async with ClientSession() as session: + layer_data = await fetch_layer_data(url, session) + features = layer_data.get("features", []) + if not features: + return None + gdf = gpd.GeoDataFrame([f["properties"] for f in features]) + return gdf.replace({None: np.nan}) + except Exception: + return None + + +def populate_feature_stat_attributes_summary(data_dict, gdf): + """Function to populate the summary stats from the WFS stats layer into the data dictionary. + Args: + data_dict (dict): Data dictionary to populate with summary stats + gdf (GeoDataFrame or None): GeoDataFrame with stat attributes from the stats layer + Returns: + Data dictionary with summary stats populated, or null-valued summary if gdf is unavailable.""" + summary_values = {} + + ### MEAN FLOWS: + ma99_hist_value = ( + round(gdf.loc[0].ma99_hist, 0) if not np.isnan(gdf.loc[0].ma99_hist) else None + ) + if ma99_hist_value is not None and ma99_hist_value > 5: + ma99_hist_value = round(gdf.loc[0].ma99_hist / 5) * 5 + summary_values["ma99_hist"] = { + "value": ma99_hist_value, + "range_low": None, + "range_high": None, + "units": "cfs", + "description": "historical mean annual flow", + } + + summary_values["ma99_delta"] = { + "value": ( + int(round(gdf.loc[0].ma99_avg_d, 0)) + if not np.isnan(gdf.loc[0].ma99_avg_d) + else None + ), + "range_low": ( + int(round(gdf.loc[0].ma99_min_d, 0)) + if not np.isnan(gdf.loc[0].ma99_min_d) + else None + ), + "range_high": ( + int(round(gdf.loc[0].ma99_max_d, 0)) + if not np.isnan(gdf.loc[0].ma99_max_d) + else None + ), + "units": "percent", + "description": "projected change in mean annual flow", + } + + ### MIN AND MAX FLOWS: + # value is max of model maximums (range_high); range_low is minimum of model maximums + summary_values["dh1_delta"] = { + "value": ( + int(round(gdf.loc[0].dh1_max_d, 0)) + if not np.isnan(gdf.loc[0].dh1_max_d) + else None + ), + "range_low": ( + int(round(gdf.loc[0].dh1_min_d, 0)) + if not np.isnan(gdf.loc[0].dh1_min_d) + else None + ), + "range_high": ( + int(round(gdf.loc[0].dh1_max_d, 0)) + if not np.isnan(gdf.loc[0].dh1_max_d) + else None + ), + "units": "percent", + "description": "projected change in maximum 1-day flow", + } + + # value is min of model minimums (range_low); range_high is maximum of model minimums + summary_values["dl1_delta"] = { + "value": ( + int(round(gdf.loc[0].dl1_min_d, 0)) + if not np.isnan(gdf.loc[0].dl1_min_d) + else None + ), + "range_low": ( + int(round(gdf.loc[0].dl1_min_d, 0)) + if not np.isnan(gdf.loc[0].dl1_min_d) + else None + ), + "range_high": ( + int(round(gdf.loc[0].dl1_max_d, 0)) + if not np.isnan(gdf.loc[0].dl1_max_d) + else None + ), + "units": "percent", + "description": "projected change in minimum 1-day flow", + } + + ### FLOOD DURATION: + summary_values["dh15_hist"] = { + "value": ( + int(round(gdf.loc[0].dh15_hist, 0)) + if not np.isnan(gdf.loc[0].dh15_hist) + else None + ), + "range_low": None, + "range_high": None, + "units": "days", + "description": "historical high flow pulse duration", + } + + summary_values["dh15_delta"] = { + "value": ( + int(round(gdf.loc[0].dh15_avg_d, 0)) + if not np.isnan(gdf.loc[0].dh15_avg_d) + else None + ), + "range_low": ( + int(round(gdf.loc[0].dh15_min_d, 0)) + if not np.isnan(gdf.loc[0].dh15_min_d) + else None + ), + "range_high": ( + int(round(gdf.loc[0].dh15_max_d, 0)) + if not np.isnan(gdf.loc[0].dh15_max_d) + else None + ), + "units": "days", + "description": "projected change in high flow pulse duration", + } + + summary_values["dl16_hist"] = { + "value": ( + int(round(gdf.loc[0].dl16_hist, 0)) + if not np.isnan(gdf.loc[0].dl16_hist) + else None + ), + "range_low": None, + "range_high": None, + "units": "days", + "description": "historical low flow pulse duration", + } + + summary_values["dl16_delta"] = { + "value": ( + int(round(gdf.loc[0].dl16_avg_d, 0)) + if not np.isnan(gdf.loc[0].dl16_avg_d) + else None + ), + "range_low": ( + int(round(gdf.loc[0].dl16_min_d, 0)) + if not np.isnan(gdf.loc[0].dl16_min_d) + else None + ), + "range_high": ( + int(round(gdf.loc[0].dl16_max_d, 0)) + if not np.isnan(gdf.loc[0].dl16_max_d) + else None + ), + "units": "days", + "description": "projected change in low flow pulse duration", + } + + ### FLOOD PULSE COUNT: + summary_values["fh1_hist"] = { + "value": ( + int(round(gdf.loc[0].fh1_hist, 0)) + if not np.isnan(gdf.loc[0].fh1_hist) + else None + ), + "range_low": None, + "range_high": None, + "units": "events", + "description": "historical high flood pulse count", + } + + summary_values["fh1_delta"] = { + "value": ( + int(round(gdf.loc[0].fh1_avg_d, 0)) + if not np.isnan(gdf.loc[0].fh1_avg_d) + else None + ), + "range_low": ( + int(round(gdf.loc[0].fh1_min_d, 0)) + if not np.isnan(gdf.loc[0].fh1_min_d) + else None + ), + "range_high": ( + int(round(gdf.loc[0].fh1_max_d, 0)) + if not np.isnan(gdf.loc[0].fh1_max_d) + else None + ), + "units": "events", + "description": "projected change in high flood pulse count", + } + + summary_values["fl1_hist"] = { + "value": ( + int(round(gdf.loc[0].fl1_hist, 0)) + if not np.isnan(gdf.loc[0].fl1_hist) + else None + ), + "range_low": None, + "range_high": None, + "units": "events", + "description": "historical low flood pulse count", + } + + summary_values["fl1_delta"] = { + "value": ( + int(round(gdf.loc[0].fl1_avg_d, 0)) + if not np.isnan(gdf.loc[0].fl1_avg_d) + else None + ), + "range_low": ( + int(round(gdf.loc[0].fl1_min_d, 0)) + if not np.isnan(gdf.loc[0].fl1_min_d) + else None + ), + "range_high": ( + int(round(gdf.loc[0].fl1_max_d, 0)) + if not np.isnan(gdf.loc[0].fl1_max_d) + else None + ), + "units": "events", + "description": "projected change in low flood pulse count", + } + + data_dict["summary"] = summary_values + return data_dict + + def package_stats_data(stream_id, ds): """ Function to package the stats data into a dictionary for JSON serialization. @@ -504,6 +655,8 @@ def run_get_arctic_hydrology_stats_data(stream_id): if isinstance(gdf, tuple): return gdf # return 400 if gdf is a tuple + stats_gdf = asyncio.run(get_stats_features(stream_id)) + try: # fetch data and metadata decode_dict = asyncio.run( @@ -546,8 +699,7 @@ def run_get_arctic_hydrology_stats_data(stream_id): except Exception: return render_template("500/server_error.html"), 500 - # add stub summary dict for frontend interoperability with conus_hydrology - data_dict["summary"] = copy.deepcopy(_SUMMARY_STUB) + data_dict = populate_feature_stat_attributes_summary(data_dict, stats_gdf) return jsonify(data_dict) @@ -865,7 +1017,7 @@ def run_get_arctic_hydrology_hydroviz(stream_id): "monthly_flow": monthly_flow, "max_flow_dates": max_flow_dates, "stats": table_stats, - "summary": copy.deepcopy(_SUMMARY_STUB), + "summary": stats.get("summary"), } return jsonify(response) From e1d8293596aa7011d49b3b0e8e1a2e221e303dd1 Mon Sep 17 00:00:00 2001 From: joshdpaul Date: Fri, 29 May 2026 08:46:20 -0800 Subject: [PATCH 05/10] fix data type errors --- routes/arctic_hydrology.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/routes/arctic_hydrology.py b/routes/arctic_hydrology.py index 4f012e62..de7d81c7 100644 --- a/routes/arctic_hydrology.py +++ b/routes/arctic_hydrology.py @@ -135,10 +135,10 @@ def populate_feature_stat_attributes_summary(data_dict, gdf): ### MEAN FLOWS: ma99_hist_value = ( - round(gdf.loc[0].ma99_hist, 0) if not np.isnan(gdf.loc[0].ma99_hist) else None + int(round(gdf.loc[0].ma99_hist, 0)) if not np.isnan(gdf.loc[0].ma99_hist) else None ) if ma99_hist_value is not None and ma99_hist_value > 5: - ma99_hist_value = round(gdf.loc[0].ma99_hist / 5) * 5 + ma99_hist_value = int(round(gdf.loc[0].ma99_hist / 5) * 5) summary_values["ma99_hist"] = { "value": ma99_hist_value, "range_low": None, @@ -618,7 +618,8 @@ def populate_feature_attributes(data_dict, gdf): # the watershed ID matches the GVV code for HUC8 in Alaska or Yukon watershed in Canada # all Yukon watersheds begin with "YTHYDRO" while HUC8s are just numeric data_dict["watershed"] = gdf.loc[0].get("ID_1", None) - data_dict["watershed_outlet"] = gdf.loc[0].get("outlet", None) + outlet = gdf.loc[0].get("outlet", None) + data_dict["watershed_outlet"] = bool(outlet) if outlet is not None and not np.isnan(outlet) else None # copy and convert gdf to WGS84 for lat/lon extraction gdf_4326 = gdf.to_crs("EPSG:4326") @@ -704,6 +705,7 @@ def run_get_arctic_hydrology_stats_data(stream_id): return jsonify(data_dict) except Exception as exc: + if hasattr(exc, "status") and exc.status == 404: return render_template("404/no_data.html"), 404 return render_template("500/server_error.html"), 500 From 5a93f310ffffc067915949e4c6630ab29aeca65e Mon Sep 17 00:00:00 2001 From: joshdpaul Date: Fri, 29 May 2026 09:27:49 -0800 Subject: [PATCH 06/10] add documentation about source and update JSON return examples --- templates/documentation/arctic_hydrology.html | 487 +++++++++++++++--- 1 file changed, 405 insertions(+), 82 deletions(-) diff --git a/templates/documentation/arctic_hydrology.html b/templates/documentation/arctic_hydrology.html index e4bc238c..5da4f261 100644 --- a/templates/documentation/arctic_hydrology.html +++ b/templates/documentation/arctic_hydrology.html @@ -8,7 +8,7 @@

Arctic Hydrology

streamflow was modeled using the Regional Arctic System Model (RASM) with dynamically downscaled CMIP6 historical (1990–2021) and projected mid-century (2034-2065) climate data. A dynamically downscaled ERA5 baseline - is available for the historical period. + (Cheng) is available for the historical period.

@@ -51,7 +51,28 @@

Modeled hydrologic statistics by stream ID

CSV output is also available by appending ?format=csv to - the URL.
+ the URL.
+ The data source can be controlled with the ?source= + parameter. Accepted values are: +
    +
  • + gcm_diff_applied_to_cheng (default) — + GCM-projected changes applied to the historical Cheng baseline. PGW + models are not included. +
  • +
  • + original_gcm — raw output from the original GCM + runs. Includes PGW models. +
  • +
  • + gcm_diff — ratio or absolute difference between + the projected and historical GCM runs. These are not actual + statistic values; apply them to your own historical baseline to + approximate future values. +
  • +
+ Chaining of multiple arguments is supported (e.g., + ?source=original_gcm&format=csv). @@ -87,7 +108,22 @@

Modeled daily streamflow climatologies by stream ID

CSV output is also available by appending ?format=csv to - the URL.
+ the URL.
+ The data source can be controlled with the ?source= + parameter. Accepted values are: +
    +
  • + gcm_diff_applied_to_cheng (default) — + GCM-projected changes applied to the historical Cheng baseline. PGW + models are not included. +
  • +
  • + original_gcm — raw output from the original GCM + runs. Includes PGW models. +
  • +
+ Chaining of multiple arguments is supported (e.g., + ?source=original_gcm&format=csv). @@ -103,52 +139,77 @@

Modeled hydrologic statistics

{ "data": { "C2LE2": { + "2034-2065": { + "dh1": 49214.59, + "dh15": 23.0, + ... + "th1": 169.46, + "tl1": 129.21 + } + }, + ... + "historical": { "1990-2021": { - "ma12": 26.52, - "ma13": 26.1, - "ma14": 24.67, - "ma15": 62.68, - "ma16": 5237.25, - "ma17": 16313.96, - "ma18": 9392.27, - "ma19": 5261.32, - "ma20": 5504.12, - "ma21": 3754.39, - "ma22": 351.99, - "ma23": 38.11, - "ma99": 3832.78 + "dh1": 39168.64, + "dh15": 22.75, + ... + "th1": 171.26, + "tl1": 134.51 + } + } + }, + "id": "81000004", + "latitude": 70.8283, + "longitude": -155.4225, + "metadata": { + "source": { + "citation": "Dylan Blaskey, Keith Musselman, Andrew Newman, & Yifan Cheng. (2024). Alaskan river discharge, temperature, and climate data for a climate reference (1990-2021) and at mid-century (2034-2065). Arctic Data Center. doi:10.18739/A25M62870." + }, + "variables": { + "dh1": { + "description": "Mean annual maximum 1-day average flow.", + "units": "cfs" + }, + "dh15": { + "description": "Median annual average duration of high flow pulses (above 75th percentile).", + "units": "days_per_year" }, ... - }, - ... - "id": "81000004", - "latitude": 70.8283, - "longitude": -155.4225, - "metadata": { - "source": { - "citation": "Dylan Blaskey, Keith Musselman, Andrew Newman, & Yifan Cheng. (2024). Alaskan river discharge, temperature, and climate data for a climate reference (1990-2021) and at mid-century (2034-2065). Arctic Data Center. doi:10.18739/A25M62870." + "th1": { + "description": "Median Julian date of annual maximum flow.", + "units": "julian_day" }, - "variables": { - "ma12": { - "description": "Mean of monthly flow values for January.", - "units": "cfs" - }, - ... - "ma99": { - "description": "Annual mean streamflow (cfs), calculated as the mean of the monthly mean flows.", - "units": "cfs" - } + "tl1": { + "description": "Median Julian date of annual minimum flow.", + "units": "julian_day" } + } + }, + "summary": { + "dh1_delta": { + "description": "projected change in maximum 1-day flow", + "range_high": 32, + "range_low": 4, + "units": "percent", + "value": 32 }, - "name": "" - } + ... + "ma99_hist": { + "description": "historical mean annual flow", + "range_high": null, + "range_low": null, + "units": "cfs", + "value": 4160 + } + }, + "watershed": "19060206", + "watershed_outlet": true }

Note that latitude and longitude values represent the approximate centroid of - the stream segment. Also note that not all stream segments are named. The - above output is structured like this: + the stream segment. The above output is structured like this:

@@ -158,7 +219,7 @@ 

Modeled hydrologic statistics

<era>: { <variable>: <value>, ... - }, + }, ... }, ... @@ -178,7 +239,18 @@

Modeled hydrologic statistics

... } }, - "name": <name of stream segment> + "summary": { + <statistic>: { + "description": <description of statistic>, + "range_high": <maximum projected change across models>, + "range_low": <minimum projected change across models>, + "units": <units of statistic>, + "value": <central projected value> + }, + ... + }, + "watershed": <HUC8 or Yukon watershed ID>, + "watershed_outlet": <true if segment is a watershed outlet> }
@@ -189,35 +261,35 @@

Modeled daily streamflow climatologies

{ "data": { "C2LE2": { - "1990-2021": [ + "2034-2065": [ { - "doy": 1, - "doy_max": 40.89, - "doy_mean": 26.663, - "doy_min": 0.222, - "water_year_index": 93 + "doy": 1, + "doy_max": 80.02, + "doy_mean": 23.776, + "doy_min": 0.209, + "water_year_index": 93 }, { - "doy": 2, - "doy_max": 40.793, - "doy_mean": 26.637, - "doy_min": 1.339, - "water_year_index": 94 + "doy": 2, + "doy_max": 61.352, + "doy_mean": 22.408, + "doy_min": 1.247, + "water_year_index": 94 }, ... { - "doy": 365, - "doy_max": 40.992, - "doy_mean": 27.598, - "doy_min": 15.445, - "water_year_index": 91 + "doy": 365, + "doy_max": 147.04, + "doy_mean": 28.096, + "doy_min": 14.252, + "water_year_index": 91 }, { - "doy": 366, - "doy_max": 32.638, - "doy_mean": 26.632, - "doy_min": 15.37, - "water_year_index": 92 + "doy": 366, + "doy_max": 101.043, + "doy_mean": 40.352, + "doy_min": 20.524, + "water_year_index": 92 } ] }, @@ -253,14 +325,15 @@

Modeled daily streamflow climatologies

} } }, - "name": "" + "watershed": "19060206", + "watershed_outlet": true }

Note that latitude and longitude values represent the approximate centroid of - the stream segment. Also note that not all stream segments are named. Modeled - data uses a 366 day year. The above output is structured like this: + the stream segment. Modeled data uses a 366 day year. The above output is + structured like this:

@@ -276,23 +349,25 @@ 

Modeled daily streamflow climatologies

], ... }, - "id": <stream ID>, - "latitude": <latitude of stream segment>, - "latitude": <latitude of stream segment>, - "longitude": <longitude of stream segment>, - "metadata": { - "source": { - "citation": <academic reference for source data> - }, - "variables": { - <variable>: { - "description": <description of variable>, - "units": <units of variable> - }, - } + ... + }, + "id": <stream ID>, + "latitude": <latitude of stream segment>, + "longitude": <longitude of stream segment>, + "metadata": { + "source": { + "citation": <academic reference for source data> }, - "name": <name of stream segment> - } + "variables": { + <variable>: { + "description": <description of variable>, + "units": <units of variable> + }, + ... + } + }, + "watershed": <HUC8 or Yukon watershed ID>, + "watershed_outlet": <true if segment is a watershed outlet> }
@@ -307,6 +382,175 @@

Available Streamflow Statistics

+ + dh1 + Mean annual maximum 1-day average flow + cubic feet per second + + + dh2 + Mean annual maximum 3-day average flow + cubic feet per second + + + dh3 + Mean annual maximum 7-day average flow + cubic feet per second + + + dh4 + Mean annual maximum 30-day average flow + cubic feet per second + + + dh5 + Mean annual maximum 90-day average flow + cubic feet per second + + + dh15 + + Median annual average duration of high flow pulses (above 75th + percentile) + + days per year + + + + dl1 + Mean annual minimum 1-day average flow + cubic feet per second + + + dl2 + Mean annual minimum 3-day average flow + cubic feet per second + + + dl3 + Mean annual minimum 7-day average flow + cubic feet per second + + + dl4 + Mean annual minimum 30-day average flow + cubic feet per second + + + dl5 + Mean annual minimum 90-day average flow + cubic feet per second + + + dl16 + + Median annual average duration of low flow pulses (below 25th + percentile) + + days per year + + + + lf1 + + Median annual number of days below threshold of 0.1 cfs per square mile + + days per year + + + + spr_dur3 + + Median spring (April–June) maximum 3-day moving average flow + + cubic feet per second + + + spr_dur7 + + Median spring (April–June) maximum 7-day moving average flow + + cubic feet per second + + + sum_dur3 + + Median summer (July–September) minimum 3-day moving average flow + + cubic feet per second + + + sum_dur7 + + Median summer (July–September) minimum 7-day moving average flow + + cubic feet per second + + + + fh1 + Mean annual count of high flow pulses above 75th percentile + events per year + + + fh5 + Mean annual flood frequency above median flow + events per year + + + fh6 + Mean annual flood frequency above 3× median flow + events per year + + + fh7 + Mean annual flood frequency above 7× median flow + events per year + + + fl1 + Mean annual count of low flow pulses below 25th percentile + events per year + + + fl3 + Mean annual count of events below 5 percent of mean flow + events per year + + + spr_freq + + Median spring (April–June) count of flow events above 10th + percentile of full record + + events per year + + + sum_freq + + Median summer (July–September) count of flow events below 90th + percentile of full record + + events per year + + + + ma3 + + Coefficient of variation (std/mean) of annual daily flows; mean of + annual CVs + + percent + + + ma4 + + Standard deviation of percentiles of log-transformed flow divided by + mean of those percentiles + + percent + + ma12 Mean of monthly flow values for January @@ -369,10 +613,89 @@

Available Streamflow Statistics

ma99 + Mean annual flow: average of monthly mean flows (ma12–ma23) + cubic feet per second + + + mh14 + Median of (annual maximum flow / median annual flow) ratios + dimensionless + + + mh20 - Annual mean streamflow, calculated as the mean of the monthly mean flows + Mean annual maximum flow divided by drainage area (specific discharge) - cubic feet per second + cubic feet per second per square mile + + + ml17 + + Base flow index: mean of annual (7-day minimum flow / mean annual flow) + ratios + + dimensionless + + + + spr_mag + + Median spring (April–June) maximum flow divided by drainage area + + cubic feet per second per square mile + + + sum_cv + + Median annual coefficient of variation of summer (July–September) + flows + + percent + + + sum_mag + + Median summer (July–September) minimum flow divided by drainage + area + + cubic feet per second per square mile + + + + ra1 + Mean rise rate: mean of positive daily flow changes + cubic feet per second per day + + + ra3 + Mean fall rate: mean of negative daily flow changes + cubic feet per second per day + + + ra8 + Median annual number of flow direction reversals + days per year + + + + spr_ord + Median Julian date of spring (April–June) maximum flow + Julian day + + + sum_ord + Median Julian date of summer (July–September) minimum flow + Julian day + + + th1 + Median Julian date of annual maximum flow + Julian day + + + tl1 + Median Julian date of annual minimum flow + Julian day From f137f03c8aacc3a7f3bd525b1c65ed09996e8ec5 Mon Sep 17 00:00:00 2001 From: joshdpaul Date: Fri, 29 May 2026 10:57:53 -0800 Subject: [PATCH 07/10] fix rounding; add gage and watershed metadata --- routes/arctic_hydrology.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/routes/arctic_hydrology.py b/routes/arctic_hydrology.py index de7d81c7..a7e8c074 100644 --- a/routes/arctic_hydrology.py +++ b/routes/arctic_hydrology.py @@ -17,7 +17,10 @@ ) from generate_requests import generate_conus_hydrology_wcs_str -from generate_urls import generate_wfs_arctic_hydrology_url, generate_wfs_arctic_hydrology_stats_url +from generate_urls import ( + generate_wfs_arctic_hydrology_url, + generate_wfs_arctic_hydrology_stats_url, +) from fetch_data import fetch_data, fetch_layer_data, describe_via_wcps from validate_request import get_axis_encodings from postprocessing import prune_nulls_with_max_intensity @@ -130,12 +133,15 @@ def populate_feature_stat_attributes_summary(data_dict, gdf): data_dict (dict): Data dictionary to populate with summary stats gdf (GeoDataFrame or None): GeoDataFrame with stat attributes from the stats layer Returns: - Data dictionary with summary stats populated, or null-valued summary if gdf is unavailable.""" + Data dictionary with summary stats populated, or null-valued summary if gdf is unavailable. + """ summary_values = {} ### MEAN FLOWS: ma99_hist_value = ( - int(round(gdf.loc[0].ma99_hist, 0)) if not np.isnan(gdf.loc[0].ma99_hist) else None + int(round(gdf.loc[0].ma99_hist, 0)) + if not np.isnan(gdf.loc[0].ma99_hist) + else None ) if ma99_hist_value is not None and ma99_hist_value > 5: ma99_hist_value = int(round(gdf.loc[0].ma99_hist / 5) * 5) @@ -615,11 +621,16 @@ def populate_feature_attributes(data_dict, gdf): # data_dict["name"] = "" # arctic rivers segments do not have stream names associated + # gauge ID is blank or null for most features, so default to None if not present or NaN + data_dict["gauge_id"] = gdf.loc[0].get("Gauge_ID", None) + # the watershed ID matches the GVV code for HUC8 in Alaska or Yukon watershed in Canada # all Yukon watersheds begin with "YTHYDRO" while HUC8s are just numeric data_dict["watershed"] = gdf.loc[0].get("ID_1", None) outlet = gdf.loc[0].get("outlet", None) - data_dict["watershed_outlet"] = bool(outlet) if outlet is not None and not np.isnan(outlet) else None + data_dict["watershed_outlet"] = ( + bool(outlet) if outlet is not None and not np.isnan(outlet) else None + ) # copy and convert gdf to WGS84 for lat/lon extraction gdf_4326 = gdf.to_crs("EPSG:4326") @@ -1010,12 +1021,12 @@ def run_get_arctic_hydrology_hydroviz(stream_id): } response = { - "gauge_id": None, - "huc8": None, - "huc8_outlet": None, + "gauge_id": stats.get("gauge_id"), + "huc8": stats.get("watershed"), + "huc8_outlet": stats.get("watershed_outlet"), "hydrograph": hydrograph, "id": stats["id"], - "name": stats["name"], + "name": stats.get("name"), "monthly_flow": monthly_flow, "max_flow_dates": max_flow_dates, "stats": table_stats, From 34abcca2c94b933d440e91bdfa86438f72903fb5 Mon Sep 17 00:00:00 2001 From: joshdpaul Date: Fri, 29 May 2026 11:55:55 -0800 Subject: [PATCH 08/10] add all stats to arctic hydro csv --- csv_functions.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/csv_functions.py b/csv_functions.py index c9396a0b..b6d255a5 100644 --- a/csv_functions.py +++ b/csv_functions.py @@ -1562,6 +1562,33 @@ def arctic_hydrology_csv(data, filename_prefix, source_metadata): metadata += f"# The following hydrologic statistics are calculated from modeled daily streamflow data. {source_notes[source_metadata]}\n" else: metadata += "# The following hydrologic statistics are calculated from modeled daily streamflow data:\n" + metadata += "# dh1: Mean annual maximum 1-day average flow (cubic feet per second - temporal).\n" + metadata += "# dh2: Mean annual maximum 3-day average flow (cubic feet per second - temporal).\n" + metadata += "# dh3: Mean annual maximum 7-day average flow (cubic feet per second - temporal).\n" + metadata += "# dh4: Mean annual maximum 30-day average flow (cubic feet per second - temporal).\n" + metadata += "# dh5: Mean annual maximum 90-day average flow (cubic feet per second - temporal).\n" + metadata += "# dh15: Median annual average duration of high flow pulses above the 75th percentile (days/year - temporal).\n" + metadata += "# dl1: Mean annual minimum 1-day average flow (cubic feet per second - temporal).\n" + metadata += "# dl2: Mean annual minimum 3-day average flow (cubic feet per second - temporal).\n" + metadata += "# dl3: Mean annual minimum 7-day average flow (cubic feet per second - temporal).\n" + metadata += "# dl4: Mean annual minimum 30-day average flow (cubic feet per second - temporal).\n" + metadata += "# dl5: Mean annual minimum 90-day average flow (cubic feet per second - temporal).\n" + metadata += "# dl16: Median annual average duration of low flow pulses below the 25th percentile (days/year - temporal).\n" + metadata += "# lf1: Median annual number of days below a threshold of 0.1 cubic feet per second per square mile (days/year - temporal).\n" + metadata += "# spr_dur3: Median spring (April-June) maximum of 3-day moving average flows (cubic feet per second - temporal).\n" + metadata += "# spr_dur7: Median spring (April-June) maximum of 7-day moving average flows (cubic feet per second - temporal).\n" + metadata += "# sum_dur3: Median summer (July-September) minimum of 3-day moving average flow (cubic feet per second - temporal).\n" + metadata += "# sum_dur7: Median summer (July-September) minimum of 7-day moving average flow (cubic feet per second - temporal).\n" + metadata += "# fh1: Mean annual count of high flow pulses above the 75th percentile (number of events/year - temporal).\n" + metadata += "# fh5: Mean annual flood frequency above median flow (number of events/year - temporal).\n" + metadata += "# fh6: Mean annual flood frequency above 3 times median flow (number of events/year - temporal).\n" + metadata += "# fh7: Mean annual flood frequency above 7 times median flow (number of events/year - temporal).\n" + metadata += "# fl1: Mean annual count of low flow pulses below the 25th percentile (number of events/year - temporal).\n" + metadata += "# fl3: Mean annual count of events below 5 percent of mean flow (number of events/year - temporal).\n" + metadata += "# spr_freq: Median spring (April-June) count of flow events above the 10th percentile of the full record (number of events/year - temporal).\n" + metadata += "# sum_freq: Median summer (July-September) count of flow events below the 90th percentile of the full record (number of events/year - temporal).\n" + metadata += "# ma3: Coefficient of variation (standard deviation/mean) of annual daily flows; mean of annual CVs (percent - temporal).\n" + metadata += "# ma4: Standard deviation of percentiles of log-transformed flow divided by mean of those percentiles (percent - spatial).\n" metadata += "# ma12: Mean of monthly flow values for January (cubic feet per second - temporal).\n" metadata += "# ma13: Mean of monthly flow values for February (cubic feet per second - temporal).\n" metadata += "# ma14: Mean of monthly flow values for March (cubic feet per second - temporal).\n" @@ -1575,6 +1602,19 @@ def arctic_hydrology_csv(data, filename_prefix, source_metadata): metadata += "# ma22: Mean of monthly flow values for November (cubic feet per second - temporal).\n" metadata += "# ma23: Mean of monthly flow values for December (cubic feet per second - temporal).\n" metadata += "# ma99: Mean of monthly flow values for the entire year. Compute the mean of the monthly mean flows for each month of the year. MA99 is the mean of these 12 values (cubic feet per second - temporal).\n" + metadata += "# mh14: Median of annual (maximum flow / median annual flow) ratios (dimensionless - temporal).\n" + metadata += "# mh20: Mean annual maximum flow divided by drainage area (cubic feet per second/square mile - temporal).\n" + metadata += "# ml17: Base flow index: mean of annual (7-day minimum flow / mean annual flow) ratios (dimensionless - temporal).\n" + metadata += "# spr_mag: Median spring (April-June) maximum flow divided by drainage area (cubic feet per second/square mile - temporal).\n" + metadata += "# sum_cv: Median annual coefficient of variation of summer (July-September) daily flows (percent - temporal).\n" + metadata += "# sum_mag: Median summer (July-September) minimum flow divided by drainage area (cubic feet per second/square mile - temporal).\n" + metadata += "# ra1: Mean rise rate: mean of positive daily flow changes (cubic feet per second/day - temporal).\n" + metadata += "# ra3: Mean fall rate: mean of negative daily flow changes (cubic feet per second/day - temporal).\n" + metadata += "# ra8: Median annual number of flow direction reversals (days - temporal).\n" + metadata += "# spr_ord: Median Julian date of spring (April-June) maximum flow (Julian day - temporal).\n" + metadata += "# sum_ord: Median Julian date of summer (July-September) minimum flow (Julian day - temporal).\n" + metadata += "# th1: Median Julian date of annual maximum flow (Julian day - temporal).\n" + metadata += "# tl1: Median Julian date of annual minimum flow (Julian day - temporal).\n" else: if isinstance(source_metadata, str) and source_metadata in source_notes: metadata += f"# Climatologies are calculated from modeled daily streamflow data. {source_notes[source_metadata]}\n" From 44788bd69277a5de547c8023a19a0943fa40da59 Mon Sep 17 00:00:00 2001 From: joshdpaul Date: Fri, 29 May 2026 13:45:45 -0800 Subject: [PATCH 09/10] fix gauge id; add documentation about models --- routes/arctic_hydrology.py | 5 +++-- templates/documentation/arctic_hydrology.html | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/routes/arctic_hydrology.py b/routes/arctic_hydrology.py index a7e8c074..752f8650 100644 --- a/routes/arctic_hydrology.py +++ b/routes/arctic_hydrology.py @@ -621,8 +621,9 @@ def populate_feature_attributes(data_dict, gdf): # data_dict["name"] = "" # arctic rivers segments do not have stream names associated - # gauge ID is blank or null for most features, so default to None if not present or NaN - data_dict["gauge_id"] = gdf.loc[0].get("Gauge_ID", None) + # gauge ID is blank for most features; normalize None/NaN to "" so the key is always present + gauge_id_raw = gdf.loc[0].get("Gauge_ID", None) + data_dict["gauge_id"] = gauge_id_raw if isinstance(gauge_id_raw, str) else "" # the watershed ID matches the GVV code for HUC8 in Alaska or Yukon watershed in Canada # all Yukon watersheds begin with "YTHYDRO" while HUC8s are just numeric diff --git a/templates/documentation/arctic_hydrology.html b/templates/documentation/arctic_hydrology.html index 5da4f261..22087be6 100644 --- a/templates/documentation/arctic_hydrology.html +++ b/templates/documentation/arctic_hydrology.html @@ -7,8 +7,11 @@

Arctic Hydrology

Canada. Stream segments were derived from the MERIT Hydro network, and streamflow was modeled using the Regional Arctic System Model (RASM) with dynamically downscaled CMIP6 historical (1990–2021) and projected - mid-century (2034-2065) climate data. A dynamically downscaled ERA5 baseline - (Cheng) is available for the historical period. + mid-century (2034-2065) climate data using the SSP3-7.0 scenario. Four + Community Earth System Model 2 (CESM2) runs are included, as well as two + pseudo-global warming (PGW) runs based on the CESM2 Large Ensemble. A + dynamically downscaled ERA5 baseline (Cheng) is available for the historical + period.

@@ -24,9 +27,9 @@

Service endpoints

Modeled hydrologic statistics by stream ID

- Query hydrologic statistics for all models and scenarios. Statistics are - computed over historical (1990–2021) and projected mid-century - (2034–2065) eras. These statistics are defined + Query hydrologic statistics for all models. Statistics are computed over + historical (1990–2021) and projected mid-century (2034–2065) eras. + These statistics are defined below.

@@ -81,9 +84,9 @@

Modeled hydrologic statistics by stream ID

Modeled daily streamflow climatologies by stream ID

- Query daily streamflow climatologies for all models and scenarios. Useful for - constructing hydrographs. Minimum, maximum, and mean values for each day of - year are computed over historical (1990–2021) and projected mid-century + Query daily streamflow climatologies for all models. Useful for constructing + hydrographs. Minimum, maximum, and mean values for each day of year are + computed over historical (1990–2021) and projected mid-century (2034–2065) eras.

From faa67a568d7df84f8936aeb36c046ce22ced840d Mon Sep 17 00:00:00 2001 From: joshdpaul Date: Mon, 1 Jun 2026 07:58:33 -0800 Subject: [PATCH 10/10] update GS layer name --- generate_urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generate_urls.py b/generate_urls.py index bc56740b..c99230be 100644 --- a/generate_urls.py +++ b/generate_urls.py @@ -138,13 +138,13 @@ def generate_wfs_arctic_hydrology_url(stream_id): if stream_id == "": wfs_url = ( GS_BASE_URL - + "wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=hydrology:arctic_rivers_segments_joined_3338&propertyName=(COMID)&outputFormat=application/json" + + "wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=hydrology:arctic_rivers_segments_joined_3338_simplified&propertyName=(COMID)&outputFormat=application/json" ) return wfs_url else: wfs_url = ( GS_BASE_URL - + f"wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=hydrology:arctic_rivers_segments_joined_3338&propertyName=(COMID,the_geom,Gauge_ID,ID_1,ID_2,Name,outlet)&outputFormat=application/json&cql_filter=(COMID={stream_id})" + + f"wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=hydrology:arctic_rivers_segments_joined_3338_simplified&propertyName=(COMID,the_geom,Gauge_ID,ID_1,ID_2,Name,outlet)&outputFormat=application/json&cql_filter=(COMID={stream_id})" ) return wfs_url