Skip to content

Commit 10ac793

Browse files
authored
updated ccss notebook example based on the new structure (#25)
Merging Irene's changes with the plotting function.
1 parent e248e86 commit 10ac793

3 files changed

Lines changed: 61 additions & 150 deletions

File tree

examples/collect_observations/ccss_swe_collect_observations.ipynb

Lines changed: 59 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,35 @@
44
"cell_type": "markdown",
55
"metadata": {},
66
"source": [
7-
"![NWM](img/NWM.png)\n",
7+
" <p align=\"center\">\n",
8+
" <img\n",
9+
" src=\"../img/2025_01_02_NS_173_Snow_Survey.jpg\"\n",
10+
" alt=\"CCSS snow survey at Phillips Station\"\n",
11+
" width=\"1000\"\n",
12+
" height=\"600\"\n",
13+
" />\n",
14+
" </p>\n",
815
"\n",
9-
"# Retrieve and Analyze National Water Model Snow Data for a Watershed of Interest\n",
10-
"Authors: Irene Garousi-Nejad (igarousi@cuahsi.org)\n",
11-
"Last updated:Oct 16, 2025"
16+
" <p align=\"center\">\n",
17+
" <span style=\"color: gray;\"><em>\n",
18+
" Source: California Department of Water Resources. California Department of Water Resources staff members Manon von Kaenel (left), Jordan Thoennes, and Andy Reising conduct the first media snow survey of the 2025 season at Phillips Station in the Sierra Nevada, about 90 miles east of Sacramento off Highway 50 in El Dorado County. Photo taken January 2, 2025. Image obtained from\n",
19+
" <a href=\"https://pixel-ca-dwr.photoshelter.com/galleries/C0000lSOVLm_Sxso/G0000CdIWl5a2_xA/I0000OkEmK_kC73A/2025-01-02-NS-173-Snow-Survey-jpg\">this link</a>.\n",
20+
" </em></span>\n",
21+
" </p>\n",
22+
"\n",
23+
"# Retrieve California Cooperative Snow Surveys (CCSS) Data for a Watershed of Interest\n",
24+
"\n",
25+
" **Author:** Irene Garousi-Nejad ([igarousi@cuahsi.org](mailto:igarousi@cuahsi.org))\n",
26+
" **Last updated:** March 24, 2026"
27+
]
28+
},
29+
{
30+
"cell_type": "markdown",
31+
"id": "622da967",
32+
"metadata": {},
33+
"source": [
34+
"#### Introduction: \n",
35+
"This notebook demonstrates how to access the California Cooperative Snow Surveys (CCSS) data, which is program led by the California Department of Water Resources (DWR) to support water supply forecasting and flood management missions. "
1236
]
1337
},
1438
{
@@ -22,7 +46,7 @@
2246
"cell_type": "markdown",
2347
"metadata": {},
2448
"source": [
25-
"Ensure that the `nwm_env` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `getting_started.md` file."
49+
"Ensure that the `cssi_evaluation` conda environment is selected as your Jupyter kernel. This environment should already be created if you followed the instructions under section \"Creating your HydroLearnEnv Virtual Environment\" in the `GettingStarted.md` file."
2650
]
2751
},
2852
{
@@ -43,55 +67,29 @@
4367
"import os\n",
4468
"import time\n",
4569
"import sys\n",
70+
"from pathlib import Path\n",
4671
"\n",
4772
"prefix = os.environ['CONDA_PREFIX']\n",
4873
"os.environ['PROJ_LIB'] = os.path.join(prefix, 'share', 'proj')\n",
4974
"\n",
5075
"# add the src directory to the path so we can import evaluation modules\n",
51-
"sys.path.append('../../src/')\n",
76+
"#sys.path.append('../../src/')\n",
77+
"repo_root = Path.cwd().resolve().parents[1]\n",
78+
"sys.path.insert(0, str(repo_root / \"src\"))\n",
5279
"\n",
5380
"import pyproj\n",
5481
"import pandas as pd\n",
5582
"import xarray as xr\n",
5683
"import geopandas as gpd\n",
5784
"from dask.distributed import Client\n",
5885
"\n",
59-
"from cssi_evaluation.utils import plot_utils\n",
60-
"from cssi_evaluation.utils import dataPrep_utils\n",
86+
"from cssi_evaluation.utils import plot_utils, dataPrep_utils\n",
6187
"from cssi_evaluation.external_data_access import observation_utils\n",
6288
"\n",
6389
"%load_ext autoreload\n",
6490
"%autoreload 2"
6591
]
6692
},
67-
{
68-
"cell_type": "markdown",
69-
"metadata": {},
70-
"source": [
71-
"We'll use dask to parallelize our code. To manage parallel computation and visualize progress of long-running tasks, we initialize a Dask “cluster,” which defines how many workers are used and how much computing power each worker has. \n",
72-
"\n",
73-
"In this setup, we create a Dask client with `Client(n_workers=6, threads_per_worker=1, memory_limit='2GB')`, which launches a cluster with 6 workers. Each worker uses a single thread, typically mapped to one CPU core, allowing for efficient parallel processing across 6 cores. Each worker also has a memory limit of 2 GB, for a total of up to 12 GB across the cluster.\n"
74-
]
75-
},
76-
{
77-
"cell_type": "code",
78-
"execution_count": null,
79-
"metadata": {
80-
"tags": []
81-
},
82-
"outputs": [],
83-
"source": [
84-
"# use a try accept loop so we only instantiate the client\n",
85-
"# if it doesn't already exist.\n",
86-
"try:\n",
87-
" print('Dashboard link:', client.dashboard_link)\n",
88-
"except: \n",
89-
" # The client should be customized to your workstation resources.\n",
90-
" client = Client(n_workers=6, threads_per_worker=1, memory_limit='2GB') \n",
91-
" print('Dashboard link:', client.dashboard_link)\n",
92-
"print(client)"
93-
]
94-
},
9593
{
9694
"cell_type": "markdown",
9795
"metadata": {},
@@ -107,25 +105,21 @@
107105
},
108106
"outputs": [],
109107
"source": [
108+
"# repository path\n",
109+
"repo_root = Path.cwd().resolve().parents[1]\n",
110+
"\n",
110111
"# path to the model domain data\n",
111-
"domain_data_path = 'examples/nwm/domain_data/' \n",
112+
"domain_data_path = f\"{repo_root}/examples/nwm/domain_data/\" \n",
112113
"\n",
113114
"# Path to the watershed shapefile\n",
114-
"watershed = f\"{domain_data_path}TolumneRiver_18040009.shp\"\n",
115-
"\n",
116-
"# file retrieved using `curl -L https://raw.githubusercontent.com/egagli/snotel_ccss_stations/main/all_stations.geojson -o all_stations.geojson`\n",
117-
"snotel_geojson = f\"{domain_data_path}all_stations.geojson\"\n",
118-
"\n",
119-
"# Path to NWM snow data\n",
120-
"conus_bucket_url = 's3://noaa-nwm-retrospective-3-0-pds/CONUS/zarr/ldasout.zarr'\n",
115+
"watershed_path = f\"{domain_data_path}TolumneRiver_18040009.shp\"\n",
121116
"\n",
122117
"# Start and end times of a water year (note that this code currently works for one water year)\n",
123118
"StartDate = '2018-10-01'\n",
124119
"EndDate = '2020-09-30'\n",
125120
"\n",
126121
"# Path to save results (obs and mod stands for observation and modeled, respectively)\n",
127-
"OBS_OutputFolder = 'examples/nwm/obs_outputs' \n",
128-
"MOD_OutputFolder = 'examples/nwm/mod_outputs'"
122+
"OBS_OutputFolder = './cssi_outputs' "
129123
]
130124
},
131125
{
@@ -159,8 +153,8 @@
159153
"all_stations_gdf = all_stations_gdf[all_stations_gdf['csvData']==True]\n",
160154
"filtered_all_stations_gdf = all_stations_gdf[all_stations_gdf.state.str.contains('California')] \n",
161155
"\n",
162-
"# Extract the bounding box coordinates of a watershed\n",
163-
"watershed_gdf = gpd.read_file(os.path.join(os.getcwd(), watershed)).to_crs(epsg=4326)\n",
156+
"# Read the watershed shapefile and standardize to WGS84\n",
157+
"watershed_gdf = gpd.read_file(watershed_path).to_crs(epsg=4326)\n",
164158
"\n",
165159
"# Combine all polygons into a single MultiPolygon\n",
166160
"watershed_union = watershed_gdf.geometry.unary_union\n",
@@ -178,6 +172,16 @@
178172
"Plot these sites on a map. Then, hover over the pins to see the site names."
179173
]
180174
},
175+
{
176+
"cell_type": "code",
177+
"execution_count": null,
178+
"id": "c535a38c",
179+
"metadata": {},
180+
"outputs": [],
181+
"source": [
182+
"gdf_in_bbox"
183+
]
184+
},
181185
{
182186
"cell_type": "code",
183187
"execution_count": null,
@@ -186,9 +190,8 @@
186190
},
187191
"outputs": [],
188192
"source": [
189-
"## TODO: REPLACE WITH CSSI_EVALUATION.PLOTS FUNCTIONS\n",
190-
"\n",
191-
"m = plot_utils.plot_sites_within_domain(gdf_in_bbox, watershed_gdf, zoom_start=9)\n",
193+
"# Plot the sites within the watershed boundary using the plot_utils function\n",
194+
"m = plot_utils.map_sites_within_watershed(gdf_in_bbox, watershed_gdf, zoom_start=9) \n",
192195
"m"
193196
]
194197
},
@@ -215,7 +218,7 @@
215218
"cell_type": "markdown",
216219
"metadata": {},
217220
"source": [
218-
"The following uses the `nwm_utils.py` script to download observed data for the sites within the domain. Since all the sites are from the (California Cooperative Snow Survey) CCSS network, we use the `getCCSSData` function from the module to get data. "
221+
"The following uses the `observation_utils.py` script to download observed data for the sites within the domain. Since all the sites are from the (California Cooperative Snow Survey) CCSS network, we use the `getCCSSData` function from the module to get data. "
219222
]
220223
},
221224
{
@@ -259,109 +262,17 @@
259262
},
260263
{
261264
"cell_type": "markdown",
262-
"metadata": {
263-
"tags": []
264-
},
265-
"source": [
266-
"### 4. Retrieve Snow Model Outputs"
267-
]
268-
},
269-
{
270-
"cell_type": "markdown",
265+
"id": "7e4506d2",
271266
"metadata": {},
272-
"source": [
273-
"NOAA shares inputs and outputs to the National Water Model retrospective simulations version 3 at <a href=\"https://noaa-nwm-retrospective-3-0-pds.s3.amazonaws.com/index.html\" style=\"color: blue; background-color: snow;\">https://noaa-nwm-retrospective-3-0-pds.s3.amazonaws.com/index.html</a>. The following code uses `fsspec` and `xarray` Python libraries to load the Zarr metadata of snow outputs (**ldasout.zarr**) into memory. Once the code is executed, you can see the wall time, which includes time spent waiting for I/O operations, such as reading data from a remote server. In our case, it took about 12 seconds to load the metadata into memory. Set up `Dask`, a parallel computing library, to enable performing operations on large datasets that don't fit into memory by breaking them into smaller, manageable pieces called chunks."
274-
]
267+
"source": []
275268
},
276269
{
277270
"cell_type": "code",
278271
"execution_count": null,
279-
"metadata": {
280-
"tags": []
281-
},
282-
"outputs": [],
283-
"source": [
284-
"%%time \n",
285-
"ds = xr.open_zarr(\n",
286-
" store=conus_bucket_url,\n",
287-
" consolidated=True,\n",
288-
" storage_options={\n",
289-
" \"anon\": True,\n",
290-
" \"client_kwargs\": {\"region_name\": \"us-east-1\"}\n",
291-
" }\n",
292-
")"
293-
]
294-
},
295-
{
296-
"cell_type": "markdown",
272+
"id": "208439e6",
297273
"metadata": {},
298-
"source": [
299-
"The following code retrieves NWM SWE output for each SNOTEL site within our watershed. For each site, it first converts latitude and longitude of the site to the projected coordinates used by the NWM. Then, it extracts the NWM SWE output for the site and the period of interest, saving the result as a DataFrame. Since NWM timestamps are in UTC, the DataFrame is converted to the local time zone to match SNOTEL observations for later comparison purposes. To fairly compare with SNOTEL, which reports SWE once daily at the start of the local day, the data is grouped by date, and the earliest record of each day is selected. Finally, the processed data is saved as a CSV file for each site."
300-
]
301-
},
302-
{
303-
"cell_type": "code",
304-
"execution_count": null,
305-
"metadata": {
306-
"tags": []
307-
},
308-
"outputs": [],
309-
"source": [
310-
"# Create a folder to save model outputs\n",
311-
"isExist = os.path.exists(MOD_OutputFolder)\n",
312-
"if isExist == True:\n",
313-
" exit\n",
314-
"else:\n",
315-
" os.mkdir(MOD_OutputFolder)"
316-
]
317-
},
318-
{
319-
"cell_type": "code",
320-
"execution_count": null,
321-
"metadata": {
322-
"tags": []
323-
},
324274
"outputs": [],
325-
"source": [
326-
"# Retrieve model outputs for the location of snotel sites\n",
327-
"input_crs = 'EPSG:4269'\n",
328-
"output_crs = pyproj.CRS(ds.crs.esri_pe_string) \n",
329-
"\n",
330-
"for i in range(0, gdf_in_bbox.shape[0]):\n",
331-
" \n",
332-
" site_name = gdf_in_bbox.iloc[i][\"name\"]\n",
333-
" print(f'[{i+1}/{len(gdf_in_bbox)}] Retrieving model output for site: {site_name}')\n",
334-
" \n",
335-
" snotel_y, snotel_x = dataPrep_utils.convert_latlon_to_yx(\n",
336-
" gdf_in_bbox.iloc[i].latitude,\n",
337-
" gdf_in_bbox.iloc[i].longitude,\n",
338-
" input_crs,\n",
339-
" output_crs\n",
340-
" )\n",
341-
" \n",
342-
" dl_start_time = time.time()\n",
343-
" ds_subset = ds[['SNEQV']].sel(y=snotel_y, x=snotel_x, method='nearest'\n",
344-
" ).sel(time=slice(StartDate, EndDate)).compute()\n",
345-
" dl_elapsed = time.time() - dl_start_time\n",
346-
" print(f'✅ Retrieved model outputs for {site_name} in {dl_elapsed:.2f} seconds\\n')\n",
347-
" \n",
348-
" df = ds_subset.to_dataframe()\n",
349-
" df=df.drop(columns=['x', 'y'])\n",
350-
" df.reset_index(inplace=True)\n",
351-
" df[\"time\"] = pd.to_datetime(df[\"time\"])\n",
352-
" df.rename(columns={df.columns[0]:'Date', df.columns[1]:'NWM_SWE_meters'}, inplace=True)\n",
353-
" df.iloc[:, 1:] = df.iloc[:, 1:].apply(lambda x: pd.to_numeric(x)/1000) # convert mm to m \n",
354-
"\n",
355-
" # convert utc to local time zone\n",
356-
" df_local = dataPrep_utils.convert_utc_to_local(gdf_in_bbox.iloc[i].state, df) \n",
357-
" \n",
358-
" # groupby the data and select the first item from each group \n",
359-
" df_local.index = pd.to_datetime(df_local['Date_Local'])\n",
360-
" df_local = df_local.groupby(pd.Grouper(freq='D')).first()\n",
361-
"\n",
362-
" # save\n",
363-
" df_local.to_csv(f'./{MOD_OutputFolder}/df_{gdf_in_bbox.iloc[i].code}_{gdf_in_bbox.iloc[i].state}_NWM.csv', index=False)"
364-
]
275+
"source": []
365276
}
366277
],
367278
"metadata": {
2.18 MB
Loading

src/cssi_evaluation/utils/plot_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# nwm_utils.plot_custom_scatter_SWE()
1313
# nwm_utils.comparison_plots()
1414
# nwm_utils.plot_grid_vector_monthly_data()
15-
# nwm_utils.plot_sites_within_domain() overlaps with plot_obs_locations
15+
# nwm_utils.map_sites_within_watershed() overlaps with plot_obs_locations. # IG, Mar 24,2026: I think plot_obs_locations() and plot_sites_within_domain() serve different purposes.
1616
# nwm_utils.plot_grid_vector_data()
1717
# plots.plot_metric_map()
1818
# plots.plot_obs_locations()
@@ -515,7 +515,7 @@ def plot_condon_diagram(metrics_df, variable, output_dir="."):
515515
# from Irene's nwm_utils.py
516516

517517

518-
def plot_sites_within_domain(gdf_sites, domain_gdf, zoom_start=10):
518+
def map_sites_within_watershed(gdf_sites, domain_gdf, zoom_start=10):
519519
"""
520520
Create and return a folium map showing observation sites within a given watershed boundary.
521521

0 commit comments

Comments
 (0)