Skip to content

Commit 83a0606

Browse files
Merge branch 'development' into 'release-candidate'
Merge development into release-candidate for release 3.1.1.3.0-rc1 See merge request NGWPC/nwm-ngen/ngen-fcst!39
2 parents fafa2a2 + 1677c18 commit 83a0606

15 files changed

Lines changed: 500 additions & 226 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Follow the following steps to test the program:
4444
where [NGEN-FCST_ROOT] is where ngen-fcst is installed
4545

4646
The program takes three command line arguments:
47-
1) Path to the NetCDF forcing file
47+
1) Path to the NetCDF forcing file or folder containing csv forcing files for all catchments in the basin
4848
2) Path to the config yaml file for a validation run (from ngen-cal)
4949
3) Path to the folder to be created for storing inputs/outputs from running ngen, relative to the Output directory of the calibration run as indicated in the config yaml file. For example, if "fcst_run1" is the 3rd argument, and "yaml_file" in the "general" section of the config file is '/home/yuqiong.liu/work/Gitlab/run/kge_DDS/noah_cfes/01123000/Output/Validation_Run/01123000_config_valid_best.yaml', then the new output directory to be created for the ngen-fcst run would be:
5050

@@ -79,7 +79,7 @@ This will print a usage statement for the container:
7979
```
8080
Usage: run-ngen-fcst.sh <forcing_file> <config_file> <output_path> [log_file] [venv_path]
8181
82-
FORCING_FILE: Path to the NetCDF forcing file.
82+
FORCING_FILE: Path to the NetCDF forcing file or a folder containing csv forcing files for all catchments in the basin.
8383
CONFIG_FILE: Path to the config yaml file for a validation run (from ngen-cal).
8484
OUTPUT_PATH: Path to the folder to be created for storing inputs/outputs from running ngen.
8585
LOG_FILE (optional): Path to the output file where the script's output will be saved. Used when running in LOCAL or DOCKER environment

docker/run-ngen-fcst.sh

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ umask 000
1414

1515
# Function to display help message
1616
show_help() {
17-
echo "Usage: $(basename "$0") <command> <forcing_file> <config_file> <output_dir> [stdout_file] [venv_path]"
17+
echo "Usage: $(basename "$0") <command> <forcing_dir> <config_file> <output_dir> [stdout_file] [venv_path]"
1818
echo ""
1919
echo ""
2020
echo "COMMAND:"
2121
echo " forecast Run forecast script."
2222
echo ""
23-
echo "FORCING_FILE: Path to the NetCDF forcing file."
23+
echo "FORCING_DIR: Path to the directory container csv forcing files."
2424
echo "CONFIG_FILE: Path to the config yaml file for a validation run (from ngen-cal)."
2525
echo "FORECAST_DIR: Name of the folder to to store the forecast output."
2626
echo "STDOUT_FILE (optional): Path to the stdout file where the script's console output will be saved. Used when running in LOCAL or DOCKER environment"
@@ -71,18 +71,18 @@ if [ $# -lt $REQUIRED_ARGS ]; then
7171
show_help
7272
fi
7373

74-
FORCING_FILE=$1
74+
FORCING_DIR=$1
7575
CONFIG_FILE=$2
7676
FORECAST_DIR=$3
7777
shift $REQUIRED_ARGS
7878

79-
echo "FORCING_FILE: ${FORCING_FILE}"
79+
echo "FORCING_DIR: ${FORCING_DIR}"
8080
echo "CONFIG_FILE: ${CONFIG_FILE}"
8181
echo "FORECAST_DIR: ${FORECAST_DIR}"
8282

8383
# Check if the forcing data exists
84-
if [ ! -f "${FORCING_FILE}" ]; then
85-
echo "Forcing data not found at ${FORCING_FILE}"
84+
if [[ ! -f "${FORCING_DIR}" && ! -d "${FORCING_DIR}" ]]; then
85+
echo "Forcing data not found at ${FORCING_DIR}"
8686
fi
8787

8888
# Check if the configuration file exists
@@ -124,9 +124,9 @@ fi
124124
# Run the Python script, redirecting its output if an output file is provided
125125
echo " Running $(basename "$SCRIPT_PATH") with input file: $CONFIG_FILE"
126126
if [ -z "$STDOUT_FILE" ]; then
127-
python "${SCRIPT_PATH}" "${FORCING_FILE}" "${CONFIG_FILE}" "${FORECAST_DIR}"
127+
python "${SCRIPT_PATH}" "${FORCING_DIR}" "${CONFIG_FILE}" "${FORECAST_DIR}"
128128
else
129-
python "${SCRIPT_PATH}" "${FORCING_FILE}" "${CONFIG_FILE}" "${FORECAST_DIR}" &> "${STDOUT_FILE}" 2>&1
129+
python "${SCRIPT_PATH}" "${FORCING_DIR}" "${CONFIG_FILE}" "${FORECAST_DIR}" &> "${STDOUT_FILE}" 2>&1
130130
fi
131131

132132
python_exit_code=$?

python/log_level.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import logging
2+
import os
3+
import time
4+
from datetime import datetime, timezone
5+
from pathlib import Path
6+
7+
def create_timestamp() -> str:
8+
now = datetime.now(timezone.utc)
9+
return now.strftime("%Y-%m-%d")
10+
11+
12+
def log_level_set():
13+
'''
14+
Set logging level and specify logger configuration.
15+
16+
Arguments
17+
---------
18+
input_parameters (dict): User input logging parameters
19+
20+
Returns
21+
-------
22+
None
23+
24+
Notes
25+
-----
26+
In the absense of user-specified logging level, level defaults to DEBUG
27+
See also https://docs.python.org/3/library/logging.html
28+
29+
'''
30+
31+
log_level = 'INFO'
32+
if True:
33+
BASE_DIR = Path(__file__).resolve().parent.parent
34+
35+
if Path("/ngencerf/data").exists():
36+
log_file_dir = Path(f'/ngencerf/data/run-logs/ngen_fcst_{create_timestamp()}/')
37+
else:
38+
log_file_dir = Path(BASE_DIR) / f'run-logs/ngen_fcst_{create_timestamp()}/'
39+
40+
log_file_name = "ngen_fcst.log"
41+
os.makedirs(log_file_dir, exist_ok=True)
42+
logFilePath = os.path.join(log_file_dir, log_file_name)
43+
try:
44+
logFile = open(logFilePath, "w")
45+
print(f"Logging into: {logFilePath}")
46+
except IOError:
47+
print(f"Can't Open local directory Log File: {logFilePath}", file=sys.stderr)
48+
49+
logging.Formatter.converter = time.gmtime
50+
logging.basicConfig(
51+
force=True,
52+
level=log_level,
53+
format='%(asctime)s.%(msecs)03d NGEN_FCST %(levelname)s %(message)s',
54+
datefmt='%Y-%m-%dT%H:%M:%S',
55+
handlers=[
56+
logging.FileHandler(logFilePath, mode='a'), # Log to a file
57+
#logging.StreamHandler(sys.stdout)
58+
])
59+
else:
60+
logging.basicConfig(
61+
level=log_level,
62+
format='%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)s - %(funcName)s]: %(message)s',
63+
stream=sys.stderr,
64+
)

python/process_forcing.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import logging
2+
from pathlib import Path
3+
import netCDF4
4+
import pandas as pd
5+
import geopandas as gpd
6+
7+
from log_level import log_level_set
8+
9+
# setup the logger
10+
log_level_set()
11+
logger = logging.getLogger(__name__)
12+
13+
def update_forcing_in_realization(
14+
forc_file: Path,
15+
real_config: dict,
16+
gpkg_file:Path,
17+
) -> dict:
18+
"""
19+
Read forcing file(s) to retrieve start time and end time of the forcing data,
20+
and adjust the realization configuration accordingly:
21+
1) update forcing information
22+
2) update start and end times
23+
24+
Arguments
25+
---------
26+
forc_file: file path to forcing data (a single .nc file or a folder containing a csv file for each catchment)
27+
real_config: dictionary containing the realization configuration
28+
gpkg_file: file path the GeoPacakge file
29+
30+
Returns
31+
-------
32+
dictionary containing the adjusted realization config
33+
34+
"""
35+
36+
# make sure forcing is provided via a single netcdf file or a folder containing a csv file for each catchment
37+
if forc_file.is_dir():
38+
# if it is a dir, the folder must contain a csv file for each catchment in the gpkg file
39+
catids = gpd.read_file(gpkg_file, layer='divides')['divide_id'].tolist()
40+
cats = [c1 for c1 in catids if not Path(forc_file,c1 + '.csv').exists()]
41+
if len(cats) > 0:
42+
raise FileNotFoundError(f'csv files not found in {forc_file} for these catchments: {cats}')
43+
else:
44+
# get start and end times from one of the csv files
45+
df1 = pd.read_csv(Path(forc_file, catids[0] + '.csv'))
46+
start_time = pd.to_datetime(df1['Time'].iloc[0],format="%Y-%m-%d %H:%M:%S")
47+
end_time = pd.to_datetime(df1['Time'].iloc[-1],format="%Y-%m-%d %H:%M:%S")
48+
49+
# update realization file for forcing
50+
real_config['global']['forcing'] = dict([
51+
('file_pattern', '.*{{id}}.*.csv'),
52+
('path', str(forc_file)),
53+
('provider', 'CsvPerFeature')])
54+
55+
elif forc_file.is_file():
56+
# read start and end times from netcdf file
57+
try:
58+
with netCDF4.Dataset(forc_file, 'r') as ncvar:
59+
t0 = pd.to_datetime(ncvar.model_initialization_time, format="%Y-%m-%d_%H:%M:%S")
60+
times = [t1 for t1 in ncvar['Time']]
61+
start_time = t0 + pd.Timedelta(seconds=3600)
62+
end_time = t0 + pd.Timedelta(seconds=(times[-1] - times[0] + 60) * 60)
63+
64+
# update forcing in realization file
65+
real_config['global']['forcing'] = dict([('path', str(forc_file)), ('provider', 'NetCDF')])
66+
67+
except Exception:
68+
logger.error(f'{forc_file} is not a valid NetCDF file')
69+
else:
70+
raise Exception(f'{forc_file} must be a valid NetCDF file or a folder containing a csv file for each catchment in {gpkg_file}')
71+
72+
logger.info(f'Start time: {start_time}')
73+
logger.info(f'End time: {end_time}')
74+
75+
# update time period in realization file
76+
real_config['time']['start_time'] = str(start_time)
77+
real_config['time']['end_time'] = str(end_time)
78+
79+
return real_config

python/read_output.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import json
2+
from pathlib import Path
3+
4+
import geopandas as gpd
5+
import pandas as pd
6+
import netCDF4
7+
8+
def read_troute_output(
9+
gage0: str,
10+
cwt_file:Path,
11+
gpkg_file: Path,
12+
out_file:Path,
13+
) -> pd.DataFrame:
14+
15+
"""
16+
Arguments:
17+
---------
18+
gage0: gage ID to retrieve streamflow simulations for
19+
cwt_file: path to crosswalk file mapping gage to catchments
20+
gpkg_file: path to geopackage file
21+
out_file: path to t-route output file (in NetCDF format)
22+
23+
Returns:
24+
---------
25+
dataframe containing time and streamflow simulations
26+
27+
"""
28+
# Handle crosswalk file (in order to get the correct feature_id when reading t-route data)
29+
x_walk = pd.Series(dtype=object)
30+
try:
31+
with open(cwt_file) as fp:
32+
data = json.load(fp)
33+
for id, values in data.items():
34+
gage = values.get('Gage_no')
35+
if gage:
36+
if not isinstance(gage, str):
37+
gage = gage[0]
38+
if gage==gage0:
39+
x_walk[id] = gage
40+
break
41+
except FileNotFoundError:
42+
raise FileNotFoundError(f"Crosswalk file '{cwt_file}' not found.")
43+
except json.JSONDecodeError:
44+
raise ValueError(f"Failed to parse JSON from crosswalk file '{cwt_file}'.")
45+
46+
if x_walk.empty:
47+
raise Exception(f'{gage0} is not found in crosswalk file {cwt_file}')
48+
49+
# get catchment at basin outlet for reading from t-route output
50+
catchment_hydro_fabric = gpd.read_file(gpkg_file, layer='divides')
51+
catchment_hydro_fabric.set_index('id', inplace=True)
52+
nexus_id = catchment_hydro_fabric.loc[x_walk.index[0].replace('cat', 'wb')]['toid']
53+
wb_lst = [x.split('-')[1] for x in list(catchment_hydro_fabric.query('toid==@nexus_id').index)]
54+
55+
# read troute output
56+
ncvar = netCDF4.Dataset(out_file, "r")
57+
fid_index = [list(ncvar['feature_id'][0:]).index(int(fid)) for fid in wb_lst]
58+
output = pd.DataFrame(data={'sim_flow': pd.DataFrame(ncvar['flow'][fid_index], index=fid_index).T.sum(axis=1)})
59+
t0 = pd.to_datetime(ncvar.file_reference_time, format="%Y-%m-%d_%H:%M:%S")
60+
output.index = [t0 + pd.Timedelta(seconds=int(t1)) for t1 in ncvar['time']]
61+
output.index.name = 'Time'
62+
63+
return output

0 commit comments

Comments
 (0)