From aafe8652d8448e3da335f820dd30a4589200eb4d Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Thu, 8 Dec 2022 20:32:57 +0000 Subject: [PATCH 01/54] initial commit --- .../troute/HYFeaturesNetwork.py | 78 +- .../troute/abstractnetwork_preprocess.py | 752 ++++++++++++++++++ .../troute/hyfeature_preprocess.py | 109 ++- 3 files changed, 873 insertions(+), 66 deletions(-) create mode 100644 src/troute-network/troute/abstractnetwork_preprocess.py diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index cd1791761..a2d085872 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -16,19 +16,6 @@ __verbose__ = False __showtiming__ = False -def node_key_func_nexus(x): - return int(x[4:]) - -def node_key_func_wb(x): - return int(x[3:]) - -def numeric_id(flowpath): - id = flowpath['id'].split('-')[-1] - toid = flowpath['toid'].split('-')[-1] - flowpath['id'] = int(id) - flowpath['toid'] = int(toid) - return flowpath - def read_qlats(forcing_parameters, segment_index, nexus_to_downstream_flowpath_dict): # STEP 5: Read (or set) QLateral Inputs if __showtiming__: @@ -111,54 +98,6 @@ def read_qlats(forcing_parameters, segment_index, nexus_to_downstream_flowpath_d return qlat_df -def read_nexus_file(nexus_file_path): - - #Currently reading data in format: - #[ - #{ - #"ID": "wb-44", - #"toID": "nex-45" - #}, - with open(nexus_file_path) as data_file: - json_data_list = json.load(data_file) - - nexus_to_downstream_flowpath_dict_str = {} - - for id_dict in json_data_list: - if "nex" in id_dict['ID']: - nexus_to_downstream_flowpath_dict_str[id_dict['ID']] = id_dict['toID'] - - # Extract the ID integer values - nexus_to_downstream_flowpath_dict = {node_key_func_nexus(k): node_key_func_wb(v) for k, v in nexus_to_downstream_flowpath_dict_str.items()} - - return nexus_to_downstream_flowpath_dict - -def read_json(file_path, edge_list): - dfs = [] - with open(edge_list) as edge_file: - edge_data = json.load(edge_file) - edge_map = {} - for id_dict in edge_data: - edge_map[ id_dict['id'] ] = id_dict['toid'] - with open(file_path) as data_file: - json_data = json.load(data_file) - for key_wb, value_params in json_data.items(): - df = pd.json_normalize(value_params) - df['id'] = key_wb - df['toid'] = edge_map[key_wb] - dfs.append(df) - df_main = pd.concat(dfs, ignore_index=True) - - return df_main - -def read_geopkg(file_path): - flowpaths = gpd.read_file(file_path, layer="flowpaths") - attributes = gpd.read_file(file_path, layer="flowpath_attributes").drop('geometry', axis=1) - #merge all relevant data into a single dataframe - flowpaths = pd.merge(flowpaths, attributes, on='id') - - return flowpaths - class HYFeaturesNetwork(AbstractNetwork): """ @@ -188,6 +127,23 @@ def __init__(self, print("creating supernetwork connections set") if __showtiming__: start_time = time.time() + + #------------------------------------------------ + # Load Geo File + #------------------------------------------------ + (self._dataframe, + self._flowpath_dict, + self._waterbody_df, + self._waterbody_types_df, + ) = hyfeature_prep.read_geo_file( + supernetwork_parameters, + waterbody_parameters, + ) + + + + + #------------------------------------------------ # Preprocess network attributes diff --git a/src/troute-network/troute/abstractnetwork_preprocess.py b/src/troute-network/troute/abstractnetwork_preprocess.py new file mode 100644 index 000000000..be1bc9734 --- /dev/null +++ b/src/troute-network/troute/abstractnetwork_preprocess.py @@ -0,0 +1,752 @@ +import json +import pathlib +from functools import partial, reduce +from itertools import chain +from datetime import datetime, timedelta +from collections import defaultdict, deque +import logging +import os +import time + +import pandas as pd +import numpy as np +import netCDF4 +from joblib import delayed, Parallel +import pyarrow as pa +import pyarrow.parquet as pq + +import troute.nhd_io as nhd_io +import troute.nhd_network as nhd_network +import troute.nhd_network_utilities_v02 as nnu + + +LOG = logging.getLogger('') + +def build_diffusive_domain( + compute_parameters, + connections, +): + + hybrid_params = compute_parameters.get("hybrid_parameters", False) + if hybrid_params: + # switch parameters + # if run_hybrid = False, run MC only + # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc + # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric + # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric + run_hybrid = hybrid_params.get('run_hybrid_routing', False) + use_topobathy = hybrid_params.get('use_natl_xsections', False) + run_refactored = hybrid_params.get('run_refactored_network', False) + + # file path parameters of non-refactored hydrofabric defined by RouteLink.nc + domain_file = hybrid_params.get("diffusive_domain", None) + topobathy_file = hybrid_params.get("topobathy_domain", None) + + # file path parameters of refactored hydrofabric for diffusive wave channel routing + refactored_domain_file = hybrid_params.get("refactored_domain", None) + refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) + #------------------------------------------------------------------------- + # for non-refactored hydofabric defined by RouteLink.nc + # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. + if run_hybrid and domain_file: + + LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') + + # read diffusive domain dictionary from yaml or json + diffusive_domain = nhd_io.read_diffusive_domain(domain_file) + + if use_topobathy and topobathy_file: + + LOG.debug('Natural cross section data on original hydrofabric are provided.') + + # read topobathy domain netcdf file, set index to 'comid' + # TODO: replace 'link' with a user-specified indexing variable name. + # ... if for whatever reason there is not a `link` variable in the + # ... dataframe returned from read_netcdf, then the code would break here. + topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) + + # TODO: Request GID make comID variable an integer in their product, so + # we do not need to change variable types, here. + topobathy_df.index = topobathy_df.index.astype(int) + + else: + topobathy_df = pd.DataFrame() + LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') + + # initialize a dictionary to hold network data for each of the diffusive domains + diffusive_network_data = {} + + else: + diffusive_domain = None + diffusive_network_data = None + topobathy_df = pd.DataFrame() + LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') + unrefactored_topobathy_df = pd.DataFrame() + #------------------------------------------------------------------------- + # for refactored hydofabric + if run_hybrid and run_refactored and refactored_domain_file: + + LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') + + # read diffusive domain dictionary from yaml or json + refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) + + if use_topobathy and refactored_topobathy_file: + + LOG.debug('Natural cross section data of refactored hydrofabric are provided.') + + # read topobathy domain netcdf file, set index to 'comid' + # TODO: replace 'link' with a user-specified indexing variable name. + # ... if for whatever reason there is not a `link` variable in the + # ... dataframe returned from read_netcdf, then the code would break here. + topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) + + # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy + # for crosswalking water elevations between non-refactored and refactored hydrofabrics. + unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) + unrefactored_topobathy_df.index = unrefactored_topobathy_df.index.astype(int) + + else: + topobathy_df = pd.DataFrame() + LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') + + # initialize a dictionary to hold network data for each of the diffusive domains + refactored_diffusive_network_data = {} + + else: + refactored_diffusive_domain = None + refactored_diffusive_network_data = None + refactored_reaches = {} + LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') + + else: + diffusive_domain = None + diffusive_network_data = None + topobathy_df = pd.DataFrame() + unrefactored_topobathy_df = pd.DataFrame() + refactored_diffusive_domain = None + refactored_diffusive_network_data = None + refactored_reaches = {} + LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') + + #============================================================================ + # build diffusive domain data and edit MC domain data for hybrid simulation + + # + if diffusive_domain: + rconn_diff0 = nhd_network.reverse_network(connections) + refactored_reaches = {} + + for tw in diffusive_domain: + mainstem_segs = diffusive_domain[tw]['links'] + # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is + # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. + upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] + if upstream_boundary_mainstem_link[0] in mainstem_segs: + mainstem_segs.remove(upstream_boundary_mainstem_link[0]) + + # ===== build diffusive network data objects ==== + diffusive_network_data[tw] = {} + + # add diffusive domain segments + diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs + + # diffusive domain tributary segments + trib_segs = [] + + for seg in mainstem_segs: + us_list = rconn_diff0[seg] + for u in us_list: + if u not in mainstem_segs: + trib_segs.append(u) + + diffusive_network_data[tw]['tributary_segments'] = trib_segs + # diffusive domain connections object + diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, reaches, rconn_diff = nnu.organize_independent_networks( + diffusive_network_data[tw]['connections'], + set(trib_segs), + set(), + ) + + diffusive_network_data[tw]['rconn'] = rconn_diff + diffusive_network_data[tw]['reaches'] = reaches[tw] + + # RouteLink parameters + diffusive_network_data[tw]['param_df'] = param_df.filter( + (mainstem_segs + trib_segs), + axis = 0, + ) + diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link + + if refactored_diffusive_domain: + diffusive_parameters = {'geo_file_path': refactored_topobathy_file} + refactored_connections = nnu.build_refac_connections(diffusive_parameters) + + # list of stream segments of a single refactored diffusive domain + refac_tw = refactored_diffusive_domain[tw]['refac_tw'] + rlinks_tw = refactored_diffusive_domain[tw]['rlinks'] + refactored_connections_tw = {} + + # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections + # for a single refactored diffusive domain defined by a current tw. + for k in rlinks_tw: + if k in refactored_connections.keys() and k != refac_tw: + refactored_connections_tw[k] = refactored_connections[k] + + refactored_diffusive_network_data[refac_tw] = {} + refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs + refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw + + for k in trib_segs: + refactored_diffusive_network_data[refac_tw]['connections'][k]= [refactored_diffusive_domain[tw]['incoming_tribs'][k]] + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, refactored_reaches_batch, refactored_conn_diff = nnu.organize_independent_networks( + refactored_diffusive_network_data[refac_tw]['connections'], + set(trib_segs), + set(), + ) + + refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] + refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = refactored_diffusive_domain[tw]['rlinks'] + refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = diffusive_network_data[tw]['upstream_boundary_link'] + else: + refactored_reaches={} + + # ==== remove diffusive domain segs from MC domain ==== + # drop indices from param_df + param_df = param_df.drop(mainstem_segs) + + # remove keys from connections dictionary + for s in mainstem_segs: + connections.pop(s) + + # update downstream connections of trib segs + for us in trib_segs: + connections[us] = [] + + return ( + param_df, + connections, + diffusive_network_data, + topobathy_df, + refactored_diffusive_domain, + refactored_reaches, + unrefactored_topobathy_df + ) + +def create_independent_networks( + waterbody_parameters, + connections, + wbody_conn, + gages, + ): + LOG.info("organizing connections into reaches ...") + start_time = time.time() + gage_break_segments = set() + wbody_break_segments = set() + + break_network_at_waterbodies = waterbody_parameters.get( + "break_network_at_waterbodies", False + ) + + # if streamflow DA, then break network at gages + break_network_at_gages = False + + if break_network_at_waterbodies: + wbody_break_segments = wbody_break_segments.union(wbody_conn.values()) + + if break_network_at_gages: + gage_break_segments = gage_break_segments.union(gages['gages'].keys()) + + independent_networks, reaches_bytw, rconn = nnu.organize_independent_networks( + connections, + wbody_break_segments, + gage_break_segments, + ) + + LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) + + return independent_networks, reaches_bytw, rconn + +def hyfeature_initial_warmstate_preprocess( + break_network_at_waterbodies, + restart_parameters, + segment_index, + waterbodies_df, + ): + + ''' + Assemble model initial condition data: + - waterbody inital states (outflow and pool elevation) + - channel initial states (flow and depth) + - initial time + + Arguments + --------- + - break_network_at_waterbodies (bool): If True, waterbody initial states will + be appended to the waterbody parameter + dataframe. If False, waterbodies will + not be simulated and the waterbody + parameter datataframe wil not be changed + - restart_parameters (dict): User-input simulation restart + parameters + - segment_index (Pandas Index): All segment IDs in the simulation + doamin + - waterbodies_df (Pandas DataFrame): Waterbody parameters + + Returns + ------- + - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial + states (outflow and pool elevation) + - q0 (Pandas DataFrame): Initial flow and depth states for each + segment in the model domain + - t0 (datetime): Datetime of the model initialization + + Notes + ----- + ''' + + #---------------------------------------------------------------------------- + # Assemble waterbody initial states (outflow and pool elevation + #---------------------------------------------------------------------------- + if break_network_at_waterbodies: + + start_time = time.time() + LOG.info("setting waterbody initial states ...") + + # if a lite restart file is provided, read initial states from it. + if restart_parameters.get("lite_waterbody_restart_file", None): + + waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( + restart_parameters['lite_waterbody_restart_file'] + ) + + # read waterbody initial states from WRF-Hydro type restart file + elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): + waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( + restart_parameters["wrf_hydro_waterbody_restart_file"], + restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], + restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", 'lake_id'), + restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], + restart_parameters.get( + "wrf_hydro_waterbody_crosswalk_filter_file_field_name", + 'NHDWaterbodyComID' + ), + ) + + # if no restart file is provided, default initial states + else: + # TODO: Consider adding option to read cold state from route-link file + waterbodies_initial_ds_flow_const = 0.0 + waterbodies_initial_depth_const = -1e9 + # Set initial states from cold-state + waterbodies_initial_states_df = pd.DataFrame( + 0, + index=waterbodies_df.index, + columns=[ + "qd0", + "h0", + ], + dtype="float32", + ) + # TODO: This assignment could probably by done in the above call + waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const + waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const + waterbodies_initial_states_df["index"] = range( + len(waterbodies_initial_states_df) + ) + + waterbodies_df = pd.merge( + waterbodies_df, waterbodies_initial_states_df, on="lake_id" + ) + + LOG.debug( + "waterbody initial states complete in %s seconds."\ + % (time.time() - start_time)) + start_time = time.time() + + #---------------------------------------------------------------------------- + # Assemble channel initial states (flow and depth) + # also establish simulation initialization timestamp + #---------------------------------------------------------------------------- + start_time = time.time() + LOG.info("setting channel initial states ...") + + # if lite restart file is provided, the read channel initial states from it + if restart_parameters.get("lite_channel_restart_file", None): + # FIXME: Change it for hyfeature! + q0, t0 = nhd_io.read_lite_restart( + restart_parameters['lite_channel_restart_file'] + ) + t0_str = None + + # when a restart file for hyfeature is provied, then read initial states from it. + elif restart_parameters.get("hyfeature_channel_restart_file", None): + q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) + channel_initial_states_file = restart_parameters["hyfeature_channel_restart_file"] + df = pd.read_csv(channel_initial_states_file) + t0_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + t0 = datetime.strptime(t0_str,"%Y-%m-%d_%H:%M:%S") + + # build initial states from user-provided restart parameters + else: + # FIXME: Change it for hyfeature! + q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) + + # get initialization time from restart file + if restart_parameters.get("wrf_hydro_channel_restart_file", None): + channel_initial_states_file = restart_parameters[ + "wrf_hydro_channel_restart_file" + ] + t0_str = nhd_io.get_param_str( + channel_initial_states_file, + "Restart_Time" + ) + else: + t0_str = "2015-08-16_00:00:00" + + # convert timestamp from string to datetime + t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") + + # get initial time from user inputs + if restart_parameters.get("start_datetime", None): + t0_str = restart_parameters.get("start_datetime") + + def _try_parsing_date(text): + for fmt in ( + "%Y-%m-%d_%H:%M", + "%Y-%m-%d_%H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%d %H:%M:%S", + "%Y/%m/%d %H:%M", + "%Y/%m/%d %H:%M:%S" + ): + try: + return datetime.strptime(text, fmt) + except ValueError: + pass + LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') + quit() + + t0 = _try_parsing_date(t0_str) + else: + if t0_str == "2015-08-16_00:00:00": + LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') + else: + LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) + + LOG.debug( + "channel initial states complete in %s seconds."\ + % (time.time() - start_time) + ) + start_time = time.time() + + return ( + waterbodies_df, + q0, + t0, + ) + # TODO: This returns a full dataframe (waterbodies_df) with the + # merged initial states for waterbodies, but only the + # initial state values (q0; not merged with the channel properties) + # for the channels -- + # That is because that is how they are used downstream. Need to + # trace that back and decide if there is one of those two ways + # that is optimal and make both returns that way. + +def build_forcing_sets( + supernetwork_parameters, + forcing_parameters, + t0, + ): + + run_sets = forcing_parameters.get("qlat_forcing_sets", None) + qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) + nts = forcing_parameters.get("nts", None) + max_loop_size = forcing_parameters.get("max_loop_size", 12) + dt = forcing_parameters.get("dt", None) + + geo_file_type = supernetwork_parameters.get('geo_file_type') + + try: + qlat_input_folder = pathlib.Path(qlat_input_folder) + assert qlat_input_folder.is_dir() == True + except TypeError: + raise TypeError("Aborting simulation because no qlat_input_folder is specified in the forcing_parameters section of the .yaml control file.") from None + except AssertionError: + raise AssertionError("Aborting simulation because the qlat_input_folder:", qlat_input_folder,"does not exist. Please check the the nexus_input_folder variable is correctly entered in the .yaml control file") from None + + forcing_glob_filter = forcing_parameters.get("qlat_file_pattern_filter", "*.NEXOUT") + + if forcing_glob_filter=="nex-*": + print("Reformating qlat nexus files as hourly binary files...") + binary_folder = forcing_parameters.get('binary_nexus_file_folder', None) + qlat_files = qlat_input_folder.glob(forcing_glob_filter) + + #Check that directory/files specified will work + if not binary_folder: + raise(RuntimeError("No output binary qlat folder supplied in config")) + elif not os.path.exists(binary_folder): + raise(RuntimeError("Output binary qlat folder supplied in config does not exist")) + elif len(list(pathlib.Path(binary_folder).glob('*.parquet'))) != 0: + raise(RuntimeError("Output binary qlat folder supplied in config is not empty (already contains '.parquet' files)")) + + #Add tnx for backwards compatability + qlat_files_list = list(qlat_files) + list(qlat_input_folder.glob('tnx*.csv')) + #Convert files to binary hourly files, reset nexus input information + qlat_input_folder, forcing_glob_filter = nex_files_to_binary(qlat_files_list, binary_folder) + forcing_parameters["qlat_input_folder"] = qlat_input_folder + forcing_parameters["qlat_file_pattern_filter"] = forcing_glob_filter + + # TODO: Throw errors if insufficient input data are available + if run_sets: + #FIXME: Change it for hyfeature + ''' + # append final_timestamp variable to each set_list + qlat_input_folder = pathlib.Path(qlat_input_folder) + for (s, _) in enumerate(run_sets): + final_chrtout = qlat_input_folder.joinpath(run_sets[s]['qlat_files' + ][-1]) + final_timestamp_str = nhd_io.get_param_str(final_chrtout, + 'model_output_valid_time') + run_sets[s]['final_timestamp'] = \ + datetime.strptime(final_timestamp_str, '%Y-%m-%d_%H:%M:%S') + ''' + elif qlat_input_folder: + # Construct run_set dictionary from user-specified parameters + + # get the first and seconded files from an ordered list of all forcing files + qlat_input_folder = pathlib.Path(qlat_input_folder) + all_files = sorted(qlat_input_folder.glob(forcing_glob_filter)) + first_file = all_files[0] + second_file = all_files[1] + + # Deduce the timeinterval of the forcing data from the output timestamps of the first + # two ordered CHRTOUT files + if geo_file_type=='HYFeaturesNetowrk': + df = read_file(first_file) + t1_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + t1 = datetime.strptime(t1_str,"%Y-%m-%d_%H:%M:%S") + df = read_file(second_file) + t2_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + t2 = datetime.strptime(t2_str,"%Y-%m-%d_%H:%M:%S") + elif geo_file_type=='NHDNetwork': + t1 = nhd_io.get_param_str(first_file, "model_output_valid_time") + t1 = datetime.strptime(t1, "%Y-%m-%d_%H:%M:%S") + t2 = nhd_io.get_param_str(second_file, "model_output_valid_time") + t2 = datetime.strptime(t2, "%Y-%m-%d_%H:%M:%S") + + dt_qlat_timedelta = t2 - t1 + dt_qlat = dt_qlat_timedelta.seconds + + # determine qts_subdivisions + qts_subdivisions = dt_qlat / dt + if dt_qlat % dt == 0: + qts_subdivisions = dt_qlat / dt + # make sure that qts_subdivisions = dt_qlat / dt + forcing_parameters['qts_subdivisions']= qts_subdivisions + + # the number of files required for the simulation + nfiles = int(np.ceil(nts / qts_subdivisions)) + + # list of forcing file datetimes + #datetime_list = [t0 + dt_qlat_timedelta * (n + 1) for n in + # range(nfiles)] + # ** Correction ** Because qlat file at time t is constantly applied throughout [t, t+1], + # ** n + 1 should be replaced by n + datetime_list = [t0 + dt_qlat_timedelta * (n) for n in + range(nfiles)] + datetime_list_str = [datetime.strftime(d, '%Y%m%d%H%M') for d in + datetime_list] + + # list of forcing files + forcing_filename_list = [d_str + forcing_glob_filter[1:] for d_str in + datetime_list_str] + + # check that all forcing files exist + for f in forcing_filename_list: + try: + J = pathlib.Path(qlat_input_folder.joinpath(f)) + assert J.is_file() == True + except AssertionError: + raise AssertionError("Aborting simulation because forcing file", J, "cannot be not found.") from None + + # build run sets list + run_sets = [] + k = 0 + j = 0 + nts_accum = 0 + nts_last = 0 + while k < len(forcing_filename_list): + run_sets.append({}) + + if k + max_loop_size < len(forcing_filename_list): + run_sets[j]['qlat_files'] = forcing_filename_list[k:k + + max_loop_size] + else: + run_sets[j]['qlat_files'] = forcing_filename_list[k:] + + nts_accum += len(run_sets[j]['qlat_files']) * qts_subdivisions + if nts_accum <= nts: + run_sets[j]['nts'] = int(len(run_sets[j]['qlat_files']) + * qts_subdivisions) + else: + run_sets[j]['nts'] = int(nts - nts_last) + + final_qlat = qlat_input_folder.joinpath(run_sets[j]['qlat_files'][-1]) + if geo_file_type=='NHDNetwork': + final_timestamp_str = nhd_io.get_param_str(final_qlat,'model_output_valid_time') + elif geo_file_type=='HYFeaturesNetowrk': + df = read_file(final_qlat) + final_timestamp_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + + run_sets[j]['final_timestamp'] = \ + datetime.strptime(final_timestamp_str, '%Y-%m-%d_%H:%M:%S') + + nts_last = nts_accum + k += max_loop_size + j += 1 + + return run_sets + +def build_qlateral_array( + run, + cpu_pool, + nexus_to_upstream_flowpath_dict, + supernetwork_parameters, + segment_index=pd.Index([]), +): + # TODO: set default/optional arguments + qts_subdivisions = run.get("qts_subdivisions", 1) + nts = run.get("nts", 1) + qlat_input_folder = run.get("qlat_input_folder", None) + qlat_input_file = run.get("qlat_input_file", None) + + geo_file_type = supernetwork_parameters.get('geo_file_type') + + if qlat_input_folder: + qlat_input_folder = pathlib.Path(qlat_input_folder) + if "qlat_files" in run: + qlat_files = run.get("qlat_files") + qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] + elif "qlat_file_pattern_filter" in run: + qlat_file_pattern_filter = run.get( + "qlat_file_pattern_filter", "*CHRT_OUT*" + ) + qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) + + qlat_file_index_col = run.get( + "qlat_file_index_col", "feature_id" + ) + qlat_file_value_col = run.get("qlat_file_value_col", "q_lateral") + gw_bucket_col = run.get("qlat_file_gw_bucket_flux_col","qBucket") + terrain_ro_col = run.get("qlat_file_terrain_runoff_col","qSfcLatRunoff") + + if geo_file_type=='NHDNetwork': + # Parallel reading of qlateral data from CHRTOUT + with Parallel(n_jobs=cpu_pool) as parallel: + jobs = [] + for f in qlat_files: + jobs.append( + #delayed(nhd_io.get_ql_from_chrtout) + #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) + delayed(nhd_io.get_ql_from_csv) + (f) + ) + ql_list = parallel(jobs) + + # get feature_id from a single CHRTOUT file + with netCDF4.Dataset(qlat_files[0]) as ds: + idx = ds.variables[qlat_file_index_col][:].filled() + + # package data into a DataFrame + qlats_df = pd.DataFrame( + np.stack(ql_list).T, + index = idx, + columns = range(len(qlat_files)) + ) + elif geo_file_type=='HYFeaturesNetowrk': + dfs=[] + for f in qlat_files: + df = read_file(f).set_index(['feature_id']) + dfs.append(df) + + # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids + nexuses_lateralflows_df = pd.concat(dfs, axis=1) + + # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids + qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) + for k,v in nexus_to_upstream_flowpath_dict.items() ), axis=1 + ).T + qlats_df.columns=range(len(qlat_files)) + + qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + elif qlat_input_file: + qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) + else: + qlat_const = run.get("qlat_const", 0) + qlats_df = pd.DataFrame( + qlat_const, + index=segment_index, + columns=range(nts // qts_subdivisions), + dtype="float32", + ) + + # TODO: Make a more sophisticated date-based filter + max_col = 1 + nts // qts_subdivisions + if len(qlats_df.columns) > max_col: + qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) + + if not segment_index.empty: + qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + + return qlats_df + +def nex_files_to_binary(nexus_files, binary_folder): + for f in nexus_files: + # read the csv file + df = pd.read_csv(f, usecols=[1,2], names=['Datetime','qlat']) + + # convert and reformat datetime column + df['Datetime']= pd.to_datetime(df['Datetime']).dt.strftime("%Y%m%d%H%M") + + # reformat the dataframe + df['feature_id'] = get_id_from_filename(f) + df = df.pivot(index="feature_id", columns="Datetime", values="qlat") + df.columns.name = None + + for col in df.columns: + table_new = pa.Table.from_pandas(df.loc[:, [col]]) + + if not os.path.exists(f'{binary_folder}/{col}NEXOUT.parquet'): + pq.write_table(table_new, f'{binary_folder}/{col}NEXOUT.parquet') + + else: + table_old = pq.read_table(f'{binary_folder}/{col}NEXOUT.parquet') + table = pa.concat_tables([table_old,table_new]) + pq.write_table(table, f'{binary_folder}/{col}NEXOUT.parquet') + + nexus_input_folder = binary_folder + forcing_glob_filter = '*NEXOUT.parquet' + + return nexus_input_folder, forcing_glob_filter + +def get_id_from_filename(file_name): + id = os.path.splitext(file_name)[0].split('-')[1].split('_')[0] + return int(id) + +def read_file(file_name): + extension = file_name.suffix + if extension=='.csv': + df = pd.read_csv(file_name) + elif extension=='.parquet': + df = pq.read_table(file_name).to_pandas().reset_index() + df.index.name = None + + return df \ No newline at end of file diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index 446f356df..2dbfa9da7 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -15,11 +15,77 @@ import troute.nhd_network as nhd_network import troute.nhd_io as nhd_io from troute.nhd_network import reverse_dict -import troute.HYFeaturesNetwork as hyf_network import troute.hyfeature_network_utilities as hnu LOG = logging.getLogger('') +def read_geo_file( + supernetwork_parameters, + waterbody_parameters, +): + + geo_file_path = supernetwork_parameters["geo_file_path"] + + file_type = Path(geo_file_path).suffix + if( file_type == '.gpkg' ): + dataframe = read_geopkg(geo_file_path) + elif( file_type == '.json') : + edge_list = supernetwork_parameters['flowpath_edge_list'] + dataframe = read_json(geo_file_path, edge_list) + else: + raise RuntimeError("Unsupported file type: {}".format(file_type)) + + # Don't need the string prefix anymore, drop it + mask = ~ dataframe['toid'].str.startswith("tnex") + dataframe = dataframe.apply(numeric_id, axis=1) + + # make the flowpath linkage, ignore the terminal nexus + flowpath_dict = dict(zip(dataframe.loc[mask].toid, dataframe.loc[mask].id)) + + # ********** need to be included in flowpath_attributes ************* + dataframe['alt'] = 1.0 #FIXME get the right value for this... + + #Load waterbody/reservoir info + if waterbody_parameters: + levelpool_params = waterbody_parameters.get('level_pool', None) + if not levelpool_params: + # FIXME should not be a hard requirement + raise(RuntimeError("No supplied levelpool parameters in routing config")) + + lake_id = levelpool_params.get("level_pool_waterbody_id", "wb-id") + waterbody_df = read_ngen_waterbody_df( + levelpool_params["level_pool_waterbody_parameter_file_path"], + lake_id, + ) + + # Remove duplicate lake_ids and rows + waterbody_df = ( + waterbody_df.reset_index() + .drop_duplicates(subset=lake_id) + .set_index(lake_id) + ) + + try: + waterbody_types_df = read_ngen_waterbody_type_df( + levelpool_params["reservoir_parameter_file"], + lake_id, + #self.waterbody_connections.values(), + ) + # Remove duplicate lake_ids and rows + waterbody_types_df =( + waterbody_types_df.reset_index() + .drop_duplicates(subset=lake_id) + .set_index(lake_id) + ) + + except ValueError: + #FIXME any reservoir operations requires some type + #So make this default to 1 (levelpool) + waterbody_types_df = pd.DataFrame(index=waterbody_df.index) + waterbody_types_df['reservoir_type'] = 1 + + return dataframe, flowpath_dict, waterbody_df, waterbody_types_df + def build_hyfeature_network(supernetwork_parameters, waterbody_parameters, ): @@ -764,7 +830,7 @@ def hyfeature_forcing( def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): """ - Reads lake.json file and prepares a dataframe, filtered + Reads .gpkg or lake.json file and prepares a dataframe, filtered to the relevant reservoirs, to provide the parameters for level-pool reservoir computation. """ @@ -777,8 +843,7 @@ def node_key_func(x): df.index = df.index.map(node_key_func) df.index.name = lake_index_field - #df = df.set_index(lake_index_field, append=True).reset_index(level=0) - #df.rename(columns={'level_0':'wb-id'}, inplace=True) + if lake_id_mask: df = df.loc[lake_id_mask] return df @@ -803,4 +868,38 @@ def node_key_func(x): if lake_id_mask: df = df.loc[lake_id_mask] - return df \ No newline at end of file + return df + +def read_geopkg(file_path): + flowpaths = gpd.read_file(file_path, layer="flowpaths") + attributes = gpd.read_file(file_path, layer="flowpath_attributes").drop('geometry', axis=1) + #merge all relevant data into a single dataframe + flowpaths = pd.merge(flowpaths, attributes, on='id') + + return flowpaths + +def read_json(file_path, edge_list): + dfs = [] + with open(edge_list) as edge_file: + edge_data = json.load(edge_file) + edge_map = {} + for id_dict in edge_data: + edge_map[ id_dict['id'] ] = id_dict['toid'] + with open(file_path) as data_file: + json_data = json.load(data_file) + for key_wb, value_params in json_data.items(): + df = pd.json_normalize(value_params) + df['id'] = key_wb + df['toid'] = edge_map[key_wb] + dfs.append(df) + df_main = pd.concat(dfs, ignore_index=True) + + return df_main + +def numeric_id(flowpath): + id = flowpath['id'].split('-')[-1] + toid = flowpath['toid'].split('-')[-1] + flowpath['id'] = int(id) + flowpath['toid'] = int(toid) + + return flowpath \ No newline at end of file From d91071849d358bb101f2a06212c8b556e5ab7861 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 9 Dec 2022 18:45:05 +0000 Subject: [PATCH 02/54] updated Abstract and HyFeatures network objects to use new functions --- src/troute-network/troute/AbstractNetwork.py | 172 +++++++++++- .../troute/HYFeaturesNetwork.py | 262 +----------------- .../troute/abstractnetwork_preprocess.py | 11 +- src/troute-nwm/src/nwm_routing/__main__.py | 50 ++-- 4 files changed, 216 insertions(+), 279 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index e0745f5fb..94594b6da 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -5,7 +5,8 @@ import time from troute.nhd_network import reverse_dict, extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition - +import troute.nhd_io as nhd_io +import troute.abstractnetwork_preprocess as abs_prep __verbose__ = False __showtiming__ = False @@ -19,7 +20,18 @@ class AbstractNetwork(ABC): "_reaches_by_tw", "_reverse_network", "_q0", "_t0", "_qlateral", "_break_segments", "_coastal_boundary_depth_df"] - def __init__(self, cols=None, terminal_code=None, break_points=None, verbose=False, showtiming=False): + def __init__( + self, + compute_parameters, + waterbody_parameters, + restart_parameters, + cols=None, + terminal_code=None, + break_points=None, + verbose=False, + showtiming=False + ): + global __verbose__, __showtiming__ __verbose__ = verbose __showtiming__ = showtiming @@ -44,7 +56,7 @@ def __init__(self, cols=None, terminal_code=None, break_points=None, verbose=Fal self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) self.set_index("key") self.sort_index() - self._waterbody_connections = None + self._waterbody_connections = {} self._gages = None self._connections = None self._independent_networks = None @@ -83,6 +95,160 @@ def __init__(self, cols=None, terminal_code=None, break_points=None, verbose=Fal self._break_segments = self._break_segments | set(self.waterbody_connections.values()) if break_points["break_network_at_gages"]: self._break_segments = self._break_segments | set(self.gages.values()) + + self._connections = extract_connections(self._dataframe, 'downstream', self._terminal_codes) + + ( + self._dataframe, + self._connections, + self.diffusive_network_data, + self.topobathy_df, + self.refactored_diffusive_domain, + self.refactored_reaches, + self.unrefactored_topobathy_df + ) = abs_prep.build_diffusive_domain( + compute_parameters, + self._dataframe, + self._connections, + ) + + ( + self._independent_networks, + self._reaches_by_tw, + self._reverse_network + ) = abs_prep.create_independent_networks( + waterbody_parameters, + self._connections, + self._waterbody_connections, + #gages, #TODO update how gages are provided when we figure out DA + ) + + ( + self._waterbody_df, + self._q0, + self._t0 + ) = abs_prep.initial_warmstate_preprocess( + break_points["break_network_at_waterbodies"], + restart_parameters, + self._dataframe.index, + self._waterbody_df, + ) + + def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): + """ + Assemble model forcings. Forcings include hydrological lateral inflows (qlats) + and coastal boundary depths for hybrid runs + + Aguments + -------- + - run (dict): List of forcing files pertaining to a + single run-set + - forcing_parameters (dict): User-input simulation forcing parameters + - hybrid_parameters (dict): User-input simulation hybrid parameters + - supernetwork_parameters (dict): User-input simulation supernetwork parameters + - segment_index (Int64): Reach segment ids + - cpu_pool (int): Number of CPUs in the process-parallel pool + + Returns + ------- + - qlats_df (Pandas DataFrame): Lateral inflow data, indexed by + segment ID + - coastal_bounary_depth_df (Pandas DataFrame): Coastal boundary water depths, + indexed by segment ID + + Notes + ----- + + """ + + # Unpack user-specified forcing parameters + dt = forcing_parameters.get("dt", None) + qts_subdivisions = forcing_parameters.get("qts_subdivisions", None) + qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) + qlat_file_index_col = forcing_parameters.get("qlat_file_index_col", "feature_id") + qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") + qlat_file_gw_bucket_flux_col = forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") + qlat_file_terrain_runoff_col = forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") + + + # TODO: find a better way to deal with these defaults and overrides. + run["t0"] = run.get("t0", self.t0) + run["nts"] = run.get("nts") + run["dt"] = run.get("dt", dt) + run["qts_subdivisions"] = run.get("qts_subdivisions", qts_subdivisions) + run["qlat_input_folder"] = run.get("qlat_input_folder", qlat_input_folder) + run["qlat_file_index_col"] = run.get("qlat_file_index_col", qlat_file_index_col) + run["qlat_file_value_col"] = run.get("qlat_file_value_col", qlat_file_value_col) + run["qlat_file_gw_bucket_flux_col"] = run.get("qlat_file_gw_bucket_flux_col", qlat_file_gw_bucket_flux_col) + run["qlat_file_terrain_runoff_col"] = run.get("qlat_file_terrain_runoff_col", qlat_file_terrain_runoff_col) + + #--------------------------------------------------------------------------- + # Assemble lateral inflow data + #--------------------------------------------------------------------------- + + # Place holder, if reading qlats from a file use this. + # TODO: add an option for reading qlat data from BMI/model engine + from_file = True + if from_file: + self._qlateral = abs_prep.build_qlateral_array( + run, + cpu_pool, + self._flowpath_dict, + supernetwork_parameters, + self._dataframe.index, + ) + + #--------------------------------------------------------------------- + # Assemble coastal coupling data [WIP] + #--------------------------------------------------------------------- + # Run if coastal_boundary_depth_df has not already been created: + if self._coastal_boundary_depth_df.empty: + coastal_boundary_elev_files = forcing_parameters.get('coastal_boundary_input_file', None) + coastal_boundary_domain_files = hybrid_parameters.get('coastal_boundary_domain', None) + + if coastal_boundary_elev_files: + #start_time = time.time() + #LOG.info("creating coastal dataframe ...") + + coastal_boundary_domain = nhd_io.read_coastal_boundary_domain(coastal_boundary_domain_files) + self._coastal_boundary_depth_df = nhd_io.build_coastal_ncdf_dataframe( + coastal_boundary_elev_files, + coastal_boundary_domain, + ) + + #LOG.debug( + # "coastal boundary elevation observation DataFrame creation complete in %s seconds." \ + # % (time.time() - start_time) + #) + + def new_q0(self, run_results): + """ + Prepare a new q0 dataframe with initial flow and depth to act as + a warmstate for the next simulation chunk. + """ + self._q0 = pd.concat( + [ + pd.DataFrame( + r[1][:, [-3, -3, -1]], index=r[0], columns=["qu0", "qd0", "h0"] + ) + for r in run_results + ], + copy=False, + ) + return self._q0 + + def update_waterbody_water_elevation(self): + """ + Update the starting water_elevation of each lake/reservoir + with flow and depth values from q0 + """ + self._waterbody_df.update(self._q0) + + def new_t0(self, dt, nts): + """ + Update t0 value for next loop iteration + """ + self._t0 += timedelta(seconds = dt * nts) @property def network_break_segments(self): diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index a2d085872..5de0729e0 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -16,87 +16,6 @@ __verbose__ = False __showtiming__ = False -def read_qlats(forcing_parameters, segment_index, nexus_to_downstream_flowpath_dict): - # STEP 5: Read (or set) QLateral Inputs - if __showtiming__: - start_time = time.time() - if __verbose__: - print("creating qlateral array ...") - qts_subdivisions = forcing_parameters.get("qts_subdivisions", 1) - nts = forcing_parameters.get("nts", 1) - nexus_input_folder = forcing_parameters.get("nexus_input_folder", None) - nexus_input_folder = pathlib.Path(nexus_input_folder) - #rt0 = time.time() #FIXME showtiming flag - if not "nexus_file_pattern_filter" in forcing_parameters: - raise( RuntimeError("No value for nexus file pattern in config" ) ) - - nexus_file_pattern_filter = forcing_parameters.get( - "nexus_file_pattern_filter", "nex-*" - ) - nexus_files = nexus_input_folder.glob(nexus_file_pattern_filter) - #TODO Find a way to test that we found some files - #without consuming the generator...Otherwise, if nexus_files - #is empty, the following concat raises a ValueError which - #It may be sufficient to catch this exception and warn that - #there may not be any pattern matching files in the dir - - nexus_files_list = list(nexus_files) - - if len(nexus_files_list) == 0: - raise ValueError('No nexus input files found. Recommend checking \ - nexus_input_folder path in YAML configuration.') - - #pd.concat((pd.read_csv(f, index_col=0, usecols=[1,2], header=None, engine='c').rename(columns={2:id_regex.match(f).group(1)}) for f in all_files[0:2]), axis=1).T - id_regex = re.compile(r".*nex-(\d+)_.*.csv") - nexuses_flows_df = pd.concat( - #Read the nexus csv file - (pd.read_csv(f, index_col=0, usecols=[1,2], header=None, engine='c', skipinitialspace=True, parse_dates=True).rename( - #Rename the flow column to the id of the nexus - columns={2:int(id_regex.match(f.name).group(1))}) - for f in nexus_files_list #Build the generator for each required file - ), axis=1).T #Have now concatenated a single df (along axis 1). Transpose it. - missing = nexuses_flows_df[ nexuses_flows_df.isna().any(axis=1) ] - if not missing.empty: - raise ValueError("The following nexus inputs are incomplete: "+str(missing.index)) - rt1 = time.time() - #print("Time to build nexus_flows_df: {} seconds".format(rt1-rt0)) - - qlat_df = pd.concat( (nexuses_flows_df.loc[int(k)].rename(index={int(k):v}) - for k,v in nexus_to_downstream_flowpath_dict.items() ), axis=1 - ).T - #qlat_df = pd.concat( (nexuses_flows_df.loc[int(k)].rename(v) - # for k,v in nexus_to_downstream_flowpath_dict.items() ), axis=1 - # ).T - - # The segment_index has the full network set of segments/flowpaths. - # Whereas the set of flowpaths that are downstream of nexuses is a - # subset of the segment_index. Therefore, all of the segments/flowpaths - # that are not accounted for in the set of flowpaths downstream of - # nexuses need to be added to the qlateral dataframe and padded with - # zeros. - all_df = pd.DataFrame( np.zeros( (len(segment_index), len(qlat_df.columns)) ), index=segment_index, - columns=qlat_df.columns ) - all_df.loc[ qlat_df.index ] = qlat_df - qlat_df = all_df.sort_index() - - # Set new nts based upon total nexus inputs - nts = (qlat_df.shape[1]) * qts_subdivisions - max_col = 1 + nts // qts_subdivisions - - #dt = 300 # [sec] - #dt_qlat = 3600 # [sec] - #nts = 24 # steps - #max_col = math.ceil(nts*dt/dt_qlat) - - if len(qlat_df.columns) > max_col: - qlat_df.drop(qlat_df.columns[max_col:], axis=1, inplace=True) - - if __verbose__: - print("qlateral array complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) - - return qlat_df class HYFeaturesNetwork(AbstractNetwork): """ @@ -114,7 +33,7 @@ def __init__(self, supernetwork_parameters, waterbody_parameters=None, restart_parameters=None, - forcing_parameters=None, + compute_parameters=None, verbose=False, showtiming=False): """ @@ -140,112 +59,30 @@ def __init__(self, waterbody_parameters, ) + cols = supernetwork_parameters.get('columns',None) + terminal_code = supernetwork_parameters.get('terminal_code',0) + break_network_at_waterbodies = supernetwork_parameters.get("break_network_at_waterbodies", False) + break_network_at_gages = supernetwork_parameters.get("break_network_at_gages", False) + break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, + "break_network_at_gages": break_network_at_gages} - - - - - #------------------------------------------------ - # Preprocess network attributes - #------------------------------------------------ - (self._dataframe, - self._flowpath_dict, - self._waterbody_types_df, - self._waterbody_df, - self.waterbody_type_specified, - cols, - terminal_code, - break_points, - ) = hyfeature_prep.build_hyfeature_network( - supernetwork_parameters, - waterbody_parameters - ) - - # called to mainly initialize _waterbody_connections, _connections, _independent_networks, - # _reverse_network, _reaches_by_tw - super().__init__(cols, terminal_code, break_points) + super().__init__( + compute_parameters, + waterbody_parameters, + restart_parameters, + cols, + terminal_code, + break_points + ) if __verbose__: print("supernetwork connections set complete") if __showtiming__: print("... in %s seconds." % (time.time() - start_time)) - - - - # list of all segments in the domain (MC + diffusive) - self.segment_index = self._dataframe.index - #if self.diffusive_network_data: - # for tw in self.diffusive_network_data: - # self.segment_index = self.segment_index.append( - # pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) - # ) - - #------------------------------------------------ - # Handle Channel Initial States - #------------------------------------------------ - if __verbose__: - print("setting waterbody and channel initial states ...") - if __showtiming__: - start_time = time.time() - - (#self._waterbody_df, - self._q0, - self._t0,) = hyfeature_prep.hyfeature_initial_warmstate_preprocess( - #break_network_at_waterbodies, - restart_parameters, - #data_assimilation_parameters, - self.segment_index, - #self._waterbody_df, - #self.link_lake_crosswalk, - ) - - if __verbose__: - print("waterbody and channel initial states complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) - start_time = time.time() # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't self._coastal_boundary_depth_df = pd.DataFrame() - - - def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, cpu_pool): - """ - Assembles model forcings for hydrological lateral inflows and coastal boundary - depths (hybrid simulations). Run this function after network initialization - and after any iteration loop in main. - """ - (self._qlateral, - self._coastal_boundary_depth_df - ) = hyfeature_prep.hyfeature_forcing( - run, - forcing_parameters, - hybrid_parameters, - self._flowpath_dict, - self.segment_index, - cpu_pool, - self._t0, - self._coastal_boundary_depth_df, - ) - - #Mask out all non-simulated waterbodies - self._dataframe['waterbody'] = self.waterbody_null - - #This also remaps the initial NHDComID identity to the HY_Features Waterbody ID for the reservoir... - self._dataframe.loc[self._waterbody_df.index, 'waterbody'] = self._waterbody_df.index.name - - #FIXME should waterbody_df and param_df overlap IDS? Doesn't seem like it should... - #self._dataframe.drop(self._waterbody_df.index, axis=0, inplace=True) - #For now, doing it in property waterbody_connections... - # Remove duplicate rows...but its not really duplicate... - #self._dataframe = ( - # self._dataframe.reset_index() - # .drop_duplicates(subset="key") - # .set_index("key") - #) - self._dataframe.sort_index(inplace=True) - self._waterbody_df.sort_index(inplace=True) def extract_waterbody_connections(rows, target_col, waterbody_null=-9999): """Extract waterbody mapping from dataframe. @@ -253,75 +90,6 @@ def extract_waterbody_connections(rows, target_col, waterbody_null=-9999): return ( rows.loc[rows[target_col] != waterbody_null, target_col].astype("int").to_dict() ) - - - def create_routing_network(self, - conn, - param_df, - wbody_conn, gages, - preprocessing_parameters, - compute_parameters, - waterbody_parameters - ): - - #-------------------------------------------------------------------------- - # Creation of routing network data objects. Logical ordering of lower-level - # function calls that build individual network data objects. - #-------------------------------------------------------------------------- - (self._independent_networks, - self._reaches_by_tw, - self._reverse_network, - self.diffusive_network_data, - self.topobathy_df, - self.refactored_diffusive_domain, - self.refactored_reaches, - self.unrefactored_topobathy_df - ) = hyfeature_prep.hyfeature_hybrid_routing_preprocess( - conn, - param_df, - wbody_conn, - gages, - preprocessing_parameters, - compute_parameters, - waterbody_parameters, - ) - return (self._independent_networks, - self._reaches_by_tw, - self._reverse_network, - self.diffusive_network_data, - self.topobathy_df, - self.refactored_diffusive_domain, - self.refactored_reaches, - self.unrefactored_topobathy_df) - - def new_q0(self, run_results): - """ - Prepare a new q0 dataframe with initial flow and depth to act as - a warmstate for the next simulation chunk. - """ - self._q0 = pd.concat( - [ - pd.DataFrame( - r[1][:, [-3, -3, -1]], index=r[0], columns=["qu0", "qd0", "h0"] - ) - for r in run_results - ], - copy=False, - ) - return self._q0 - - def update_waterbody_water_elevation(self): - """ - Update the starting water_elevation of each lake/reservoir - with flow and depth values from q0 - """ - self._waterbody_df.update(self._q0) - - def new_t0(self, dt, nts): - """ - Update t0 value for next loop iteration - """ - self._t0 += timedelta(seconds = dt * nts) @property def downstream_flowpath_dict(self): diff --git a/src/troute-network/troute/abstractnetwork_preprocess.py b/src/troute-network/troute/abstractnetwork_preprocess.py index be1bc9734..ab3c1942d 100644 --- a/src/troute-network/troute/abstractnetwork_preprocess.py +++ b/src/troute-network/troute/abstractnetwork_preprocess.py @@ -24,6 +24,7 @@ def build_diffusive_domain( compute_parameters, + param_df, connections, ): @@ -244,8 +245,9 @@ def create_independent_networks( waterbody_parameters, connections, wbody_conn, - gages, + gages = pd.DataFrame() #FIXME update default value when we update 'break_network_at_gages', ): + LOG.info("organizing connections into reaches ...") start_time = time.time() gage_break_segments = set() @@ -256,6 +258,7 @@ def create_independent_networks( ) # if streamflow DA, then break network at gages + #TODO update to work with HYFeatures, need to determine how we'll do DA... break_network_at_gages = False if break_network_at_waterbodies: @@ -274,7 +277,7 @@ def create_independent_networks( return independent_networks, reaches_bytw, rconn -def hyfeature_initial_warmstate_preprocess( +def initial_warmstate_preprocess( break_network_at_waterbodies, restart_parameters, segment_index, @@ -529,7 +532,7 @@ def build_forcing_sets( # Deduce the timeinterval of the forcing data from the output timestamps of the first # two ordered CHRTOUT files - if geo_file_type=='HYFeaturesNetowrk': + if geo_file_type=='HYFeaturesNetwork': df = read_file(first_file) t1_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") t1 = datetime.strptime(t1_str,"%Y-%m-%d_%H:%M:%S") @@ -602,7 +605,7 @@ def build_forcing_sets( final_qlat = qlat_input_folder.joinpath(run_sets[j]['qlat_files'][-1]) if geo_file_type=='NHDNetwork': final_timestamp_str = nhd_io.get_param_str(final_qlat,'model_output_valid_time') - elif geo_file_type=='HYFeaturesNetowrk': + elif geo_file_type=='HYFeaturesNetwork': df = read_file(final_qlat) final_timestamp_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index d3c8c6f33..dfd6d7b7d 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -30,6 +30,7 @@ import troute.nhd_network_utilities_v02 as nnu import troute.routing.diffusive_utils as diff_utils import troute.hyfeature_network_utilities as hnu +import troute.abstractnetwork_preprocess as abs_prep LOG = logging.getLogger('') @@ -84,14 +85,6 @@ def main_v04(argv): restart_parameters, forcing_parameters, verbose=True, showtiming=showtiming) - - network.create_routing_network(network.connections, - network.dataframe, - network.waterbody_connections, - network.gages, - preprocessing_parameters, - compute_parameters, - waterbody_parameters,) elif supernetwork_parameters["geo_file_type"] == 'NHDNetwork': network = NHDNetwork(supernetwork_parameters, @@ -110,10 +103,17 @@ def main_v04(argv): task_times['network_creation_time'] = network_end_time - network_start_time # Create run_sets: sets of forcing files for each loop + run_sets = abs_prep.build_forcing_sets( + supernetwork_parameters, + forcing_parameters, + network.t0 + ) + ''' if supernetwork_parameters["geo_file_type"] == 'NHDNetwork': run_sets = nnu.build_forcing_sets(forcing_parameters, network.t0) elif supernetwork_parameters["geo_file_type"] == 'HYFeaturesNetwork': run_sets = hnu.build_forcing_sets(forcing_parameters, network.t0) + ''' # Create da_sets: sets of TimeSlice files for each loop if "data_assimilation_parameters" in compute_parameters: @@ -126,22 +126,22 @@ def main_v04(argv): parity_sets = [] # Create forcing data within network object for first loop iteration - network.assemble_forcings(run_sets[0], forcing_parameters, hybrid_parameters, cpu_pool) + network.assemble_forcings(run_sets[0], forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool) # Create data assimilation object from da_sets for first loop iteration # TODO: Add data_assimilation for hyfeature network - if 1==2: - data_assimilation = AllDA(data_assimilation_parameters, - run_parameters, - waterbody_parameters, - network, - da_sets[0]) + data_assimilation = AllDA( + data_assimilation_parameters, + run_parameters, + waterbody_parameters, + network, + da_sets[0] + ) if showtiming: forcing_end_time = time.time() task_times['forcing_time'] += forcing_end_time - network_end_time - parallel_compute_method = compute_parameters.get("parallel_compute_method", None) subnetwork_target_size = compute_parameters.get("subnetwork_target_size", 1) qts_subdivisions = forcing_parameters.get("qts_subdivisions", 1) @@ -183,18 +183,18 @@ def main_v04(argv): network.dataframe, network.q0, network._qlateral, - pd.DataFrame(), #data_assimilation.usgs_df, - pd.DataFrame(), #data_assimilation.lastobs_df, - pd.DataFrame(), #data_assimilation.reservoir_usgs_df, - pd.DataFrame(), #data_assimilation.reservoir_usgs_param_df, - pd.DataFrame(), #data_assimilation.reservoir_usace_df, - pd.DataFrame(), #data_assimilation.reservoir_usace_param_df, - {}, #data_assimilation.assimilation_parameters, + data_assimilation.usgs_df, + data_assimilation.lastobs_df, + data_assimilation.reservoir_usgs_df, + data_assimilation.reservoir_usgs_param_df, + data_assimilation.reservoir_usace_df, + data_assimilation.reservoir_usace_param_df, + data_assimilation.assimilation_parameters, assume_short_ts, return_courant, - network._waterbody_df, ## check: network._waterbody_df ?? def name is different from return self._ .. + network.waterbody_dataframe, waterbody_parameters, - network._waterbody_types_df, ## check: network._waterbody_types_df ?? def name is different from return self._ .. + network.waterbody_types_dataframe, network.waterbody_type_specified, network.diffusive_network_data, network.topobathy_df, From 4c5e60e837edb9ba710fee349d86a2df2a55c954 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Mon, 12 Dec 2022 17:35:11 +0000 Subject: [PATCH 03/54] bug fixes to get hyfeatures to run properly --- src/troute-network/troute/AbstractNetwork.py | 21 +++++++++---- .../troute/HYFeaturesNetwork.py | 15 ++++++---- .../troute/abstractnetwork_preprocess.py | 30 ++++++++++++++++--- src/troute-nwm/src/nwm_routing/__main__.py | 1 + .../unittest_hyfeature.yaml | 2 +- 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 94594b6da..189a48700 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -16,8 +16,9 @@ class AbstractNetwork(ABC): """ __slots__ = ["_dataframe", "_waterbody_connections", "_gages", "_terminal_codes", "_connections", "_waterbody_df", - "_waterbody_types_df", "_independent_networks", - "_reaches_by_tw", "_reverse_network", "_q0", "_t0", + "_waterbody_types_df", "_waterbody_type_specified", + "_independent_networks", "_reaches_by_tw", + "_reverse_network", "_q0", "_t0", "_qlateral", "_break_segments", "_coastal_boundary_depth_df"] def __init__( @@ -31,7 +32,7 @@ def __init__( verbose=False, showtiming=False ): - + global __verbose__, __showtiming__ __verbose__ = verbose __showtiming__ = showtiming @@ -65,6 +66,7 @@ def __init__( self._q0 = None self._t0 = None self._qlateral = None + self._waterbody_type_specified = None #qlat_const = forcing_parameters.get("qlat_const", 0) #FIXME qlat_const """ Figure out a good way to default initialize to qlat_const/c @@ -86,9 +88,10 @@ def __init__( ~self._dataframe["downstream"].isin(self._dataframe.index) ]["downstream"].values ) + # There can be an externally determined terminal code -- that's this value self._terminal_codes.add(terminal_code) - + self._break_segments = set() if break_points: if break_points["break_network_at_waterbodies"]: @@ -97,7 +100,7 @@ def __init__( self._break_segments = self._break_segments | set(self.gages.values()) self._connections = extract_connections(self._dataframe, 'downstream', self._terminal_codes) - + ( self._dataframe, self._connections, @@ -111,7 +114,7 @@ def __init__( self._dataframe, self._connections, ) - + ( self._independent_networks, self._reaches_by_tw, @@ -310,6 +313,12 @@ def waterbody_dataframe(self): @property def waterbody_types_dataframe(self): return self._waterbody_types_df + + @property + def waterbody_type_specified(self): + if self._waterbody_type_specified is None: + self._waterbody_type_specified = False + return self._waterbody_type_specified @property def connections(self): diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 5de0729e0..e730753b5 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -22,8 +22,7 @@ class HYFeaturesNetwork(AbstractNetwork): """ __slots__ = ["_flowpath_dict", - "segment_index", - "waterbody_type_specified", + "segment_index", "diffusive_network_data", "topobathy_df", "refactored_diffusive_domain", @@ -31,7 +30,8 @@ class HYFeaturesNetwork(AbstractNetwork): "unrefactored_topobathy_df"] def __init__(self, supernetwork_parameters, - waterbody_parameters=None, + waterbody_parameters, + data_assimilation_parameters, restart_parameters=None, compute_parameters=None, verbose=False, @@ -58,11 +58,14 @@ def __init__(self, supernetwork_parameters, waterbody_parameters, ) - + cols = supernetwork_parameters.get('columns',None) terminal_code = supernetwork_parameters.get('terminal_code',0) - break_network_at_waterbodies = supernetwork_parameters.get("break_network_at_waterbodies", False) - break_network_at_gages = supernetwork_parameters.get("break_network_at_gages", False) + break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) + streamflow_da = data_assimilation_parameters.get('streamflow_da', False) + break_network_at_gages = False + if streamflow_da: + break_network_at_gages = streamflow_da.get('streamflow_nudging', False) break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} diff --git a/src/troute-network/troute/abstractnetwork_preprocess.py b/src/troute-network/troute/abstractnetwork_preprocess.py index ab3c1942d..2b295be9f 100644 --- a/src/troute-network/troute/abstractnetwork_preprocess.py +++ b/src/troute-network/troute/abstractnetwork_preprocess.py @@ -366,7 +366,7 @@ def initial_warmstate_preprocess( ) waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on="lake_id" + waterbodies_df, waterbodies_initial_states_df, on="wb-id" ) LOG.debug( @@ -625,6 +625,10 @@ def build_qlateral_array( supernetwork_parameters, segment_index=pd.Index([]), ): + + start_time = time.time() + LOG.info("Creating a DataFrame of lateral inflow forcings ...") + # TODO: set default/optional arguments qts_subdivisions = run.get("qts_subdivisions", 1) nts = run.get("nts", 1) @@ -674,7 +678,9 @@ def build_qlateral_array( index = idx, columns = range(len(qlat_files)) ) - elif geo_file_type=='HYFeaturesNetowrk': + + qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + elif geo_file_type=='HYFeaturesNetwork': dfs=[] for f in qlat_files: df = read_file(f).set_index(['feature_id']) @@ -683,13 +689,24 @@ def build_qlateral_array( # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids nexuses_lateralflows_df = pd.concat(dfs, axis=1) - # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids + # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) for k,v in nexus_to_upstream_flowpath_dict.items() ), axis=1 ).T qlats_df.columns=range(len(qlat_files)) + qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + + # The segment_index has the full network set of segments/flowpaths. + # Whereas the set of flowpaths that are downstream of nexuses is a + # subset of the segment_index. Therefore, all of the segments/flowpaths + # that are not accounted for in the set of flowpaths downstream of + # nexuses need to be added to the qlateral dataframe and padded with + # zeros. + all_df = pd.DataFrame( np.zeros( (len(segment_index), len(qlats_df.columns)) ), index=segment_index, + columns=qlats_df.columns ) + all_df.loc[ qlats_df.index ] = qlats_df + qlats_df = all_df.sort_index() - qlats_df = qlats_df[qlats_df.index.isin(segment_index)] elif qlat_input_file: qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) else: @@ -708,6 +725,11 @@ def build_qlateral_array( if not segment_index.empty: qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + + LOG.debug( + "lateral inflow DataFrame creation complete in %s seconds." \ + % (time.time() - start_time) + ) return qlats_df diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index dfd6d7b7d..dd6ee9283 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -82,6 +82,7 @@ def main_v04(argv): if supernetwork_parameters["geo_file_type"] == 'HYFeaturesNetwork': network = HYFeaturesNetwork(supernetwork_parameters, waterbody_parameters, + data_assimilation_parameters, restart_parameters, forcing_parameters, verbose=True, showtiming=showtiming) diff --git a/test/unit_test_hyfeature/unittest_hyfeature.yaml b/test/unit_test_hyfeature/unittest_hyfeature.yaml index a04032d8c..5ecaeb3ca 100644 --- a/test/unit_test_hyfeature/unittest_hyfeature.yaml +++ b/test/unit_test_hyfeature/unittest_hyfeature.yaml @@ -60,7 +60,7 @@ compute_parameters: qts_subdivisions : 12 dt : 300 # [sec] qlat_input_folder : channel_forcing/ - qlat_file_pattern_filter : "*.CHRTOUT_DOMAIN1" + qlat_file_pattern_filter : "*NEXOUT.csv" nexus_input_folder : channel_forcing/ nexus_file_pattern_filter : "*NEXOUT.csv" #OR "*NEXOUT.parquet" OR "nex-*" binary_nexus_file_folder : binary_files # this is required if nexus_file_pattern_filter="nex-*" From edfd87304e298f6f5e130a672cef7ba6dda9bc20 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Thu, 15 Dec 2022 22:00:55 +0000 Subject: [PATCH 04/54] updates to NHDNetwork and functions in AbstractNetwork --- src/troute-network/troute/AbstractNetwork.py | 44 ++- .../troute/HYFeaturesNetwork.py | 34 +-- src/troute-network/troute/NHDNetwork.py | 258 ++++-------------- .../troute/abstractnetwork_preprocess.py | 15 +- .../troute/hyfeature_preprocess.py | 22 +- src/troute-network/troute/nhd_io.py | 2 +- src/troute-network/troute/nhd_preprocess.py | 232 ++++++++++++++++ src/troute-nwm/src/nwm_routing/__main__.py | 19 +- test/LowerColorado_TX/test_AnA.yaml | 30 +- test/ngen/test_AnA.yaml | 106 +++++++ 10 files changed, 474 insertions(+), 288 deletions(-) create mode 100644 test/ngen/test_AnA.yaml diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 189a48700..0a19d9156 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -1,14 +1,12 @@ from abc import ABC, abstractmethod from functools import partial import pandas as pd -from datetime import datetime +from datetime import datetime, timedelta import time from troute.nhd_network import reverse_dict, extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition import troute.nhd_io as nhd_io import troute.abstractnetwork_preprocess as abs_prep -__verbose__ = False -__showtiming__ = False class AbstractNetwork(ABC): """ @@ -19,7 +17,9 @@ class AbstractNetwork(ABC): "_waterbody_types_df", "_waterbody_type_specified", "_independent_networks", "_reaches_by_tw", "_reverse_network", "_q0", "_t0", - "_qlateral", "_break_segments", "_coastal_boundary_depth_df"] + "_qlateral", "_break_segments", "_coastal_boundary_depth_df", + "diffusive_network_data", "topobathy_df", "refactored_diffusive_domain", + "refactored_reaches", "unrefactored_topobathy_df", "segment_index"] def __init__( self, @@ -27,15 +27,15 @@ def __init__( waterbody_parameters, restart_parameters, cols=None, - terminal_code=None, break_points=None, verbose=False, showtiming=False ): - + global __verbose__, __showtiming__ __verbose__ = verbose __showtiming__ = showtiming + if cols: self._dataframe = self._dataframe[list(cols.values())] # Rename parameter columns to standard names: from route-link names @@ -57,9 +57,8 @@ def __init__( self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) self.set_index("key") self.sort_index() - self._waterbody_connections = {} - self._gages = None - self._connections = None + self._waterbody_connections = {} #TODO set in individual network objects?... + self._gages = None #TODO set in individual network objects?... self._independent_networks = None self._reverse_network = None self._reaches_by_tw = None @@ -78,20 +77,6 @@ def __init__( dtype="float32", ) """ - # there may be off-domain nodes that are not explicitly identified - # but which are terminal (i.e., off-domain) as a result of a mask or some other - # an interior domain truncation that results in a - # otherwise valid node value being pointed to, but which is masked out or - # being intentionally separated into another domain. - self._terminal_codes = set( - self._dataframe[ - ~self._dataframe["downstream"].isin(self._dataframe.index) - ]["downstream"].values - ) - - # There can be an externally determined terminal code -- that's this value - self._terminal_codes.add(terminal_code) - self._break_segments = set() if break_points: if break_points["break_network_at_waterbodies"]: @@ -99,8 +84,6 @@ def __init__( if break_points["break_network_at_gages"]: self._break_segments = self._break_segments | set(self.gages.values()) - self._connections = extract_connections(self._dataframe, 'downstream', self._terminal_codes) - ( self._dataframe, self._connections, @@ -126,6 +109,11 @@ def __init__( #gages, #TODO update how gages are provided when we figure out DA ) + if __verbose__: + print("setting waterbody and channel initial states ...") + if __showtiming__: + start_time = time.time() + ( self._waterbody_df, self._q0, @@ -137,6 +125,12 @@ def __init__( self._waterbody_df, ) + if __verbose__: + print("waterbody and channel initial states complete") + if __showtiming__: + print("... in %s seconds." % (time.time() - start_time)) + start_time = time.time() + def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): """ Assemble model forcings. Forcings include hydrological lateral inflows (qlats) diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index e730753b5..9366659b1 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -1,17 +1,9 @@ from .AbstractNetwork import AbstractNetwork -import pathlib -import json import pandas as pd import numpy as np import time -import re import troute.nhd_io as nhd_io #FIXME -from itertools import chain -import geopandas as gpd -from pathlib import Path -import math import troute.hyfeature_preprocess as hyfeature_prep -from datetime import datetime, timedelta __verbose__ = False __showtiming__ = False @@ -23,11 +15,7 @@ class HYFeaturesNetwork(AbstractNetwork): """ __slots__ = ["_flowpath_dict", "segment_index", - "diffusive_network_data", - "topobathy_df", - "refactored_diffusive_domain", - "refactored_reaches", - "unrefactored_topobathy_df"] + ] def __init__(self, supernetwork_parameters, waterbody_parameters, @@ -52,15 +40,21 @@ def __init__(self, #------------------------------------------------ (self._dataframe, self._flowpath_dict, + self._connections, self._waterbody_df, self._waterbody_types_df, + self._terminal_codes, ) = hyfeature_prep.read_geo_file( supernetwork_parameters, waterbody_parameters, ) + if __verbose__: + print("supernetwork connections set complete") + if __showtiming__: + print("... in %s seconds." % (time.time() - start_time)) + cols = supernetwork_parameters.get('columns',None) - terminal_code = supernetwork_parameters.get('terminal_code',0) break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) streamflow_da = data_assimilation_parameters.get('streamflow_da', False) break_network_at_gages = False @@ -74,14 +68,10 @@ def __init__(self, waterbody_parameters, restart_parameters, cols, - terminal_code, - break_points - ) - - if __verbose__: - print("supernetwork connections set complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) + break_points, + verbose=__verbose__, + showtiming=__showtiming__, + ) # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index b72996a71..ff2c3ab0d 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -11,74 +11,15 @@ __showtiming__ = True #FIXME pass flag __verbose__ = True #FIXME pass verbosity -def read_qlats(forcing_parameters, segment_index): - # STEP 5: Read (or set) QLateral Inputs - if __showtiming__: - start_time = time.time() - if __verbose__: - print("creating qlateral array ...") - qts_subdivisions = forcing_parameters.get("qts_subdivisions", 1) - nts = forcing_parameters.get("nts", 1) - qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) - qlat_input_file = forcing_parameters.get("qlat_input_file", None) - if qlat_input_folder: - qlat_input_folder = pathlib.Path(qlat_input_folder) - if "qlat_files" in forcing_parameters: - qlat_files = forcing_parameters.get("qlat_files") - qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] - elif "qlat_file_pattern_filter" in forcing_parameters: - qlat_file_pattern_filter = forcing_parameters.get( - "qlat_file_pattern_filter", "*CHRT_OUT*" - ) - qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) - - qlat_file_index_col = forcing_parameters.get( - "qlat_file_index_col", "feature_id" - ) - qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") - - qlat_df = nhd_io.get_ql_from_wrf_hydro_mf( - qlat_files=qlat_files, - #ts_iterator=ts_iterator, - #file_run_size=file_run_size, - index_col=qlat_file_index_col, - value_col=qlat_file_value_col, - ) - qlat_df = qlat_df[qlat_df.index.isin(segment_index)] - elif qlat_input_file: - qlat_df = nhd_io.get_ql_from_csv(qlat_input_file) - else: - qlat_const = forcing_parameters.get("qlat_const", 0) - qlat_df = pd.DataFrame( - qlat_const, - index=segment_index, - columns=range(nts // qts_subdivisions), - dtype="float32", - ) - - max_col = 1 + nts // qts_subdivisions - - if len(qlat_df.columns) > max_col: - qlat_df.drop(qlat_df.columns[max_col:], axis=1, inplace=True) - - if not segment_index.empty: - qlat_df = qlat_df[qlat_df.index.isin(segment_index)] - - if __verbose__: - print("qlateral array complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) - - return qlat_df class NHDNetwork(AbstractNetwork): """ """ - __slots__ = ["waterbody_type_specified", "link_lake_crosswalk", "link_gage_df", - "usgs_lake_gage_crosswalk", "usace_lake_gage_crosswalk", - "diffusive_network_data", "topobathy_df", "refactored_diffusive_domain", - "refactored_reaches", "unrefactored_topobathy_df", "segment_index"] + __slots__ = [ + "_link_lake_crosswalk", "_usgs_lake_gage_crosswalk", + "_usace_lake_gage_crosswalk", "_flowpath_dict" + ] def __init__( self, @@ -90,9 +31,8 @@ def __init__( data_assimilation_parameters=None, preprocessing_parameters=None, verbose=False, - showtiming=False, - layer_string=None, - driver_string=None,): + showtiming=False, + ): """ """ @@ -104,167 +44,65 @@ def __init__( if __showtiming__: start_time = time.time() - #------------------------------------------------ - # Preprocess network attributes + # Load Geo Data #------------------------------------------------ - - (self._connections, - self._dataframe, - self._waterbody_connections, - self._waterbody_df, - self._waterbody_types_df, - break_network_at_waterbodies, - self.waterbody_type_specified, - self.link_lake_crosswalk, - self._independent_networks, - self._reaches_by_tw, - self._reverse_network, - self.link_gage_df, - self.usgs_lake_gage_crosswalk, - self.usace_lake_gage_crosswalk, - self.diffusive_network_data, - self.topobathy_df, - self.refactored_diffusive_domain, - self.refactored_reaches, - self.unrefactored_topobathy_df - ) = nhd_prep.build_nhd_network( + + ( + self._dataframe, + self._connections, + self._terminal_codes, + self._waterbody_df, + self._waterbody_types_df, + self._waterbody_type_specified, + self._waterbody_connections, + self._link_lake_crosswalk, + self._gages, + self._usgs_lake_gage_crosswalk, + self._usace_lake_gage_crosswalk, + ) = nhd_prep.read_geo_file( supernetwork_parameters, waterbody_parameters, - preprocessing_parameters, - compute_parameters, - data_assimilation_parameters - ) - - # list of all segments in the domain (MC + diffusive) - self.segment_index = self._dataframe.index - if self.diffusive_network_data: - for tw in self.diffusive_network_data: - self.segment_index = self.segment_index.append( - pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) - ) - - ''' - #FIXME the base class constructor is finiky - #as it requires the _dataframe, then sets some - #initial default properties...which, at the moment - #are used by the subclass constructor. - #So it needs to be called at just the right spot... - cols = supernetwork_parameters.get( - 'columns', - { - 'key' : 'link', - 'downstream': 'to', - 'dx' : 'Length', - 'n' : 'n', - 'ncc' : 'nCC', - 's0' : 'So', - 'bw' : 'BtmWdth', - 'waterbody' : 'NHDWaterbodyComID', - 'gages' : 'gages', - 'tw' : 'TopWdth', - 'twcc' : 'TopWdthCC', - 'alt' : 'alt', - 'musk' : 'MusK', - 'musx' : 'MusX', - 'cs' : 'ChSlp', - } - ) - terminal_code = supernetwork_parameters.get("terminal_code", 0) - break_network_at_waterbodies = supernetwork_parameters.get( - "break_network_at_waterbodies", False - ) - break_network_at_gages = supernetwork_parameters.get( - "break_network_at_gages", False + data_assimilation_parameters, ) - break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, - "break_network_at_gages": break_network_at_gages} - super().__init__(cols, terminal_code, break_points) - ''' - if __verbose__: print("supernetwork connections set complete") if __showtiming__: print("... in %s seconds." % (time.time() - start_time)) - - #----------------------------------------------------- - # Set initial waterbody and channel states - #----------------------------------------------------- + + cols = supernetwork_parameters.get('columns',None) + break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) + streamflow_da = data_assimilation_parameters.get('streamflow_da', False) + break_network_at_gages = False + if streamflow_da: + break_network_at_gages = streamflow_da.get('streamflow_nudging', False) + break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, + "break_network_at_gages": break_network_at_gages} - if __verbose__: - print("setting waterbody and channel initial states ...") - if __showtiming__: - start_time = time.time() + self._flowpath_dict = {} - (self._waterbody_df, - self._q0, - self._t0,) = nhd_prep.nhd_initial_warmstate_preprocess( - break_network_at_waterbodies, - restart_parameters, - data_assimilation_parameters, - self.segment_index, - self._waterbody_df, - self.link_lake_crosswalk, - ) + super().__init__( + compute_parameters, + waterbody_parameters, + restart_parameters, + cols, + break_points, + verbose=__verbose__, + showtiming=__showtiming__, + ) - if __verbose__: - print("waterbody and channel initial states complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) - start_time = time.time() + # list of all segments in the domain (MC + diffusive) + self.segment_index = self._dataframe.index + if self.diffusive_network_data: + for tw in self.diffusive_network_data: + self.segment_index = self.segment_index.append( + pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) + ) # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't self._coastal_boundary_depth_df = pd.DataFrame() - - - def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, cpu_pool): - """ - Assembles model forcings for hydrological lateral inflows and coastal boundary - depths (hybrid simulations). Run this function after network initialization - and after any iteration loop in main. - """ - (self._qlateral, - self._coastal_boundary_depth_df - ) = nhd_prep.nhd_forcing( - run, - forcing_parameters, - hybrid_parameters, - self.segment_index, - cpu_pool, - self._t0, - self._coastal_boundary_depth_df, - ) - - def new_q0(self, run_results): - """ - Prepare a new q0 dataframe with initial flow and depth to act as - a warmstate for the next simulation chunk. - """ - self._q0 = pd.concat( - [ - pd.DataFrame( - r[1][:, [-3, -3, -1]], index=r[0], columns=["qu0", "qd0", "h0"] - ) - for r in run_results - ], - copy=False, - ) - - #def update_waterbody_water_elevation(self): - def update_waterbody_water_elevation(self): - """ - Update the starting water_elevation of each lake/reservoir - with flow and depth values from q0 - """ - self._waterbody_df.update(self._q0) - - def new_t0(self, dt, nts): - """ - Update t0 value for next loop iteration - """ - self._t0 += timedelta(seconds = dt * nts) def extract_waterbody_connections(rows, target_col, waterbody_null=-9999): """Extract waterbody mapping from dataframe. diff --git a/src/troute-network/troute/abstractnetwork_preprocess.py b/src/troute-network/troute/abstractnetwork_preprocess.py index 2b295be9f..25d76ffde 100644 --- a/src/troute-network/troute/abstractnetwork_preprocess.py +++ b/src/troute-network/troute/abstractnetwork_preprocess.py @@ -154,7 +154,7 @@ def build_diffusive_domain( # diffusive domain tributary segments trib_segs = [] - + for seg in mainstem_segs: us_list = rconn_diff0[seg] for u in us_list: @@ -315,6 +315,9 @@ def initial_warmstate_preprocess( ----- ''' + # generalize waterbody ID's to be used with any network + index_id = waterbodies_df.index.names[0] + #---------------------------------------------------------------------------- # Assemble waterbody initial states (outflow and pool elevation #---------------------------------------------------------------------------- @@ -335,7 +338,7 @@ def initial_warmstate_preprocess( waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( restart_parameters["wrf_hydro_waterbody_restart_file"], restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], - restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", 'lake_id'), + restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", index_id), restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], restart_parameters.get( "wrf_hydro_waterbody_crosswalk_filter_file_field_name", @@ -366,7 +369,7 @@ def initial_warmstate_preprocess( ) waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on="wb-id" + waterbodies_df, waterbodies_initial_states_df, on=index_id ) LOG.debug( @@ -551,7 +554,7 @@ def build_forcing_sets( # determine qts_subdivisions qts_subdivisions = dt_qlat / dt if dt_qlat % dt == 0: - qts_subdivisions = dt_qlat / dt + qts_subdivisions = int(dt_qlat / dt) # make sure that qts_subdivisions = dt_qlat / dt forcing_parameters['qts_subdivisions']= qts_subdivisions @@ -661,9 +664,9 @@ def build_qlateral_array( jobs = [] for f in qlat_files: jobs.append( - #delayed(nhd_io.get_ql_from_chrtout) + delayed(nhd_io.get_ql_from_chrtout) #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) - delayed(nhd_io.get_ql_from_csv) + #delayed(nhd_io.get_ql_from_csv) (f) ) ql_list = parallel(jobs) diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index 2dbfa9da7..462615722 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -45,6 +45,26 @@ def read_geo_file( # ********** need to be included in flowpath_attributes ************* dataframe['alt'] = 1.0 #FIXME get the right value for this... + # numeric code used to indicate network terminal segments + terminal_code = supernetwork_parameters.get("terminal_code", 0) + + # There can be an externally determined terminal code -- that's this first value + terminal_codes = set() + terminal_codes.add(terminal_code) + # ... but there may also be off-domain nodes that are not explicitly identified + # but which are terminal (i.e., off-domain) as a result of a mask or some other + # an interior domain truncation that results in a + # otherwise valid node value being pointed to, but which is masked out or + # being intentionally separated into another domain. + terminal_codes = terminal_codes | set( + dataframe[~dataframe["downstream"].isin(dataframe.index)]["downstream"].values + ) + + # build connections dictionary + connections = nhd_network.extract_connections( + dataframe, "downstream", terminal_codes=terminal_codes + ) + #Load waterbody/reservoir info if waterbody_parameters: levelpool_params = waterbody_parameters.get('level_pool', None) @@ -84,7 +104,7 @@ def read_geo_file( waterbody_types_df = pd.DataFrame(index=waterbody_df.index) waterbody_types_df['reservoir_type'] = 1 - return dataframe, flowpath_dict, waterbody_df, waterbody_types_df + return dataframe, flowpath_dict, connections, waterbody_df, waterbody_types_df, terminal_codes def build_hyfeature_network(supernetwork_parameters, waterbody_parameters, diff --git a/src/troute-network/troute/nhd_io.py b/src/troute-network/troute/nhd_io.py index 3515cd2d6..c278d5a9a 100644 --- a/src/troute-network/troute/nhd_io.py +++ b/src/troute-network/troute/nhd_io.py @@ -746,7 +746,7 @@ def write_chrtout( LOG.debug("%d CHRTOUT files will be written." % (nfiles_to_write)) LOG.debug("Extracting flow DataFrame on qts_subdivisions from FVD DataFrame") start = time.time() - + flow = flowveldepth.loc[:, ::3].iloc[:, qts_subdivisions-1::qts_subdivisions] LOG.debug("Extracting flow DataFrame took %s seconds." % (time.time() - start)) diff --git a/src/troute-network/troute/nhd_preprocess.py b/src/troute-network/troute/nhd_preprocess.py index dc303815b..2422bac84 100644 --- a/src/troute-network/troute/nhd_preprocess.py +++ b/src/troute-network/troute/nhd_preprocess.py @@ -14,6 +14,238 @@ LOG = logging.getLogger('') +def read_geo_file(supernetwork_parameters, waterbody_parameters, data_assimilation_parameters): + ''' + Construct network connections network, parameter dataframe, waterbody mapping, + and gage mapping. This is an intermediate-level function that calls several + lower level functions to read data, conduct network operations, and extract mappings. + + Arguments + --------- + supernetwork_parameters (dict): User input network parameters + + Returns: + -------- + connections (dict int: [int]): Network connections + param_df (DataFrame): Geometry and hydraulic parameters + wbodies (dict, int: int): segment-waterbody mapping + gages (dict, int: int): segment-gage mapping + + ''' + + # crosswalking dictionary between variables names in input dataset and + # variable names recognized by troute.routing module. + cols = supernetwork_parameters.get( + 'columns', + { + 'key' : 'link', + 'downstream': 'to', + 'dx' : 'Length', + 'n' : 'n', + 'ncc' : 'nCC', + 's0' : 'So', + 'bw' : 'BtmWdth', + 'waterbody' : 'NHDWaterbodyComID', + 'gages' : 'gages', + 'tw' : 'TopWdth', + 'twcc' : 'TopWdthCC', + 'alt' : 'alt', + 'musk' : 'MusK', + 'musx' : 'MusX', + 'cs' : 'ChSlp', + } + ) + + # numeric code used to indicate network terminal segments + terminal_code = supernetwork_parameters.get("terminal_code", 0) + + # read parameter dataframe + param_df = nhd_io.read(pathlib.Path(supernetwork_parameters["geo_file_path"])) + + # select the column names specified in the values in the cols dict variable + param_df = param_df[list(cols.values())] + + # rename dataframe columns to keys in the cols dict variable + param_df = param_df.rename(columns=nhd_network.reverse_dict(cols)) + + # handle synthetic waterbody segments + synthetic_wb_segments = supernetwork_parameters.get("synthetic_wb_segments", None) + synthetic_wb_id_offset = supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) + if synthetic_wb_segments: + # rename the current key column to key32 + key32_d = {"key":"key32"} + param_df = param_df.rename(columns=key32_d) + # create a key index that is int64 + # copy the links into the new column + param_df["key"] = param_df.key32.astype("int64") + # update the values of the synthetic reservoir segments + fix_idx = param_df.key.isin(set(synthetic_wb_segments)) + param_df.loc[fix_idx,"key"] = (param_df[fix_idx].key + synthetic_wb_id_offset).astype("int64") + + # set parameter dataframe index as segment id number, sort + param_df = param_df.set_index("key").sort_index() + + # get and apply domain mask + if "mask_file_path" in supernetwork_parameters: + data_mask = nhd_io.read_mask( + pathlib.Path(supernetwork_parameters["mask_file_path"]), + layer_string=supernetwork_parameters.get("mask_layer_string", None), + ) + data_mask = data_mask.set_index(data_mask.columns[0]) + param_df = param_df.filter(data_mask.index, axis=0) + + # map segment ids to waterbody ids + wbodies = {} + if "waterbody" in cols: + wbodies = nhd_network.extract_waterbody_connections( + param_df[["waterbody"]] + ) + param_df = param_df.drop("waterbody", axis=1) + + # map segment ids to gage ids + gages = {} + if "gages" in cols: + gages = nhd_network.gage_mapping(param_df[["gages"]]) + param_df = param_df.drop("gages", axis=1) + + # There can be an externally determined terminal code -- that's this first value + terminal_codes = set() + terminal_codes.add(terminal_code) + # ... but there may also be off-domain nodes that are not explicitly identified + # but which are terminal (i.e., off-domain) as a result of a mask or some other + # an interior domain truncation that results in a + # otherwise valid node value being pointed to, but which is masked out or + # being intentionally separated into another domain. + terminal_codes = terminal_codes | set( + param_df[~param_df["downstream"].isin(param_df.index)]["downstream"].values + ) + + # build connections dictionary + connections = nhd_network.extract_connections( + param_df, "downstream", terminal_codes=terminal_codes + ) + param_df = param_df.drop("downstream", axis=1) + + param_df = param_df.astype("float32") + + break_network_at_waterbodies = waterbody_parameters.get( + "break_network_at_waterbodies", False + ) + + # if waterbodies are being simulated, adjust the connections graph so that + # waterbodies are collapsed to single nodes. Also, build a mapping between + # waterbody outlet segments and lake ids + if break_network_at_waterbodies: + connections, link_lake_crosswalk = nhd_network.replace_waterbodies_connections( + connections, wbodies + ) + else: + link_lake_crosswalk = None + + #============================================================================ + # Retrieve and organize waterbody parameters + + waterbody_type_specified = False + if break_network_at_waterbodies: + + # Read waterbody parameters from LAKEPARM file + level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) + waterbodies_df = nhd_io.read_lakeparm( + level_pool_params['level_pool_waterbody_parameter_file_path'], + level_pool_params.get("level_pool_waterbody_id", 'lake_id'), + wbodies.values() + ) + + # Remove duplicate lake_ids and rows + waterbodies_df = ( + waterbodies_df.reset_index() + .drop_duplicates(subset="lake_id") + .set_index("lake_id") + ) + + # Declare empty dataframe + waterbody_types_df = pd.DataFrame() + + # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True + reservoir_da = data_assimilation_parameters.get( + 'reservoir_da', + {} + ) + + if reservoir_da: + usgs_hybrid = reservoir_da.get( + 'reservoir_persistence_usgs', + False + ) + usace_hybrid = reservoir_da.get( + 'reservoir_persistence_usace', + False + ) + param_file = reservoir_da.get( + 'gage_lakeID_crosswalk_file', + None + ) + else: + param_file = None + usace_hybrid = False + usgs_hybrid = False + + # check if RFC-type reservoirs are set to true + rfc_params = waterbody_parameters.get('rfc') + if rfc_params: + rfc_forecast = rfc_params.get( + 'reservoir_rfc_forecasts', + False + ) + param_file = rfc_params.get('reservoir_parameter_file',None) + else: + rfc_forecast = False + + if (param_file and reservoir_da) or (param_file and rfc_forecast): + waterbody_type_specified = True + ( + waterbody_types_df, + usgs_lake_gage_crosswalk, + usace_lake_gage_crosswalk + ) = nhd_io.read_reservoir_parameter_file( + param_file, + usgs_hybrid, + usace_hybrid, + rfc_forecast, + level_pool_params.get("level_pool_waterbody_id", 'lake_id'), + reservoir_da.get('crosswalk_usgs_gage_field', 'usgs_gage_id'), + reservoir_da.get('crosswalk_usgs_lakeID_field', 'usgs_lake_id'), + reservoir_da.get('crosswalk_usace_gage_field', 'usace_gage_id'), + reservoir_da.get('crosswalk_usace_lakeID_field', 'usace_lake_id'), + wbodies.values(), + ) + else: + waterbody_type_specified = True + waterbody_types_df = pd.DataFrame(data = 1, index = waterbodies_df.index, columns = ['reservoir_type']) + usgs_lake_gage_crosswalk = None + usace_lake_gage_crosswalk = None + + else: + # Declare empty dataframes + waterbody_types_df = pd.DataFrame() + waterbodies_df = pd.DataFrame() + usgs_lake_gage_crosswalk = None + usace_lake_gage_crosswalk = None + + return ( + param_df, + connections, + terminal_codes, + waterbodies_df, + waterbody_types_df, + waterbody_type_specified, + wbodies, + link_lake_crosswalk, + gages, + usgs_lake_gage_crosswalk, + usace_lake_gage_crosswalk) + + def build_nhd_network(supernetwork_parameters,waterbody_parameters, preprocessing_parameters,compute_parameters, data_assimilation_parameters): diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index dd6ee9283..a0b5ce40b 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -239,13 +239,11 @@ def main_v04(argv): cpu_pool) # get reservoir DA initial parameters for next loop iteration - # TODO: Add data_assimilation for hyfeature network - if 1==2: - data_assimilation.update(run_results, - data_assimilation_parameters, - run_parameters, - network, - da_sets[run_set_iterator + 1]) + data_assimilation.update(run_results, + data_assimilation_parameters, + run_parameters, + network, + da_sets[run_set_iterator + 1]) if showtiming: forcing_end_time = time.time() @@ -253,7 +251,9 @@ def main_v04(argv): if showtiming: output_start_time = time.time() - + + ''' + #TODO Update this to work with either network type... nwm_output_generator( run, run_results, @@ -268,10 +268,11 @@ def main_v04(argv): network._waterbody_df, ## check: network._waterbody_df ?? def name is different from return self._ .. network._waterbody_types_df, ## check: network._waterbody_types_df ?? def name is different from return self._ .. data_assimilation_parameters, - pd.DataFrame(), #data_assimilation.lastobs_df, + data_assimilation.lastobs_df, pd.DataFrame(), #network.link_gage_df, None, #network.link_lake_crosswalk, ) + ''' if showtiming: output_end_time = time.time() diff --git a/test/LowerColorado_TX/test_AnA.yaml b/test/LowerColorado_TX/test_AnA.yaml index 3f73a4cb4..799222edf 100644 --- a/test/LowerColorado_TX/test_AnA.yaml +++ b/test/LowerColorado_TX/test_AnA.yaml @@ -9,14 +9,15 @@ network_topology_parameters: #---------- supernetwork_parameters: #---------- - geo_file_path: domain/RouteLink_NWMv2.1.nc + geo_file_path: domain/RouteLink.nc mask_file_path: domain/coastal_subset.txt + geo_file_type: 'NHDNetwork' waterbody_parameters: #---------- break_network_at_waterbodies: True level_pool: #---------- - level_pool_waterbody_parameter_file_path: domain/LAKEPARM_NWMv2.1.nc + level_pool_waterbody_parameter_file_path: domain/LAKEPARM.nc rfc: #---------- reservoir_parameter_file : domain/reservoir_index_AnA.nc @@ -33,19 +34,20 @@ compute_parameters: cpu_pool : 36 restart_parameters: #---------- - wrf_hydro_channel_restart_file : restart/HYDRO_RST.2020-08-26_00:00_DOMAIN1 + start_datetime: 2021-08-23_13:00 + wrf_hydro_channel_restart_file : restart/HYDRO_RST.2021-08-23_12:00_DOMAIN1 #lite_channel_restart_file : restart/RESTART.2020082600_DOMAIN1 - wrf_hydro_channel_ID_crosswalk_file : domain/RouteLink_NWMv2.1.nc - wrf_hydro_waterbody_restart_file : restart/HYDRO_RST.2020-08-26_00:00_DOMAIN1 + wrf_hydro_channel_ID_crosswalk_file : domain/RouteLink.nc + wrf_hydro_waterbody_restart_file : restart/HYDRO_RST.2021-08-23_12:00_DOMAIN1 #lite_waterbody_restart_file : restart/waterbody_restart_202006011200 - wrf_hydro_waterbody_ID_crosswalk_file : domain/LAKEPARM_NWMv2.1.nc - wrf_hydro_waterbody_crosswalk_filter_file: domain/RouteLink_NWMv2.1.nc + wrf_hydro_waterbody_ID_crosswalk_file : domain/LAKEPARM.nc + wrf_hydro_waterbody_crosswalk_filter_file: domain/RouteLink.nc hybrid_parameters: - run_hybrid_routing: True + run_hybrid_routing: False diffusive_domain : domain/coastal_domain_subset.yaml - use_natl_xsections: True + use_natl_xsections: False topobathy_domain : domain/final_diffusive_natural_xs.nc - run_refactored_network: True + run_refactored_network: False refactored_domain: domain/refactored_coastal_domain_subset.yaml refactored_topobathy_domain: domain/refac_final_diffusive_natural_xs.nc coastal_boundary_domain: domain/coastal_boundary_domain.yaml @@ -55,8 +57,8 @@ compute_parameters: dt : 300 # [sec] qlat_input_folder : channel_forcing qlat_file_pattern_filter : "*.CHRTOUT_DOMAIN1" - coastal_boundary_input_file : boundary_forcing - nts : 2592 # 288 for 1day; 2592 for 9 days + coastal_boundary_input_file : #boundary_forcing + nts : 288 # 288 for 1day; 2592 for 9 days max_loop_size : 24 # [hr] data_assimilation_parameters: #---------- @@ -68,10 +70,10 @@ compute_parameters: #---------- streamflow_nudging : False diffusive_streamflow_nudging : False - gage_segID_crosswalk_file : domain/RouteLink_NWMv2.1.nc + gage_segID_crosswalk_file : domain/RouteLink.nc crosswalk_gage_field : 'gages' crosswalk_segID_field : 'link' - wrf_hydro_lastobs_file : lastobs/nudgingLastObs.2020-06-01_12:00:00.nc + wrf_hydro_lastobs_file : lastobs/nudgingLastObs.2021-08-23_13:00:00.nc lastobs_output_folder : lastobs/ reservoir_da: #---------- diff --git a/test/ngen/test_AnA.yaml b/test/ngen/test_AnA.yaml new file mode 100644 index 000000000..03e35cc35 --- /dev/null +++ b/test/ngen/test_AnA.yaml @@ -0,0 +1,106 @@ +# $ python -m nwm_routing -f -V3 test_AnA.yaml +#-------------------------------------------------------------------------------- +log_parameters: + #---------- + showtiming: True + log_level : DEBUG +#-------------------------------------------------------------------------------- +network_topology_parameters: + #---------- + supernetwork_parameters: + #---------- + geo_file_type: HYFeaturesNetwork + ngen_nexus_file: /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + geo_file_path: /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + columns: + key: 'id' + downstream: 'toid' + dx : 'lengthkm' + n : 'n' + ncc : 'nCC' + s0 : 'So' + bw : 'BtmWdth' + waterbody : 'rl_NHDWaterbodyComID' + gages : 'rl_gages' + tw : 'TopWdth' + twcc : 'TopWdthCC' + musk : 'MusK' + musx : 'MusX' + cs : 'ChSlp' + waterbody_parameters: + #---------- + break_network_at_waterbodies: True + level_pool: + #---------- + level_pool_waterbody_parameter_file_path: /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + reservoir_parameter_file: /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + +#-------------------------------------------------------------------------------- +compute_parameters: + #---------- + parallel_compute_method: serial + compute_kernel : V02-structured + assume_short_ts : True + restart_parameters: + #---------- + wrf_hydro_channel_restart_file : + #lite_channel_restart_file : restart/RESTART.2020082600_DOMAIN1 + wrf_hydro_channel_ID_crosswalk_file : /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + wrf_hydro_waterbody_restart_file : #restart/HYDRO_RST.2021-08-23_12:00_DOMAIN1 + #lite_waterbody_restart_file : restart/waterbody_restart_202006011200 + wrf_hydro_waterbody_ID_crosswalk_file : /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + wrf_hydro_waterbody_crosswalk_filter_file: /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + hybrid_parameters: + run_hybrid_routing: False + diffusive_domain : domain/coastal_domain_subset.yaml + use_natl_xsections: False + topobathy_domain : domain/final_diffusive_natural_xs.nc + run_refactored_network: False + refactored_domain: domain/refactored_coastal_domain_subset.yaml + refactored_topobathy_domain: domain/refac_final_diffusive_natural_xs.nc + coastal_boundary_domain: domain/coastal_boundary_domain.yaml + forcing_parameters: + #---------- + qts_subdivisions : 12 + dt : 300 # [sec] + qlat_input_folder : /home/sean.horvath/projects/data/large_network/sample_qlat_files + qlat_file_pattern_filter : "*.CHRTOUT_DOMAIN1.csv" +# coastal_boundary_input_file : boundary_forcing + nts : 288 # 288 for 1day; 2592 for 9 days + max_loop_size : 24 # [hr] + data_assimilation_parameters: + #---------- + usgs_timeslices_folder : usgs_TimeSlice/ + usace_timeslices_folder : usace_TimeSlice/ + timeslice_lookback_hours : 48 + qc_threshold : 1 + streamflow_da: + #---------- + streamflow_nudging : False + diffusive_streamflow_nudging : False + gage_segID_crosswalk_file : /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + crosswalk_gage_field : 'gages' + crosswalk_segID_field : 'link' + wrf_hydro_lastobs_file : #lastobs/nudgingLastObs.2021-08-23_12:00:00.nc + lastobs_output_folder : #lastobs/ + reservoir_da: + #---------- + reservoir_persistence_usgs : False + reservoir_persistence_usace : False + gage_lakeID_crosswalk_file : domain/reservoir_index_AnA.nc +#-------------------------------------------------------------------------------- +output_parameters: + #---------- +# test_output: output/lcr_flowveldepth.pkl + lite_restart: + #---------- + lite_restart_output_directory: + chrtout_output: + #---------- +# wrf_hydro_channel_output_source_folder: channel_forcing/ + chanobs_output: + #---------- +# chanobs_output_directory: output/ +# chanobs_filepath : lcr_chanobs.nc +# lakeout_output: lakeout/ + \ No newline at end of file From 1cffd02ceb0e6d4ad8e7c4ec3bea4cfecd9d8e7e Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 16 Dec 2022 17:42:44 +0000 Subject: [PATCH 05/54] moved renaming param_df column into individual networks, removed cols from input argument for AbstractNetwork --- src/troute-network/troute/AbstractNetwork.py | 27 +---------------- .../troute/HYFeaturesNetwork.py | 30 +++++++++++++++++-- src/troute-network/troute/NHDNetwork.py | 3 +- .../troute/hyfeature_preprocess.py | 4 +-- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 0a19d9156..f79954761 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta import time -from troute.nhd_network import reverse_dict, extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition +from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition import troute.nhd_io as nhd_io import troute.abstractnetwork_preprocess as abs_prep @@ -26,7 +26,6 @@ def __init__( compute_parameters, waterbody_parameters, restart_parameters, - cols=None, break_points=None, verbose=False, showtiming=False @@ -36,36 +35,12 @@ def __init__( __verbose__ = verbose __showtiming__ = showtiming - if cols: - self._dataframe = self._dataframe[list(cols.values())] - # Rename parameter columns to standard names: from route-link names - # key: "link" - # downstream: "to" - # dx: "Length" - # n: "n" # TODO: rename to `manningn` - # ncc: "nCC" # TODO: rename to `mannningncc` - # s0: "So" # TODO: rename to `bedslope` - # bw: "BtmWdth" # TODO: rename to `bottomwidth` - # waterbody: "NHDWaterbodyComID" - # gages: "gages" - # tw: "TopWdth" # TODO: rename to `topwidth` - # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` - # alt: "alt" - # musk: "MusK" - # musx: "MusX" - # cs: "ChSlp" # TODO: rename to `sideslope` - self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) - self.set_index("key") - self.sort_index() - self._waterbody_connections = {} #TODO set in individual network objects?... - self._gages = None #TODO set in individual network objects?... self._independent_networks = None self._reverse_network = None self._reaches_by_tw = None self._q0 = None self._t0 = None self._qlateral = None - self._waterbody_type_specified = None #qlat_const = forcing_parameters.get("qlat_const", 0) #FIXME qlat_const """ Figure out a good way to default initialize to qlat_const/c diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 9366659b1..a7bb9a3a0 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -4,6 +4,7 @@ import time import troute.nhd_io as nhd_io #FIXME import troute.hyfeature_preprocess as hyfeature_prep +from troute.nhd_network import reverse_dict __verbose__ = False __showtiming__ = False @@ -49,6 +50,10 @@ def __init__(self, waterbody_parameters, ) + self._waterbody_connections = {} + self._waterbody_type_specified = None + self._gages = None + if __verbose__: print("supernetwork connections set complete") if __showtiming__: @@ -62,12 +67,33 @@ def __init__(self, break_network_at_gages = streamflow_da.get('streamflow_nudging', False) break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} + + if cols: + self._dataframe = self._dataframe[list(cols.values())] + # Rename parameter columns to standard names: from route-link names + # key: "link" + # downstream: "to" + # dx: "Length" + # n: "n" # TODO: rename to `manningn` + # ncc: "nCC" # TODO: rename to `mannningncc` + # s0: "So" # TODO: rename to `bedslope` + # bw: "BtmWdth" # TODO: rename to `bottomwidth` + # waterbody: "NHDWaterbodyComID" + # gages: "gages" + # tw: "TopWdth" # TODO: rename to `topwidth` + # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` + # alt: "alt" + # musk: "MusK" + # musx: "MusX" + # cs: "ChSlp" # TODO: rename to `sideslope` + self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) + self.set_index("key") + self.sort_index() super().__init__( compute_parameters, waterbody_parameters, - restart_parameters, - cols, + restart_parameters, break_points, verbose=__verbose__, showtiming=__showtiming__, diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index ff2c3ab0d..9edd4c73b 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -85,8 +85,7 @@ def __init__( super().__init__( compute_parameters, waterbody_parameters, - restart_parameters, - cols, + restart_parameters, break_points, verbose=__verbose__, showtiming=__showtiming__, diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index 462615722..9bbe0e90f 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -57,12 +57,12 @@ def read_geo_file( # otherwise valid node value being pointed to, but which is masked out or # being intentionally separated into another domain. terminal_codes = terminal_codes | set( - dataframe[~dataframe["downstream"].isin(dataframe.index)]["downstream"].values + dataframe[~dataframe["toid"].isin(dataframe.index)]["toid"].values ) # build connections dictionary connections = nhd_network.extract_connections( - dataframe, "downstream", terminal_codes=terminal_codes + dataframe, "toid", terminal_codes=terminal_codes ) #Load waterbody/reservoir info From 82f4ac20fb38b2d70cb0e3fd399a9d9b2deffdaf Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 16 Dec 2022 20:14:40 +0000 Subject: [PATCH 06/54] move renaming dataframe columns and terminal codes/connections creation into read_geo_file function for HyFeatures --- .../troute/HYFeaturesNetwork.py | 23 -------------- .../troute/hyfeature_preprocess.py | 30 +++++++++++++++++-- src/troute-nwm/src/nwm_routing/__main__.py | 1 + 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index a7bb9a3a0..48314144e 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -59,7 +59,6 @@ def __init__(self, if __showtiming__: print("... in %s seconds." % (time.time() - start_time)) - cols = supernetwork_parameters.get('columns',None) break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) streamflow_da = data_assimilation_parameters.get('streamflow_da', False) break_network_at_gages = False @@ -67,28 +66,6 @@ def __init__(self, break_network_at_gages = streamflow_da.get('streamflow_nudging', False) break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} - - if cols: - self._dataframe = self._dataframe[list(cols.values())] - # Rename parameter columns to standard names: from route-link names - # key: "link" - # downstream: "to" - # dx: "Length" - # n: "n" # TODO: rename to `manningn` - # ncc: "nCC" # TODO: rename to `mannningncc` - # s0: "So" # TODO: rename to `bedslope` - # bw: "BtmWdth" # TODO: rename to `bottomwidth` - # waterbody: "NHDWaterbodyComID" - # gages: "gages" - # tw: "TopWdth" # TODO: rename to `topwidth` - # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` - # alt: "alt" - # musk: "MusK" - # musx: "MusX" - # cs: "ChSlp" # TODO: rename to `sideslope` - self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) - self.set_index("key") - self.sort_index() super().__init__( compute_parameters, diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index 9bbe0e90f..4f3a7bac7 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -43,7 +43,31 @@ def read_geo_file( flowpath_dict = dict(zip(dataframe.loc[mask].toid, dataframe.loc[mask].id)) # ********** need to be included in flowpath_attributes ************* - dataframe['alt'] = 1.0 #FIXME get the right value for this... + dataframe['alt'] = 1.0 #FIXME get the right value for this... + + cols = supernetwork_parameters.get('columns',None) + + if cols: + dataframe = dataframe[list(cols.values())] + # Rename parameter columns to standard names: from route-link names + # key: "link" + # downstream: "to" + # dx: "Length" + # n: "n" # TODO: rename to `manningn` + # ncc: "nCC" # TODO: rename to `mannningncc` + # s0: "So" # TODO: rename to `bedslope` + # bw: "BtmWdth" # TODO: rename to `bottomwidth` + # waterbody: "NHDWaterbodyComID" + # gages: "gages" + # tw: "TopWdth" # TODO: rename to `topwidth` + # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` + # alt: "alt" + # musk: "MusK" + # musx: "MusX" + # cs: "ChSlp" # TODO: rename to `sideslope` + dataframe = dataframe.rename(columns=reverse_dict(cols)) + dataframe.set_index("key", inplace=True) + dataframe.sort_index() # numeric code used to indicate network terminal segments terminal_code = supernetwork_parameters.get("terminal_code", 0) @@ -57,12 +81,12 @@ def read_geo_file( # otherwise valid node value being pointed to, but which is masked out or # being intentionally separated into another domain. terminal_codes = terminal_codes | set( - dataframe[~dataframe["toid"].isin(dataframe.index)]["toid"].values + dataframe[~dataframe["downstream"].isin(dataframe.index)]["downstream"].values ) # build connections dictionary connections = nhd_network.extract_connections( - dataframe, "toid", terminal_codes=terminal_codes + dataframe, "downstream", terminal_codes=terminal_codes ) #Load waterbody/reservoir info diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index a0b5ce40b..ac89e44dd 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -236,6 +236,7 @@ def main_v04(argv): network.assemble_forcings(run_sets[run_set_iterator + 1], forcing_parameters, hybrid_parameters, + supernetwork_parameters, cpu_pool) # get reservoir DA initial parameters for next loop iteration From 830c6efc9283d3e3fe675a87bf92d3d0b9111dae Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Wed, 21 Dec 2022 15:47:24 +0000 Subject: [PATCH 07/54] move functions from ..._preprocess.py files into the network objects as inherent functions --- src/troute-network/troute/AbstractNetwork.py | 654 ++++++++++++++++-- .../troute/HYFeaturesNetwork.py | 198 +++++- src/troute-network/troute/NHDNetwork.py | 263 ++++++- .../troute/hyfeature_preprocess.py | 2 +- src/troute-nwm/src/nwm_routing/__main__.py | 11 +- 5 files changed, 1042 insertions(+), 86 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index f79954761..dc5e2f72d 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -1,12 +1,20 @@ from abc import ABC, abstractmethod from functools import partial import pandas as pd +import numpy as np +import pyarrow.parquet as pq from datetime import datetime, timedelta +from joblib import delayed, Parallel +import netCDF4 import time +import logging +import pathlib from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition -import troute.nhd_io as nhd_io -import troute.abstractnetwork_preprocess as abs_prep +from troute.nhd_network_utilities_v02 import organize_independent_networks, build_channel_initial_state, build_refac_connections +import troute.nhd_io as nhd_io + +LOG = logging.getLogger('') class AbstractNetwork(ABC): """ @@ -15,11 +23,11 @@ class AbstractNetwork(ABC): __slots__ = ["_dataframe", "_waterbody_connections", "_gages", "_terminal_codes", "_connections", "_waterbody_df", "_waterbody_types_df", "_waterbody_type_specified", - "_independent_networks", "_reaches_by_tw", - "_reverse_network", "_q0", "_t0", + "_independent_networks", "_reaches_by_tw", "_flowpath_dict", + "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", "_qlateral", "_break_segments", "_coastal_boundary_depth_df", - "diffusive_network_data", "topobathy_df", "refactored_diffusive_domain", - "refactored_reaches", "unrefactored_topobathy_df", "segment_index"] + "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", + "_refactored_reaches", "_unrefactored_topobathy_df", "_segment_index"] def __init__( self, @@ -53,58 +61,19 @@ def __init__( ) """ self._break_segments = set() + if break_points: if break_points["break_network_at_waterbodies"]: self._break_segments = self._break_segments | set(self.waterbody_connections.values()) if break_points["break_network_at_gages"]: - self._break_segments = self._break_segments | set(self.gages.values()) + self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) - ( - self._dataframe, - self._connections, - self.diffusive_network_data, - self.topobathy_df, - self.refactored_diffusive_domain, - self.refactored_reaches, - self.unrefactored_topobathy_df - ) = abs_prep.build_diffusive_domain( - compute_parameters, - self._dataframe, - self._connections, - ) + self.build_diffusive_domain(compute_parameters) - ( - self._independent_networks, - self._reaches_by_tw, - self._reverse_network - ) = abs_prep.create_independent_networks( - waterbody_parameters, - self._connections, - self._waterbody_connections, - #gages, #TODO update how gages are provided when we figure out DA - ) - - if __verbose__: - print("setting waterbody and channel initial states ...") - if __showtiming__: - start_time = time.time() + self.create_independent_networks(waterbody_parameters) + + self.initial_warmstate_preprocess(break_points["break_network_at_waterbodies"],restart_parameters) - ( - self._waterbody_df, - self._q0, - self._t0 - ) = abs_prep.initial_warmstate_preprocess( - break_points["break_network_at_waterbodies"], - restart_parameters, - self._dataframe.index, - self._waterbody_df, - ) - - if __verbose__: - print("waterbody and channel initial states complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) - start_time = time.time() def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): """ @@ -162,12 +131,10 @@ def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernet # TODO: add an option for reading qlat data from BMI/model engine from_file = True if from_file: - self._qlateral = abs_prep.build_qlateral_array( + self.build_qlateral_array( run, cpu_pool, - self._flowpath_dict, - supernetwork_parameters, - self._dataframe.index, + supernetwork_parameters, ) #--------------------------------------------------------------------- @@ -277,17 +244,21 @@ def reaches_by_tailwater(self): @property def waterbody_dataframe(self): - return self._waterbody_df + return self._waterbody_df.sort_index() @property def waterbody_types_dataframe(self): - return self._waterbody_types_df + return self._waterbody_types_df.sort_index() @property def waterbody_type_specified(self): if self._waterbody_type_specified is None: self._waterbody_type_specified = False return self._waterbody_type_specified + + @property + def link_lake_crosswalk(self): + return self._link_lake_crosswalk @property def connections(self): @@ -335,6 +306,47 @@ def t0(self, value): self._t0 = value else: self._t0 = datetime.strptime(value, "%Y-%m-%d_%H:%M:%S") + + @property + def segment_index(self): + """ + Segment IDs of all reaches in parameter dataframe + and diffusive domain. + """ + # list of all segments in the domain (MC + diffusive) + self._segment_index = self.dataframe.index + if self.diffusive_network_data: + for tw in self.diffusive_network_data: + self._segment_index = self._segment_index.append( + pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) + ) + return self._segment_index + + @property + def link_gage_df(self): + link_gage_df = pd.DataFrame.from_dict(self._gages) + link_gage_df.index.name = 'link' + return link_gage_df + + @property + def diffusive_network_data(self): + return self._diffusive_network_data + + @property + def topobathy_df(self): + return self._topobathy_df + + @property + def refactored_diffusive_domain(self): + return self._refactored_diffusive_domain + + @property + def refactored_reaches(self): + return self._refactored_reaches + + @property + def unrefactored_topobathy_df(self): + return self._unrefactored_topobathy_df @property @abstractmethod @@ -425,3 +437,531 @@ def astype(self, type, columns=None): else: self._dataframe = self._dataframe.astype(type) + + def build_diffusive_domain(self, compute_parameters): + """ + + """ + hybrid_params = compute_parameters.get("hybrid_parameters", False) + if hybrid_params: + # switch parameters + # if run_hybrid = False, run MC only + # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc + # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric + # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric + run_hybrid = hybrid_params.get('run_hybrid_routing', False) + use_topobathy = hybrid_params.get('use_natl_xsections', False) + run_refactored = hybrid_params.get('run_refactored_network', False) + + # file path parameters of non-refactored hydrofabric defined by RouteLink.nc + domain_file = hybrid_params.get("diffusive_domain", None) + topobathy_file = hybrid_params.get("topobathy_domain", None) + + # file path parameters of refactored hydrofabric for diffusive wave channel routing + refactored_domain_file = hybrid_params.get("refactored_domain", None) + refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) + #------------------------------------------------------------------------- + # for non-refactored hydofabric defined by RouteLink.nc + # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. + if run_hybrid and domain_file: + + LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') + + # read diffusive domain dictionary from yaml or json + diffusive_domain = nhd_io.read_diffusive_domain(domain_file) + + if use_topobathy and topobathy_file: + + LOG.debug('Natural cross section data on original hydrofabric are provided.') + + # read topobathy domain netcdf file, set index to 'comid' + # TODO: replace 'link' with a user-specified indexing variable name. + # ... if for whatever reason there is not a `link` variable in the + # ... dataframe returned from read_netcdf, then the code would break here. + self._topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) + + # TODO: Request GID make comID variable an integer in their product, so + # we do not need to change variable types, here. + self._topobathy_df.index = self._topobathy_df.index.astype(int) + + else: + self._topobathy_df = pd.DataFrame() + LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') + + # initialize a dictionary to hold network data for each of the diffusive domains + self._diffusive_network_data = {} + + else: + diffusive_domain = None + self._diffusive_network_data = None + self._topobathy_df = pd.DataFrame() + LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') + self._unrefactored_topobathy_df = pd.DataFrame() + #------------------------------------------------------------------------- + # for refactored hydofabric + if run_hybrid and run_refactored and refactored_domain_file: + + LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') + + # read diffusive domain dictionary from yaml or json + self._refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) + + if use_topobathy and refactored_topobathy_file: + + LOG.debug('Natural cross section data of refactored hydrofabric are provided.') + + # read topobathy domain netcdf file, set index to 'comid' + # TODO: replace 'link' with a user-specified indexing variable name. + # ... if for whatever reason there is not a `link` variable in the + # ... dataframe returned from read_netcdf, then the code would break here. + self._topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) + + # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy + # for crosswalking water elevations between non-refactored and refactored hydrofabrics. + self._unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) + self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) + + else: + self._topobathy_df = pd.DataFrame() + LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') + + # initialize a dictionary to hold network data for each of the diffusive domains + refactored_diffusive_network_data = {} + + else: + self._refactored_diffusive_domain = None + refactored_diffusive_network_data = None + self._refactored_reaches = {} + LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') + + else: + diffusive_domain = None + self._diffusive_network_data = None + self._topobathy_df = pd.DataFrame() + self._unrefactored_topobathy_df = pd.DataFrame() + self._refactored_diffusive_domain = None + refactored_diffusive_network_data = None + self._refactored_reaches = {} + LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') + + #============================================================================ + # build diffusive domain data and edit MC domain data for hybrid simulation + + # + if diffusive_domain: + rconn_diff0 = reverse_network(self._connections) + self._refactored_reaches = {} + + for tw in diffusive_domain: + mainstem_segs = diffusive_domain[tw]['links'] + # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is + # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. + upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] + if upstream_boundary_mainstem_link[0] in mainstem_segs: + mainstem_segs.remove(upstream_boundary_mainstem_link[0]) + + # ===== build diffusive network data objects ==== + self._diffusive_network_data[tw] = {} + + # add diffusive domain segments + self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs + + # diffusive domain tributary segments + trib_segs = [] + + for seg in mainstem_segs: + us_list = rconn_diff0[seg] + for u in us_list: + if u not in mainstem_segs: + trib_segs.append(u) + + self._diffusive_network_data[tw]['tributary_segments'] = trib_segs + # diffusive domain connections object + self._diffusive_network_data[tw]['connections'] = {k: self._connections[k] for k in (mainstem_segs + trib_segs)} + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, reaches, rconn_diff = organize_independent_networks( + self._diffusive_network_data[tw]['connections'], + set(trib_segs), + set(), + ) + + self._diffusive_network_data[tw]['rconn'] = rconn_diff + self._diffusive_network_data[tw]['reaches'] = reaches[tw] + + # RouteLink parameters + self._diffusive_network_data[tw]['param_df'] = self._dataframe.filter( + (mainstem_segs + trib_segs), + axis = 0, + ) + self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link + + if self._refactored_diffusive_domain: + diffusive_parameters = {'geo_file_path': refactored_topobathy_file} + refactored_connections = build_refac_connections(diffusive_parameters) + + # list of stream segments of a single refactored diffusive domain + refac_tw = self._refactored_diffusive_domain[tw]['refac_tw'] + rlinks_tw = self._refactored_diffusive_domain[tw]['rlinks'] + refactored_connections_tw = {} + + # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections + # for a single refactored diffusive domain defined by a current tw. + for k in rlinks_tw: + if k in refactored_connections.keys() and k != refac_tw: + refactored_connections_tw[k] = refactored_connections[k] + + refactored_diffusive_network_data[refac_tw] = {} + refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs + refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw + + for k in trib_segs: + refactored_diffusive_network_data[refac_tw]['connections'][k]= [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( + refactored_diffusive_network_data[refac_tw]['connections'], + set(trib_segs), + set(), + ) + + self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] + refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] + refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] + else: + self._refactored_reaches={} + + # ==== remove diffusive domain segs from MC domain ==== + # drop indices from param_df + self._dataframe = self._dataframe.drop(mainstem_segs) + + # remove keys from connections dictionary + for s in mainstem_segs: + self._connections.pop(s) + + # update downstream connections of trib segs + for us in trib_segs: + self._connections[us] = [] + + def create_independent_networks(self, waterbody_parameters): + + LOG.info("organizing connections into reaches ...") + start_time = time.time() + gage_break_segments = set() + wbody_break_segments = set() + + break_network_at_waterbodies = waterbody_parameters.get( + "break_network_at_waterbodies", False + ) + + # if streamflow DA, then break network at gages + #TODO update to work with HYFeatures, need to determine how we'll do DA... + break_network_at_gages = False + + if break_network_at_waterbodies: + wbody_break_segments = wbody_break_segments.union(self._waterbody_connections.values()) + + if break_network_at_gages: + gage_break_segments = gage_break_segments.union(self.gages['gages'].keys()) + + ( + self._independent_networks, + self._reaches_by_tw, + self._reverse_network + ) = organize_independent_networks( + self.connections, + wbody_break_segments, + gage_break_segments, + ) + + LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) + + def initial_warmstate_preprocess(self, break_network_at_waterbodies, restart_parameters,): + + ''' + Assemble model initial condition data: + - waterbody inital states (outflow and pool elevation) + - channel initial states (flow and depth) + - initial time + + Arguments + --------- + - break_network_at_waterbodies (bool): If True, waterbody initial states will + be appended to the waterbody parameter + dataframe. If False, waterbodies will + not be simulated and the waterbody + parameter datataframe wil not be changed + - restart_parameters (dict): User-input simulation restart + parameters + - segment_index (Pandas Index): All segment IDs in the simulation + doamin + - waterbodies_df (Pandas DataFrame): Waterbody parameters + + Returns + ------- + - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial + states (outflow and pool elevation) + - q0 (Pandas DataFrame): Initial flow and depth states for each + segment in the model domain + - t0 (datetime): Datetime of the model initialization + + Notes + ----- + ''' + + # generalize waterbody ID's to be used with any network + index_id = self.waterbody_dataframe.index.names[0] + + #---------------------------------------------------------------------------- + # Assemble waterbody initial states (outflow and pool elevation + #---------------------------------------------------------------------------- + if break_network_at_waterbodies: + + start_time = time.time() + LOG.info("setting waterbody initial states ...") + + # if a lite restart file is provided, read initial states from it. + if restart_parameters.get("lite_waterbody_restart_file", None): + + waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( + restart_parameters['lite_waterbody_restart_file'] + ) + + # read waterbody initial states from WRF-Hydro type restart file + elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): + waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( + restart_parameters["wrf_hydro_waterbody_restart_file"], + restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], + restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", index_id), + restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], + restart_parameters.get( + "wrf_hydro_waterbody_crosswalk_filter_file_field_name", + 'NHDWaterbodyComID' + ), + ) + + # if no restart file is provided, default initial states + else: + # TODO: Consider adding option to read cold state from route-link file + waterbodies_initial_ds_flow_const = 0.0 + waterbodies_initial_depth_const = -1e9 + # Set initial states from cold-state + waterbodies_initial_states_df = pd.DataFrame( + 0, + index=self.waterbody_dataframe.index, + columns=[ + "qd0", + "h0", + ], + dtype="float32", + ) + # TODO: This assignment could probably by done in the above call + waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const + waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const + waterbodies_initial_states_df["index"] = range( + len(waterbodies_initial_states_df) + ) + + self._waterbody_df = pd.merge( + self.waterbody_dataframe, waterbodies_initial_states_df, on=index_id + ) + + LOG.debug( + "waterbody initial states complete in %s seconds."\ + % (time.time() - start_time)) + start_time = time.time() + + #---------------------------------------------------------------------------- + # Assemble channel initial states (flow and depth) + # also establish simulation initialization timestamp + #---------------------------------------------------------------------------- + start_time = time.time() + LOG.info("setting channel initial states ...") + + # if lite restart file is provided, the read channel initial states from it + if restart_parameters.get("lite_channel_restart_file", None): + # FIXME: Change it for hyfeature! + self._q0, self._t0 = nhd_io.read_lite_restart( + restart_parameters['lite_channel_restart_file'] + ) + t0_str = None + + # when a restart file for hyfeature is provied, then read initial states from it. + elif restart_parameters.get("hyfeature_channel_restart_file", None): + self._q0 = build_channel_initial_state(restart_parameters, self.segment_index) + channel_initial_states_file = restart_parameters["hyfeature_channel_restart_file"] + df = pd.read_csv(channel_initial_states_file) + t0_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + self._t0 = datetime.strptime(t0_str,"%Y-%m-%d_%H:%M:%S") + + # build initial states from user-provided restart parameters + else: + # FIXME: Change it for hyfeature! + self._q0 = build_channel_initial_state(restart_parameters, self.segment_index) + + # get initialization time from restart file + if restart_parameters.get("wrf_hydro_channel_restart_file", None): + channel_initial_states_file = restart_parameters[ + "wrf_hydro_channel_restart_file" + ] + t0_str = nhd_io.get_param_str( + channel_initial_states_file, + "Restart_Time" + ) + else: + t0_str = "2015-08-16_00:00:00" + + # convert timestamp from string to datetime + self._t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") + + # get initial time from user inputs + if restart_parameters.get("start_datetime", None): + t0_str = restart_parameters.get("start_datetime") + + def _try_parsing_date(text): + for fmt in ( + "%Y-%m-%d_%H:%M", + "%Y-%m-%d_%H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%d %H:%M:%S", + "%Y/%m/%d %H:%M", + "%Y/%m/%d %H:%M:%S" + ): + try: + return datetime.strptime(text, fmt) + except ValueError: + pass + LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') + quit() + + self._t0 = _try_parsing_date(t0_str) + else: + if t0_str == "2015-08-16_00:00:00": + LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') + else: + LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) + + LOG.debug( + "channel initial states complete in %s seconds."\ + % (time.time() - start_time) + ) + + def build_qlateral_array(self, run, cpu_pool, supernetwork_parameters,): + + start_time = time.time() + LOG.info("Creating a DataFrame of lateral inflow forcings ...") + + # TODO: set default/optional arguments + qts_subdivisions = run.get("qts_subdivisions", 1) + nts = run.get("nts", 1) + qlat_input_folder = run.get("qlat_input_folder", None) + qlat_input_file = run.get("qlat_input_file", None) + + geo_file_type = supernetwork_parameters.get('geo_file_type') + + if qlat_input_folder: + qlat_input_folder = pathlib.Path(qlat_input_folder) + if "qlat_files" in run: + qlat_files = run.get("qlat_files") + qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] + elif "qlat_file_pattern_filter" in run: + qlat_file_pattern_filter = run.get( + "qlat_file_pattern_filter", "*CHRT_OUT*" + ) + qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) + + qlat_file_index_col = run.get( + "qlat_file_index_col", "feature_id" + ) + + if geo_file_type=='NHDNetwork': + # Parallel reading of qlateral data from CHRTOUT + with Parallel(n_jobs=cpu_pool) as parallel: + jobs = [] + for f in qlat_files: + jobs.append( + delayed(nhd_io.get_ql_from_chrtout) + #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) + #delayed(nhd_io.get_ql_from_csv) + (f) + ) + ql_list = parallel(jobs) + + # get feature_id from a single CHRTOUT file + with netCDF4.Dataset(qlat_files[0]) as ds: + idx = ds.variables[qlat_file_index_col][:].filled() + + # package data into a DataFrame + qlats_df = pd.DataFrame( + np.stack(ql_list).T, + index = idx, + columns = range(len(qlat_files)) + ) + + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + elif geo_file_type=='HYFeaturesNetwork': + dfs=[] + for f in qlat_files: + df = read_file(f).set_index(['feature_id']) + dfs.append(df) + + # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids + nexuses_lateralflows_df = pd.concat(dfs, axis=1) + + # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids + qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) + for k,v in self.downstream_flowpath_dict.items() ), axis=1 + ).T + qlats_df.columns=range(len(qlat_files)) + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + # The segment_index has the full network set of segments/flowpaths. + # Whereas the set of flowpaths that are downstream of nexuses is a + # subset of the segment_index. Therefore, all of the segments/flowpaths + # that are not accounted for in the set of flowpaths downstream of + # nexuses need to be added to the qlateral dataframe and padded with + # zeros. + all_df = pd.DataFrame( np.zeros( (len(self.segment_index), len(qlats_df.columns)) ), index=self.segment_index, + columns=qlats_df.columns ) + all_df.loc[ qlats_df.index ] = qlats_df + qlats_df = all_df.sort_index() + + elif qlat_input_file: + qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) + else: + qlat_const = run.get("qlat_const", 0) + qlats_df = pd.DataFrame( + qlat_const, + index=self.segment_index, + columns=range(nts // qts_subdivisions), + dtype="float32", + ) + + # TODO: Make a more sophisticated date-based filter + max_col = 1 + nts // qts_subdivisions + if len(qlats_df.columns) > max_col: + qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) + + if not self.segment_index.empty: + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + LOG.debug( + "lateral inflow DataFrame creation complete in %s seconds." \ + % (time.time() - start_time) + ) + + self._qlateral = qlats_df + + + +def read_file(file_name): + extension = file_name.suffix + if extension=='.csv': + df = pd.read_csv(file_name) + elif extension=='.parquet': + df = pq.read_table(file_name).to_pandas().reset_index() + df.index.name = None + + return df \ No newline at end of file diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 48314144e..f5d3cd236 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -1,22 +1,99 @@ from .AbstractNetwork import AbstractNetwork import pandas as pd import numpy as np +import geopandas as gpd import time +import os +import json +from pathlib import Path + import troute.nhd_io as nhd_io #FIXME import troute.hyfeature_preprocess as hyfeature_prep -from troute.nhd_network import reverse_dict +from troute.nhd_network import reverse_dict, extract_connections __verbose__ = False __showtiming__ = False +def read_geopkg(file_path): + flowpaths = gpd.read_file(file_path, layer="flowpaths") + attributes = gpd.read_file(file_path, layer="flowpath_attributes").drop('geometry', axis=1) + #merge all relevant data into a single dataframe + flowpaths = pd.merge(flowpaths, attributes, on='id') + + return flowpaths + +def read_json(file_path, edge_list): + dfs = [] + with open(edge_list) as edge_file: + edge_data = json.load(edge_file) + edge_map = {} + for id_dict in edge_data: + edge_map[ id_dict['id'] ] = id_dict['toid'] + with open(file_path) as data_file: + json_data = json.load(data_file) + for key_wb, value_params in json_data.items(): + df = pd.json_normalize(value_params) + df['id'] = key_wb + df['toid'] = edge_map[key_wb] + dfs.append(df) + df_main = pd.concat(dfs, ignore_index=True) + + return df_main + +def numeric_id(flowpath): + id = flowpath['id'].split('-')[-1] + toid = flowpath['toid'].split('-')[-1] + flowpath['id'] = int(id) + flowpath['toid'] = int(toid) + +def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): + """ + Reads .gpkg or lake.json file and prepares a dataframe, filtered + to the relevant reservoirs, to provide the parameters + for level-pool reservoir computation. + """ + def node_key_func(x): + return int(x[3:]) + if os.path.splitext(parm_file)[1]=='.gpkg': + df = gpd.read_file(parm_file, layer="lake_attributes").set_index('id') + elif os.path.splitext(parm_file)[1]=='.json': + df = pd.read_json(parm_file, orient="index") + + df.index = df.index.map(node_key_func) + df.index.name = lake_index_field + + if lake_id_mask: + df = df.loc[lake_id_mask] + return df + +def read_ngen_waterbody_type_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): + """ + """ + #FIXME: this function is likely not correct. Unclear how we will get + # reservoir type from the gpkg files. Information should be in 'crosswalk' + # layer, but as of now (Nov 22, 2022) there doesn't seem to be a differentiation + # between USGS reservoirs, USACE reservoirs, or RFC reservoirs... + def node_key_func(x): + return int(x[3:]) + + if os.path.splitext(parm_file)[1]=='.gpkg': + df = gpd.read_file(parm_file, layer="crosswalk").set_index('id') + elif os.path.splitext(parm_file)[1]=='.json': + df = pd.read_json(parm_file, orient="index") + + df.index = df.index.map(node_key_func) + df.index.name = lake_index_field + if lake_id_mask: + df = df.loc[lake_id_mask] + + return df + class HYFeaturesNetwork(AbstractNetwork): """ """ - __slots__ = ["_flowpath_dict", - "segment_index", - ] + __slots__ = [] def __init__(self, supernetwork_parameters, waterbody_parameters, @@ -50,9 +127,12 @@ def __init__(self, waterbody_parameters, ) + #TODO Update for waterbodies and DA specific to HYFeatures... self._waterbody_connections = {} self._waterbody_type_specified = None self._gages = None + self._link_lake_crosswalk = None + if __verbose__: print("supernetwork connections set complete") @@ -136,4 +216,114 @@ def gages(self): @property def waterbody_null(self): return np.nan #pd.NA + + def read_geo_file( + self, + supernetwork_parameters, + waterbody_parameters, + ): + + geo_file_path = supernetwork_parameters["geo_file_path"] + + file_type = Path(geo_file_path).suffix + if( file_type == '.gpkg' ): + self._dataframe = read_geopkg(geo_file_path) + elif( file_type == '.json') : + edge_list = supernetwork_parameters['flowpath_edge_list'] + self._dataframe = read_json(geo_file_path, edge_list) + else: + raise RuntimeError("Unsupported file type: {}".format(file_type)) + + # Don't need the string prefix anymore, drop it + mask = ~ self.dataframe['toid'].str.startswith("tnex") + self._dataframe = self.dataframe.apply(numeric_id, axis=1) + + # make the flowpath linkage, ignore the terminal nexus + self._flowpath_dict = dict(zip(self.dataframe.loc[mask].toid, self.dataframe.loc[mask].id)) + + # ********** need to be included in flowpath_attributes ************* + self._dataframe['alt'] = 1.0 #FIXME get the right value for this... + + cols = supernetwork_parameters.get('columns',None) + + if cols: + self._dataframe = self.dataframe[list(cols.values())] + # Rename parameter columns to standard names: from route-link names + # key: "link" + # downstream: "to" + # dx: "Length" + # n: "n" # TODO: rename to `manningn` + # ncc: "nCC" # TODO: rename to `mannningncc` + # s0: "So" # TODO: rename to `bedslope` + # bw: "BtmWdth" # TODO: rename to `bottomwidth` + # waterbody: "NHDWaterbodyComID" + # gages: "gages" + # tw: "TopWdth" # TODO: rename to `topwidth` + # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` + # alt: "alt" + # musk: "MusK" + # musx: "MusX" + # cs: "ChSlp" # TODO: rename to `sideslope` + self._dataframe = self.dataframe.rename(columns=reverse_dict(cols)) + self._dataframe.set_index("key", inplace=True) + self._dataframe = self.dataframe.sort_index() + + # numeric code used to indicate network terminal segments + terminal_code = supernetwork_parameters.get("terminal_code", 0) + + # There can be an externally determined terminal code -- that's this first value + self._terminal_codes = set() + self._terminal_codes.add(terminal_code) + # ... but there may also be off-domain nodes that are not explicitly identified + # but which are terminal (i.e., off-domain) as a result of a mask or some other + # an interior domain truncation that results in a + # otherwise valid node value being pointed to, but which is masked out or + # being intentionally separated into another domain. + self._terminal_codes = self.terminal_codes | set( + self.dataframe[~self.dataframe["downstream"].isin(self.dataframe.index)]["downstream"].values + ) + + # build connections dictionary + self._connections = extract_connections( + self.dataframe, "downstream", terminal_codes=self.terminal_codes + ) + + #Load waterbody/reservoir info + if waterbody_parameters: + levelpool_params = waterbody_parameters.get('level_pool', None) + if not levelpool_params: + # FIXME should not be a hard requirement + raise(RuntimeError("No supplied levelpool parameters in routing config")) + + lake_id = levelpool_params.get("level_pool_waterbody_id", "wb-id") + self._waterbody_df = read_ngen_waterbody_df( + levelpool_params["level_pool_waterbody_parameter_file_path"], + lake_id, + ) + + # Remove duplicate lake_ids and rows + self._waterbody_df = ( + self.waterbody_dataframe.reset_index() + .drop_duplicates(subset=lake_id) + .set_index(lake_id) + ) + + try: + self._waterbody_types_df = read_ngen_waterbody_type_df( + levelpool_params["reservoir_parameter_file"], + lake_id, + #self.waterbody_connections.values(), + ) + # Remove duplicate lake_ids and rows + self._waterbody_types_df =( + self.waterbody_types_dataframe.reset_index() + .drop_duplicates(subset=lake_id) + .set_index(lake_id) + ) + + except ValueError: + #FIXME any reservoir operations requires some type + #So make this default to 1 (levelpool) + self._waterbody_types_df = pd.DataFrame(index=self.waterbody_dataframe.index) + self._waterbody_types_df['reservoir_type'] = 1 diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index 9edd4c73b..06b614405 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -1,12 +1,12 @@ from .AbstractNetwork import AbstractNetwork -import xarray as xr -import pathlib -from collections import defaultdict import troute.nhd_io as nhd_io import troute.nhd_preprocess as nhd_prep import pandas as pd import time -from datetime import datetime, timedelta +import pathlib +from collections import defaultdict + +from troute.nhd_network import reverse_dict, extract_waterbody_connections, gage_mapping, extract_connections, replace_waterbodies_connections __showtiming__ = True #FIXME pass flag __verbose__ = True #FIXME pass verbosity @@ -17,8 +17,8 @@ class NHDNetwork(AbstractNetwork): """ __slots__ = [ - "_link_lake_crosswalk", "_usgs_lake_gage_crosswalk", - "_usace_lake_gage_crosswalk", "_flowpath_dict" + "_usgs_lake_gage_crosswalk", + "_usace_lake_gage_crosswalk", ] def __init__( @@ -48,6 +48,12 @@ def __init__( # Load Geo Data #------------------------------------------------ + self.read_geo_file( + supernetwork_parameters, + waterbody_parameters, + data_assimilation_parameters, + ) + ''' ( self._dataframe, self._connections, @@ -65,13 +71,12 @@ def __init__( waterbody_parameters, data_assimilation_parameters, ) - + ''' if __verbose__: print("supernetwork connections set complete") if __showtiming__: print("... in %s seconds." % (time.time() - start_time)) - cols = supernetwork_parameters.get('columns',None) break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) streamflow_da = data_assimilation_parameters.get('streamflow_da', False) break_network_at_gages = False @@ -90,14 +95,6 @@ def __init__( verbose=__verbose__, showtiming=__showtiming__, ) - - # list of all segments in the domain (MC + diffusive) - self.segment_index = self._dataframe.index - if self.diffusive_network_data: - for tw in self.diffusive_network_data: - self.segment_index = self.segment_index.append( - pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) - ) # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't @@ -125,14 +122,244 @@ def gages(self): """ if self._gages is None and "gages" in self._dataframe.columns: self._gages = nhd_io.build_filtered_gage_df(self._dataframe[["gages"]]) - else: - self._gages = {} + return self._gages @property def waterbody_null(self): return -9999 + + @property + def usgs_lake_gage_crosswalk(self): + return self._usgs_lake_gage_crosswalk + + @property + def usace_lake_gage_crosswalk(self): + return self._usace_lake_gage_crosswalk #@property #def wbody_conn(self): # return self._waterbody_connections + + def read_geo_file( + self, + supernetwork_parameters, + waterbody_parameters, + data_assimilation_parameters + ): + ''' + Construct network connections network, parameter dataframe, waterbody mapping, + and gage mapping. This is an intermediate-level function that calls several + lower level functions to read data, conduct network operations, and extract mappings. + + Arguments + --------- + supernetwork_parameters (dict): User input network parameters + + Returns: + -------- + connections (dict int: [int]): Network connections + param_df (DataFrame): Geometry and hydraulic parameters + wbodies (dict, int: int): segment-waterbody mapping + gages (dict, int: int): segment-gage mapping + + ''' + + # crosswalking dictionary between variables names in input dataset and + # variable names recognized by troute.routing module. + cols = supernetwork_parameters.get( + 'columns', + { + 'key' : 'link', + 'downstream': 'to', + 'dx' : 'Length', + 'n' : 'n', + 'ncc' : 'nCC', + 's0' : 'So', + 'bw' : 'BtmWdth', + 'waterbody' : 'NHDWaterbodyComID', + 'gages' : 'gages', + 'tw' : 'TopWdth', + 'twcc' : 'TopWdthCC', + 'alt' : 'alt', + 'musk' : 'MusK', + 'musx' : 'MusX', + 'cs' : 'ChSlp', + } + ) + + # numeric code used to indicate network terminal segments + terminal_code = supernetwork_parameters.get("terminal_code", 0) + + # read parameter dataframe + self._dataframe = nhd_io.read(pathlib.Path(supernetwork_parameters["geo_file_path"])) + + # select the column names specified in the values in the cols dict variable + self._dataframe = self.dataframe[list(cols.values())] + + # rename dataframe columns to keys in the cols dict variable + self._dataframe = self.dataframe.rename(columns=reverse_dict(cols)) + + # handle synthetic waterbody segments + synthetic_wb_segments = supernetwork_parameters.get("synthetic_wb_segments", None) + synthetic_wb_id_offset = supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) + if synthetic_wb_segments: + # rename the current key column to key32 + key32_d = {"key":"key32"} + self._dataframe = self.dataframe.rename(columns=key32_d) + # create a key index that is int64 + # copy the links into the new column + self._dataframe["key"] = self.dataframe.key32.astype("int64") + # update the values of the synthetic reservoir segments + fix_idx = self.dataframe.key.isin(set(synthetic_wb_segments)) + self._dataframe.loc[fix_idx,"key"] = (self.dataframe[fix_idx].key + synthetic_wb_id_offset).astype("int64") + + # set parameter dataframe index as segment id number, sort + self._dataframe = self.dataframe.set_index("key").sort_index() + + # get and apply domain mask + if "mask_file_path" in supernetwork_parameters: + data_mask = nhd_io.read_mask( + pathlib.Path(supernetwork_parameters["mask_file_path"]), + layer_string=supernetwork_parameters.get("mask_layer_string", None), + ) + data_mask = data_mask.set_index(data_mask.columns[0]) + self._dataframe = self.dataframe.filter(data_mask.index, axis=0) + + # map segment ids to waterbody ids + self._waterbody_connections = {} + if "waterbody" in cols: + self._waterbody_connections = extract_waterbody_connections( + self.dataframe[["waterbody"]] + ) + self._dataframe = self.dataframe.drop("waterbody", axis=1) + + # map segment ids to gage ids + self._gages = {} + if "gages" in cols: + self._gages = gage_mapping(self.dataframe[["gages"]]) + self._dataframe = self.dataframe.drop("gages", axis=1) + + # There can be an externally determined terminal code -- that's this first value + self._terminal_codes = set() + self._terminal_codes.add(terminal_code) + # ... but there may also be off-domain nodes that are not explicitly identified + # but which are terminal (i.e., off-domain) as a result of a mask or some other + # an interior domain truncation that results in a + # otherwise valid node value being pointed to, but which is masked out or + # being intentionally separated into another domain. + self._terminal_codes = self.terminal_codes | set( + self.dataframe[~self.dataframe["downstream"].isin(self.dataframe.index)]["downstream"].values + ) + + # build connections dictionary + self._connections = extract_connections( + self.dataframe, "downstream", terminal_codes=self.terminal_codes + ) + self._dataframe = self.dataframe.drop("downstream", axis=1) + + self._dataframe = self.dataframe.astype("float32") + + break_network_at_waterbodies = waterbody_parameters.get( + "break_network_at_waterbodies", False + ) + + # if waterbodies are being simulated, adjust the connections graph so that + # waterbodies are collapsed to single nodes. Also, build a mapping between + # waterbody outlet segments and lake ids + if break_network_at_waterbodies: + self._connections, self._link_lake_crosswalk = replace_waterbodies_connections( + self.connections, self.waterbody_connections + ) + else: + self._link_lake_crosswalk = None + + #============================================================================ + # Retrieve and organize waterbody parameters + + self._waterbody_type_specified = False + if break_network_at_waterbodies: + + # Read waterbody parameters from LAKEPARM file + level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) + self._waterbody_df = nhd_io.read_lakeparm( + level_pool_params['level_pool_waterbody_parameter_file_path'], + level_pool_params.get("level_pool_waterbody_id", 'lake_id'), + self.waterbody_connections.values() + ) + + # Remove duplicate lake_ids and rows + self._waterbody_df = ( + self.waterbody_dataframe.reset_index() + .drop_duplicates(subset="lake_id") + .set_index("lake_id") + ) + + # Declare empty dataframe + self._waterbody_types_df = pd.DataFrame() + + # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True + reservoir_da = data_assimilation_parameters.get( + 'reservoir_da', + {} + ) + + if reservoir_da: + usgs_hybrid = reservoir_da.get( + 'reservoir_persistence_usgs', + False + ) + usace_hybrid = reservoir_da.get( + 'reservoir_persistence_usace', + False + ) + param_file = reservoir_da.get( + 'gage_lakeID_crosswalk_file', + None + ) + else: + param_file = None + usace_hybrid = False + usgs_hybrid = False + + # check if RFC-type reservoirs are set to true + rfc_params = waterbody_parameters.get('rfc') + if rfc_params: + rfc_forecast = rfc_params.get( + 'reservoir_rfc_forecasts', + False + ) + param_file = rfc_params.get('reservoir_parameter_file',None) + else: + rfc_forecast = False + + if (param_file and reservoir_da) or (param_file and rfc_forecast): + self._waterbody_type_specified = True + ( + self._waterbody_types_df, + self._usgs_lake_gage_crosswalk, + self._usace_lake_gage_crosswalk + ) = nhd_io.read_reservoir_parameter_file( + param_file, + usgs_hybrid, + usace_hybrid, + rfc_forecast, + level_pool_params.get("level_pool_waterbody_id", 'lake_id'), + reservoir_da.get('crosswalk_usgs_gage_field', 'usgs_gage_id'), + reservoir_da.get('crosswalk_usgs_lakeID_field', 'usgs_lake_id'), + reservoir_da.get('crosswalk_usace_gage_field', 'usace_gage_id'), + reservoir_da.get('crosswalk_usace_lakeID_field', 'usace_lake_id'), + self.waterbody_connections.values(), + ) + else: + self._waterbody_type_specified = True + self._waterbody_types_df = pd.DataFrame(data = 1, index = self.waterbody_dataframe.index, columns = ['reservoir_type']) + self._usgs_lake_gage_crosswalk = None + self._usace_lake_gage_crosswalk = None + + else: + # Declare empty dataframes + self._waterbody_types_df = pd.DataFrame() + self._waterbody_df = pd.DataFrame() + self._usgs_lake_gage_crosswalk = None + self._usace_lake_gage_crosswalk = None diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index 4f3a7bac7..5ab0a5bec 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -67,7 +67,7 @@ def read_geo_file( # cs: "ChSlp" # TODO: rename to `sideslope` dataframe = dataframe.rename(columns=reverse_dict(cols)) dataframe.set_index("key", inplace=True) - dataframe.sort_index() + dataframe = dataframe.sort_index() # numeric code used to indicate network terminal segments terminal_code = supernetwork_parameters.get("terminal_code", 0) diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index ac89e44dd..23f056209 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -253,7 +253,6 @@ def main_v04(argv): if showtiming: output_start_time = time.time() - ''' #TODO Update this to work with either network type... nwm_output_generator( run, @@ -266,14 +265,14 @@ def main_v04(argv): qts_subdivisions, compute_parameters.get("return_courant", False), cpu_pool, - network._waterbody_df, ## check: network._waterbody_df ?? def name is different from return self._ .. - network._waterbody_types_df, ## check: network._waterbody_types_df ?? def name is different from return self._ .. + network.waterbody_dataframe, + network.waterbody_types_dataframe, data_assimilation_parameters, data_assimilation.lastobs_df, - pd.DataFrame(), #network.link_gage_df, - None, #network.link_lake_crosswalk, + network.link_gage_df, + network.link_lake_crosswalk, ) - ''' + if showtiming: output_end_time = time.time() From e2f06870fcb7fca3f3cabaa19e63a12f9498c2a1 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Wed, 21 Dec 2022 18:07:44 +0000 Subject: [PATCH 08/54] fixed errors in data_assimilation.update() function --- src/troute-network/troute/DataAssimilation.py | 48 ++++++++++++------- src/troute-nwm/src/nwm_routing/__main__.py | 17 +++++-- test/LowerColorado_TX/test_AnA.yaml | 8 ++-- test/ngen/test_AnA.yaml | 6 +-- 4 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/troute-network/troute/DataAssimilation.py b/src/troute-network/troute/DataAssimilation.py index 68d5c8b1e..195f0d75e 100644 --- a/src/troute-network/troute/DataAssimilation.py +++ b/src/troute-network/troute/DataAssimilation.py @@ -6,6 +6,7 @@ import time import xarray as xr from collections import defaultdict +from datetime import datetime #FIXME parameterize into construciton showtiming = True @@ -798,9 +799,27 @@ def __init__(self, data_assimilation_parameters, run_parameters, waterbody_param # an error. Need to think through this more. if not self._usgs_df.empty: self._usgs_df = self._usgs_df.loc[:,network.t0:] + + def update_after_compute(self, run_results, data_assimilation_parameters, run_parameters,): + ''' + + ''' + # get reservoir DA initial parameters for next loop itteration + self._reservoir_usgs_param_df, self._reservoir_usace_param_df = _set_reservoir_da_params(run_results) + streamflow_da_parameters = data_assimilation_parameters.get('streamflow_da', None) + + if streamflow_da_parameters: + if streamflow_da_parameters.get('streamflow_nudging', False): + self._last_obs_df = new_lastobs(run_results, run_parameters.get("dt") * run_parameters.get("nts")) - def update(self, run_results, data_assimilation_parameters, run_parameters, network, da_run): + def update_for_next_loop( + self, + data_assimilation_parameters, + run_parameters, + network, + da_run + ): ''' Function to update data assimilation object for the next loop iteration. @@ -834,9 +853,6 @@ def update(self, run_results, data_assimilation_parameters, run_parameters, netw - reservoir_usace_df (DataFrame): USACE reservoir observations - reservoir_usace_param_df (DataFrame): USACE reservoir DA parameters ''' - # get reservoir DA initial parameters for next loop itteration - self._reservoir_usgs_param_df, self._reservoir_usace_param_df = _set_reservoir_da_params(run_results) - # update usgs_df if it is not empty streamflow_da_parameters = data_assimilation_parameters.get('streamflow_da', None) reservoir_da_parameters = data_assimilation_parameters.get('reservoir_da', None) @@ -917,21 +933,21 @@ def update(self, run_results, data_assimilation_parameters, run_parameters, netw # but there are DA parameters from the previous loop, then create a # dummy observations df. This allows the reservoir persistence to continue across loops. # USGS Reservoirs - if not network._waterbody_types_df.empty: - if 2 in network._waterbody_types_df['reservoir_type'].unique(): - if self._reservoir_usgs_df.empty and len(self._reservoir_usgs_param_df.index) > 0: + if not network.waterbody_types_dataframe.empty: + if 2 in network.waterbody_types_dataframe['reservoir_type'].unique(): + if self.reservoir_usgs_df.empty and len(self.reservoir_usgs_param_df.index) > 0: self._reservoir_usgs_df = pd.DataFrame( data = np.nan, - index = self._reservoir_usgs_param_df.index, + index = self.reservoir_usgs_param_df.index, columns = [network.t0] ) # USACE Reservoirs - if 3 in network._waterbody_types_df['reservoir_type'].unique(): - if self._reservoir_usace_df.empty and len(self._reservoir_usace_param_df.index) > 0: + if 3 in network._waterbody_types_dataframe['reservoir_type'].unique(): + if self.reservoir_usace_df.empty and len(self.reservoir_usace_param_df.index) > 0: self._reservoir_usace_df = pd.DataFrame( data = np.nan, - index = self._reservoir_usace_param_df.index, + index = self.reservoir_usace_param_df.index, columns = [network.t0] ) @@ -940,17 +956,13 @@ def update(self, run_results, data_assimilation_parameters, run_parameters, netw if 4 in network._waterbody_types_df['reservoir_type'].unique(): waterbody_parameters = update_lookback_hours(run_parameters.get("dt"), run_parameters.get("nts"), waterbody_parameters) ''' - - if streamflow_da_parameters: - if streamflow_da_parameters.get('streamflow_nudging', False): - self._last_obs_df = new_lastobs(run_results, run_parameters.get("dt") * run_parameters.get("nts")) - + # Trim the time-extent of the streamflow_da usgs_df # what happens if there are timeslice files missing on the front-end? # if the first column is some timestamp greater than t0, then this will throw # an error. Need to think through this more. - if not self._usgs_df.empty: - self._usgs_df = self._usgs_df.loc[:,network.t0:] + if not self.usgs_df.empty: + self._usgs_df = self.usgs_df.loc[:,network.t0:] @property def assimilation_parameters(self): diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index 23f056209..225de164a 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -218,6 +218,13 @@ def main_v04(argv): network.new_q0(run_results) network.update_waterbody_water_elevation() + # update reservoir parameters and lastobs_df + data_assimilation.update_after_compute( + run_results, + data_assimilation_parameters, + run_parameters, + ) + # TODO move the conditional call to write_lite_restart to nwm_output_generator. if "lite_restart" in output_parameters: nhd_io.write_lite_restart( @@ -240,11 +247,11 @@ def main_v04(argv): cpu_pool) # get reservoir DA initial parameters for next loop iteration - data_assimilation.update(run_results, - data_assimilation_parameters, - run_parameters, - network, - da_sets[run_set_iterator + 1]) + data_assimilation.update_for_next_loop( + data_assimilation_parameters, + run_parameters, + network, + da_sets[run_set_iterator + 1]) if showtiming: forcing_end_time = time.time() diff --git a/test/LowerColorado_TX/test_AnA.yaml b/test/LowerColorado_TX/test_AnA.yaml index 799222edf..813e90a8d 100644 --- a/test/LowerColorado_TX/test_AnA.yaml +++ b/test/LowerColorado_TX/test_AnA.yaml @@ -68,17 +68,17 @@ compute_parameters: qc_threshold : 1 streamflow_da: #---------- - streamflow_nudging : False + streamflow_nudging : True diffusive_streamflow_nudging : False gage_segID_crosswalk_file : domain/RouteLink.nc crosswalk_gage_field : 'gages' crosswalk_segID_field : 'link' - wrf_hydro_lastobs_file : lastobs/nudgingLastObs.2021-08-23_13:00:00.nc + wrf_hydro_lastobs_file : lastobs/nudgingLastObs.2021-08-23_12:00:00.nc lastobs_output_folder : lastobs/ reservoir_da: #---------- - reservoir_persistence_usgs : False - reservoir_persistence_usace : False + reservoir_persistence_usgs : True + reservoir_persistence_usace : True gage_lakeID_crosswalk_file : domain/reservoir_index_AnA.nc #-------------------------------------------------------------------------------- output_parameters: diff --git a/test/ngen/test_AnA.yaml b/test/ngen/test_AnA.yaml index 03e35cc35..1794094fb 100644 --- a/test/ngen/test_AnA.yaml +++ b/test/ngen/test_AnA.yaml @@ -91,7 +91,7 @@ compute_parameters: #-------------------------------------------------------------------------------- output_parameters: #---------- -# test_output: output/lcr_flowveldepth.pkl + test_output: output/lcr_flowveldepth.pkl lite_restart: #---------- lite_restart_output_directory: @@ -100,7 +100,7 @@ output_parameters: # wrf_hydro_channel_output_source_folder: channel_forcing/ chanobs_output: #---------- -# chanobs_output_directory: output/ -# chanobs_filepath : lcr_chanobs.nc + chanobs_output_directory: output/ + chanobs_filepath : lcr_chanobs.nc # lakeout_output: lakeout/ \ No newline at end of file From 2e64b781feacc8099a3aeb678e72243fc9b10763 Mon Sep 17 00:00:00 2001 From: kumdonoaa <54687070+kumdonoaa@users.noreply.github.com> Date: Fri, 23 Dec 2022 09:48:11 -0600 Subject: [PATCH 09/54] t-route on larger hyfeature domain with hybrid routing (#602) * t-route on larger hyfeature domain with hybrid routing * remove debug line Co-authored-by: Dong Kim --- .../troute/HYFeaturesNetwork.py | 5 +- .../troute/hyfeature_network_utilities.py | 11 ++ .../troute/hyfeature_preprocess.py | 19 ++- src/troute-nwm/src/nwm_routing/output.py | 5 +- .../troute/routing/diffusive_utils.py | 8 +- .../channel_forcing/201512010000NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512010100NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512010200NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512010300NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512010400NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512010500NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512010600NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512010700NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512010800NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512010900NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512011000NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512011100NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512011200NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512011300NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512011400NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512011500NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512011600NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512011700NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512011800NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512011900NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512012000NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512012100NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512012200NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512012300NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/201512020000NEXOUT.csv | 149 ++++++++++++++++++ .../channel_forcing/binary_files/.gitkeep | 0 .../channel_forcing/schout_1.nc | Bin 0 -> 25915 bytes .../domain/coastal_boundary_domain.yaml | 4 + .../domain/coastal_domain.yaml | 17 ++ .../domain/gauge_01013500.gpkg | Bin 0 -> 1454080 bytes .../hyfeature_01013500.yaml | 105 ++++++++++++ test/hyfeature_01013500/lakeout/.gitkeep | 0 test/hyfeature_01013500/lastobs/.gitkeep | 0 test/hyfeature_01013500/output/.gitkeep | 0 .../restart/201512010000NEXOUT.csv | 149 ++++++++++++++++++ .../rfc_TimeSeries/.gitkeep | 0 .../usace_TimeSlice/.gitkeep | 0 .../usgs_TimeSlice/.gitkeep | 0 43 files changed, 4035 insertions(+), 13 deletions(-) create mode 100644 test/hyfeature_01013500/channel_forcing/201512010000NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512010100NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512010200NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512010300NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512010400NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512010500NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512010600NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512010700NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512010800NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512010900NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512011000NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512011100NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512011200NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512011300NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512011400NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512011500NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512011600NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512011700NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512011800NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512011900NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512012000NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512012100NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512012200NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512012300NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/201512020000NEXOUT.csv create mode 100644 test/hyfeature_01013500/channel_forcing/binary_files/.gitkeep create mode 100644 test/hyfeature_01013500/channel_forcing/schout_1.nc create mode 100644 test/hyfeature_01013500/domain/coastal_boundary_domain.yaml create mode 100644 test/hyfeature_01013500/domain/coastal_domain.yaml create mode 100644 test/hyfeature_01013500/domain/gauge_01013500.gpkg create mode 100644 test/hyfeature_01013500/hyfeature_01013500.yaml create mode 100644 test/hyfeature_01013500/lakeout/.gitkeep create mode 100644 test/hyfeature_01013500/lastobs/.gitkeep create mode 100644 test/hyfeature_01013500/output/.gitkeep create mode 100644 test/hyfeature_01013500/restart/201512010000NEXOUT.csv create mode 100644 test/hyfeature_01013500/rfc_TimeSeries/.gitkeep create mode 100644 test/hyfeature_01013500/usace_TimeSlice/.gitkeep create mode 100644 test/hyfeature_01013500/usgs_TimeSlice/.gitkeep diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index cd1791761..0f43de5ab 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -156,7 +156,6 @@ def read_geopkg(file_path): attributes = gpd.read_file(file_path, layer="flowpath_attributes").drop('geometry', axis=1) #merge all relevant data into a single dataframe flowpaths = pd.merge(flowpaths, attributes, on='id') - return flowpaths class HYFeaturesNetwork(AbstractNetwork): @@ -274,10 +273,10 @@ def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, cpu_pool ) #Mask out all non-simulated waterbodies - self._dataframe['waterbody'] = self.waterbody_null + #self._dataframe['waterbody'] = self.waterbody_null #This also remaps the initial NHDComID identity to the HY_Features Waterbody ID for the reservoir... - self._dataframe.loc[self._waterbody_df.index, 'waterbody'] = self._waterbody_df.index.name + #self._dataframe.loc[self._waterbody_df.index, 'waterbody'] = self._waterbody_df.index.name #FIXME should waterbody_df and param_df overlap IDS? Doesn't seem like it should... #self._dataframe.drop(self._waterbody_df.index, axis=0, inplace=True) diff --git a/src/troute-network/troute/hyfeature_network_utilities.py b/src/troute-network/troute/hyfeature_network_utilities.py index e1fc65860..f375cb49a 100644 --- a/src/troute-network/troute/hyfeature_network_utilities.py +++ b/src/troute-network/troute/hyfeature_network_utilities.py @@ -235,6 +235,17 @@ def build_qlateral_array( ).T qlats_df.columns=range(len(nexus_files)) qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + + # The segment_index has the full network set of segments/flowpaths. + # Whereas the set of flowpaths that are downstream of nexuses is a + # subset of the segment_index. Therefore, all of the segments/flowpaths + # that are not accounted for in the set of flowpaths downstream of + # nexuses need to be added to the qlateral dataframe and padded with + # zeros. + all_df = pd.DataFrame( np.zeros( (len(segment_index), len(qlats_df.columns)) ), index=segment_index, + columns=qlats_df.columns ) + all_df.loc[ qlats_df.index ] = qlats_df + qlats_df = all_df.sort_index() elif qlat_input_file: qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) else: diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index 446f356df..5d7605b7c 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -32,7 +32,7 @@ def build_hyfeature_network(supernetwork_parameters, break_network_at_gages = supernetwork_parameters.get("break_network_at_gages", False) break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} - + file_type = Path(geo_file_path).suffix if( file_type == '.gpkg' ): dataframe = hyf_network.read_geopkg(geo_file_path) @@ -379,9 +379,24 @@ def hyfeature_hybrid_routing_preprocess( break_network_at_waterbodies = waterbody_parameters.get( "break_network_at_waterbodies", False ) - + # if streamflow DA, then break network at gages break_network_at_gages = False + + # temporary code to build diffusive_domain using given IDs of head segment and tailwater segment of mainstem. + ''' + headlink_mainstem = 242 + twlink_mainstem = 160 + uslink_mainstem = headlink_mainstem + dslink_mainstem = 1 # initial value + mainstem_list =[headlink_mainstem] + while dslink_mainstem != twlink_mainstem: + dslink_mainstem = connections[uslink_mainstem][0] + mainstem_list.append(dslink_mainstem) + uslink_mainstem = dslink_mainstem + diffusive_domain={} + diffusive_domain[twlink_mainstem] = mainstem_list + ''' if break_network_at_waterbodies: wbody_break_segments = wbody_break_segments.union(wbody_conn.values()) diff --git a/src/troute-nwm/src/nwm_routing/output.py b/src/troute-nwm/src/nwm_routing/output.py index 70b674db2..6ba07b2a0 100644 --- a/src/troute-nwm/src/nwm_routing/output.py +++ b/src/troute-nwm/src/nwm_routing/output.py @@ -200,9 +200,7 @@ def nwm_output_generator( time_index, tmp_variable = map(list,zip(*i_df.columns.tolist())) LOG.info("- writing t-route flow results to LAKEOUT files") start = time.time() - - for i in range(i_df.shape[1]): - + for i in range(i_df.shape[1]): nhd_io.write_waterbody_netcdf( wbdyo, i_df.iloc[:,[i]], @@ -216,7 +214,6 @@ def nwm_output_generator( time_index[i], ) - LOG.debug("writing LAKEOUT files took a total time of %s seconds." % (time.time() - start)) if rsrto: diff --git a/src/troute-routing/troute/routing/diffusive_utils.py b/src/troute-routing/troute/routing/diffusive_utils.py index 053124b1b..0365e740e 100644 --- a/src/troute-routing/troute/routing/diffusive_utils.py +++ b/src/troute-routing/troute/routing/diffusive_utils.py @@ -122,7 +122,7 @@ def fp_network_map( # usrch_hseg_mainstem_j=j # store frj of mainstem headseg's reach in the just upstream of the current reach of frj # frnw_g[frj, 2 + nusrch + r] = usrch_hseg_mainstem_j + 1 - + if seg_list.count(dbfksegID) > 0: # a reach where downstream boundary condition is set. frnw_g[ @@ -147,7 +147,7 @@ def fp_network_map( frnw_g[frj, 3 + i] = ( frnw_g[frj, 3 + i] + 1 ) # upstream reach indicds for frj reach - + return frnw_g @@ -1191,7 +1191,7 @@ def diffusive_input_data_v02( coastal_boundary_depth_df, unrefactored_topobathy_bytw, ): - + """ Build input data objects for diffusive wave model @@ -1384,7 +1384,7 @@ def diffusive_input_data_v02( frj = frj + 1 pynw[frj] = head_segment - frnw_col = 8 + frnw_col = 15 frnw_g = fp_network_map( mainstem_seg_list, mx_jorder, diff --git a/test/hyfeature_01013500/channel_forcing/201512010000NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512010000NEXOUT.csv new file mode 100644 index 000000000..b4656dc5b --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512010000NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512010000 +133,18696681.535335124 +134,11720330.829173712 +135,11751498.617024466 +136,17588287.71593899 +137,17166981.711141553 +138,14991922.130953647 +139,11760708.36161216 +140,16800323.408857446 +141,10063944.234220717 +142,10239862.96942797 +143,11912343.682888241 +144,12936164.78680624 +145,14772847.25383078 +146,13996448.526926506 +147,16676007.679551266 +148,19445480.214207396 +149,11314361.21525342 +150,15963292.408103788 +151,12478549.416551892 +152,16217038.028068095 +153,11217379.719040768 +154,14565790.643050238 +155,15973972.357611049 +156,18749613.424213722 +157,18620241.914954953 +158,11077442.358734693 +159,19006489.38663701 +160,13367497.458088988 +161,19014858.98138808 +224,18652231.187229596 +225,14503674.35296427 +226,12575713.569132315 +227,17415272.438164257 +228,13832723.597585684 +229,16095846.70441041 +230,14958179.393112225 +231,15876573.949386608 +232,15796015.743860502 +233,17198088.612650298 +234,13463469.965306655 +235,12985379.281432094 +236,19761404.474337418 +237,17461436.908612806 +238,17804354.126282558 +239,19770978.828136586 +240,19593045.782101043 +241,14665992.990525 +242,10357935.368434671 +243,17647844.44922342 +274,12566516.899752557 +275,17613535.766979832 +276,14312623.911697898 +277,13904329.172794603 +278,15871692.83429994 +304,15654509.743680041 +305,19376963.481166072 +306,10551160.324025063 +307,18871349.117328722 +313,11804088.916932497 +314,10822585.4058739 +315,18719335.910855725 +316,14322902.329353098 +317,16560179.491485354 +318,15223682.284178978 +320,13208250.776574783 +321,13501785.466755988 +322,19665767.94897104 +324,18146022.144702133 +325,16332388.303802406 +360,13944848.433964804 +361,12649097.254388731 +362,14436048.385149866 +363,19453065.48482711 +364,12347115.765595892 +365,17970542.034300737 +374,16981723.580120485 +375,16514970.602137107 +376,15649700.9848823 +378,10091722.16650288 +379,19650560.60709577 +380,19760271.060520686 +381,18433421.62768803 +383,16548861.206133924 +384,13786154.221711874 +385,19125582.639907874 +394,19881594.238329872 +395,13370814.77197794 +396,15270803.244221915 +431,18593350.34560284 +432,13656193.78252832 +433,19827847.27528677 +434,13554371.96907444 +435,15663858.401981357 +436,18286470.780639898 +460,13918942.003921902 +461,19148321.55512499 +462,14191421.52584503 +464,17812720.27766811 +465,12714260.37081688 +466,12673412.88935984 +468,11135953.222150428 +469,11795132.659906255 +470,19908094.93325103 +471,19488268.253792137 +473,19590543.965774868 +490,15024954.519573634 +492,12151574.96806234 +493,11186674.37949089 +494,19190385.928781327 +495,10961738.722292107 +496,18784578.247330107 +498,19526134.103956573 +499,18735713.432407603 +500,12587457.729077986 +501,15406662.310942918 +502,15649507.111158464 +551,14948607.263991158 +555,10262525.970981276 +558,17738962.116639916 +559,19017589.922479354 +606,12222456.657154663 +608,19641728.874914832 +609,12297539.349783054 +614,12426360.018246742 +615,16775830.954957636 +617,19943068.188316442 +618,18816661.047346003 +678,17110045.914965123 +679,16106684.477563497 +682,16121835.889774654 +683,18430473.274426043 +687,15600613.958502874 +688,18641055.278140035 +690,15582607.783910388 +694,13720372.05782337 +696,16954996.654046718 +699,12314701.242593523 +701,15068147.95409713 +706,19209775.7739928 +708,11107628.84138904 +800,14255482.200622268 +803,18204664.24879384 +809,11305648.07844574 +811,10119787.708434837 +825,15433212.169987174 +830,15084924.313316822 +847,16997500.922211077 +848,10859453.257113755 diff --git a/test/hyfeature_01013500/channel_forcing/201512010100NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512010100NEXOUT.csv new file mode 100644 index 000000000..3b7e1487b --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512010100NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512010100 +133,19025267.426681425 +134,18800867.071693607 +135,13389118.893287843 +136,12134730.632806165 +137,16343647.024853103 +138,11098485.582482202 +139,19257495.450910307 +140,18105613.198798064 +141,14270511.592861507 +142,14176015.499803968 +143,19432587.044482205 +144,14229161.369808467 +145,11067016.890161535 +146,17177737.585795633 +147,14066273.995418351 +148,14378317.071670145 +149,11859707.82822853 +150,16565727.40334988 +151,19210170.634957723 +152,14854684.373184182 +153,16444060.482892968 +154,18945429.400835358 +155,15399888.647404617 +156,16051911.910940958 +157,16863950.665541947 +158,19850721.28320149 +159,12555350.810913397 +160,19702324.881679982 +161,15073880.240958404 +224,17373673.983734455 +225,19133399.52727156 +226,16163779.775725838 +227,14699345.8645511 +228,10476130.522406101 +229,18779181.065280072 +230,15717327.078375895 +231,18792679.172165453 +232,12076978.062353468 +233,10823039.702716453 +234,17944941.88397073 +235,18964623.911098853 +236,13807104.770999854 +237,16201853.977747604 +238,19573741.340550706 +239,14699416.897240847 +240,11467007.820121106 +241,13777541.706077918 +242,18549793.596002117 +243,13454810.59310272 +274,19249847.48269006 +275,15491529.872272864 +276,19454690.896478307 +277,12480484.556017214 +278,14427372.812561939 +304,17712544.963038404 +305,10165620.891198318 +306,14432996.79816689 +307,11002013.849102572 +313,11122953.229370648 +314,12358368.815617304 +315,12727549.24296314 +316,16125715.763377566 +317,14864701.23468276 +318,18684159.12587593 +320,11807670.196715135 +321,14706285.27580605 +322,15697489.821421843 +324,16787134.248114645 +325,16680335.322742093 +360,19506818.392544433 +361,15914125.506084822 +362,12372292.494045567 +363,18316255.48238801 +364,13564134.897929229 +365,16411353.044881102 +374,17032362.973122314 +375,14293249.187804688 +376,11982828.905749833 +378,11434960.021783052 +379,16577124.417594757 +380,10050802.554404646 +381,11806916.352053229 +383,18508967.22699229 +384,15920771.344584793 +385,19169354.44128325 +394,13138609.427773155 +395,18782454.618887246 +396,10391853.386503533 +431,18596235.502708174 +432,12732570.98514615 +433,13975836.671613272 +434,18476619.38203145 +435,11622756.933782153 +436,12385941.192188092 +460,18552534.2275033 +461,18396677.425804727 +462,18343714.03889743 +464,14816398.388283703 +465,15276412.300267527 +466,19523593.163652547 +468,11236291.675904294 +469,13623792.940384563 +470,12477498.312685398 +471,15279128.558217805 +473,10845675.236567922 +490,14830317.22639515 +492,13280576.79222387 +493,15322435.570889343 +494,13394502.22918567 +495,15371067.593251795 +496,17435538.944546625 +498,11777823.1330591 +499,18519980.84341023 +500,16663370.954123454 +501,13856123.365446396 +502,16786412.444738932 +551,10268384.812428987 +555,13982353.68312166 +558,19992673.20846562 +559,13780700.12490289 +606,12520425.898408147 +608,19267135.219955564 +609,13146051.106238112 +614,10851586.06156377 +615,11903418.213834118 +617,17942122.631760623 +618,15674324.135215051 +678,19760905.6878068 +679,11618356.140761778 +682,10621994.546108738 +683,18273529.995702963 +687,15463516.627772667 +688,12540974.554618716 +690,14676446.085630085 +694,17005076.168008856 +696,11933793.15038783 +699,13211578.351581354 +701,13374570.68836468 +706,12008091.469788268 +708,19729805.24921903 +800,16852466.704272836 +803,18617994.537082635 +809,17224107.659445602 +811,15456431.678296342 +825,17308247.540378183 +830,12097974.5647065 +847,10484830.142974785 +848,15560288.650491498 diff --git a/test/hyfeature_01013500/channel_forcing/201512010200NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512010200NEXOUT.csv new file mode 100644 index 000000000..d477e4e07 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512010200NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512010200 +133,14813585.65837286 +134,19631591.95413714 +135,16355643.79068061 +136,17076515.719075345 +137,11670965.2692758 +138,18588322.877037376 +139,19703248.091595475 +140,11409001.850636557 +141,19867628.82555518 +142,19932644.489572667 +143,12558690.330002038 +144,15106266.066751746 +145,12974760.037388712 +146,17857932.429530203 +147,17880922.505451232 +148,17537107.547187157 +149,18886679.713862255 +150,18559660.015842266 +151,14829156.464620266 +152,17792263.2188332 +153,15140011.896018118 +154,19672824.226522878 +155,10977283.20322123 +156,10562493.168343773 +157,19866799.76925748 +158,14507479.216075547 +159,15507164.98007178 +160,12244576.168119304 +161,14083521.646263499 +224,11412360.789452557 +225,15016127.65967229 +226,19313455.342845462 +227,13511356.532696307 +228,19045250.936020114 +229,13067780.915553939 +230,14107329.032999003 +231,16052264.725566413 +232,18378334.869051967 +233,10679422.664015094 +234,15909441.736441776 +235,12991798.139098333 +236,12228403.686630854 +237,14768516.961240606 +238,13771600.455122825 +239,18102619.087921515 +240,14272026.891193356 +241,10207593.528651873 +242,16819317.352535956 +243,18190470.158632673 +274,11323222.737024352 +275,12381116.225966986 +276,18346444.691267177 +277,18677565.19013718 +278,18967785.325811718 +304,10118559.909871284 +305,17955253.590412665 +306,13860857.503659602 +307,15667186.813082863 +313,16733890.262467116 +314,12375935.835619135 +315,19341461.01244939 +316,12271698.072324585 +317,17891782.474621437 +318,17295745.62866164 +320,13193966.269193538 +321,12283880.460709713 +322,16370761.76312802 +324,14060738.5183041 +325,15016485.204653118 +360,11944520.639243577 +361,19513852.011390794 +362,16539253.467628442 +363,17991375.921164446 +364,18244833.311895125 +365,13278913.039909765 +374,12166133.215054642 +375,13598225.580472032 +376,16049631.175061082 +378,13990927.504138831 +379,19382200.545237646 +380,16726182.754627794 +381,18082090.03327663 +383,10914083.606769945 +384,13383368.628246848 +385,13073641.64409304 +394,17907484.83606971 +395,10441625.516957635 +396,14963577.184174761 +431,15448880.919192959 +432,19650735.192025624 +433,19502209.470652882 +434,17009310.97992621 +435,11344378.649815395 +436,13188021.937494995 +460,10713811.954148488 +461,12811182.955520667 +462,12487252.905315263 +464,13700327.102470811 +465,12109672.733231371 +466,12472181.224960105 +468,12206805.686938368 +469,18807536.70613156 +470,17564416.47335747 +471,10291013.592341319 +473,18078121.048620034 +490,12828141.626525953 +492,14695196.57886737 +493,10562862.796963288 +494,19052324.734356456 +495,11934830.135811752 +496,10880204.110048238 +498,11831944.760078775 +499,18844964.593496162 +500,12374324.743517797 +501,14715402.149977539 +502,15028118.795769164 +551,18415974.863703094 +555,18087515.019507844 +558,10201510.277180912 +559,19291660.563672826 +606,16481618.916605975 +608,10802240.75317911 +609,14615496.07407726 +614,17720485.827294 +615,15635342.441234328 +617,15362765.06523436 +618,18277758.37483217 +678,10316276.374141445 +679,17603531.307595603 +682,17419736.733545754 +683,19826975.567426898 +687,15224613.03052854 +688,17888730.802589502 +690,10900196.6469206 +694,14090368.715013936 +696,12107583.388888543 +699,18863515.666697398 +701,14182305.502284925 +706,11322073.365556566 +708,12119850.977143254 +800,17860125.32806575 +803,15742681.53752603 +809,19411747.24558669 +811,17252217.228060152 +825,17007629.682991855 +830,15080314.322480157 +847,14331768.70838651 +848,18393278.89693209 diff --git a/test/hyfeature_01013500/channel_forcing/201512010300NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512010300NEXOUT.csv new file mode 100644 index 000000000..a0f7ea2d8 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512010300NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512010300 +133,19507775.829798847 +134,10270809.247695567 +135,18207781.316983066 +136,13973054.888058964 +137,11599388.735894565 +138,11589612.569557896 +139,15185034.92499781 +140,17189774.49624737 +141,17178896.20846297 +142,19222160.77846468 +143,14561626.238759877 +144,11472229.192273332 +145,12624659.02453426 +146,15984770.54560968 +147,12475098.255853929 +148,11244310.781225445 +149,10160334.949586084 +150,12636472.473451225 +151,10317016.388586728 +152,15439855.260126177 +153,12463386.469413064 +154,11881342.24672136 +155,18444067.998573862 +156,17137683.545728944 +157,12338503.618055433 +158,18925182.31740847 +159,13123935.841174357 +160,12049017.61956165 +161,13648030.130353339 +224,18289825.088368423 +225,15514278.8651799 +226,14056311.519950308 +227,16721860.979530465 +228,16954616.26290336 +229,13616677.777372554 +230,16443655.481961343 +231,16083454.090917975 +232,17644377.806916073 +233,16501998.03144365 +234,19512950.16149872 +235,11652559.868063204 +236,18847886.80881168 +237,14826334.1410219 +238,15026065.834352694 +239,14304225.383623943 +240,18613108.18918646 +241,13721401.531194275 +242,14579748.06679618 +243,19426357.23841073 +274,10221871.14026647 +275,13541988.985150084 +276,16066918.556116946 +277,14503132.819969464 +278,13638723.387797493 +304,13609855.574397705 +305,10856641.134169172 +306,12152441.492202424 +307,15989320.272339255 +313,17536746.225137584 +314,10019197.39674735 +315,19135770.3396668 +316,12951704.172347525 +317,16677324.496353999 +318,13575897.059267512 +320,11333893.592201322 +321,11874725.21871491 +322,11497588.477896672 +324,19242089.506261222 +325,18397707.793608777 +360,15074195.83119975 +361,19770095.443329997 +362,19661341.993851934 +363,12227284.380309427 +364,19105752.263880663 +365,19589504.075853806 +374,19311054.690893345 +375,19742102.135996252 +376,19437138.949865974 +378,18914829.878043238 +379,16905079.04357485 +380,11306006.487247054 +381,16365661.284765076 +383,14089783.909546133 +384,14968346.270010926 +385,19965833.414695613 +394,17428716.056414053 +395,10604351.542592376 +396,13443923.270036057 +431,17107755.238312438 +432,18882971.677484952 +433,19203425.3488463 +434,18004626.560265657 +435,18816276.06003591 +436,11437532.777397934 +460,12321756.736718677 +461,12826570.899412086 +462,17715342.532908887 +464,18703020.85977031 +465,17779334.91423672 +466,11195469.99467086 +468,16915861.211434875 +469,17678072.26614743 +470,17759635.027775764 +471,19454451.895143516 +473,11912309.598836195 +490,12930708.258501228 +492,12475357.466479834 +493,16730866.132092569 +494,10783410.431999685 +495,14849337.442520432 +496,19409105.908366326 +498,16714389.223750414 +499,14041651.459642693 +500,14054647.98310486 +501,17279154.142727807 +502,11034478.361974252 +551,15813821.336444758 +555,12739299.213240236 +558,13974434.412310446 +559,19149683.89298045 +606,12560884.949706938 +608,10036640.83692969 +609,14716545.51641753 +614,19944616.85576451 +615,10181387.59715082 +617,12843237.185600828 +618,14044137.382854708 +678,10070258.345130242 +679,16947294.19327282 +682,11998252.278635997 +683,11856854.05430325 +687,16093555.825970445 +688,10785516.514710417 +690,11939779.075640716 +694,17115998.95222427 +696,15298998.706037719 +699,19001206.978299826 +701,19926727.856654964 +706,12945670.934858805 +708,18018957.94394853 +800,12774238.910572892 +803,16322445.874409541 +809,14775731.32580046 +811,18842722.670760736 +825,16648302.157916177 +830,11613489.901933145 +847,11200912.112502038 +848,18802457.101443514 diff --git a/test/hyfeature_01013500/channel_forcing/201512010400NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512010400NEXOUT.csv new file mode 100644 index 000000000..5340caad8 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512010400NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512010400 +133,16624067.198002463 +134,16701474.631279215 +135,11916886.088741435 +136,12296171.415325688 +137,17785200.38180621 +138,17098014.11126949 +139,14227614.551861236 +140,14010122.925541515 +141,17667617.50499244 +142,14391344.64719854 +143,14101529.125432406 +144,13398359.372667914 +145,15625132.922573976 +146,11765992.294646604 +147,19876506.122922923 +148,12018520.330371037 +149,17191275.96180353 +150,18629039.249652114 +151,18724941.59397547 +152,12719522.046884317 +153,12443136.925965205 +154,19923799.745452333 +155,11464762.068644365 +156,17940719.776134204 +157,12922657.442498092 +158,14274231.174138721 +159,11452301.213950608 +160,12522177.728458202 +161,15046973.234725516 +224,11040175.281069996 +225,19575167.58895203 +226,17135017.753997322 +227,18568885.205023102 +228,12461951.49451172 +229,14893736.406002263 +230,18950650.93659047 +231,11895155.770208858 +232,18097070.09530419 +233,15163096.395760994 +234,15224509.48052274 +235,18390666.58605964 +236,18078588.76092837 +237,17664784.467801515 +238,18439293.537467398 +239,10551192.371586308 +240,15887120.10344953 +241,13306963.512828216 +242,15230196.858964454 +243,16167117.024255373 +274,17514605.11987166 +275,11717271.50814497 +276,11034589.867467616 +277,12212797.172316175 +278,12126017.256422145 +304,11460812.001505721 +305,16723011.332881536 +306,15687621.498689357 +307,11608208.653075708 +313,18030400.862418566 +314,11957793.734099532 +315,18794200.628788628 +316,19787371.56087964 +317,15664333.778182905 +318,15396646.7653161 +320,15602223.246627271 +321,18379312.06458383 +322,14081404.306247992 +324,13350981.497779299 +325,17899966.532870386 +360,10413334.677976763 +361,11649626.243788002 +362,10085157.625647834 +363,12509389.876761561 +364,18815003.491088796 +365,17126734.261851817 +374,18793933.45460366 +375,19239913.181010745 +376,10801346.495966429 +378,19047028.453966442 +379,12484352.7875893 +380,15288514.253671644 +381,17192856.304107968 +383,15654703.739134002 +384,15513837.079829164 +385,12130101.333351092 +394,11956642.610330794 +395,10398749.403445443 +396,19114089.785234146 +431,13531213.684769623 +432,18116787.947606586 +433,19452692.059855957 +434,15556881.323994495 +435,16773940.887756258 +436,15707594.883645678 +460,15176350.705371618 +461,15582052.83367668 +462,14553448.45747705 +464,19313588.250526376 +465,13063801.36381758 +466,13516643.822003476 +468,18871402.298068978 +469,17800381.571582288 +470,17598694.656919524 +471,18362938.220248204 +473,19571165.54514528 +490,18125550.080001578 +492,17604768.68485496 +493,10506708.785031682 +494,18959129.81849666 +495,17092588.94103542 +496,10656636.505814223 +498,11196359.759679373 +499,16028070.967475649 +500,14553642.055905666 +501,16010296.749107117 +502,12236356.732652519 +551,18428326.711237177 +555,18692451.64385458 +558,12603303.275260221 +559,15219654.400748387 +606,13471098.66464168 +608,12119395.61731958 +609,16558792.433737032 +614,12770895.84069705 +615,18278357.743474048 +617,12530175.281581944 +618,13365952.640545746 +678,16083211.493979283 +679,11945246.64050348 +682,18759635.587475874 +683,11330424.115816435 +687,18844663.00350344 +688,17524908.43312303 +690,13690674.316788506 +694,14572843.603453457 +696,15708756.439630669 +699,19424273.543857083 +701,16290152.396006793 +706,17137812.875580575 +708,17439871.722893454 +800,15909352.327366821 +803,10319146.258169243 +809,15298240.698970076 +811,17816990.40006434 +825,18224647.796984863 +830,11413587.958350835 +847,17179795.712580673 +848,16930449.906506505 diff --git a/test/hyfeature_01013500/channel_forcing/201512010500NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512010500NEXOUT.csv new file mode 100644 index 000000000..b92d78b49 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512010500NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512010500 +133,17570633.6072222 +134,15359806.768552866 +135,14679566.306417022 +136,11745786.307494136 +137,11177843.319333075 +138,11344789.876383597 +139,17853076.438491933 +140,10414186.038461545 +141,15488033.152088717 +142,15890559.308096088 +143,16370107.413521886 +144,11635657.85147363 +145,17217900.535545245 +146,18539930.11569707 +147,18465179.905616246 +148,16675728.018267062 +149,12548018.638677409 +150,17327563.4876865 +151,14755559.116198067 +152,14395399.068281345 +153,11669461.270624284 +154,12797805.93435724 +155,10887352.853730405 +156,12530327.583118733 +157,12357948.685838835 +158,13244310.154739825 +159,18893602.710435517 +160,16514705.24492548 +161,10769885.558897296 +224,13022526.677137649 +225,14906777.058990665 +226,19791244.897508718 +227,18468020.168211542 +228,12458697.27224163 +229,16823071.58203976 +230,15589832.673860364 +231,10915603.435184874 +232,13973496.404794043 +233,16512917.449292885 +234,16617784.192495555 +235,12878895.809008267 +236,19488837.52844655 +237,10270784.46194499 +238,19038056.237818785 +239,11477183.450084645 +240,16918458.465678044 +241,19972887.17553453 +242,17578509.703527566 +243,10087632.265871435 +274,13913108.902958747 +275,13063362.178235844 +276,14864597.517632458 +277,17804448.542377006 +278,11403565.319002599 +304,19241969.89386881 +305,17092513.9278523 +306,16889951.862891056 +307,16481079.870196637 +313,16789066.077022064 +314,18367350.50685037 +315,14191080.612590756 +316,17646833.006305147 +317,19934937.162064232 +318,13040581.905489527 +320,11223832.83845142 +321,12296046.044461364 +322,13316573.280744301 +324,10666548.991568262 +325,12462056.374081738 +360,12246488.835143074 +361,17212020.937188756 +362,10068038.858788276 +363,19373370.981737137 +364,16552442.724906087 +365,11224141.900669934 +374,18037802.66306242 +375,18210382.625954706 +376,14981309.272626828 +378,19770917.43209529 +379,19359394.130246937 +380,11471035.933702167 +381,14602556.64186909 +383,18559196.069880232 +384,13269916.32327642 +385,15419362.905898616 +394,10273731.25307661 +395,18243884.952437866 +396,19127765.23153933 +431,14469865.671165314 +432,18588586.931271583 +433,11993683.11471103 +434,14316248.86016209 +435,17215524.62764406 +436,18860266.036932897 +460,10705365.620626075 +461,13464263.415343195 +462,16978351.606547847 +464,12214499.356963921 +465,11215422.276740428 +466,11875086.521281049 +468,15289480.533973923 +469,11303678.46252009 +470,13646988.441038443 +471,10995504.257622726 +473,16074380.425980091 +490,10926681.179548714 +492,12173275.988308692 +493,18766993.264005966 +494,13680499.693031263 +495,10595039.296226459 +496,17287355.04446635 +498,15850706.754557217 +499,12622569.709688876 +500,19361951.734266795 +501,13021296.91331253 +502,17094145.27099135 +551,12746897.644282993 +555,18337858.96456883 +558,18870599.08657017 +559,11820479.176336577 +606,11211605.793128023 +608,15157390.120206706 +609,16245695.095342286 +614,12428460.209463907 +615,14182138.76295748 +617,15673829.384473972 +618,11141914.843006343 +678,11086302.46220981 +679,13369924.061683917 +682,10308535.1850316 +683,13930202.394700307 +687,14854892.071519412 +688,11269823.677846815 +690,15800105.92263526 +694,14150205.332837168 +696,10470403.655001918 +699,15275324.919245427 +701,14246987.469775777 +706,19457923.97522168 +708,13021095.025887601 +800,15276563.579066418 +803,18659309.836831365 +809,17516854.06623692 +811,10008845.826295411 +825,14253481.5835094 +830,11263004.65750787 +847,15602387.563571885 +848,11895202.388221879 diff --git a/test/hyfeature_01013500/channel_forcing/201512010600NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512010600NEXOUT.csv new file mode 100644 index 000000000..f2ed7b528 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512010600NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512010600 +133,12548242.139791183 +134,18472281.709284924 +135,13869838.623740034 +136,11012906.933001375 +137,16288925.033694964 +138,19942388.17444042 +139,15140056.328493651 +140,17909763.35431278 +141,15914317.335574904 +142,10572887.89194976 +143,13638877.413834311 +144,19351783.088042423 +145,14022086.587899107 +146,15737407.027132642 +147,16898624.067079086 +148,10688581.025203414 +149,19853149.560290284 +150,17189715.60105254 +151,17797827.256199118 +152,14834849.720104996 +153,18673178.552720234 +154,17769545.67561833 +155,19128457.816798583 +156,13616502.946416728 +157,10823451.541937886 +158,18236126.181614514 +159,17457973.84265238 +160,13828952.514942763 +161,14601654.4546378 +224,13091221.9647676 +225,17276175.38723354 +226,12284221.940438265 +227,19740663.64346554 +228,12355263.398584101 +229,15434102.983660355 +230,18014256.58621262 +231,10995291.276111212 +232,14888865.388406247 +233,15188055.221348442 +234,18495145.470591117 +235,16943755.728173714 +236,17464387.92783963 +237,19274560.525088787 +238,19222723.586679593 +239,14825022.573273614 +240,18054970.0938859 +241,10905874.688806599 +242,14317188.649800543 +243,19326653.920123853 +274,13581253.580915187 +275,11502058.338268284 +276,10171665.25502564 +277,11933298.641605245 +278,13940653.409901623 +304,18847999.449718766 +305,19981474.49301652 +306,16632716.586889949 +307,15924121.521763828 +313,18471899.56821485 +314,11912521.265886042 +315,17258751.386113968 +316,17236001.787711386 +317,16957757.47631838 +318,18892008.26213243 +320,14485754.68263601 +321,18176835.58717621 +322,19437603.951150447 +324,19320671.997000303 +325,10928167.104844626 +360,19297444.600521073 +361,15674992.304708408 +362,12759129.121036889 +363,10631692.051351404 +364,19952112.97814078 +365,17814036.449729204 +374,12666041.277178263 +375,10094126.528781924 +376,18365984.07690435 +378,10416303.292479299 +379,15778193.250019338 +380,16268610.734743858 +381,16115124.655514386 +383,12962913.058411423 +384,10097094.822482415 +385,10362393.68772607 +394,15111132.241619822 +395,18597251.657659724 +396,14133124.310515866 +431,17164510.975651983 +432,13912253.170775857 +433,16889065.264177926 +434,13254029.09473024 +435,18679035.172794722 +436,12101361.301112387 +460,18286209.538871497 +461,17247684.372142717 +462,13806722.154294394 +464,19652096.398694508 +465,12344008.59946972 +466,10029439.743799198 +468,19043960.146091137 +469,19454128.698834203 +470,12668515.30353914 +471,11859335.89972704 +473,14970235.753896013 +490,17569448.861426707 +492,10862733.531175286 +493,12669951.9503266 +494,12276245.515492044 +495,10986431.139772687 +496,13136422.551560711 +498,17924141.99061787 +499,12110821.853104014 +500,15363355.824445851 +501,13498823.346308118 +502,12688058.13614792 +551,14466625.216913477 +555,10888400.43528233 +558,17539231.424940906 +559,19663372.411793236 +606,14806707.387292206 +608,18143120.091993444 +609,13408347.545482093 +614,18253334.73509082 +615,16148274.058420826 +617,10126026.512256242 +618,10650959.079629548 +678,18652266.957812976 +679,12065504.3199461 +682,10885276.033702424 +683,11891182.136992164 +687,13087255.848606626 +688,18855868.89450114 +690,10156827.182720399 +694,11410273.144117538 +696,19527712.240677837 +699,15453120.332762288 +701,10304140.886074118 +706,16444193.832407957 +708,15816049.567097569 +800,15007597.714111663 +803,10875322.01902811 +809,15301722.408828948 +811,10131357.863981623 +825,12515921.33459301 +830,19471423.053961106 +847,16231871.128198594 +848,13943551.240248615 diff --git a/test/hyfeature_01013500/channel_forcing/201512010700NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512010700NEXOUT.csv new file mode 100644 index 000000000..3ef376f3f --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512010700NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512010700 +133,16225721.961933624 +134,19713344.218333103 +135,13878485.210380638 +136,10233050.408518815 +137,17311581.42343085 +138,10829432.785840511 +139,13157047.984628342 +140,11614013.908992646 +141,12753148.731743624 +142,12267158.894294731 +143,12372811.023341306 +144,19557584.724832967 +145,11619978.191086061 +146,19322214.99051555 +147,16051293.948893521 +148,16998549.97834091 +149,17710330.221837427 +150,13275843.024806732 +151,19164344.18657358 +152,19263865.092049696 +153,11884275.62035871 +154,16889147.882478353 +155,16213260.815464402 +156,10339402.348008186 +157,15962285.002259769 +158,14851011.441069644 +159,10759157.838741265 +160,18899246.681176707 +161,19773875.595870554 +224,19245332.16788052 +225,17008086.361636028 +226,19074280.12485002 +227,15991447.379254416 +228,17836566.25836336 +229,11748853.147600878 +230,12062736.595403876 +231,19444788.414955337 +232,10654774.187230153 +233,16365113.237420924 +234,15882720.951773986 +235,15271494.820423821 +236,19856789.17170298 +237,14136673.458181122 +238,17768794.881872173 +239,16966717.2325087 +240,11488781.064718584 +241,19578942.543187577 +242,13149055.339785513 +243,17891959.930088945 +274,14913224.970355932 +275,16848167.912194762 +276,15236148.529360417 +277,16852113.840943597 +278,14697517.069750482 +304,12031418.300136749 +305,11512143.676707247 +306,15085576.536926925 +307,13715041.585452601 +313,11378321.297864974 +314,11250013.542088943 +315,10733426.181018682 +316,14995565.092948373 +317,14251869.406897182 +318,18261495.151546717 +320,17377125.89114098 +321,16062259.786182005 +322,19514147.14122439 +324,12924059.718352478 +325,17285077.204561703 +360,14978376.289470427 +361,10798880.782816269 +362,13602451.166603347 +363,17666332.854741823 +364,10273426.075930823 +365,19371476.605887108 +374,19158843.761539735 +375,17732601.825389784 +376,17271609.067925103 +378,13327791.0780929 +379,18214324.666846335 +380,12787567.745786164 +381,16587220.833130857 +383,12751988.139233042 +384,15289372.870368488 +385,12427274.568173598 +394,14181988.37379254 +395,19100255.224289 +396,11551003.199906752 +431,18182786.412588283 +432,19826501.694190927 +433,13372084.197095089 +434,15811999.862037037 +435,19740243.080647346 +436,19792846.90859864 +460,14679348.716688948 +461,13027728.894054685 +462,15098087.52236684 +464,11185117.152026515 +465,18490523.296137206 +466,13147418.485882195 +468,16169506.222447922 +469,14746472.448981017 +470,16527072.124034703 +471,16480754.825112648 +473,17672174.011317454 +490,13370643.690989912 +492,18608623.0999013 +493,11556364.19219278 +494,11113233.04251996 +495,14696592.553734109 +496,19447345.153480664 +498,12137234.365866601 +499,17362626.173126873 +500,16059665.024756843 +501,17534234.841836378 +502,15252696.199139092 +551,15620033.028492179 +555,12590520.466743154 +558,10659360.637718158 +559,18431402.121533282 +606,13363566.286783729 +608,16239353.030901946 +609,16180896.367898239 +614,16364744.035103291 +615,18401079.34869273 +617,10152144.501607489 +618,18875280.449812744 +678,15715436.968133513 +679,12978904.26725875 +682,12299858.635922873 +683,12479656.52354877 +687,16835189.471577868 +688,10933067.35824435 +690,18714922.689143002 +694,17317985.185156003 +696,15019886.581061192 +699,15661388.029074397 +701,13897462.28921207 +706,14511893.961781137 +708,14953921.29501732 +800,19751493.09208426 +803,16082147.922570828 +809,10048234.035414215 +811,17501326.670673184 +825,14400120.340959974 +830,17977331.579777736 +847,12022320.765374638 +848,12711608.561902817 diff --git a/test/hyfeature_01013500/channel_forcing/201512010800NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512010800NEXOUT.csv new file mode 100644 index 000000000..85f161be6 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512010800NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512010800 +133,10936742.489983285 +134,18585701.147098124 +135,10886449.398883566 +136,12841044.286538811 +137,14558197.142663442 +138,17715469.06328641 +139,12408099.281089548 +140,15836845.294371326 +141,17370655.025318302 +142,14870943.046445187 +143,19072133.497545525 +144,16294708.850505546 +145,16501156.84516893 +146,19455887.762091126 +147,13482311.534578366 +148,18293516.90866916 +149,13756068.679176012 +150,17581087.849015236 +151,12654554.518688012 +152,15658387.565313093 +153,15937248.622657064 +154,12911892.95780938 +155,14193542.528369956 +156,18683301.39315369 +157,10371933.95145165 +158,19632073.304290302 +159,10206514.91660322 +160,12524480.299659349 +161,12794120.205770055 +224,13053923.330110367 +225,19880366.789464787 +226,15529196.269586794 +227,10090653.84187333 +228,16840421.95677267 +229,19326997.58738091 +230,18037088.502679363 +231,10967126.086425591 +232,15216013.193189327 +233,10249044.200648349 +234,19205192.027890358 +235,13194541.70671699 +236,17451516.124760907 +237,19443495.567201693 +238,14511917.947686907 +239,14218293.447256574 +240,15029097.183792412 +241,19545998.077391148 +242,13523650.08052063 +243,12911375.013816964 +274,15998832.672791675 +275,17199862.564748403 +276,19969409.385825604 +277,15341134.415214438 +278,12137638.030215975 +304,15584155.958717596 +305,18319417.389409557 +306,19624517.57103246 +307,10901835.968534365 +313,17336643.035630733 +314,16004013.872892635 +315,18099960.10674946 +316,16076525.335559435 +317,17473915.219683494 +318,18342417.822471503 +320,19136860.43159157 +321,15975270.15894264 +322,17058142.15202005 +324,18299035.03894484 +325,11279381.966681708 +360,15071543.203339461 +361,16796618.852479532 +362,12597311.276247088 +363,16343398.271593817 +364,19161338.24136766 +365,11889447.538329333 +374,11770480.973690243 +375,13564667.865680777 +376,17718479.814804755 +378,15115693.22886426 +379,17936708.339471757 +380,15116405.541996684 +381,12807246.547315076 +383,14893357.93782772 +384,17149777.740633152 +385,19024415.938167833 +394,12987044.537019491 +395,12040891.157801697 +396,16409487.755674591 +431,11034078.739500713 +432,19960458.51650111 +433,18115601.111138675 +434,14980634.204502465 +435,14425178.890877198 +436,19816356.238804758 +460,11848386.049597252 +461,19360202.22633358 +462,10926892.840612287 +464,16642039.629815888 +465,19991966.76211337 +466,15544204.67055113 +468,19423541.410045896 +469,17185028.79673499 +470,12846186.112276074 +471,15247740.335157875 +473,15705365.44987891 +490,11095556.540371904 +492,16964486.549106907 +493,11779177.437866878 +494,11351774.26925571 +495,13537439.956773706 +496,11721996.63751477 +498,16932244.10269429 +499,13870051.68882583 +500,17030426.682301544 +501,12468459.877705332 +502,11911623.003764374 +551,16686490.564472748 +555,16666213.348288778 +558,14658571.632446818 +559,12311274.715760594 +606,18283375.544841334 +608,10520679.638786998 +609,16151686.867708135 +614,16495117.54397491 +615,11487912.281897552 +617,14411931.059763683 +618,11940970.806301812 +678,11544352.526710523 +679,14872312.271922171 +682,11091724.677596252 +683,19754588.87076526 +687,12376531.088030683 +688,17452926.11786389 +690,15358892.48339295 +694,15382092.56855915 +696,14580701.99161703 +699,11591227.393095406 +701,17120039.361146048 +706,11554481.93756252 +708,16508485.409344409 +800,16485463.126729421 +803,11185842.39793163 +809,10686683.630928759 +811,10039388.23380837 +825,14072487.590213642 +830,12613611.867708772 +847,19398403.83696589 +848,13848440.496724682 diff --git a/test/hyfeature_01013500/channel_forcing/201512010900NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512010900NEXOUT.csv new file mode 100644 index 000000000..8d86311b7 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512010900NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512010900 +133,13044456.023379253 +134,18595145.850615874 +135,12209988.570263013 +136,16486712.129492022 +137,15875507.910259709 +138,18113024.967245784 +139,19872763.365879875 +140,13615309.964100257 +141,18569755.81149479 +142,10508320.495875027 +143,11880438.07725378 +144,11969157.112469297 +145,13107972.020382144 +146,13027151.831922598 +147,12699168.689710908 +148,11927864.611107005 +149,10663919.283817442 +150,10712800.193486651 +151,18467254.53168402 +152,13574056.275846224 +153,11078846.952973623 +154,15094745.903251529 +155,17006105.341256548 +156,13833337.529146269 +157,13710194.510595512 +158,17804269.799749367 +159,18678777.0298963 +160,16018792.395275913 +161,16897950.000272773 +224,10163938.900149792 +225,19128921.130100414 +226,16160171.16690967 +227,14772666.51597678 +228,16825421.161028124 +229,14055280.837218793 +230,13055974.813318703 +231,14572874.284532502 +232,10087619.435568737 +233,15387142.679377858 +234,13647418.983093204 +235,10781637.308056433 +236,13813579.669271879 +237,19143241.438296005 +238,14288531.923323086 +239,15983169.744985439 +240,15573934.769135442 +241,19683810.51834563 +242,17967700.85011314 +243,17549306.83338567 +274,15262223.555121586 +275,19251044.334439512 +276,17260497.899464883 +277,14774091.807269564 +278,18927162.596392542 +304,18317604.79568863 +305,16968465.885143206 +306,17574502.669363968 +307,14473671.087668162 +313,15768439.427780539 +314,19826294.481736768 +315,19584154.623614803 +316,12491180.424310336 +317,14274485.283065863 +318,19725780.485225007 +320,13153328.827806044 +321,13742629.432149472 +322,14726410.014977884 +324,18548077.444661252 +325,18527598.969735622 +360,19302920.744968917 +361,15508000.578783486 +362,15056825.10902799 +363,11831067.384991383 +364,17630769.054064646 +365,17673685.354231827 +374,18343306.704135828 +375,19017381.040919717 +376,17379227.98636373 +378,12794297.875772225 +379,10634132.231831575 +380,19065267.599842343 +381,18443184.48797884 +383,11507300.683129001 +384,19753146.836041443 +385,14775540.460890554 +394,19274908.78078281 +395,17656190.003184866 +396,17189864.32941736 +431,18721137.328410387 +432,19123278.243430354 +433,12734550.002055084 +434,14485769.149035063 +435,19806345.48476808 +436,10364577.144656718 +460,11016834.273939699 +461,16616186.899770346 +462,12799501.46003016 +464,14130919.825157521 +465,10804319.61874852 +466,12927469.693419244 +468,10734345.920273768 +469,18117079.73418842 +470,13871938.465709647 +471,17421106.014267646 +473,10142783.677036384 +490,19430379.136934962 +492,18769912.410769265 +493,12046609.15297976 +494,19140037.551352616 +495,19536411.292037062 +496,14558049.473648071 +498,14114867.520600736 +499,19398498.4020425 +500,14045447.941319501 +501,18464811.06005076 +502,19961435.302994136 +551,12318444.55836603 +555,17807861.72284172 +558,17395024.632300887 +559,10144741.99839741 +606,16503425.665099056 +608,14413327.355118845 +609,15377887.032677043 +614,16216396.213922799 +615,15959794.944547199 +617,12598068.842539508 +618,15069617.577893808 +678,11885328.531475104 +679,15008821.726361249 +682,10039856.111030895 +683,12172175.181075815 +687,16088329.35058095 +688,12613640.03308392 +690,10949709.48777437 +694,18284065.488587752 +696,10941666.330489397 +699,15754540.450657295 +701,19352657.665177226 +706,16798018.0739936 +708,10646610.995638413 +800,10766423.656039644 +803,13797594.35441811 +809,13401019.78130773 +811,19682318.17822782 +825,10266444.310270382 +830,14504482.677304786 +847,14664670.176845685 +848,14999009.367786333 diff --git a/test/hyfeature_01013500/channel_forcing/201512011000NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512011000NEXOUT.csv new file mode 100644 index 000000000..72e1d20be --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512011000NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512011000 +133,11283499.343628542 +134,13471713.87619271 +135,11530314.730531303 +136,11752010.3903142 +137,10740211.450754048 +138,10188874.063492531 +139,17683291.49832895 +140,17789548.122509066 +141,18264396.29910592 +142,13425665.852675775 +143,15765273.919241443 +144,18476823.873615474 +145,17207523.490822203 +146,15117263.835935581 +147,12266140.485725533 +148,15301676.927706514 +149,13296103.79874314 +150,15196412.052111827 +151,17261104.279343523 +152,17937891.107866418 +153,18081178.00835072 +154,14133470.17095154 +155,14641313.555745777 +156,16523991.89230209 +157,16768767.525951553 +158,11591422.25554772 +159,13292512.574045077 +160,11765298.945854975 +161,13209631.273733277 +224,19366370.676611904 +225,11791699.110227793 +226,14366063.855027728 +227,14257171.107411638 +228,18851619.808542196 +229,16325730.182789516 +230,13192530.443059858 +231,11462341.425259015 +232,12449453.502311721 +233,15587572.35062448 +234,18117339.352386612 +235,16684737.739519473 +236,14217298.137238555 +237,19541474.94484891 +238,16059438.651280418 +239,14211211.04875281 +240,15243350.28077138 +241,16482781.48427283 +242,18340828.91289966 +243,13738275.912018752 +274,13408210.606664406 +275,12622235.110145107 +276,17381408.818202604 +277,14300125.358626924 +278,18492149.723582 +304,16506294.8742483 +305,13517777.7917598 +306,12684186.242521442 +307,18219427.2076102 +313,11602288.89220993 +314,11487489.481839633 +315,18888114.761727605 +316,16311100.510848247 +317,15022941.748023108 +318,14247556.99421765 +320,17683497.22050764 +321,11995623.536424544 +322,17357093.424799137 +324,11969127.930011606 +325,11492040.476598183 +360,15928286.172480348 +361,13677155.812921893 +362,14579686.064757671 +363,13250384.264971916 +364,16683299.604837148 +365,11197774.738432925 +374,19719745.336338922 +375,13743835.069996089 +376,17634598.0224978 +378,19703642.0616733 +379,13753127.125480328 +380,10744752.85863066 +381,15607518.028536016 +383,17004178.548657313 +384,12611162.800944392 +385,13132541.213419318 +394,17685114.50705853 +395,11860145.179605952 +396,16839495.068826705 +431,15505285.347579267 +432,18433881.270361394 +433,14200508.048425399 +434,14470866.430883376 +435,18067696.797138635 +436,12736069.141167875 +460,13738498.36527102 +461,11770308.175437272 +462,18527216.748379797 +464,13262154.965876818 +465,18260939.30485538 +466,11896590.980191443 +468,16637972.627582377 +469,11288069.0567564 +470,18360950.992249925 +471,17499759.679260593 +473,16828400.13103528 +490,16694546.778777614 +492,18479199.423275866 +493,18948521.231966875 +494,16779485.422722753 +495,17244280.99352946 +496,15030858.879655413 +498,10802660.630461186 +499,18981100.622512884 +500,19003482.74738074 +501,13253135.417303279 +502,13383658.883419933 +551,15986595.729898047 +555,14387418.96579838 +558,13260088.281428874 +559,12704789.37950016 +606,14379269.00767842 +608,13107835.196755007 +609,13943281.442239862 +614,10402010.765880033 +615,14840272.80110988 +617,19415727.65059828 +618,19909264.517223474 +678,17290353.705615856 +679,13627414.409634575 +682,16287277.98001301 +683,11954674.901568623 +687,16074307.593664512 +688,16332050.726473343 +690,19004331.99077651 +694,10358466.604231844 +696,14235776.555858687 +699,13455847.26664504 +701,11408562.261547308 +706,10072947.568189796 +708,10797765.894750297 +800,14519924.668738833 +803,16503111.572687378 +809,19503052.269312635 +811,10447064.112792393 +825,15348667.4911084 +830,13151120.388946354 +847,14202093.358224226 +848,16500804.28225704 diff --git a/test/hyfeature_01013500/channel_forcing/201512011100NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512011100NEXOUT.csv new file mode 100644 index 000000000..740f2ed42 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512011100NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512011100 +133,15911027.631766295 +134,10862122.742179828 +135,13332014.165050738 +136,13065991.692707624 +137,16626280.128860898 +138,19156417.413650513 +139,11340496.638566768 +140,12227843.33718826 +141,14524278.797895074 +142,10502415.738284573 +143,18383625.574843124 +144,12644615.664483096 +145,19103605.3322438 +146,10488186.733634926 +147,15217783.80762139 +148,13583759.21078399 +149,12816395.848091712 +150,12479881.18665913 +151,13248810.881854836 +152,18299236.996789344 +153,12040855.823027855 +154,17576087.63340696 +155,18842599.189243313 +156,14987194.231565967 +157,16204149.418565826 +158,18196806.946955957 +159,13642278.120461795 +160,14029207.826972364 +161,16983054.50347667 +224,13412092.043743165 +225,12813214.963146402 +226,10446929.21809254 +227,19349638.836820737 +228,17492838.7037666 +229,18331406.862611786 +230,11182278.63567352 +231,18602652.58519996 +232,17013614.937878653 +233,16508048.000877118 +234,17460392.36398697 +235,19061782.8082781 +236,12393220.828042967 +237,17329427.505576998 +238,16220667.493695974 +239,18748939.380529426 +240,14518077.748820785 +241,11900063.252565378 +242,16935546.38740927 +243,12386554.386705093 +274,12248532.545670148 +275,19168337.935273334 +276,12430345.65319385 +277,12764949.661852807 +278,13323805.32111333 +304,11512700.131143712 +305,15680596.62134495 +306,19302856.913821883 +307,13984621.605796305 +313,16177150.162868626 +314,15851596.564286435 +315,18182231.493439123 +316,14730344.011158315 +317,18187700.20792254 +318,11929386.235580051 +320,17129683.67664476 +321,17658067.293972183 +322,16047281.564539507 +324,15437113.475726448 +325,12504882.024968797 +360,18229520.751985546 +361,13699794.37175584 +362,19727886.630139995 +363,11229849.526842486 +364,15782932.871483494 +365,16207767.661600318 +374,14595738.954453398 +375,10845099.822264988 +376,11208093.039905103 +378,10517187.590097474 +379,18044322.670850616 +380,14075689.178175043 +381,18143611.88887545 +383,14381143.592750028 +384,11402797.53425912 +385,16247652.688659722 +394,13995110.938321577 +395,10601399.479066145 +396,14297343.214978322 +431,10920681.87218734 +432,11063357.237717085 +433,16339260.157844502 +434,14403905.259473411 +435,11099716.975439686 +436,12574495.725345913 +460,12969946.717215622 +461,10616301.321809648 +462,15088593.062222546 +464,14493039.766490586 +465,10661450.0122162 +466,16605156.641817313 +468,19535092.70843687 +469,13763950.42390339 +470,12381980.54917192 +471,16302468.063698541 +473,15491990.099766083 +490,15731784.0575676 +492,13115393.987384908 +493,14739950.521464065 +494,10452017.980981728 +495,13990990.952894246 +496,13899875.800593287 +498,19226230.729208127 +499,17061195.244058464 +500,13909218.365403464 +501,10667005.29665461 +502,12325762.965240188 +551,10498954.744411312 +555,15298305.215529071 +558,18748581.10088542 +559,14030917.90920568 +606,15959349.772888329 +608,14280457.182177315 +609,18529908.31352552 +614,11850163.914027669 +615,10791124.07693611 +617,10728924.424373766 +618,19089710.271165464 +678,14311142.781226717 +679,12511857.770619344 +682,13937964.921906155 +683,16570264.435390819 +687,13738074.164309245 +688,13481266.395108873 +690,14061963.722976293 +694,12298638.35376597 +696,18206479.883302424 +699,13577771.116516393 +701,16857445.126349516 +706,11460877.09599454 +708,10070111.176692318 +800,11279594.961857442 +803,16784986.205390032 +809,12669206.71589864 +811,14072093.785045443 +825,17543081.66963505 +830,17225557.392815888 +847,11501044.045629371 +848,12906861.988723079 diff --git a/test/hyfeature_01013500/channel_forcing/201512011200NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512011200NEXOUT.csv new file mode 100644 index 000000000..c95b44132 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512011200NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512011200 +133,12468705.569476806 +134,19776225.348325983 +135,14234607.149891883 +136,18681770.25188347 +137,17851318.760997057 +138,15716907.722206907 +139,10958499.0815715 +140,11701972.412652422 +141,19102455.397254437 +142,18214278.15484542 +143,10479939.643182667 +144,11769025.55267783 +145,12754025.422182433 +146,18163285.51859399 +147,14939058.101693802 +148,18107205.648137394 +149,15322333.492233604 +150,12297039.733209271 +151,18734784.974650186 +152,11011558.605689427 +153,17852444.067676567 +154,15874786.420585236 +155,10950446.071260082 +156,12158896.429673694 +157,17794768.897447303 +158,10330747.796754863 +159,15599575.653926628 +160,15796985.238546066 +161,15635183.967429042 +224,13755547.963229906 +225,11723569.051634666 +226,18944794.592039816 +227,13867026.198009485 +228,19241219.829762645 +229,16828638.527989894 +230,18893850.136294015 +231,16911641.315808542 +232,19046316.52465595 +233,11764784.754949903 +234,13100032.95523874 +235,19933743.845417537 +236,17197787.982219942 +237,14107608.060630383 +238,14747978.55743365 +239,15010705.083024526 +240,14812483.132620636 +241,13831588.204794373 +242,19110633.31079343 +243,18899580.25038252 +274,19152725.131364454 +275,19839906.667037215 +276,19637965.86498088 +277,10282760.01454451 +278,12288073.782399155 +304,11109913.477088071 +305,18798127.50749392 +306,14061003.432268085 +307,18421938.925714538 +313,15405367.105498713 +314,12977699.527704032 +315,18531395.301406346 +316,16513469.074098907 +317,18664516.992052972 +318,12777258.905557614 +320,17897387.331456568 +321,12415834.14510912 +322,17316255.405101992 +324,18878342.41869892 +325,12067605.595502017 +360,11523059.477419272 +361,16492776.100546218 +362,14525097.57759038 +363,14338830.904098146 +364,12742031.433328751 +365,16676197.04999794 +374,13733365.382912975 +375,18810224.93684171 +376,19318171.20505048 +378,15326131.145540394 +379,14787418.931863438 +380,17344066.94237161 +381,11115142.172885882 +383,19535045.061870012 +384,18788194.44097321 +385,15078491.459409062 +394,13427251.23756781 +395,12241543.856671887 +396,10806482.3873919 +431,17949119.55478279 +432,18054563.400558252 +433,11453040.637698304 +434,16375128.128403332 +435,14207676.448366497 +436,17826236.807586856 +460,19192561.15167409 +461,17098790.618722368 +462,18198963.10192252 +464,15208375.2888075 +465,10789175.310646705 +466,18999468.88501866 +468,11754565.013397843 +469,19043811.333368674 +470,15366900.883250337 +471,19107187.48318942 +473,18361074.58830143 +490,13859971.280509625 +492,14627507.990802985 +493,14824084.593970815 +494,18102181.903605144 +495,13295741.510248441 +496,13755942.936372964 +498,10861327.623390004 +499,16015316.924027115 +500,16611593.36560147 +501,12839787.50659274 +502,14956352.045895074 +551,12711216.255179336 +555,14620832.5055365 +558,19782552.332817405 +559,15925433.920626188 +606,13054828.042425264 +608,18856380.69879213 +609,15607383.984345626 +614,12461565.593052153 +615,14831530.007151794 +617,10921398.477021078 +618,10687169.615241699 +678,15541554.58254564 +679,14202569.688584648 +682,14361101.507390644 +683,11570020.999861896 +687,14190108.443578452 +688,19619788.739641502 +690,14898312.376232795 +694,14725571.828321388 +696,12281020.453210441 +699,18204547.581782203 +701,15619050.52609957 +706,11198003.940037386 +708,18544000.775886852 +800,15364471.265418362 +803,13537948.487300983 +809,10553610.314291503 +811,17452679.721668687 +825,11100601.031434411 +830,17938410.963347502 +847,17156249.420980085 +848,13184487.067298818 diff --git a/test/hyfeature_01013500/channel_forcing/201512011300NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512011300NEXOUT.csv new file mode 100644 index 000000000..02a9e8126 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512011300NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512011300 +133,10824385.076478701 +134,10905827.724990474 +135,17588143.952708717 +136,13125854.53261936 +137,17633749.888847142 +138,18158775.28354628 +139,12577995.408932382 +140,17920926.15810017 +141,19038800.05289761 +142,18792921.606369 +143,14324339.847159144 +144,19638220.638110932 +145,13339718.04927589 +146,12152630.074108439 +147,11942922.014859796 +148,11262969.822491704 +149,15015752.822498308 +150,17761329.19945757 +151,12647095.277446821 +152,16010128.503556505 +153,18102920.793719165 +154,12872816.04905536 +155,13533885.85338186 +156,18171150.248744663 +157,17072093.264224295 +158,14732339.333643015 +159,13092917.439676737 +160,15654032.406239556 +161,10497611.629008824 +224,14303618.004944924 +225,16862081.83376398 +226,11832048.081396386 +227,17504129.40232608 +228,19756453.40145434 +229,14930557.307708941 +230,16442104.205540981 +231,17050344.70424986 +232,16979930.19212614 +233,19624262.142267376 +234,14458344.011241928 +235,10635876.740515962 +236,10998740.751132231 +237,15439924.341647336 +238,16829439.90624032 +239,18454467.230107866 +240,18298640.966770373 +241,18650837.94569344 +242,17931534.550007127 +243,14085060.90257448 +274,11003599.59559783 +275,18372854.66395851 +276,15352589.437738504 +277,19262112.27366581 +278,19171540.940194122 +304,10848301.262128014 +305,10564794.74444054 +306,17588989.602626845 +307,13926375.045046605 +313,16600649.64624058 +314,17238357.648426935 +315,13325882.382202514 +316,15022152.661979299 +317,11181186.14337333 +318,16574354.125231672 +320,18114702.394006215 +321,10552971.275052804 +322,18904307.736375324 +324,19753075.66051503 +325,14336242.678762794 +360,12770157.433308475 +361,12312742.144673537 +362,11368333.43231231 +363,14878286.945263196 +364,11601207.718703415 +365,16749331.722402956 +374,14224458.446155524 +375,12395184.973195747 +376,15567234.715009859 +378,17165608.362868834 +379,19133466.120360978 +380,16216586.477214172 +381,15036610.232279262 +383,18131158.744339008 +384,15436611.739177328 +385,19506892.623702798 +394,12108159.64812494 +395,18156787.612809915 +396,17156299.048354715 +431,11661624.148268111 +432,10034159.315358408 +433,13749170.312534438 +434,16277355.657036997 +435,16674115.02057617 +436,15957627.817107145 +460,15288172.089707918 +461,19855774.41162058 +462,17228194.118479222 +464,10038522.890929872 +465,17887890.02030462 +466,10543770.907876374 +468,15842424.575689506 +469,18380885.567161836 +470,18149715.70116122 +471,12738670.332142986 +473,11466564.61429992 +490,10133944.266585123 +492,13136039.441944612 +493,11560452.022542918 +494,11759938.237780359 +495,15240555.894411903 +496,12359361.107015818 +498,14159077.35319902 +499,11756055.24010112 +500,10017294.831525164 +501,11113449.168319914 +502,10821251.647553576 +551,19557788.543478303 +555,10553327.413634125 +558,15377581.462011665 +559,17990210.164780844 +606,18750599.291780498 +608,17188858.11556678 +609,19808521.05831082 +614,14781484.274525598 +615,13719761.471499573 +617,10406765.779993692 +618,19936770.57194157 +678,10785953.93175182 +679,17254541.734835252 +682,17226877.81261266 +683,10149684.3541082 +687,14756654.180543005 +688,16855301.13544428 +690,10283569.514740288 +694,15718202.162936116 +696,18792216.28052453 +699,17681652.97905475 +701,19231887.441617098 +706,14485563.78726667 +708,13684651.944883928 +800,11843401.698438073 +803,16926103.406348683 +809,10501802.680447351 +811,19654064.92839695 +825,11273142.94500475 +830,14156920.493190417 +847,15548640.603490286 +848,19142904.893719584 diff --git a/test/hyfeature_01013500/channel_forcing/201512011400NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512011400NEXOUT.csv new file mode 100644 index 000000000..0f7ff81df --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512011400NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512011400 +133,19757927.42661348 +134,19988828.8492961 +135,16798429.503994323 +136,14688843.893590454 +137,16589867.636657096 +138,10243153.731277779 +139,18079711.7762728 +140,12371146.283060277 +141,19626959.704035334 +142,15980575.872474357 +143,13123352.807295803 +144,15820227.138222434 +145,13017908.13699205 +146,17120196.60957566 +147,19131699.150077946 +148,12018201.705862489 +149,16216072.434995364 +150,13867059.309974631 +151,14139225.080135614 +152,10812340.689165939 +153,15745703.534586484 +154,17958917.794563785 +155,13427433.097620765 +156,16014578.581538443 +157,15286246.545423966 +158,16863350.537901446 +159,18110072.596093804 +160,18873465.36859861 +161,17971709.161662385 +224,17701622.351699363 +225,10381733.572087418 +226,17774881.785725363 +227,17190834.701929614 +228,16130412.833118714 +229,19946199.025694933 +230,14460070.697251398 +231,16129872.499589851 +232,17695142.033849567 +233,18737948.631898258 +234,15436229.663837176 +235,15876971.627526108 +236,14675015.964710942 +237,15108491.392004393 +238,17683395.264574062 +239,19200334.583568167 +240,12388781.051446266 +241,19495096.219362434 +242,14949714.478574038 +243,18075775.57975784 +274,19882423.651215307 +275,11821974.206231344 +276,13940721.163322408 +277,17648848.926641148 +278,19305583.456880733 +304,19843000.05652865 +305,11579836.790755235 +306,10695089.359968398 +307,17204238.590071224 +313,17034478.37304555 +314,16175192.898014918 +315,18124598.823358938 +316,10921029.160404067 +317,14468438.95243878 +318,19563882.674522977 +320,11723465.449185897 +321,16882690.730358467 +322,12389912.507507399 +324,16852480.06212406 +325,15562199.684244774 +360,13900233.69135373 +361,17686895.07642142 +362,17368211.10298441 +363,13392648.07377605 +364,18465966.47147958 +365,15977515.796616212 +374,15006975.547626108 +375,14837215.669261388 +376,14665014.953352049 +378,14270252.507853642 +379,13240276.808319904 +380,15014128.929655265 +381,11324428.76410842 +383,14563781.611385321 +384,19670916.598893404 +385,15022051.57880934 +394,19930953.09432198 +395,11593890.041645546 +396,18918145.959271375 +431,12832237.652475877 +432,12363472.713212684 +433,12899005.167503996 +434,11809918.216084512 +435,15583920.647045255 +436,17528928.712084305 +460,15326215.943721782 +461,10500584.041915154 +462,18844620.538150635 +464,18740264.565714426 +465,19772733.465099204 +466,13054569.210935462 +468,17519242.897360265 +469,12410902.017403955 +470,14775560.26020782 +471,15264671.783141606 +473,19673893.601819687 +490,18633684.578726724 +492,19721048.21055022 +493,11219949.372992238 +494,13620294.790946579 +495,13124279.56192018 +496,13145349.850035448 +498,16099776.825304192 +499,10778855.314754285 +500,16069628.987361208 +501,18015523.985972803 +502,17252073.452309866 +551,14633953.493757866 +555,16070285.015190698 +558,13100798.558186255 +559,13724111.837863874 +606,11266794.21542759 +608,11101906.261127671 +609,18778790.27705044 +614,19487216.164710563 +615,12242966.610512648 +617,16052913.594208572 +618,15614420.727196742 +678,15724309.102961332 +679,15251304.498918522 +682,14147873.35733108 +683,13012055.697246991 +687,13107748.368578725 +688,10919295.540279318 +690,18851145.220741935 +694,15595984.54432226 +696,10992919.049524097 +699,15328712.679046255 +701,16146062.413611893 +706,15970463.563132409 +708,17940319.570077322 +800,14645167.455763232 +803,15636056.252293853 +809,10247515.979054037 +811,12777266.131130524 +825,14373306.0671474 +830,11030552.277770258 +847,12669782.218347577 +848,18483402.862548605 diff --git a/test/hyfeature_01013500/channel_forcing/201512011500NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512011500NEXOUT.csv new file mode 100644 index 000000000..517b05d80 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512011500NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512011500 +133,18043785.07483334 +134,18593014.4740562 +135,12598655.82602744 +136,19498534.086129185 +137,14892282.71711566 +138,16653749.627245072 +139,17531800.608420502 +140,12814877.94513645 +141,13582448.938453794 +142,19857381.55122216 +143,12374991.878821116 +144,17945505.214512605 +145,16621194.143141948 +146,10607098.252909737 +147,16862040.32243427 +148,17325819.670044847 +149,14374078.466126239 +150,14703595.09895498 +151,16504855.039940603 +152,19318044.61325788 +153,11776216.030383019 +154,17416299.081317097 +155,10710860.940379225 +156,14248183.531660976 +157,10215222.766026676 +158,12329237.710347151 +159,16550662.538951224 +160,10611046.089709355 +161,12030316.892285833 +224,12893919.314693218 +225,12196862.78429293 +226,13935228.006083783 +227,17898283.65337009 +228,15768822.579079106 +229,11496875.057789985 +230,13930538.375365932 +231,14273964.647950012 +232,18763236.170198105 +233,10074924.9186562 +234,13835056.804035453 +235,19867878.32251373 +236,18102930.28853655 +237,16484893.84596964 +238,10096725.879391035 +239,19097814.35277749 +240,14249202.845107246 +241,18228056.45095613 +242,19424596.1603688 +243,10339668.528951816 +274,15610784.67530518 +275,18851242.82006253 +276,10475677.459726268 +277,13982731.170100946 +278,10935368.680357045 +304,11151798.527758421 +305,17574737.59549989 +306,14428320.162169317 +307,10471107.781284425 +313,18325701.095196433 +314,18058021.90748815 +315,13210588.254762264 +316,17228909.54316538 +317,12330564.555978797 +318,11129300.316096399 +320,18944261.284126185 +321,10177195.224374369 +322,12177213.902512696 +324,14783936.671635794 +325,11744900.5475634 +360,12362292.735182807 +361,14890127.801806647 +362,12600856.172626037 +363,10246631.552026074 +364,18215856.822377227 +365,18542682.48825422 +374,17363753.59887395 +375,10733356.896121817 +376,11145584.482037619 +378,11480744.881143397 +379,10709434.353018206 +380,19084350.739795856 +381,14502361.582613043 +383,15895195.231413009 +384,17682080.921389606 +385,14116314.706791727 +394,17270514.00393997 +395,18215061.70432639 +396,16728315.514293808 +431,13092927.064416662 +432,18578250.109315764 +433,11282738.59233893 +434,17514503.458070375 +435,13698118.213521367 +436,14341668.429395258 +460,19810271.013645194 +461,17483349.75161779 +462,18527742.86384751 +464,18243932.765460018 +465,13941254.257897811 +466,10954218.844008895 +468,13592547.58104076 +469,19530920.633079108 +470,19333461.906884335 +471,10527822.148421988 +473,11678976.6439477 +490,14314123.16445806 +492,12252222.634218236 +493,14513285.074206926 +494,11320111.472617537 +495,15642257.194266647 +496,11039281.243218549 +498,19383939.392700456 +499,19419895.112220276 +500,19069204.300119333 +501,19224087.00517555 +502,15808494.795277506 +551,15807822.750444654 +555,11284970.08104112 +558,12104669.28946013 +559,11641962.04644205 +606,16062770.14761424 +608,18459231.95826851 +609,17400810.437080197 +614,15691081.427781645 +615,17732913.08049149 +617,13457538.630244393 +618,16757389.448272754 +678,16193889.031199953 +679,12275913.84036763 +682,13116740.021376181 +683,17542411.93462979 +687,13517194.341820076 +688,15464835.474193014 +690,16710736.116922105 +694,15920685.547168173 +696,11894610.681491747 +699,10643640.424620358 +701,15209307.235137777 +706,19593789.4620459 +708,11978024.158376265 +800,12094956.548885144 +803,12324758.243836872 +809,19211464.2103931 +811,18342707.83647711 +825,10201647.589688497 +830,19545465.35108032 +847,13345686.612886276 +848,17481128.151214115 diff --git a/test/hyfeature_01013500/channel_forcing/201512011600NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512011600NEXOUT.csv new file mode 100644 index 000000000..928cf7aab --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512011600NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512011600 +133,11862023.10192982 +134,10841193.354211645 +135,11479603.088807307 +136,17228227.875009336 +137,13081700.743267747 +138,15751625.362160867 +139,10066873.17320124 +140,18156127.64568131 +141,14344963.65435265 +142,19428555.762033284 +143,13568019.016550919 +144,16735798.59482538 +145,19235781.059974056 +146,11432007.015096547 +147,18634222.327937227 +148,10125543.21246174 +149,17495214.839089163 +150,16308939.438972056 +151,11127199.860032361 +152,10135275.535357498 +153,16999447.92378182 +154,13176900.627407249 +155,11554830.383529793 +156,10783823.425990991 +157,19202351.2371199 +158,19177951.746094152 +159,10943006.186745021 +160,13907796.545660675 +161,12178688.421824386 +224,16388523.792008746 +225,12002026.081393763 +226,15535955.029425617 +227,16390705.880644009 +228,19434448.81584963 +229,17475459.657456096 +230,11850004.066861505 +231,11375820.934638409 +232,10919534.78877602 +233,15711164.105072025 +234,14615037.82421682 +235,10990881.657499822 +236,11411210.675530579 +237,11821110.703262968 +238,16399701.189824581 +239,13551808.765245734 +240,17098186.439075008 +241,10257689.274792245 +242,17715059.479882002 +243,14934986.5974558 +274,19889617.60434179 +275,18848553.259611256 +276,12856135.703501344 +277,17983334.256680686 +278,14981296.039360913 +304,10631216.755396564 +305,10638345.280401895 +306,15673932.686446726 +307,14556241.04701968 +313,12292892.635976851 +314,11378745.99132236 +315,14310313.125867315 +316,10601148.321721835 +317,10975235.635085035 +318,10943081.120425351 +320,19783939.641021132 +321,19048057.6117568 +322,11790865.366238266 +324,11553374.619381953 +325,15494456.26833301 +360,10642259.413013713 +361,14171486.488264497 +362,14932118.396108646 +363,10769618.819379788 +364,12342858.179256788 +365,18238518.459330276 +374,11122845.760242095 +375,11101282.225845234 +376,19763205.651570402 +378,10207510.658075837 +379,13445783.656284753 +380,18206846.957120486 +381,13807927.566050168 +383,12771600.723811127 +384,18125182.273510814 +385,12975684.655868247 +394,11773793.796490382 +395,11225251.919729557 +396,14186080.849996708 +431,11915435.574397467 +432,15222917.744369894 +433,10513264.464446168 +434,18949001.892883573 +435,13796017.749815864 +436,13348818.947686268 +460,18982000.243127845 +461,16044902.144311067 +462,15384960.050332349 +464,13737716.344739504 +465,12439651.6187824 +466,12276921.02321096 +468,16018047.25797629 +469,15350208.10554048 +470,10434195.46129146 +471,13076266.501987526 +473,15223469.65877841 +490,19821851.153994583 +492,16017620.195321176 +493,12397092.794271195 +494,13493251.965242293 +495,16054456.349450931 +496,11722582.49590405 +498,13376969.953466678 +499,19390923.087703392 +500,11611180.95323711 +501,13749030.731229732 +502,12025652.758913662 +551,18769012.57798698 +555,18144290.52963475 +558,13302441.384648122 +559,14217806.489370659 +606,18167434.710388657 +608,16933031.85009423 +609,10220439.669270026 +614,10644691.878649365 +615,19703161.54095803 +617,16691012.681376979 +618,19170215.450206712 +678,13908642.286042934 +679,15261950.463313594 +682,11236733.685339902 +683,18691168.575473912 +687,15598972.286048472 +688,19212304.859301772 +690,18907154.019786566 +694,15027186.247749211 +696,13436266.251147231 +699,12754866.062229656 +701,14417512.48359789 +706,11086828.702025577 +708,19258317.91496004 +800,10411748.201407129 +803,18540010.011244483 +809,18230846.485404473 +811,16139249.641744656 +825,16423738.358084194 +830,19331061.14268145 +847,18467302.350836203 +848,10224108.71642562 diff --git a/test/hyfeature_01013500/channel_forcing/201512011700NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512011700NEXOUT.csv new file mode 100644 index 000000000..ac672af2d --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512011700NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512011700 +133,11399582.130012471 +134,12352158.848672032 +135,19894698.300341178 +136,15958590.725279294 +137,16436682.523357574 +138,15571019.109057218 +139,16983545.123054173 +140,18822461.59338652 +141,11619405.57663882 +142,17352261.327186603 +143,18850097.097340368 +144,19958624.988849953 +145,18072388.163370755 +146,18768158.763159074 +147,11651062.294345157 +148,15594597.14334728 +149,14614571.32818158 +150,17423038.176581584 +151,13462597.230586795 +152,16221944.128309622 +153,13705040.544625878 +154,19825472.39221429 +155,11947519.023360165 +156,15706844.465022296 +157,11709028.150676455 +158,17611673.10689021 +159,14904614.451282956 +160,11502186.912859865 +161,19630277.80223138 +224,10529741.250242796 +225,17461703.05004973 +226,19315348.86865665 +227,18925486.638461493 +228,16237716.475003995 +229,11700990.293931063 +230,13742814.046570757 +231,16490963.27418124 +232,17660230.889817607 +233,12183293.25068058 +234,19702286.7677231 +235,15133606.98042205 +236,13687116.47397841 +237,19202823.014998388 +238,15625035.401408108 +239,12195515.8973598 +240,11596572.407688614 +241,12313004.6605562 +242,16672702.704892676 +243,10330132.651292315 +274,15107893.65133486 +275,17610597.0760251 +276,19654532.923650146 +277,12800883.71781101 +278,12106613.482515214 +304,17069140.314549446 +305,13184231.371509274 +306,14435462.043939585 +307,15356420.082987059 +313,19658246.965707693 +314,15311748.915378794 +315,11250776.560029767 +316,14401066.387493221 +317,19232447.23277638 +318,18677991.19215429 +320,19433914.97877746 +321,19691222.953070458 +322,18202520.088379025 +324,16422810.79264377 +325,16008382.092427645 +360,15486177.934022445 +361,11144735.573279314 +362,13470046.290513765 +363,10943274.9604129 +364,14930838.583243735 +365,11810897.737660622 +374,16315479.345878573 +375,13967467.853172436 +376,17475430.46212116 +378,10490374.35946102 +379,10075204.214976542 +380,11770607.826548975 +381,17883810.5418564 +383,18630235.070087455 +384,19823695.427936047 +385,15493158.3024997 +394,10906313.71259834 +395,13729599.153110482 +396,13456953.292814542 +431,16378993.84899538 +432,10658841.953099048 +433,17410760.23608927 +434,12094659.375282975 +435,11906070.25497621 +436,14746140.973115504 +460,11623984.351425534 +461,18841758.163509093 +462,19684244.033018723 +464,10105771.01796329 +465,16251926.154323742 +466,18077750.940853585 +468,12590879.882060394 +469,12193925.966141146 +470,12865592.405565018 +471,10539469.23592674 +473,12807583.072838938 +490,17588950.055905916 +492,12509747.409440767 +493,14800656.609371606 +494,16855399.195941802 +495,12683613.749427866 +496,10987587.380163243 +498,10510192.935571447 +499,10075276.969514662 +500,19891421.248674065 +501,19129800.15130948 +502,11859750.556901816 +551,16526714.200700033 +555,13290804.96034885 +558,10036688.40792192 +559,14685354.761609854 +606,12819845.340337237 +608,15589922.267978135 +609,14509228.743610842 +614,12087094.672546245 +615,11482533.994983945 +617,17092518.136256184 +618,18157694.513273537 +678,11676908.736073775 +679,10643568.331856405 +682,17763742.10001211 +683,11723140.833022404 +687,19014467.651611213 +688,13703395.003438665 +690,17666493.966169205 +694,11076452.289351009 +696,19467458.874507856 +699,14188335.991152281 +701,10719634.40115002 +706,11149491.013145074 +708,19790765.95172973 +800,13940287.176425695 +803,16187934.571108712 +809,10719242.374831643 +811,11119450.198133415 +825,10434415.319118377 +830,17148513.498922795 +847,14150152.833431453 +848,14804602.69970246 diff --git a/test/hyfeature_01013500/channel_forcing/201512011800NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512011800NEXOUT.csv new file mode 100644 index 000000000..5a20e0556 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512011800NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512011800 +133,15052131.466066647 +134,18569842.785624072 +135,16007612.138975091 +136,19391305.972561873 +137,18420927.15902116 +138,19972713.406669516 +139,19002597.506970298 +140,16235199.766195605 +141,10374014.922057772 +142,19525398.505773664 +143,17739769.010633852 +144,13805810.322571646 +145,17322999.775681574 +146,18270933.439057704 +147,13492707.626181085 +148,14620795.79156239 +149,10649210.628918033 +150,13972015.220697539 +151,10452814.09942805 +152,16703074.723268073 +153,16565997.291570254 +154,15284482.537444029 +155,12941663.14121902 +156,19253209.8943536 +157,15792019.329482831 +158,14687139.598892845 +159,16042200.8082271 +160,10334449.320687495 +161,12312869.846443206 +224,13117712.504715985 +225,17002197.308595657 +226,18131733.956366442 +227,12083252.266541924 +228,13599487.085680448 +229,19133377.701987967 +230,12163693.751476899 +231,11683103.53307155 +232,14702947.644460345 +233,12936995.8180395 +234,19454738.33920467 +235,16689176.84588214 +236,14882508.559656257 +237,15430064.85056202 +238,15098582.730123816 +239,19913724.874176346 +240,18758644.876675222 +241,16810866.510644075 +242,11371394.118495623 +243,13268052.867486384 +274,11670890.300165491 +275,10821028.380964056 +276,18274542.920680963 +277,10927088.377441606 +278,10536917.380306128 +304,13399359.866602028 +305,13798697.177506326 +306,12923567.178471845 +307,14327708.175601743 +313,14912828.874996591 +314,14603664.541447747 +315,11069806.834946573 +316,14755128.496190786 +317,13370182.35486582 +318,13584104.789989134 +320,18398616.49684302 +321,14020085.47069 +322,12554171.13014568 +324,19454316.432323534 +325,12527184.55517988 +360,19281810.316563208 +361,17945742.622397456 +362,17776615.67428916 +363,11188803.255868316 +364,17631299.484584868 +365,12301890.40838583 +374,10941680.455584148 +375,11595724.720266964 +376,11716559.489252312 +378,14900855.608411213 +379,13127909.040856412 +380,14342274.781107027 +381,19536390.062838677 +383,16964012.097368833 +384,16911453.32846344 +385,19578839.55147674 +394,19850588.6599903 +395,12067670.834355436 +396,15694740.994919736 +431,16372016.33838179 +432,18511071.96344714 +433,19989547.39605974 +434,18034384.823668636 +435,11129976.962940186 +436,13567262.714987736 +460,17522379.734714225 +461,14417873.990503501 +462,14701612.179151474 +464,16711805.497744681 +465,14015708.310010828 +466,16087486.81494433 +468,10021201.229712818 +469,17470831.083962083 +470,14339382.308156647 +471,17906490.779624075 +473,18909296.689808473 +490,13305188.556133267 +492,17228082.940641683 +493,10463989.772295756 +494,14305661.099906478 +495,14911974.122090701 +496,14106437.279252263 +498,14981615.289003104 +499,16959345.5180485 +500,15565898.327021481 +501,11903190.516609417 +502,14842299.00871599 +551,12109669.047774661 +555,11592410.0262057 +558,13016695.1288301 +559,17565475.030467276 +606,17478655.149214387 +608,18720387.037967835 +609,11263814.000139995 +614,10349958.28670859 +615,12435208.057697864 +617,13781381.830521535 +618,18803987.43973607 +678,18659087.12016557 +679,19920436.140268818 +682,11209887.685284305 +683,14296648.92762484 +687,13609057.61989132 +688,19909487.07081805 +690,12604668.465690635 +694,15942104.74283477 +696,19341151.8122492 +699,15394008.599262603 +701,18334187.21396399 +706,15449381.987023689 +708,19405071.867830917 +800,16868206.484720256 +803,19905095.654825944 +809,16158949.909678418 +811,17618387.39541391 +825,10923112.260179589 +830,15944245.44275169 +847,17458043.362738226 +848,15432067.65026923 diff --git a/test/hyfeature_01013500/channel_forcing/201512011900NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512011900NEXOUT.csv new file mode 100644 index 000000000..006efee08 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512011900NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512011900 +133,12909813.012070512 +134,17993244.831230506 +135,18340053.808578186 +136,17361674.177882776 +137,10487623.440105831 +138,14534309.441489574 +139,10159384.49695857 +140,19178491.44891578 +141,12864845.771999499 +142,15959045.74661101 +143,10971371.81542664 +144,10172408.377606697 +145,10101715.057456749 +146,12557036.171724426 +147,19912057.783266522 +148,11954915.206528991 +149,15979859.87694914 +150,19711763.851339586 +151,12952641.8418098 +152,17890322.856393874 +153,15004855.932072662 +154,11015287.599837607 +155,13068307.238895306 +156,17438055.173905153 +157,16015918.992065303 +158,14221865.345456678 +159,11007384.06421649 +160,17509202.69793656 +161,19501295.71297114 +224,10551124.552495273 +225,10769592.202465167 +226,19366995.111911424 +227,13590057.679178812 +228,16480707.975617709 +229,13316012.196373913 +230,15403813.441552022 +231,12507867.201834105 +232,18629396.47543787 +233,16182442.453480985 +234,12076777.901863934 +235,18509927.422466718 +236,12668451.232341403 +237,17180758.619683594 +238,18052728.436275214 +239,11083966.994553663 +240,10223910.8503469 +241,12275510.051752549 +242,18389643.76281242 +243,19649460.882998846 +274,16504102.704913937 +275,18670192.165921383 +276,10931933.017589178 +277,11267994.344286103 +278,10044611.957720138 +304,11010895.958460664 +305,11357191.601652836 +306,10934728.272985358 +307,13432872.35978604 +313,18692057.097663168 +314,14670225.238270145 +315,17618645.700552236 +316,17456800.882191993 +317,16498344.21856435 +318,14622613.760725059 +320,14649149.411309078 +321,19237426.54528134 +322,12258576.571947113 +324,16866360.9404137 +325,10968061.643291552 +360,16837456.729829393 +361,16771204.883236412 +362,13281684.018300956 +363,19631600.702188678 +364,17236531.89288404 +365,15008644.640218621 +374,11642061.051871575 +375,18798534.751607805 +376,17740373.022021078 +378,16341826.714281548 +379,13540934.051449874 +380,10351906.579597387 +381,11251709.002025813 +383,19014717.802934766 +384,13547786.072518095 +385,16531061.784717033 +394,15693205.84179075 +395,18184886.92905707 +396,11453305.19256465 +431,10972061.214321524 +432,10781743.46392549 +433,13001109.676630482 +434,11233673.681005992 +435,19769509.220431402 +436,12294113.497760545 +460,11845077.208958896 +461,10235250.439243175 +462,19988102.849319477 +464,14322317.156024076 +465,18358922.116925046 +466,16777404.479851488 +468,17467277.638397038 +469,17284875.213550813 +470,18530841.072343923 +471,19528278.992126126 +473,10227460.837157026 +490,18530676.31328667 +492,10138214.871829133 +493,15303989.427843835 +494,10063908.955003712 +495,11655340.780017069 +496,17145306.568648294 +498,16934453.547437787 +499,13960667.765906971 +500,10265058.83004442 +501,18263401.01941747 +502,15711698.425650248 +551,11417340.263362195 +555,14815436.647323495 +558,16173241.43995735 +559,16122824.51368078 +606,10050326.50867997 +608,10482465.411913907 +609,18061105.530198634 +614,10017246.920093568 +615,17326859.140020784 +617,12949703.527372833 +618,14556677.10970248 +678,17717085.77599679 +679,17857155.23496081 +682,18052991.502681583 +683,14397547.420765929 +687,11288646.095324399 +688,13452776.57258157 +690,12949636.303904891 +694,15607121.384891137 +696,14889363.779749451 +699,18977009.999894325 +701,19305969.415126115 +706,15900178.24867865 +708,12168617.750030503 +800,15175869.382991266 +803,12250917.992886635 +809,11954186.331899857 +811,16253394.636944983 +825,14180517.566898495 +830,10066248.731348848 +847,19396666.69295633 +848,15271161.806074038 diff --git a/test/hyfeature_01013500/channel_forcing/201512012000NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512012000NEXOUT.csv new file mode 100644 index 000000000..9eb6fa429 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512012000NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512012000 +133,12345807.184046466 +134,13724946.923306776 +135,16082022.822393216 +136,13594529.200914174 +137,18537162.32229837 +138,18666678.518965453 +139,10466645.957844723 +140,10098263.075373126 +141,17230882.698126096 +142,17810494.36615175 +143,15921318.777770298 +144,12754719.98201971 +145,10734696.058534002 +146,12459568.349141855 +147,13640420.451739173 +148,15562243.778301958 +149,16711813.982776623 +150,19560650.36754088 +151,10372129.443085033 +152,17635632.99284856 +153,13855931.749945648 +154,10920898.549223963 +155,15229743.98003272 +156,16533419.860474141 +157,17882798.358623654 +158,11581696.420932807 +159,10285759.456492202 +160,14547198.8920106 +161,12413219.464672536 +224,12283441.141119087 +225,19736357.83330159 +226,17001263.530774686 +227,15794372.137394503 +228,12291785.572144857 +229,11649078.789904688 +230,15696056.85571686 +231,17936740.212552126 +232,17387840.245831985 +233,14594071.312476274 +234,13763627.593371019 +235,17418299.551135644 +236,19566920.96696402 +237,15669216.28725753 +238,15678313.511214428 +239,10120922.054562848 +240,15292782.97954509 +241,10924760.976687739 +242,18333081.194298834 +243,15899795.837819798 +274,15964516.876482293 +275,17512162.74217567 +276,16119186.926195167 +277,15864660.749707803 +278,11885754.87063966 +304,18966405.680908423 +305,12577074.843199095 +306,17674093.513102636 +307,13771321.797161147 +313,18461188.530158382 +314,18457881.932114158 +315,14840390.588666713 +316,18781948.390735663 +317,15877265.812118758 +318,16028719.41294435 +320,11754161.623222817 +321,10041891.823061908 +322,10619396.512467727 +324,17238466.961769067 +325,15347926.044830892 +360,11434380.970293405 +361,13996987.110116482 +362,11834825.126504166 +363,17247181.45997849 +364,17298923.699610557 +365,17950365.785849452 +374,18084083.419711106 +375,11899477.14611136 +376,17146863.82938432 +378,15281729.601991046 +379,15820048.267629517 +380,19000382.254439186 +381,17974441.04236867 +383,15599265.074768417 +384,13146260.041626174 +385,17558808.817950606 +394,15867512.493666217 +395,18641202.341099046 +396,16460659.757001888 +431,18105167.22661807 +432,19990460.015683487 +433,11015627.192459583 +434,13548592.364552345 +435,18827594.744831607 +436,11504990.565017289 +460,15152228.534656618 +461,14658072.627874305 +462,11928010.91416575 +464,16370799.360230427 +465,11424665.843700089 +466,18827635.529874563 +468,19464677.5973111 +469,10320045.015707225 +470,15335623.01125729 +471,10819687.766306596 +473,18019721.315062135 +490,11960837.451388016 +492,15281811.045255285 +493,13329825.489929497 +494,12320066.610532522 +495,14598838.842597492 +496,19028019.382433794 +498,16025604.434081655 +499,15310368.551213762 +500,10894300.310443927 +501,19848045.169921678 +502,14738248.252153836 +551,18577871.8554117 +555,18884724.063999265 +558,10775018.733679436 +559,14917028.46318824 +606,16900357.021789033 +608,19737729.493450016 +609,15970624.696990196 +614,18817436.09737635 +615,13540889.68104873 +617,10651791.310450703 +618,19679171.98061631 +678,13451112.622110108 +679,10709715.492394926 +682,16832434.664334524 +683,16450220.798792688 +687,12237603.205383174 +688,13689886.009368641 +690,13137499.190344797 +694,17994140.67805027 +696,11146620.47952448 +699,14269356.460790152 +701,13827626.504746554 +706,16602716.113183795 +708,17210555.022903044 +800,10552406.619056268 +803,17250482.182049643 +809,17457799.954184927 +811,15430114.769764625 +825,19190278.355096772 +830,15021789.822323825 +847,19818142.6004026 +848,16107909.814069945 diff --git a/test/hyfeature_01013500/channel_forcing/201512012100NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512012100NEXOUT.csv new file mode 100644 index 000000000..4d7ba9491 --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512012100NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512012100 +133,10577181.509925721 +134,10289805.761551075 +135,12369871.627850384 +136,14693909.477064528 +137,17924621.506719936 +138,15597230.480686184 +139,14543829.660329364 +140,11456347.55926473 +141,13958164.273131216 +142,11930313.122803796 +143,11209484.542183293 +144,19361438.3960717 +145,10284290.518006079 +146,15315517.516072795 +147,13669984.20523206 +148,19071739.851921506 +149,18592462.495433718 +150,12070740.793852253 +151,16502899.114869732 +152,14767876.764172439 +153,15159365.981335854 +154,11854274.628533624 +155,19685965.55467701 +156,19247461.69413415 +157,16624373.322681949 +158,18603322.568521313 +159,11639660.400700763 +160,14141174.980475273 +161,10152243.817198187 +224,12727961.570518844 +225,13576157.154841192 +226,15631001.778068066 +227,18954081.08392527 +228,12376168.069217991 +229,12692560.426751057 +230,14594763.637659904 +231,16331668.41539928 +232,11226547.258329418 +233,14307324.777165476 +234,18255928.687953867 +235,17464399.107856628 +236,11773473.991911184 +237,12214146.834451312 +238,18606263.996177595 +239,11282816.6614345 +240,16936343.283562493 +241,13590734.424904006 +242,13438600.482687945 +243,18011613.650346983 +274,13998200.994050644 +275,14203104.531202741 +276,15104173.505527329 +277,12451198.34809636 +278,17915285.164694212 +304,17083848.286504876 +305,17936294.44838742 +306,15939045.390287377 +307,17606531.676404346 +313,12523218.255222809 +314,12419768.541525807 +315,16557154.450612076 +316,13896605.617698941 +317,14615791.354199512 +318,16155268.02947827 +320,13876180.096462578 +321,12183391.89126996 +322,13984154.991205962 +324,19977633.0101848 +325,11464941.40758617 +360,13950059.033963844 +361,17920854.34353119 +362,19936404.22368092 +363,16328662.752858829 +364,11882410.943638789 +365,12910279.379145402 +374,13010375.991803948 +375,19557077.888530564 +376,19898355.085971907 +378,14299879.725653052 +379,10927661.544877838 +380,10335059.431786483 +381,18504988.727065682 +383,17851118.033250682 +384,17660610.481048346 +385,19780402.022956446 +394,17645161.500283238 +395,16234904.054633942 +396,13192579.542220118 +431,14544184.58057057 +432,14048509.303936925 +433,17735776.837387778 +434,10921398.587497855 +435,19879575.835054442 +436,14063524.821294308 +460,16865304.70398857 +461,19174852.70175297 +462,12867249.37856147 +464,18043896.161096644 +465,10153599.324246539 +466,19190130.856074486 +468,19791119.566109657 +469,17347630.06283403 +470,10866672.15168466 +471,19121212.489707075 +473,17766311.103807285 +490,16678090.819611913 +492,12019806.658312496 +493,10451981.300627466 +494,14860030.043395504 +495,11631584.009801017 +496,14854689.571065202 +498,13650722.917913124 +499,17194218.31546724 +500,19140413.986531682 +501,18882727.430103224 +502,12466342.445060754 +551,16779765.164211586 +555,11076001.463562934 +558,14530124.657101119 +559,15762030.854293723 +606,19363581.963563412 +608,17921946.6311329 +609,10181834.877232132 +614,18040677.518679984 +615,19760338.941269044 +617,12640562.837292759 +618,16164766.459785566 +678,10573180.6981352 +679,12405187.314305495 +682,17485933.8017473 +683,12722485.219789745 +687,11430803.129665375 +688,15872854.42869391 +690,10723336.954518473 +694,12533921.499401534 +696,14210728.666169722 +699,13104373.099439956 +701,13093794.384464541 +706,14522095.862615049 +708,13903398.04373926 +800,16201356.392987967 +803,19540018.37283867 +809,16437398.835897166 +811,15329431.486450871 +825,15984517.339584842 +830,19303651.639670223 +847,10225173.065985389 +848,19424966.091160707 diff --git a/test/hyfeature_01013500/channel_forcing/201512012200NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512012200NEXOUT.csv new file mode 100644 index 000000000..359f35c1e --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512012200NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512012200 +133,18933529.020823114 +134,16922958.133123726 +135,10527462.497850485 +136,13610357.598805036 +137,17874908.344357595 +138,12421112.084836489 +139,11995827.277026987 +140,10498438.41607399 +141,14827634.19010017 +142,16743829.130280534 +143,19838326.912528522 +144,13480286.204767846 +145,17290616.575743362 +146,11647575.96009986 +147,15095394.119731005 +148,14949555.238205258 +149,10386372.9487026 +150,14013962.537063673 +151,19399244.96683151 +152,16985582.42762189 +153,13747448.887163937 +154,14889646.105379067 +155,14446292.349597424 +156,16223388.087986447 +157,16244392.378958281 +158,11781455.814125998 +159,11584382.857679054 +160,18166574.91364036 +161,18069021.8885548 +224,16179471.205113837 +225,14319404.067273457 +226,19478341.836734746 +227,11285389.064746555 +228,16442314.371385582 +229,15117638.460107623 +230,14726813.125982251 +231,12046898.112587944 +232,14129608.07378465 +233,15475906.72317278 +234,12746890.061921434 +235,16538372.928785946 +236,19515268.69624145 +237,12681991.323429741 +238,12809897.730954282 +239,18907138.329873495 +240,12375901.477737054 +241,11619865.311549295 +242,10639979.042076442 +243,13400266.127261853 +274,10139516.611121926 +275,12406343.342107886 +276,17252049.328741375 +277,17118946.011044517 +278,14511745.181154925 +304,12230134.458813468 +305,14199358.934886726 +306,15678571.610466927 +307,11457103.307076033 +313,14119836.90876666 +314,11061642.197389597 +315,12251268.574182527 +316,17329457.005311348 +317,17925828.551433086 +318,16739558.669536937 +320,14083638.920251524 +321,13403659.463392 +322,19480733.8459732 +324,19000973.985890906 +325,13805274.81015791 +360,18221086.704901345 +361,17901642.71988389 +362,11940669.73276575 +363,14514523.18832717 +364,16430264.353715438 +365,10362346.334640251 +374,13370090.57929864 +375,14672820.9768205 +376,10615719.300948719 +378,13377723.722648464 +379,15189447.208240747 +380,16686281.128269281 +381,10313650.50290676 +383,11936046.751182651 +384,15834986.588096023 +385,17228803.51884896 +394,15470168.34686197 +395,10193673.277497 +396,15891374.782248918 +431,17502848.827535678 +432,19292748.739483923 +433,12774201.354682378 +434,13805082.375689786 +435,10974069.426850658 +436,16650164.02630698 +460,13557484.879986078 +461,16776238.925656918 +462,11616987.681145994 +464,12547581.912216147 +465,15879497.369045362 +466,18453239.679357443 +468,15815325.581983518 +469,14966485.824528925 +470,15743682.73079394 +471,15308463.142080732 +473,19066461.645356204 +490,14519240.022436433 +492,14494899.614275752 +493,13716142.583246808 +494,11300665.572854785 +495,10413384.731689928 +496,13084561.35848602 +498,17173083.586533822 +499,10110903.322032712 +500,17950977.6162762 +501,12580846.574789064 +502,18474433.01547721 +551,13347152.9339006 +555,11324698.904136628 +558,16783836.696854454 +559,12649597.867588188 +606,14472266.144244306 +608,11368252.34559335 +609,11391965.094571931 +614,12404327.82062793 +615,13668537.867102867 +617,16438774.976877533 +618,11244257.21137396 +678,15982053.979291037 +679,12961464.313095257 +682,19826561.18786917 +683,12589065.220975032 +687,15153830.447126407 +688,19669146.9443357 +690,19547634.11341484 +694,18212121.88600457 +696,13036300.656611945 +699,12668598.17534019 +701,19679132.666173648 +706,15112408.949210946 +708,15230761.974919673 +800,15602747.269841028 +803,18382243.982330803 +809,18648616.45409593 +811,11726084.75148018 +825,19158786.987080976 +830,15773395.561289495 +847,14552745.793644123 +848,14166124.65664011 diff --git a/test/hyfeature_01013500/channel_forcing/201512012300NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512012300NEXOUT.csv new file mode 100644 index 000000000..0738911fb --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512012300NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512012300 +133,13781446.95230217 +134,15307140.133053347 +135,12002799.844160546 +136,10844566.374231804 +137,19451111.89676602 +138,18627058.963760726 +139,12409910.995679215 +140,15997012.745406304 +141,18155474.76301588 +142,14077301.56899345 +143,18077110.21274927 +144,18168695.741870172 +145,10627326.599876413 +146,12546397.979745582 +147,19642952.465802178 +148,16486275.710439436 +149,10479479.96673278 +150,12963523.332320243 +151,11795573.168944567 +152,14404048.8568156 +153,17549483.17985766 +154,16315913.65816009 +155,14619433.3875968 +156,11688903.447045395 +157,11368825.498861896 +158,10155188.029656775 +159,15727108.20119699 +160,11282978.99717341 +161,12353904.350635057 +224,14082099.6828412 +225,16074957.58179966 +226,12481547.676778432 +227,12236283.862146188 +228,19057752.728092708 +229,10064187.76366188 +230,10799959.446784172 +231,10357855.154151218 +232,13659536.765797563 +233,17165666.14385467 +234,11130246.856728483 +235,14012019.158084495 +236,13943907.001723427 +237,15229562.206565257 +238,14215331.05753867 +239,19202372.971207052 +240,10227320.803428102 +241,17686991.296223238 +242,17800902.13863954 +243,14198987.868168626 +274,14613153.761657856 +275,14536810.560554195 +276,18123180.076054506 +277,16311627.922606003 +278,19325192.92886735 +304,14607665.220832707 +305,14625865.058964614 +306,17230470.891990382 +307,14252050.988339346 +313,13767963.133435117 +314,16797394.286897216 +315,14306994.718654133 +316,13585847.417880416 +317,19404257.91434756 +318,15431772.312126046 +320,18757545.784354817 +321,12812340.09634956 +322,14046477.94951092 +324,13921494.142870836 +325,18259010.251061063 +360,11703224.63864358 +361,17084064.836490154 +362,12993876.52928431 +363,15883727.772308208 +364,17600406.758487333 +365,17354922.23067102 +374,17789741.673233677 +375,11601793.714743594 +376,16049917.408318223 +378,18403344.688005388 +379,12739984.864044767 +380,18427773.89074364 +381,12132977.489905894 +383,15406700.24753768 +384,14051201.87620678 +385,18887013.007073298 +394,16901889.15031447 +395,17788947.000345416 +396,17155722.944866356 +431,17842740.72772959 +432,10289841.878120216 +433,13757761.150290087 +434,10519426.3994196 +435,13040375.22304809 +436,14630350.536625203 +460,19646032.380755246 +461,11534014.98897737 +462,13603697.320042498 +464,16543974.859128078 +465,19043768.100645937 +466,10086224.13355527 +468,12155484.315721642 +469,11500564.860832224 +470,13190278.428307 +471,14145729.285624556 +473,14219500.49127929 +490,19447851.955039427 +492,14719593.446049545 +493,13407916.758983422 +494,18598375.430384673 +495,14063051.211064829 +496,16809303.319428653 +498,17164956.934727453 +499,16133501.336448085 +500,16100201.141105944 +501,19704309.7166971 +502,16400050.748925613 +551,13060446.360443065 +555,17930247.674321994 +558,17725829.51139 +559,14492474.542206617 +606,14829128.020030994 +608,12082246.580009378 +609,12783995.853008017 +614,12350153.876850827 +615,11316265.97799082 +617,12074211.063058732 +618,10690082.13502015 +678,17508471.75968074 +679,12839997.911482047 +682,12421063.864967441 +683,17036912.1850265 +687,19701819.960765578 +688,12452848.965775019 +690,10035717.155198505 +694,15123313.699876148 +696,11723751.235209111 +699,19812139.455930322 +701,13742781.153090442 +706,19746206.366938762 +708,18432184.887418453 +800,10974865.629537825 +803,11962970.224658199 +809,12855457.660206642 +811,12282903.794648666 +825,13952597.908489168 +830,10028616.580283362 +847,19533433.34433536 +848,18772844.811335552 diff --git a/test/hyfeature_01013500/channel_forcing/201512020000NEXOUT.csv b/test/hyfeature_01013500/channel_forcing/201512020000NEXOUT.csv new file mode 100644 index 000000000..3c405c2be --- /dev/null +++ b/test/hyfeature_01013500/channel_forcing/201512020000NEXOUT.csv @@ -0,0 +1,149 @@ +feature_id,201512020000 +133,14683505.655558081 +134,13038974.016153352 +135,15373879.171217239 +136,16608495.296502989 +137,12539021.851245843 +138,16463090.13808762 +139,17726001.6334638 +140,16261204.525833178 +141,14191316.851084277 +142,16509366.850355763 +143,14603620.893773569 +144,11400740.372227252 +145,18962815.070429012 +146,11858693.820839247 +147,15917683.310145898 +148,13964356.356656943 +149,14831027.894670304 +150,12843647.898968473 +151,17264304.53361477 +152,16112294.138729129 +153,16315167.017983291 +154,12696078.568164311 +155,14656255.990901291 +156,10556959.628984887 +157,16317546.051481796 +158,18643054.832425416 +159,16564913.172942791 +160,12815057.411520865 +161,11302502.038945502 +224,11537323.201573562 +225,11288391.715695838 +226,18567700.065961994 +227,16564375.210422043 +228,17269304.24921383 +229,10674885.984719718 +230,13931582.653718254 +231,12641437.64351085 +232,16138099.333855063 +233,13432613.592322681 +234,10338595.947876766 +235,10288814.193165686 +236,14169316.866710603 +237,10069231.58018795 +238,14103072.076626634 +239,17943286.473442513 +240,12138578.32748955 +241,14222463.918715905 +242,14524237.908444418 +243,17635818.672243897 +274,14619032.35131947 +275,12486801.538629994 +276,12154769.466183098 +277,18869257.194131285 +278,14934591.288329307 +304,17402221.083647013 +305,14078728.509298129 +306,17924309.33325946 +307,13317221.764746467 +313,10673887.107040282 +314,12882505.465119738 +315,10836961.743554968 +316,15339319.022830624 +317,10698483.575132972 +318,11599956.737095376 +320,11135132.316936897 +321,10308243.199745785 +322,19843390.67523258 +324,12697940.75493419 +325,18212220.150022056 +360,17576149.44245755 +361,18613909.770436563 +362,18429301.35402385 +363,17903960.898817495 +364,12723820.67134142 +365,15900872.220625782 +374,13597290.65381185 +375,19440598.309617683 +376,15980089.214734428 +378,12954468.46167203 +379,11248386.732483963 +380,16226622.73328791 +381,15956426.109387495 +383,17233649.701662067 +384,19879754.341317996 +385,13772563.969183676 +394,11074762.925737998 +395,13430970.793386484 +396,17948236.287386715 +431,10675571.248769606 +432,17417559.121820416 +433,17030476.111964963 +434,14682161.951885693 +435,12937816.648142323 +436,17795608.01127138 +460,17691234.055700444 +461,16392348.144152608 +462,13184906.91692089 +464,18770802.507084135 +465,16609718.961881274 +466,17943922.3624308 +468,13279578.135927234 +469,13201077.293251142 +470,14186745.016055599 +471,19210677.600523643 +473,13636372.994253052 +490,18401837.08568862 +492,10633819.673446441 +493,10303543.113260726 +494,13070121.373631522 +495,19865428.723082785 +496,19747081.865706764 +498,13614579.74685932 +499,11496482.138811437 +500,19095544.435275733 +501,10568813.694206478 +502,15038954.073883396 +551,15957910.059641164 +555,15170723.618386187 +558,14906478.251517205 +559,13467368.225474557 +606,17841375.52466653 +608,10331855.293934416 +609,13318949.165310064 +614,12256570.546669971 +615,14359672.251996849 +617,14556157.131628186 +618,15554456.622795764 +678,11914014.920833653 +679,16383986.905304076 +682,10013325.243051598 +683,15459200.939436283 +687,14105364.735803423 +688,10555520.901215108 +690,12861585.393577086 +694,17932441.12199968 +696,11644167.854783302 +699,14189494.956517119 +701,18076801.75171847 +706,18023555.630633086 +708,16741835.208864866 +800,12500047.800809208 +803,12264455.627680615 +809,10219652.602806432 +811,16012718.561346883 +825,13959816.350116415 +830,18060805.60441895 +847,15508943.636226349 +848,18842850.475525323 diff --git a/test/hyfeature_01013500/channel_forcing/binary_files/.gitkeep b/test/hyfeature_01013500/channel_forcing/binary_files/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test/hyfeature_01013500/channel_forcing/schout_1.nc b/test/hyfeature_01013500/channel_forcing/schout_1.nc new file mode 100644 index 0000000000000000000000000000000000000000..dea94bc498e6aa88554700acad0c098fe0ea431a GIT binary patch literal 25915 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 aUbOEqd6e-T*(W_O7r(q^a?=eJQ~nJa#1`WK literal 0 HcmV?d00001 diff --git a/test/hyfeature_01013500/domain/coastal_boundary_domain.yaml b/test/hyfeature_01013500/domain/coastal_boundary_domain.yaml new file mode 100644 index 000000000..5640e89ea --- /dev/null +++ b/test/hyfeature_01013500/domain/coastal_boundary_domain.yaml @@ -0,0 +1,4 @@ +160: 0 # Lower Colorado River + + + diff --git a/test/hyfeature_01013500/domain/coastal_domain.yaml b/test/hyfeature_01013500/domain/coastal_domain.yaml new file mode 100644 index 000000000..a6274534d --- /dev/null +++ b/test/hyfeature_01013500/domain/coastal_domain.yaml @@ -0,0 +1,17 @@ +160: + links: + - 273 + - 274 + - 275 + - 276 + - 277 + - 278 + - 159 + - 160 + rfc: + - wgrfc + rpu: + - 12d + upstream_boundary_link_mainstem: + - 273 + diff --git a/test/hyfeature_01013500/domain/gauge_01013500.gpkg b/test/hyfeature_01013500/domain/gauge_01013500.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..e57e5500c0c70abd7bcb7a8128728a0f69b4061f GIT binary patch literal 1454080 zcmeF)2V4_N`!Dc>P^Cl^1Vto*NbiJZNkSE+ND~n>A_Rz3N$7$!yNJDDZ&<*tfEC5w zz}~U<-mok8w+TT6&bjA*&U^3s-uEt_a~8ilvzy&#XJ&V1H%ow@7hlL@L?tH2hYJ}t zl#UcBDGHmxpin686bdy0za>8d@egSU@S94Z7+fXnF?f(ii}@}8=}s~C&6L3$8Tw;P zsc!Ud^pEtn^cVEUiWQ2naybe*GHG)Cq#dN5Q&+cKko@iCiA1C zdCAd9vC&*1KaR)6K}+VROMa#CBZV}#ovt+=*9^2_w?{^^Jf^ePH%4= z1`KC6cVB-uhNn+}n|~m~*Qf1r1`On2xN%$tF#LUkv4DrC4cp#wZ>mjwcW>gfZa>3@Za8M(_YPY^kA-Td?_W`TH`gjQTL#d|dMf(&e=L5XPQ= z6voWN#CUETFO?U^O-YKxX0#5{|2Bwi*3cZnpuw(0{1c-Xzw-RUfQq;A?*z2PCbSn$ z%iqJ`5e%c=`n8Z&<^-AXo_?bM0t8WmoL(Q8cPRwXO(Z$y% zAP`N8Paq>HmMav-GY0#6dUO0k7=zqGTE@V%MHMqLZLwGs7K@MPwulzdx_Nr|v|fV| z!{5yv&54hTTYxy+XpV%u1fifssEv$R%CZ^(W>gA4A(EFa7!!vxDK|Vtm?$#0^0AM?O*>A^Q~cB7K;&o{j^B$!%b=MGk_B9TWZR<~&rjf{ zw>dh*+>Ew!!_(W#i@9y{{(dZ?h4F%a*}-UaoJH%#?h$9FUq@KHzip4MASqnP50B#} z^P;$d3<1NiDWj2xqO3+J^DiG^M854&CXRbG40cJFCT! zMr9>FjMz&-%hK0!vrTcnCt@Jlkl*TT3YLy&SxSO@JpF=jqW@Kkf6I?LPUKJc{lK@# z{FkbB>(Ug#{pV%LQ$bc^xc#4&rRIauR3f<6?UsoSp7Q_mRf$`l)}Nq|*F{E7!@`1^ z7y7GUCPpXYXo(v<(FU}fA=|bhltiQ@ zk(Jd5viQ?*wfx+sde?H6TtRm!ISm5?>iD!@`}W&@NkD76Pm-jT1?^9E2v>{YT%s!d zp2w6QiL(YjiqA_HCx}b0X(Ue&k<3pL@^LjRI%7+A-N3*!E?gkwM&PnG8Yf6xGX{Ek z|2jo7BpD5HbsQz+$MXyg^h3<_3cirM-4MDHAqNIGcYnTZP}8WGFrCa zrtvKrZX~s6uv?uM+E#{SqB;;~j=$5oHaD)0(y|)f27h{lB}bvDB1kCXNc-!HiVMDE zQKAPcPxP(wX;jgK8EbMuIN9 jt8ZO|2ar%&nRB z*4EZmmR7@un-W8&Zdjd1GLM&##*c{6HMJaWie718sBR=L8gntj+|tV4mT6<{Xw9^8 zbhNdxG35jW4)FE&3=9d?C3ZpARM*kU+FBPYY@1?hZRbd&5PONcK%Txnp}OL|=OPsR z2(H_h6pXcTl6m2}$i?yJc)JC<`G@M_#t&bZ63OExMsXw1ZRREGnp)fZo+U9Mx=kK4 zN4wuL354MZk>Saa+@$d2@VGb(H(Oyvj^@n&O)hI)Q(I=MTv6e10v?x#E;&CTS`@1m z8!S#tAdZYARUAoTSPy};LU@LH;y}AemUrU>1WIO#j;LrkAKV}t&|WB+miCqiWf(=S zA4OeWCq4)R&FI?kA{iLRiB1kris45v0{jDfe0|(fBx%k6XqidWU*sp?o)!axoV;XS zLIjV2{-cl=AKEDSk?TWIr|QK1`V^P&WZaA6;|^QXhcq^?y!juDUzz^oLkiSZL@Ah{ zVifZ8w1YhY7!J0?=0);^{0O2%b$lc#_Tt3mat{*4+_q&gmt<*E$ZeQ*?Uhw~OB*7L z?UYqJ8*4jpS#4V_+R5(VXlX@c|0nfDR3N;Xin^|;t1l zT~UfW9)gZQf1DdG#50YfQqc4L^{=*R)|S@RW>%JF*33XFTPGVECrev%roH8`5P3z% zo{mkcjEKUsHHqOdiD^mguD1R3_Ev4{=s0YUoT5ojlcrV2g~#G)vvxMMRYJQfv$Y=< zAggH8(?+uEaoECe3^28~!l<^1?XJ|4Im}O{WjrOz@gk#n+&DhQWtum)d7aH^?XA+r z%5j*Rw4#1beaTKICdQ^Di6000wX(PtbK6^uHIq3kn5L+qt)bjc4nI-}yz~@7(=#Xn z5!zeK%7HnI!?H4{IwW(4;~u`=ym97g4x2i^c(C?Tj{6J4hSgg%JJ*IvDQanJHLW<3 zpNc0wnpfS#c2{oYfR*ciY%JU!!f_9a9=Fj?j>DEI&2@{@;<$fM99RCtq;oBos;Hx_ z)3kC)%$irKo6$S6X=iWOXxf4@9Dwx$@J6o`v0bCPVNgC0Wtz)1jq=G5g;Q#Mu3a}838f^ zWCX|vkP-Ml5`iu>D{9*zBZY1>M{4^+KJuMuCe&6VH*y*@8>*y_-(qA&Rd{}F-zXZru5=l|($6#5(bJ^E$(A^HaT40;xQ1ZI&xWCX|vkP#pw zKt_O!02u)?0%Qcp2#^sVBS1!gj6ll>XeiLEq}tx@r#foM(;TJRzu`}H>@3&zmcNXq zEX_tra(BMPJ%6gBh79o`ZSMM0&!|e%^rc$f_ouNt&}bUsFF8==C`r+@q?+#iQyo>P zG@a(P2&j(a{r{E+lKeLr0Wtz)1jq=G5g;Q#Mu3a}838f^WCX|vkP-MFMd1Ib`~Nih zQVRVVy%N93A2I@D1jq=G5g;Q#Mu3a}838f^WCX|vkP#pwKt|yIdIU6RavJIyQu6Ym zZ}Ag*=rXi~J!YKt_O!02u)?0%Qcp2#^sVBS1!gi~tz{ zG6G}-{^KK{BCVk=e&Zj}*{5}s)>4IdrP;Pr}B;(tQBM?UuY#v=0`{K zlB1Jiqq#zU9FH3xj#=rFUupbEVGK7VDKcEhYn{i%-;ERK#=zev@bGZ+XBf0jZ*Lt2 z3}-iYUw=17&|p_$VPAJf(+7$^iQ((hc0C3REWvQ&xC~(U`vzkL9-cmo05>l;mq3O; z$1}jqkmKy@A85)j5N{_VF@Yfr9~H-A7_`|Y0|Q1XKQRt_BVa@%CJ2Pd;rs-l)9)J* z5uT8cC}fP{G2(av0YexQp1`m&Fk%D`aKn}x`nUy~|F+S68CFJp7;ZkU`K}%1L}69^ zOJU_F2zbfD)?xdfg;l(c=CFGD1i1MJ{=+be*ZFS@<3PHcmLE!I&p!%dW@2JIH;$Le zi~GA}v&|ZsL)cU@6QdZv^8CYq{{3>DmPkLNK0-G&A-e0=eLke8Qfi(SPZo^c~u z?Dq9~Hu7KC39Vg}~{^`*q zaFwpk+_rgt4`sA4UhprAWvjy^ zS~qr&ILZGyK;r#vdvF9v;X;0R95_SA_wzv?UT{$-vpIfEOK7?%>CAP^n?)-&bX9_!Xw{`g>uy3l}o|W5iLte zkdLQd5U!VgRl?u$qlyyw6MjGNEi(V5n%TNEMR5OlS@Kkn)fjI7r)8=6pfr^Tu64U* zqJyXW|9n;Ab4cq?P{`{dBd1|uLCp*ORWK8ylX0}f?UiT)TF#JdTOeB|w>U0;((pv_ z!i6cxJbX$@!mpEqtD8Fqf5c35<}u>P%4!5z{Asvaer{8}YdK4 zRSxmlqn&H1KWI@~wuz>R$tPfrxuWMMLo$8G*QgyOS7aw0DN{y2p){m{&7xSL(!_5FJf9;)0rkDqWUEo z<;nFR4z+xq%^z-0d+Tr7f+3#%fk7NEMxf~0x!o<%WdsG_&nYrQYcb?UifUTAs8SJA zenN)m-x)^vAsXVYe4jt+HnqHh{ky%;cGl6lABqu@mbw0IJ<;DbB0fbR8Xt*H#_0hq zrf7)dS7(%{38G<=-xliM+$HtwB95)rUyAMD?3A|r?EkQCY0FJx{KHs=sQ;yMD(;5; zvvT^|I{vVnHg)*^T_bto*fwp%|17pm3$-YxYJaJmHpS&1mDAtW(Hz@<^Kjn3GL|8o z<+Oq^rsw!ab-rnkLOh!AcMEEJD`}2qn=$96k&AyMlYE{VjihVx$<36sAQiNRfEHB3yXA)-s#rXgejZ%hg=A%fS6og~L!bOqZg z(U9n=m|{sIT-1C0t8U-r2dausa^$VRf90*fy!7sJIOErZ+x&tC-e7P27-vhHp3t-h6AH{Tww%{(RLW`w=qCd#_u0)WRxGE zDW{>MLoG}Z?Rs;=(EJb8-}bg;1vNy&#gEi&uZzgw1O@tf`e0FSx0XYi3|(TROqbz} z@hDF(PaikjIY1B3l%Y$kh}dELGaiQ)9dP7wbM*}J#+WE(CMEJk^CWqA@uPUea9Vte z1><-L(ZZNmtRFYKy|9olQOJ!XZW@OpJmKj62*$KXMOUAf+?+QiBQiM=_4W6UZL<(o z5{FwPk=%%IVMI(khM}6b8~2%-cEU&ol|D-OBGI4Er+=e=q`#%Vz~2JAN3W+}rC*@e z(ofJ2)2r#b={xCL=woQdN>vMyo`sj8GY>;;-VR;;zD0aZ<5WF;_8E z(N*cL(nY1CN(U8b<)6x*mES49P=2I*NBO#Vhlrgbf5-?lM_}eKsg9Ht?5;FQo`Rel zRZ31v%VRKwR@<3E(Q$wLc~}gKGs=1<KJg zFv-XNlALm!I@WyT=u>s9(@iYdH9qU-EZFkiCe?HB!VlZsr^A(R{OL#Gf|7mHC&N|u zw=3>}<3}vGTnNYRoZDdw?3I4LM=tC?OIL0Uyr+6&OcqQx%b}IPL0zV7Nres84wEW| zGC&8`4m|hcFl^cT#Lcd7LDI~WJ@E0=QITr! z1b@%hTj81p=a(vQ;b+_Eb?}Cyo2d$LV!^Q)C2&rs;iKi?rTYvQPJu7qs1*D}$~cfR!IF40!=p-TS^}61=vYKH@msK`!^W zG@QBk*vvAxQYTD0N66w-?5Q=&hNFTO^fH8>sVy8E2Y)}G8$DOR;%rjyW)creX{uGY z!BJf}KO*2k4}E;i;jHg+dW2uqzdg4l8S$8NX~!5iTe@&SD4hC|wIB;NW*D7#HHO8B z%fHk&6{Zz$>@fxIpz?SEv3$*R&op0{&wH@d56-2PjFf}V>h|324QoGNZvG^R#o4Yq zwIh+9{PgpLK`>7_psN#n^QiVw9aws`F3)|tNGSRhj;Cng|J2${OohwaC;Iy>wFP6dk^w`qNVB0(6_2$BP z8y42Df_KeXIBW{xY{iOo@Dt76GsnR%s(8g4;rUPc9v8w>*jJ;s!NH+A)X{KU>?~F# z{7gpnWehwZMr(c*k^j-csS)twf%*Z}aLlAKgAwo|?pc+i@VKoLj|_#~7uTFP4gat! z2@im0B*u@w0Iv$>P<`NC+e=y3VR>g~886uE)UCcZ;ITswAK}0;bw`HZf%W`~$J@hI zRdcf+!lBz{x|qR!t9mSb2Kzp~ETsqEGrO|?HGJvyh(nt2q@mTfKfn>kczG&tZ-yu3 zD{S@r07n{jkF(ZkgiZFIZ2S_-;s}rJWl5ucIo^0-^8#Ky^X(TIc*W4?y0>7@q|JMY z`saN$qvKh4Lj)_EsGkXkGaC=W`Coss6p8d#yLh|d9WNBR5%u}@ZD*4Ki4w)j(~!KP6+#u4qut&tO>b7A8Y-RpYr^ZkcT z3*lUjo{b?qdB|>SK78&`pCVIO=id8;!(pZ1!pD|y)TMomelR_4DT`>&{8AX*J>ehk zCVV5j`rw;dCwT4Q3C8xY{+`@iQ`pjYXov$mPAR~q2P|_YWRfGie&?$Dbl70?j|!r_ zJfWCAj|zwCQ6KezWtM(*e?J=A<5AkBA3XKT*)I>^$f7d0{;2KHBV(Xy!xW`c@EqVxuZ`xJapK4M;CbAlgfU9@VI%@{a&y}k4Fc3 z!*10}Dh9w&0dvl&!JBpzuO!X~+jaXHZQ`>yhu!q3Lt$;F@Au}wT?a>A9RbJBIWr;> zu3Vz7oeZy~RX(zZ*WRGtPJ-PZ96V|a_nNYPcRoC_sPkGiSkv`f01ckDK<~1*TciIztM z1Af_AIugH_c8=EKU!pWSM_WSnmZD#Fj&|al*6dE@jC?Em+3cY`o<9~27%U|%#n!u= z$?4nK0vGngMF89naMV?+!%)K>=lGQ6 zCbYh`am2OFikY3xluh}SllpyD;BI(N;!ul7EO{l68b3j3!d zYM+JWnm#G~9-I9JUdr8NybFY_3FaE;DNr@gQ~ z%|B!3B-GU#D?4n3Gk^F)9KbK3$H-hh; zE-BwKp2e{$b_iv{EAMo&b%AxA_l|RcQZ@PSYEr%&ku=T?0bM#HPu zou(`=U~x_y=<_le_B_`mbwNJb_NF6NGV5&buCN_-p+Xwc`&2RyrRU*V$R+$ycmfV! zUcNP^x%rluj>|X0G$-E>)9idbOvmOkFwM&EfoW#GCZ-wrYM7?y(=koUC%zP(nonHu zrsNaXyu!S3mCYXlj^~RK&M{MngJO-x2^N6ht z%j=G5XkIr=hvjKt8j^>rcgoN_RZN5Pi0vPer-W%xo+74!dBnZ`fIJ$e{yCWVWnwZo z6%$`#KYWCkcndJ`O2)*K*ej1jOx%emx)HnQ8i$Dsv5U?Tm~cj6!Y1~WH4>BlTuk~6 z!=&#JO!@?1;^dEsqaP*?KA6}K!o+R>CQKp-wnPkUIG9+nFtO-^iMazNW=u>>Z7?w* z0%1%XH6v3@42g&s5Qncf5f%MjnDindqt_FYo(xQMv@z+Sg^6|-OtgrYbR$C2mH69! zn#!1H5b@}ufQdSBWYvfVAv#MDDGGEq3jGcG{Fmv6=o{!W=vnj;m_`1O5g;Q#Mu3a} z838f^WCX|vkP#pwKt_O!02u)?0xct;p+K{eib_n5;3q_L!-aUdYE%lo3Ql#@lBYRJ zMMUBIRf*v-iD^l{C3cphnNZ`xV{ya3c@Y^+S(=TM=re@}|W?m(kyNMT$sML?OOBt_GbisYx_n<&^K6)H`q zc`Y!}|Cf?_Nl`qB3Hd`t;J-Nns~&v<|d0&!d(*Xt!2SmBV)D^j$+(2w1VvoLSGX8zEWm`E_`jMkMVH$ zLHDnwYVg9>lcNapulCv`1AkK-w~Dak{8d(;a#);OlP}&PEaR!M?FrmLYe%;caMm>! zo9pmSjq+f^H)9W1oPrBdq-PTz#m;ir2S(8U+zS*L*ci9 z+Zy%ZvY;}SF?==0mts7Y#o4d5f-MK{JTa;0>oL^E$C)fnN7)PPcX)n8 ziQ7o2$zX9Bx1L}TZcGfIc1&k+y0OFAYv7G<-u--!Yjcaha*>;#*lBvUSoAEBjp$h- zrlSqBT6bULtY*cET5*0X7!*=D!)n`W5Rh@Tp= z5kEC#E9(Et_M|9|#Dx4IBS1!gi~tz{G6IqaOzA64r_3JNm8Ndc>MJun%8e;rqgb3f ziFx{7QFz!(1eJHmBsm zI$spn>tM6SM9SDm7Ki0r%I=+w)BNbr#w=JNXAR31Hc4Bsr4YXLaN>Li_;%^(H`C$y zN2GW4gZ0MLSuTXhBi>b0qvVepDG-6}4_MTh#7gu~NTssA2BICXl5Wm91Lz8T-G;cj93_ZP!DbBFZr2N&I) ze0vUj_m)GB2b|BQH!g(Tw+`6v2j2>u!&nAiP+Ct3gC81Zuvf#?{cWj)6Lwd!OJSo3 zXHohKf6LsBuvF0uk@=smRPTWEt}bp2MgHlX1~=}34HjyO(rt4;vJb&?zek9y6|*Gw zBz!I}UX=gqVYLJ2;P2l*iPE=aK5M)R8(pgu*>Yt%`wl#A!G2NxRL<*hk6}ibj3|Ba z%Q@ArU?;t4qV3&V%5Q9dD@$@j`G@9jX8(Xc%&!soW>R3TG)nlF`R=0p-mON{@*cQ*8*(xKu7$e(c!? z6ifJHKf8hwMZ<^oWEe`OPUDc{Fu#7KCam*+bRVXy7C^L^mKGZt~C!>i`?{u}@sUGG#kAHJZsGcE*vAlyG} z1-xh7ITA(zrcw#<>h(s z-h);5Wzf=OhIGmv5C2RGaqI;9zh3J-8D4w5vZx2__)Mio5o}eVcF!2DnwR=&2HeLn z$<7`g)o^S79Qcyo$tf=IZJ7bZ3t&l0CTYP$m)#Dy?6$VIwk0mMnc}DNnU-wnZDGo@ zSD3JcwZ*Y-4)mu<_hhT>$#K6sYLoc-+ChAM?KoMRMyJfA;HiASKWIMX-`;G@!$v57 zsJ`hfz(zMUqrZRSkd5Z^7Vo`taEqK_V@jfj!G$$foCae_pE+@-N5Wq^aP|&?i@H|Z zM8oFa@q^H7{9+uWFh3{U!6-@YJoi)7__VWC)oUlpkEANf4viy{%gwLOy zS+oN_)Mchs8ce5Z*Y1IjOk0;p*zVpHr9*JXO;tw-3yiLLorISi+N+!n7cGk@ItN#< z&kZ5`_Neyht8le)*fPS)tEv_6z%z`8KPH?k^z?cRJ8e#|%z&@om74kro?Y-Uo$!>d zt4}t-o7R3fK-gQ$Oz{W&jq9qI31@XzA1EzE^Va7?0AbUTos$*e(5S?BR4hPd~zI9t<7OA3kuzr-ZQEX!=Aqm{xi69^o^)HXikYLr2t_WW!m9B4h$# z%8F_M;gY3K-9q8{3mx_nR-V_@aIx8@Ok za;C^N9qyZa^ETlv5zYnUVA`Q$2IJt;lSdCtfPGIoB@w>)d=zauY&wa#i}37YU7Y8_ zQ>~WMa^dInFZqk%I#x$7!scmnYF5Gbq~+&|(ieD8%is$UFK-eKpL&wB4W5=UOrNM} zV;p#SyI^|4j6}lfuiOtDfQ>IER}!}AeS&fvmefqwt`Y1yIBetoMjq0?^!&-Hho=g% zDN%5pa|U}~3clqL=Sew@<|@5wf0kw{MjfOQDQvjJuoHU;JmZxK)fK1f`ge!fT4}^L zF27Qeak=7isvCPD{Neua#t>ZY9O)m*UYzdEX??jQYL4v1-zc(lAig|gXK63)_uDr~ zJQ!f-z^2dmxZt~~8GCg4+&<}vl{9HOTeY<0YH)s{xP7u0w@(g4|DQ@*Pr-lW4;cY6 z0{^KISP&@Fk+KQ>FiPI=R#y^|%a)pT_OiJcm0aDuOZ+5Uu9g<2vhR&+-4UzA9!i#w zToPbzkRmwRkKz6ws;Han*u zJW|~ub`yNkvo6*duIXc~ybE3*^ghG`KDlefrbF=OON|4(;57Oj-&%O=G-W$KICIU# zw^!j#ayGq&z&W$}&%O^!e`I$Kg{#^RCuSjK(rmCtZ~VCkz!*np*-NyB$J zgZ4kX!H0bgjMGwqne!S>Ccr^V{yt4O;Fb0+0sOcwc(@)sqBo^99o9OqZi>mOFMjqug+KL>S% z-RPl;H(|A`XGaa-l9;a%gck%Dx!S=4yM5hFSmyeW1FmrIij$q|;VUnsoC4qurTyXv z5B#vJA{?IPXn2^gV)hg3B-nWWx9+##{i&BX=D-i0EKea!4c0Izf`xIVrwJR~b6>j% zmcKq$|2CZVCxj?eqYaUYBx<@G17esqf$}ocDHj;lkeU<*4}X^xX8b#e}~(ZYre1XEz^xOxX4D zvPNxKQsbjj6MPI|BahYP#OLeUjP+o``>SRp5ble8eW~F3&z7OZK z6kS)s0a|_gyn=<@Vsw|lt5$Bz`V5b{!IN45_c?HTw^S^P^J<3ey;<;dT8|$}@Hlqn z!6MjJYEC~5IG{d##U%K4q}RBfu$|`GyaIT)*}Q5K_z86_HwQkmsw2e?-kVv<%7Ax7 zzp&^FU*_D{o(6BXSYSwe*T44D{!}5nbb*)75IB5in8O&@Y5DFh5%B8Koxa7xk8h0a zm;_HYH{3fK9`ra>J{zuKeaeo8UmRTbb23a@I*v`WXF(@VI2FMZI>(Q2*pasti{Ogc z0V@axONH8QfYqbsy(O%B(s^?g95FtN!-M1Zl$#!d2M;(flW>sE<#m_fsT(ifB`jEV zqW1&%Tny7H3a;*Q`fUTebKeiMQE;Hgsb|Ey9N0m7caDT*{q!DohD&4Li#+8A#l0uY zoW%Df($jkcKR1WFZ{2W&OXN4EUg!(!<@<37$I6^g9sp~;4(mslUOV{_@h*!#8;^7& zeDQqgK;qpP10Q9H@=Jfcaxewft#U0If%F|gdlS>(dDpvbCA|Hq`n(Ld@_mmfgum}A zy_*9Eby)srI9yymvu+No+A*O%3|7ePBbq&;5w>#%F;q^5K#Np|&^SpbtkEm%@+g@5dg8AEl1ER|PM4 zwteDZ_@&PE;pgGGsul%1;8ISfhaX^<0(*nCuyO4Z6&<`g(kP=1e)dU)x^#jd)r|4@a=G`xFec12$Y z1K5zU|9)@SyU)&p#_+4NQ?+^UxPWsp#K2hA-7Ozx5Wcy9Pk573c87zos!YQjQ&`nH z>)8$X=OOK``Y>HH``bPE=#_iBd%&TQU!T5)PwxzSp#i6Ti+)POyK&y=kJ$GFS5DI( zsvnnzotCKGJ_b7v*SXgTrmienR0eCz?>4&^oO9B>NO4ix>N7A5#H4!xiYI7J)p}4II{!AX))5TtXup>7w`JHw8hwhaN(pq&kNy@ z5Nn00@WpL6cCUe7vn(gif^!?F*Uj;6A-~gJE0(~agVzln2EQ!(+?!Z_z%jdVDA-UzQATn>{N+?+|aNI_jPfIJ}`GTyq=wJNQ1TXAHIJB zKbl#1MHLo4JvT!E_3_KQekIXxf#;8O;(E5@>no1uVVf~CRuT1gO`h*dTinkt+-tPc z7B0F`c1HnM*464K77*@wS4pT#2t3nX-Xs&qY0V zNAdl8rZw?~4?9bnreC5VMJuN0wI57dq62SlBfgE#v=_Z6gemIWGnsax*M%@0#65m% z(Nh3SOY!2i#7rhrG}F$?&XV1IWwmg#&nI^3%@)IKR{ZY64&+5>h$ z2FBmab=YI_TKAubS1VZE4x_ZpDOr;K=^=XzZe(Rzc{Dm=O_VYH*~E>lOME$uKIiB(i_Ix;y^%mZ`etF|6m{YfzMYM-H$sI+0Y`TEG5SD*8sZk&4?6F5! zMEO@Hi zkQKtq);m_=vbO2)yxX3}mcfC%{w6?lSbiT;CtkcES>nu`lt<<`P$R!&j68!id^TpgoE5ev7=AJ$d)A;*G)&j0db40tyn|ylOsA~n_92$v z@?`W2I9N-@#ux4`J*u^e9m{9kUn;l^_xE=ip#hH%U2w``173oM^{T;6 zesu@@VYl@LONjlo65M)x0uEfnqdCLc=lbT}h0h!Gpbv&mh9}g>l;Of$pjj~tt~ncY zVgzhgV6?s;{7S3TXf9mNDYdkROCLPmbPFE1U+1y|ETi=^CbS$U?kzWN+QP>l$g&>7 z^E96C><%e`_GLf;*&)?qQiy*8iwln!`&K>ydMak zKE3UA0K6&hDl-sfSdA|Xf!$B%WDSRJE_|%Pg}-Rc-4g{5@Dr3p!mF0)QIg=8~Od}id>RdSnf%fe3kC*i{W_jS6!Y3Fw)CBl8CSkLPPAMH2t0pZog z(sUbmk6P@|BzT}g;CK#vKT+c}VY+j4qc1F}L6K`mQ>swWFR{C@BakjF6V%8>LqOYp zgk=qvD`r#A!n^gmu)m_bo!2&^tlY-pto!ziy%AQLznkKKhNq`f1=aZd3br^y5raSAkITaA^E|M%E$sxOINY4zcVce!(pI)JeYL1(_hQqy-?^nlMrMBH|nf}sN6JVgcg=D^&Q0(jEB ztJj`l`7Lo~GsGv2WTu9~ZCc^_7f?%cQt4!LedS1BS&*oVAXa9PFIy{_;V>msEY za6zb=eGU9cisrupj?CUcRhW(vP<4CJQus$$ey~2Qn!IWEWLUr8+*l{r^wIVoxiEdU zVpuf1bLDm0!SJ(<)759eWheRL9O3)YuT@vWTNf=@WCf4ywQbWwxPM<86Jz+U!nc)f z#kgs=c-t)_SniX+F`_Eo6BWEQYnu%SfldtJ91#F2FbK zMo&+L>#oV)Q<;q##T>Vg=-N+z?c(eT$GuN^>H+iiPg*<{-gkSJ$4i{rpR)EGS_c=X zdsOU$pT|4xybWKTmBpO}Cte>M^B$g>pT8^xe#hMV{R^B-J77X|g=CkWc&$ALUGcpq zbMK<0M_v%_=mWD$A6XO6SWN5}(QpkO!B$&Kbaf(DY%^1ti`RJ8EWJQ=oS$V=_hW%wyVFi7%z?L>I`8mX_?! z-3HIRVDpfjxz8ni=B8qrv^l%inQdmD>&J?l1uOBju=V6IvK=WiMMLZwt=a|21^txD zbfqDvostXt56-WnX0>S-h&OymmXPH1yZLnRBXkkYY&+X+0*<5FgXi1i@JR8r;@Bl~ z;i0piZHqU^k{n4qf~=lXTVe*EE8YL&7W}c)Rkts!x5DI&6CPQp%BeP;6vy9C1TFUv5(;3H{=N{M=(vuW>? z{qR1IHFf-pB&c5Tl`7e9d=mLr?DekZ*Z#PD#8N|gXQ5f6*`;tz?Gj$%)7!)4)O}u;pl#k z{21`S)q8L1Pr{XhlBJG1+&DwS&=9_SFx!P#{*;RLK6Q9)z4JIC-d!%{g!X`M-y87u z8eIKA|D`3oX}^Z@bvUvw<;e`V6+Z`21X;R{cgro{Re)^uLD9NsdhQC1h; zI5MJk9Xz!Ad2I{WTQ)gwH~i2)Wt}4|^jzM-3@yXXX)~Yt!q)?J*1E%?!zXm}h9xbB zdc*1!_HgU#22s0a+0?Gth&%qaO@jjBu6|P=pD7*&uxo1N#5pCy2jVn5pVVn`U*|Pj zZn86aPgYgimqn8{X6vP=-?i3k>P%UR+dM1L^Z#-M6#223kUwMu$Ow=TAR|CVfQ-QZ ztqA0$D$*(W>~;pGL-KT}4BUpeNjph7fNOufxqaC+i_xb)y@t(S)_P!C78~3;r|imy zlo436$AMR@hgi~U=TM3RdLeOLp0U!GU_9^Vu|_j|g3*wZEXGpwZks;w#K4j4=kPJN zVU&ENOFogfN?|r#r8wG&u00&Zwn!#L)m$u88zZ^Nuoztmw zUgO%?t@oUGLvx#)9ytdMu%zmb?=_>bHVow^8=N{b`d-Z@|wN>|XE==JZJMu&`dOF5=vcuFh;SMnu9xZ`K zmZz?7ME>pu^LrnIpV{|IZh$QY)IB>1TP&W#-2-df37V)}j9XAEWYZSFPnC*3d%?QW z;bkM?vZ}EIMDSvZXGx}96mf` zUvI(-O#JUFz?}`Bj?ID3?fm9myaL}%>1|j`*mZGN`FYFn=Q!@l4HCjXXWx;shRgR9 zjN!vWx{oP(zYI5XBF@Ms!HZW-e{~SvoPH-h7Y^$j$S#0W9v$>M3Qt&mhm!}dIM~bd z3M~Ee;X)yizqn-9b$E@l?hQ7q;Wj2qYbKVjmw9?`DQ@v!Z8&8Qr+l5+e+2x#(NxnD z?)k8b)U_peg}eK;NPqap&o@JChG6)6zuMJv7vmQ0yJe+;uy&GhPklIwJ>^Cc ztbbLd@XjK9WxIAt*aZ0IhNt6a!s$zn@Mpn3@AOnzCFq}ohUssDBhSd)eKsHc6OEWH zhhgv0Qkr~tM%Kh$=U~bn*N?&Qu(iyRT6nRBOeF)J$4bk-4S!Vkx_bfRF6Y8}K7I@v zm!4mB2;(;2F3efhZ5H~6frDxiV9LM&3moB~&Y4%7;2D<~*M`77r_*T~@S(0>Vu!;Y zb!zLjV%%t{{^4S0xGQJ(p4qTsSy3Ddo*ZrEElod<7G`t-#guHAh} z#~bcTA80ogmZ_MhX#?jBI_6Xe&&c6ET}6z09Wn5n3oqGo`TGc%F}(YUQh49ZUYRUd zi#|z>u#ffIDXOrlbmXBK@N2E?&d)LKHtE*K`(f~_4+|g{o>NbxcmHG;Gy%Tzp_&x{FA3|z=D}+g&R;Bq6$jR->cN#;K4wgT zHQ94I6wE|@x?wb90(|58nL+Zf(e}q0#P+=T%6fSL3+~G9It3roT#>#8 zwyV)MUkl4mdEb8{d|$P&Aq@U_#jpGtoH{b*?GSj!w`}0$XKiH-(PG|s zT(;W?F0d}YZ3-)ootvc#>k4E>S;1!e6K-q3!eaRVJJ{-7wX-tpRQKMs4;)l+6Kydb zInyz)W5b_~T$Mhfh1Q92>FoydJ}*ps34dK>p*axV`zZ6mJ@`U|t%5JSxHQo5D!h!F z`z-*@D|J|M1|D^6{EMM*_>r~LLvZZ&RX2yj^Vu@IDtN}MnYE+f7uR1N-v|eOoL?Oc zJ71Y@whHd9P_Z=*mM_Shxd5K6wr}Ma*u(p6LlJDf`sS=ucy!8;kOEl7;B;XYynm1T zzI52v{9;NjeB!)?UIP3$Q7>jZywO;1Vg&rLhiS+Zn7w?(^P#Ybzqe;G{935u>jS5x z_wO?Yc8r$Y?gG2(4KQ5@H|(jgwBCiAF$eMSoa?=_no#wY70z_e^#7m^)x8 zyP(i@xZZe8nzS?9bjZb55gGU^w)m?dw&LgiZAH)jOG!#m=|zk^uk2ua9^PHHLjNe7Jbp^vQ+Ue)4b8s! zIJeZU(otHAYrVzh%V)!H-cw9eVRv7@j<;ae_S+YGz~eWLS5wE>?Y;DwwRZ6LZ7HQL z@TSXOr;diFnq2c54a?3tx4aB~e($hMDeO0}qg=OjxJTpNfpZi7W;fM?4?oyiZk8g*||x_4#US6H;j#jFGg8wJcrkFD=X6AtJK%awJ;W3Vt&@F5bi7%u9%t<4ITX50i+V%V3_z4H;4X}S<*z?nHkwe57WJyEG4^ZCw6?@oO>IQa#@O13o|Lh*vnJYjTN}}PUu^Nj47+o~_f>1dL)bGT z{H)C*jijX`*;+$iMV3sSDDGfciw1X@HggzCc#s4gtj?|Ll%!wRXMNF@3e*fq|4ve4 zet%MC0qT@w2}#b^KADu==ozYUPqU_>FKXKHPNNojmP6NBuvC{5e_m|^r8_(z=rJo8 zw%2<>nFiNQpt8&0@->Q-5;#HKkZrUAk1Tb2)OZLUUY^O$hKD@J7uoBjCcDQ0VzojP(@WY*=z}0@9D2*wNi~70T4M z3d$t-fkzI@4jy%8Gi4rZTN}Ze0{48=UG&PI{-sA*8{wPNS6?CSzjd};_k0sPnHha- z2TTu>uiXaM9^F=10ee*CZmxj4xJ+DE1z&Jgov|C98Z%(QZn)BCO!9ts!<*14d*O1X zRX`1VH+N0uet1{pdfTJ0jXrNQ(Z}EKrJ!*VmS)(8*1(^ZFZ^@{9%wOc&|%nVlj^l| za5d*_|D*7+GnCzz;Iaor=Evb#lSVGS27BHbu5%K;xcSZ4df52ID*9>oU2)8ayYM09 zk&S2IXNJew58;OHF6-;yq@Dc}AHjLm5v+?alijW2DJ*s7(B-SJx3~U}7jP%NRk8K( z_{ry7-oQ6rQ)&0$>s2?WyoVQdWGs9HH%zQL_Yt;ToNf6WUU==0=2zHg%&;S`iToi+ z+#m3jJr!Xe;0mtpj{D6JZB5dVg^C~$VDcs+r_U)L} z0aniouOMFG6!t-@Mj76;G}B8FcJTTvuL>vhzHpxoAL%VUSPfqBc|*Dye9U6&JPmjv zOR8g6*w1udeK$DwhGwZYeEs;sUOnLay9xbt;kl33#P))y zLc*6l*i&ubksCgC83YfQ$bV=D-#q`Y(g&Wk^LRhvezKBpQHVdh=A`Aqez4Drvadlf zZKT^fHavX!=p`Yr%mrO4 znTL=ejfl!TL_M2C8HysIQmM?DheA@6sE8&*WGs;(m69QZQpijxLrJ7aks{Ci?8ouC z_uIXH-22bH_w{?7UgsR=-CBD;kG0oc^IjXaT^ql~0e)EgULll>@8o{g84gk$Y7T>? zy`$c{!XLakz0ScUoL%cZ;r6^5p(uEjx6}x1ea{vnH~v8Jlav%2G4}Z$wtFJRPF@7hQ}>j?_Pqp z3aV^<2VV|)y6!4`+y0AiAG|j+vLY5<eX`E~ z3oK^fYn2QSJPy_S28$C!Un<<3w}NjRKB!2w&w%%g1iqVu3xlFYZ^4%ZR5PdHr}Y+o zS#aWbs}}`r!rhFwDC8D$v78sE%Z)&wIYS^MejHA?67u3#{{{) zB9Enc4=3FAA-DAjywI92oCo%lF1cF;9}{FRM}~8Xoii9PkacTz%3&9!?g@w3>i>M=ts)!CQh{Ri|J+ zN$Ye~_-W}3KLss=2|?#Z4cKR7;}|pCVSZ^w3x38{R?i0W{q~Y0U}o2Icer4)l>56j zk}i!5=ZB|razgds_aCesgkk&Ok^C*N`}iI`aail&r*}rM=g8$HUcJm2@A8M%dFb(W%tEli6{R@8-G%~R4fJXcS0 zhBFsle!iUy7dNPLg>NqxxoH8vkKFys11^%>5lU{Mm}+!-r4Q^k+InC=TsC-Z`%zd{ z;YJGSR}!zv1L2;Jt1DgMf-7rxUx4RYVn41F@9BsC?!(aAh~&B_1r4%^s7`-G06%8O z;Ab0*>h%6Unvco^NBx(sKAU(VYDfR7AI_@G%vOZt@Et3+jOp1X;_R9y6-9FXpNVlD zg`D}pBKjWNF)7Jr7Gq92;{ zkm_FZcaNQXTjnDFuxmX3L(HIG4J#JF$ydj7CUNBA-B>mS&%qmS9u%4e@f2(Abm?Z$ZH#Hq15z;w0lACJ<=D}o;|<`S9>vg?0_{l zA8jJNQt*|V1$>#dkIDs~JGR`-8qRXDzC-%*`AioZ_%X3Sj2jlZxXIZTj{os3jP&-B zSSLI9s?FyK((A8EI5@$V+&T}Fzu)nyK|5D?QnIX>boj&dcAoH)utIHK_=;u4L0{Oz z>`)%*4?}xw{o$(&2I72hLt6fUAb9GKb_D4j4%L09VEHdOlcX0hr0hKd*9r65^TR<~ z%=SjW-;c7kk-qNNVI2v-{o}b&0B%;&wTgj@KU~fyE!ACZc@=)C6R}tj7HKuGh=^m+SDI9g4vo%E@u4coI|x5U;>5d7FxXAPM@x~U5OU*Q^BLpqs1 zkI1jo8-r`FZePp}I~?85Hvv0EEWXAHH{^%D{RKaLv5cDq-Xe4$^ACLb!{`}CxKHV{ z7eh5#i(AHLXkJupo4^AE3(S~3EHeWep8B?4Wyco43rCyQ_w~Z;O5s}t;KU3T%`Q0B^z~99`0J0- zs5V%(?8Sr#yhG{dr&q8oo7f96c)WkD_6s<{!8vI$e2~*2whAujF7{jse{ixLErU(w zztdjG@1Ik0Wir^?mvM{mn*neeGQudm9( zdAly(Oof{=WSx~@mAppUb$IN40HF%^J=3Ci!P?*FY3srsi{>#f!xcUfgdu$Cjtk|Q z2OeM?K1q<_+0Qg+E->qY7Qzx{kCmouhi@ooQ3K#p&HE{%?ig2DRZhJFhi=lMRKu|& zvD9IBNs}~9AC9u`rOKj2jLo=3`;HQ`=|v+!z;h+&T!~4q{mGrkQ1vU&cOg?&qpuFc z4{{1U!fYc!O-YfU9JQy$-l`#zCeACdU*F8a+(-y7 zr=HEtSuxu>nBBchMI(flo11bPWjw#mDJn8B3 zK}W9a?;@UjC-EjNd)gm713DM8quPVg0x#R&PsHNLD`jG|X83b>J9S+l24GI?rOd#A z``=QX;M6lZG#h+U*z-Q>ZTMLX3*{OdYRadr`w&SwwSjKKtP9lV@X0ba%55CK`l%b0 zc!VPT`!pp1ZnUi?Cg4HZCt4;vzLt-0DZ-%Qi_sZ#xHRb}H67MF*h900_oVKm7Qp2p zz6?rmO3A0PX}H zQv<*MT#D1PhzdD~D6dXt2>POUWwqHRp&ET{yt2Byx}1Wls)CxLhK2%}Clvqrdj&;; zpQ|;+-(m%k*jxK)d0RaTa~-jywz?|CZrN;}P?;T6RE;p?;-0PdUH-}wIeH=#d$Nl$ z?#n0HKjT3#HJ^RK za(Lom_peZRiQCVdmGJ6=Kc^z#+h-DX$iSf|cm25t^Ilx_Qx-N_=SsT@i%vGguYqrF zXrPcOHEOb#ssI=6^Pzi7Qvjg^KTKId_gBldu0((CS*kv4viKT3e!E2t)exTCzJ?zET+%SH4Gwn}qK97_aVK_= z;l---@JqXM2@9B6*oJ;(1?FR4ba-8RAU*y)<7C1Hem1p&Zl#wKR9iT9`V`&LF1Can z%ort0_u;1xsZOx|>Miv1t3Dp5y21)AuJrTgj%HFlVQSAgdVJ0&AE~}@im4LaPoHU0 z{oymCEOduvL=r*p^wOpD^EIt|Nt}Xh_v+Kn_lENXaR$zz9iYd5cC46)fV1b#r<-jF zFA)iQeVPqlHyA*~z#WzAzQ-dyEnn(~U4oDGtoVKfcKE}1>nglT$@B+#ec4LB)WpFq zLjmLGV4bjzf$Q+LgEBwPz_peKS(D+3=)LvF;n(`dx)mT2G4d03|FMkRDTDOy?1xxd3_cp*+7}#di zVC@@qlxA4CPLrkx%WZfh*#?XKIYJ>PMk)u=h)#I4Hk0CqiIb1!Qp7aO?|zvwAFl9U zMJS^L+1v1#GKz_$yS8_z!7vL$5f<=+V>@Y)@B~*Ru@c_H z8A6eS@48n|*~icbEHubuSYM<}e3 zLcW)P&1Hm2exGq~FZ!U}chPzD?>AS1YTHTDO)t}DA9qw%r|8FT)vTOY_-BAyw%6|1)sh zqTru@|NNT*|E9pdDe!L!{F?&*rog``@NWwIn*#r)z`rT*|3wO%Hs_sP9?JdqmMuMH z&Xjt43?19Qq8)O<9-ez;zPQw3LjA)o;%wXBqfX@V(6s2)_xYXvd(3BrdP6<(2V zLkWlVDOZR~u)&KiN(NjVUroG(HJ4j3RKODW(1HfMw#MIrRsc7BN~bo!`Hqqd}I7JJ^Zq@Z0cZ3?T^bE<8!xh1sT@=*Qm^>7jmujlxYR z)i|F(-8(|238QW5RPt=>dUg+v%b}0`^^XpVolybG}-ccm*#PX`?mc`=2UFBj&ea@-K8gr55Rv?b}2Zhpkf% z&@5pV%U^^B+%%+1`-vR5@WU0t5pMKQW*CCc$yidW;GWfMDN7OGX7U2D06Ee@=osCb z-waYW!tsq>^c-yQyn*Tu&*kX3988V^sFDxeVcb?(Lz&zSKv|9cP=K=P?D(x3ea{nR zg?|S66;%n57d4cLm8Xg1jAM&bS_l^AVM0zzw509Wc6$F`b@mxN)!FC&DeEbWtMQ-z z(?2)(__-;i&Bi5A~j*h-<7=`c)PQn>p%d?7-Z@PGy1)KZh-jH3C;q{ocx zyhy85>*Oiga zpN%P=I?k+t<6E-Y-oa0-b$oc>B!)*Leeee9up?sdVCOs50XSAv;IK3tw&SbBXIL$z zevdM&>+wfz1pd@`VFv+km9E+L9p+5kX=DUnPGGhB34b}mp}Py#&lWy01>+Kl#hdW) zQ0`0qGv&|5$SNwTZ$e?J$CikdY1tbRq_9K#KNo&<`6V6$T=2Qe`JW4l8*#R|BUW&3 zU*wON;XXzIPjH4sV&`>^!Q8xq3mK)c2*0TAi3hxYX$7AjtYO_P8NydPJ)hdV3U}?@x8HWcss|-FWM!$^D_(k! z8N<(V6&f7j7{R9kTjA7%R}z2ZFvV9kDr*n(RXl2&Sd9&3giScEz$Ncjdo!&;y|c}; zCmP-?F|QVjb#q7 z&Biu~EZASWWyTX8o3bME;2lN{2Ls_%8SjQk@AST08UZ_Il#b=YMz;@&#*y(a2u|LG zJ9fPHz6DG8a?uLluYQ>g58y_tZ_M}Mz%+Z=D!58Ul;Z&`X5@Oh8Q$|_4PPOA-QiAm zFMOn&SPQHPC=Zz4+JuT4igG+<-hrX13|Ro^D`;1>#;s>E?DgbJz2t_pl@HLc0&J;lQ3(AK@E(Cqlo$cQ?NII0!Fb z7)+gk<6JEN48z@BCKU_hv7q~42mdH6l2OL46vHJS4R zuHVS0?FoC9)tZuC$*S@=7+x-JF;0H|0%irZD7eB&_8RHpwaH z^M5LRHwNeT3*UYW2MHcONt&6xeR(bXXPwjv(ics)-spfCw|{!}4K8`XDLMcP?~idJ zojzQBbsUzk3FW4nc*f7HK-K0l-+z}(pIbQ$7x-X1ouB5Uqjxd0FM)ZNwoZ<~{i4*6 zweZJD=LFK>q8lk2;0+Zi)zD}3&p!*4@a#$le@XPCBm_}(s<-|NZ5 z0KALzSAStToY{J0X*0}KS=Epad+YqU-2e;On1`3b0w%=@FX1&}QhG1o z_0zW>K7*rr6eu0As=*GT3Lf!hFYbbi9pm~c;2%>d(;s0g`-kP^{pNo>ZY5 z@_zp9WfC|A@7(bAL;;-lXLT8iA}UZp*?l==d=^dy9@tj(C^ZxQvM#Ad6lOVCd-xX2 z-=Mp075wd@;r^Sjso&Hkb$CbiAIl8bi%E7w58itB+77aO>rXsrWDawr-Zn~uQ)W0b zY~hv@YxPrM30s2V0e>;5BU0eHyP@)d@atpp!lWm=ZsbM4=WKcUlVRbE$~AFt$i}QZ z(mlOxS-0R9!a~PM>+IK*ErdUQ=-Wd2Q+NBVD!4Dhej({z!Q(P5u!GH^KC(Q1kr$KE z2e0OA%_U6@^jPr?&d)y=K>CYoY$^?&FVWWn4<=MZBF6B+bBJl`ICU9ZKm< zf^%;EmXL+z`qt!--V=Nm!jbK^_-hXtDU64dFKhBGVU{z>Q0Bpv54#mW!IgyedYmNU)`TLORCTcJ&Q zK|~XaJZvMgnVYn5%)F2du<`KBYqGv{c;wGy4Ex^f&mis6AsuWDpEE!0NqTqYUYY~k z@S92{9e(Pfyc^tGu;?XuJ_)_!Qh~5^^KN$X`b?fwlnjRhNc|J&{bI1RR`10 zcmT%i3{M*Te(2>>GQYo%{mGgS%gL=`yaR_XTTCg1+f|e3^>GRJhoBeme5DjBSsxqC zpQLrd9|D@`X1r|{)C<2@=1IASa8BooWuIY%41Y?&u+L*Ky<|`c;e`TXh(R-qOQ;; z_`B0GT|bzY7CJ!IkE)!TCqiNK8hrz@etep*c`F8%cVCdz2@jvhuuO)deiw3d!Fs3I zICJ4rr!3cZWH`%IX)&x-D$&b@2vRtt{~NGg)`r6y;{(hhjXuYpVb@JAQVmVS8m zaNams{;r$s4E+uVl^(Ddf@QM`RT)%qe%C*b$okV{Y{h3TID#G?V(Vz~|54743H}@6<&VyGTN~~~(e^6&!;`GRpjxfI`E9u!USeD zln+N^>#Sk77Oi9fcx>^76}#bxjAI{`!uG80zI$Mnc>R?M@Nn14n!RvV#Y=lCtdjXd zXdg^jbT-o%cC`s`*bfWFw|%jOYs6z79e`KXE?Vmd>y|0AAB44Jh1~t%B#W=shu|wJ z2k)PTjmB8B55o;#Zcbc+^<1;3?O@^dRP|Ii;Jy7;azl?md)H(6aI>LQq9aVnRw^xn zJMYSkIKe^UYbo{cE5>2M1$GdU6Ksc%##ApRHvriY(rWe|KIO2VMs6_TbYkYp2z+J6 zs@oS%I@{kq1uxZHS>_L~NM5jLK007RYnLPi!C!TAEd}7diqhk!NH3{~lY}2}M(jHS z*Yv*cl7p!{VMGMnoKq>T0WT}sDH#brJKJZo2`&{ir^LW>J*BxG5_ePVYlob!G8?d5PHrDs7aoYBP) zB?HG*jX&;&P4y;x*1${SJDrB$4|2oSN^rG|ocK6Q#PV!ZhZ(Lst)syo10-bD!>YY0 zf$ZzBvanr{O9$3?Ke9#;7J8^Tt_K@!)_J!WE@c*bV+ebS8eUihn>O)Rn7~|2OE;*% zAH_Itn!~b5b|X|c;EVDFD_C3fdy)ZcprYqx1E2AGy3HK!`c7CLgk{2*8TP@g>W+j1 z99I9X+7^CM)VtRaekHYmaD!u~nRt)DZ!MP>1;CG|HdMI7EESWAXJMCg18;BmBcoaN zWmuZSRq80bR9tms3Y;vT-4Ot@I?qeWfmhQWMVx>=F6|M140A;EtPg=Fu4qQrz%eb0 zN6x^>FR`o@mVLH1B?7+MWfjsx9)EO~StOjjbNQcP_{?hu))=_#{j|?7cqb+M;T72U zjo~PZCepvH%`pyMqQ&dL2jAH}B%TNxT3%Y*w;=jrT$l^(A)yhnx)Ha&KLx0(1% z7s6Yw3aa?PcQ4<}c?|n6?any~3&(!5DT9~3z9JO~+ic?CuY~ z30nmi5}h!AtockA{Q9z)HEF&r{a&Bpw0B|MbcbXQ{vyrpd+;^FJsepM%-2H8+w#W% z>F_s~I~KxUB4nN4z};CwO(ioq3zJbl}v9wn@@%&b;|1 zaKLzN;9FR)%}K@vP8QzE@DASf{7;e#948SGN?L4FjX(fAGw&pOHyq%$^n5sME-?QB z>4@d`nPOoc=FNOPaN52gzngG~ z`~j}5`Lsq9HeFuukaV){{Ztv)`|kbq{qTmFrbX&-_AcRa(q3yaqxE1FTJ)xmFweKs z^UdM!r=L9|{rcmjAkxRoKL{*^Y58|f*tuR`!2w{ zJceJB9&gUIxDFqB$z?MLo3r!0%7TCB-|8dHT6I&e2)0a}Iy?k3G5J1z2DiMMKTLZ5 zRJ&3;d_*_h^)npr<953r&O1Bxopb`tMDho0SElJZ44XXIe}!=a?yoQY)1<2$HMw}< zldGA7zre@ti-jzPYmM}ozrr^>9{gSntGT3vk*-Ld@1X?;Ul8OPfq&l_8Zdy9u3BFt zEiM`^%5Vk)YLX5*#^uJh+z?*oo*?z(sK6*M%!SDC3;GTk?9@Pk7 zr6T{?*9*{Kw)lVuet6$ckwr`xtQO3liC+x=BrY#zh0i}OyCVx5=@Bay!0Bc)#cD9o zaDEjx{I=J>ZX-Oxzd)WJwz5_4-Ui1BJX8^aQ-|)3Si@dfs+yv(hu`NJJ6NIPG_e@2 z3S&Fq1RH)ce@{9nOW>y;Y=7{r%Tjpod)?X7@b<`gzn8=QO4}qZ!R+^ZL#5zKEB(94 zF!!BC-c@kxvyzRuaN6$r>vHg_v8NryaKbVx>9ug-{UYaDxcuIOdkXN5ke{@6xIp9& zl^m=uQ!+f!2a9zn5~L4bl%afs=LYjl7l;JY;1r&%^zi$&v>8qU4_i&l(2q~J)qPwH zzV>Z`9{wuz6>Sy#^XFo^M_*~2&>;O;f$pq2S%%GUNbhd?@w^Xu+o5-r9evamwj?w9*v@!dPJR=~MG7CSn@3>E{LlJJJa7#45%rHxoUxs_RV<*QuM zJ7%1$Mc|9OpI$x*kJoe>{>Jss>Yv^d42v;)A0+cz&7$bz{_u}G!jD)51d^o|LQR)hx`L^~XSsqT(Ivj$TI^5!4 z!nGy~RqWt5X?q;1;8#oUFp|fcnntOY!W5H;21j^aLDoF7KJ(MK5$6n>7Y4QF!l7<~ zc1Pfov2m%l;B&SY72RM%i6b6l{i=}WLUV`JgTn~2KEA7JJ>dn<+^#3o6WH?Z4o%iu$>WvxP4KiryCw{IUjInux`J;8SmuMlE3lo2gw3 zw6V&0>iDT$u-$plC=ocl-AS0-dQT{Y<+&6rQg|bqY`4mSsDJ(fbYwOa|OYB%-Oc8zzn7Fi^E}t=M#zYa60i|&1KjiAn~g#+D`F z8%7Dkrs~F7B|2!&DgRI=+r#Nu9WC28VwLtZ-{2-#O5_EzjxOeMEZNw|&)3b~OmxA^ z&-b#BpXYXwd1vD$^pxJ*IHd%)*6=T2(?k8aTCJLNz&eD!y2LHV0ur7I109S&Re3m)2ZGkgi0( z`ZcG>{>hkS%;kMXXrC4SBzoY4r7o-5;AM(2DbGm9DGhEkM*d~Hzq=IfS+us3*ou*B zj^iohRN~DSeisvA@Au;_r(q8N(3mCL&|dPL_(Aq;H+O98JP%t$%lm1-mZk0+OW@9n zM1~yvHfj}zgbCXJp(QVu!0JZ1g*Nc`JzXANSl{euWB`2k7Mm0^96)q0FND|3@3~8k z+}?H&<9Y@A+H$C6!*`{o{F>oiLk9~ZVZPzB0yS`0eBg0k_)|{ojcQne{~*sE`0q(o zde2dLw&$oZ`|3Ew*`44tW?zY+G`mZj68+&Y6{UY(xi!13oD%jE67Q{GE^SO8*8Or@ zcigg@5EK=f6}6%s&5bLt($qvYFB5ztpex?=SQJ zJ_SM#FXX0#lPd=Pe(B=t@uM5>x}s6mp){@JjcHX^`CHwWP#IbzrJKj%Ckwtk1Nko_ z{<8xAUewC)YQ_&xAd`w*Fv3d@0^Vb@8x6<8~EEIIh0k^EiAh zd!d0MJhi6g5(~WKpfvM-*hFlP@-KYed$#S4_OOn?QJrDfZE@d*<1kzIds89A4_~*+ z@futZ@~b5k{%y6^xEemNeCyU8_*2m#<^fn_P;~4!%=*LI;MM{Lz6WtD3>Kc)2 zm|(B%Y<>%ywY^a{hR@ty;`mi@a@o%a586P1cXNJf#-6{q8C4_kH8@%QD?|! z`tQ?Lsb7YDhGRYr;{2@Qg3YC{-C?iI&*16LTi95TqnP71alL`(au~hZRGY0fmFNSf zs2b_7T2WG<@2aA%q)aUMc{1GlNDkq&_BMmXW+f)3enM>W63^9_fwNU3eIHdtMfGq; zK^{uXK5}Uc+h1#;LKLU%x32KmV=>auBU1iImKIsQL#x*>A<+!1pzk323K zcg7i(C5BXH;7BH|w@a_Ww!5D0egQ9fQUCBKTrxcPjRp@1@9veb!vNpib8E!Tp}~_N z-zEg}*+m~b0GBih%gV!hyX!MU;V(~*a=F4i0|g}+aD<^^OcK1W)hP8j{7RKIb-q0s z$Og?`eelcu{kF=mmg0=V6fD`PscjAK*AW$5bsh~W@53BPaQC~eK0{dj``3lf;Z2I; zOQYcM?4sj*4(JdpR`tk-seQZbSHX{ebnO|1rI%=3WQHB~Qq?3bpo6Tx$Wa=0AMXm* zg!5?V~hjSkyl!k_?aCal|7Mc77wBAO|yUy)JhHzJ9aB z+XVhru={8=tWtPC$Q)Llu6AsJ6Eoz*gW>oFP3a!k%C`C4HCXD0z3UVlQTDavCR}xx zh-W~9OqTcObRQfM`%r`hHnaG6Xblz+ei{37UJy?G?(kj@EV3P_XTXAKPVHD5 zSnIi6JbXTL+iM@Vm#MyueBF6Gt5n_-SW|pn3b_Dt&}-wmZg@h^;2|d#P-eF8+4Ku8 zmmzXR;M-hFLpaf3FX*+sw*!t2Dt)^erY+{aVFkMnub;Oc&eISv_J?UF)XLmptN5=+ z&cPXX-aiN@!xc_uTEl7AdBaQK6zjfA2jGURCmUbGYd4B8dcit@Dh=cSV{sTia~^D$ zGV+cbKuuGBATAT{w>*|LK-9k{xbIsXjd=`1fYbc`x8$KH{VwoWU^q zvl(VkI;U|C-t}NAqYicxo+oe*4s^SwMRwqKW;$8_hD}R#be3QMA>Li)2oq`+DIQ6y zb#R`3eD&%et5U8>BfN%h)089W5pypZ&KKMN z-sc?rmQU#xS<{+yt@5pc|E`JYC8_E^CF$&BmGW%wLY=->g^JSb8~T*gXN%X_K8pHm zVXIDmf2WGNvN9pUT%LQFU?=vOy%XYG*2cutNk}^{``Pxjf3~nyot?B&3*RijO}Rut zVJr1lSv%J?nkzx?*L1Es8;-i>pY^Bz*{U!?Svz;cT#Ql4zBh`7LV1y`OSx&*mVw$U0^42^@9(#c(=odFrbBefUJ`uNbFo)R=u=^=kVA^`y zIRi^LNm*ad8-6Od|C26U^!u{-Q@9~~&7?MLmF`dUz@6W>>Z`)#Q7x;vb|LSjc589K zi@b-Y_25&F|M1L%1-AV(3xk<#B(d-bo!zxxw2I*e`;NE`!3pIHntsA23xbkA!A?$U zSLaz`@N1Q2R4=?wbIOnpc2$&K(FAW`v)5h$&wulPu?}uNWbCmWuFW!EK>Bmj!<5}H z<3N%wS+@-_G44MNZ!*g;ybdepHgCy?#aWuphQsd|YDGrjUag^w<8VsVRKJ}S%8J(8 zvR1IeYs0D*conlo_B!}%-Xj)vYYb+rvxy=7t~2C;BFwFODq9(@BLZ21;aBdqA(Ak^ zXzACtut{XdBOy5FLT|+n*l{A`3=ceA{Af^YH`43=BZpt8+qv13Mj~Jiu9Qt}aMN+q z?}KoeWe-OUEK>WaQfCjo&*&4e0$5{!Ygq&=VHfBX59?GEiamwB)-UZmNro4my-$O? zguct|f|vB%KCQVI?{}?!c5fZ*Cb%rb74}KG7q<%L_i&B*3NyZ5eV7N{ed@Qrhz;8D zFYc!Q!JQ_@D}L1we#dt|tQ9twic9r_uS^vzErW&c{GPf8%W9;&zYAwnHFb;Z!(fVa z`q~0GyCAS&102>UYLyFjJeJzs0MFgYDKTG)mhDH~cFCJD65&gf72A%$!lw7Oroxxy z?3JFuc77I8h45YZyv*;g&m?2seYmGyw`$t~l;sI(^ONBPPnzyT!N%{OHOIl7_HW*> z+ai5+Ro6U#O@>~l`@kX%nO}=x@eO*MDhKf%i(XE*cd&v1^_c;zIVy1fEzEh+?CC97 znnOnKE9|hYsP6?_?Ww(+2468bEXsQbt$sg#ITfVePtDg+2jJ`Ocdd8A9tp`T6tv~| z1r9St!ENWIzfxhh$Za0i;klA+u5Cy!dsSx3UUm8oKPt+2nffA5FsHW{Xn#ZZ3+7s$~D4h2l$LKQ!Nq%8J=G1=VSIB` zo`)8}dW&+;4#B_czs3u|#(tZs2jGRnj<2}jN}m{}epqcs919!F{b5qO4-Q>xrpW}C z@cIUI!_VDMy8S^4Db{qAbiu+hni)UgFIv`9op4vcP{%jez-7NiJ3Jw{ntK?&R5R(_ z0?P<~(ESMC%k(X1gvI>1kM+P8_}_i2hc(Qra$mzU>-VgE3D3{Zd*1@zV7uh<92Po! zNTdOl2;k4EhG#CMZF>&q~imOCc<* z7nl4P4nFf{>0S7P^P=vD@KiO!u56e|?4QrniA7naF)-GYiHpS(+!d7@UpUq z`cv?7m5vuFFtgJgrsMECW^MKxa8L0YRX@1!*#Z6QaL&eQCl9!}=S657oTqg&*%|J7 zXk2*}zId~%{V*J4@`-i{9`Kps*bjegG2IXizivFFZ4IwcO7o9|`2$Y*n8QD4oJHs0 zo!4Zux4~xTqJEx*d&D>QZh`fZf2*8B75suXu#OlL3;(8{m&@B6-2&@qr~l z>TrMAvaiQs-QolH72!h4P1yk0`cS}-9IRW*>v$A)3X+tRf;DzrzUc$g*tYFl0w={Q zeDH+Zrw&Jm!VZlIOWonQJ9O^OBugMA3_s^%?|M9$q&mA#iK>Po!Cbw7%G=RGIH&Ct zD&7>&#C(fbervgcptJ$K5K@{QS5{Iv>AHxU5<;Lor}9^Ou6PY6Zu%eW~RWGzIu=!xBObUa7-O*^cvnJNtwx+=2Gq>c$yU zxH~6`V1i$IhS8eweNrj+i5Qf3f0u6b)W$Ngd@`x&?_9$Va=1cSRZX4X{wQ%)%in`I zoYE=tBYx3O@cphFka<;s zNh{S%LgFLZecR4EOML!2#U+s9b1~;)jCxuyKg5v>OWLgFAY084?dVdeV_wBlYX*b zFYJAOw6JFoHN!lr^mh1m+bi#ASnHPV?o;q7@ycZc?6E>BFbdw97kXZejQ@t6y!j^TyN0*b|$J&^2UJooW?!*ue_bJjO55px33~z?RtnJ5s7{cy*+kJdsjs@9H z;_&xP0TPaI;K$^HT(HfIuo*XOCn@OhaR3YNjr=`_%F)8FXk3v8AF>S+_k+bcY>KbL zGEL=ULh$o1Ie0V(p}JUz$BllQlZq9XE{yc+!pMETi0hvexmBF0~;6Rec3FDDF`3Bod1 z*s|~;BXcydbbZ-KTRMHuVMQhSGlz;w3a5?;&+aNn-tPbL6xm!>a#3{Rjd_I`wGF|+ za=7G~Vfo@}!+&Q!a#z8>W4ipYQno>1RZNXp8;ZC_cY%zvKm*Dh$<()#p8f%j)?1pFYL;+;m<9 z!_!&bnL%(^#cvNMcvHxu>9|U)Eqn6Be;dr#GbLdMzu)(~hzj4(kBl^c%~LiN%EPY8 zx>3tuoA28)m%wvZVD4t7H!n42iyMvEXHK!d6j|ITtE&>+F(aN&ri%#0;ISR{90ANx z#L{LCJDwH7vyDs*dVgI>K0;Us{q=n)ZT^0Edu~j2?!A?BW3zK_y8O@n`Ziqfxg+Lc zmM7{*Pk5oVzfF<~kAv+jxZcIVyVe$@JcVGXztv&c-lx`wxXrFlWm5Gp4Yzlgf(3DVBfI3KW=CpPycXuh3aBr7i{>9|4RYvaMsA?ARNBz<-S&Ugs z^-en+b1{FuB|NQnmofq8D9-fngpX^VCv5#Qis5^~V&~%F%QB0;6v1Ub zba%Xkx1M`*{|U?;D8GLKzI|2Y!+1$jXfGw?}kn#t1e#(WS`-w_U#{VgXy)UUB7uDUYWW_QQ`G zzNPTPI(sXPoZxw9vfG4Vi+26P-Y}cGI=cj{7tbCX1gBPhU%w3Q{B00>7B=ZHahHZq zra!+M0~gf1OMRL=p3WJSLfTw2h{a&wXTOg z`Cc8F%%Lw-SlilgK>=)>FD$DM*ZhjDCw*hf8i%c>kPe(5`NQZ3~|G8fRm&vuA$%b#IR~;qa`_p@!QdI!66fmtX zgKsDDF+GOoO2WAkkX`_*qkoU8SBd^;0R4@BivO(hpzj8#qDbEcP(_1YGvnz2MS}Bd zD9e5hKf=ZSxSDKH1{2c&AyTCu>SkXwTQvW(!>>Hq|7TP(8vb}-1%og(%_{7#+&PrJ32b*08 z{Py;AXBjM^Qy&`yTj(yzD2C5S>(w2Dw`gqhd;~8bHZ%CZ9~u*=4`9KC%F6Drfz2b% z0=T*J!69e(fyd3xT)2pki?xH5A9iJA!`2+Sj#oKGZNP3+osK`tI0yGwK6|MMCs+-5oP~=# zvS_Q}2aGJ*r{Tj+M<(UqdBG7Jp)i;8{ze&i7hlodU^tIwe%4C(*&r=H5Kis=b!r(I zesm4#WfCV-v^DKgm@;^-#imF?;^{ zEzHCc*#2z`J^qJh8iY99vMY`L{f4d;Qbox4DUS5xZ;6yp1>w-X5A^u0b)5t+EMliZ z8U>qVG`AQxC6)5O7-*WisnPnD(mPs_+w5?fh26I)p=$iK570A>QjDLt3lL!2h0^eeQyE0 z>2-VUX?P^<^yG9ih7tbxW=V}}j&#FE9ZbI`AlxouiEA@_>h|rlo$&os?tLfV4&G&3 zb>RI1|YKUkO*4ewW<>4}52TEC~+@FSOQ!Ro}8KAcqZo)H=^@ zgvGgz?;!{FzvxUqBw+R1*3mp)eP+L-Zy+gn(%#B9d6^3 zzd3{SQxlBvRDrJtO8uOK7c*>2Q6$6V6SjPZ(>gsG*TPcUcoM$AJ6f(#R>L8Kx?cuh zVUCEktKhG13#onZ2E*LFQgGnwL|+?Rf9P8A68N%Bq;ezdu%P6# z1bmhje&z*S<@{Sj4E`Zn+))KTav#|&0vAQg%07Wx0tNgQ!f9S{fkiM!w#H3DK{8inOW`s6TTwZ!Vmy!Z~Jol2fUJ1%ghH(5fXU)4VLm-k>&>P)p#?11nyKm zH{l5X+Qg_n3cx!s)-}f3buGzV3Va9uC6_4>Q>7 zRM__(c&Yi0$gS{I50jxLxYO z;w7;0DdpH2c&M(`O%y&IzWUiy*y@#1r6AmUb?47=GJO3-K_1w?CTL|D9JTPP9Xre~ zl4)KHEBd}HWQO-vd4xQIM^{L*(=d|cZJki?0Det9wfh%*fxo)10Oqfk$@vcdNa^6o zho?$TQNF^SNsL4e{QKaV{x9&@?PHC1V7sH11H z&=721Z2T!1_SoJ3c@UQP5dQ5tJXRa~^%Kk~_iHj1u5YUt9f0L(w=!ITEl#S9eT1Dk z&d-mA6&?=#=!aV+r?@Y`Vi(U$e1P|SR}%?`OD8mby@!YT(w?Hu>Z5{I*RqJ@D9skJ>TVC8LMd4R7`;AbjBR(laMW-=5hz?G4Y%YGQm1 zXQWFCc*0vgx}Rx*p9k$&nr<)nT1E*f)oq_uz{G9dSqy3*0Pr@Uv#wT@Qx5V!c0$|Bxhgfa+ zNyV0RzVKlBk=zY%!S-h*?(lGcM~x=@&N!NIhTllpyi1Ou}mJ$kw0#{^S`lo=h0Y2kN@Z&GNdw7k$Dy}Pf4E4LWate zIYXhOQb|NYB}Imck|AU)bA$#%AyXkU6%`s(q9o6KpQmr@x4xfs|GaqbN0OVftMooXv)E1nl+u4@GnILtBtTk;WjH%n5X=cwy5d^pVBV$q|W;=7ku{shxsb_ zLB7*>7WjJ6eGf*sWQOk;6D$*z8%zh6pUz%f0edWr#?!!w*ZWT`qo?=ev5LIqZmg1C za-sPJhlziB_#0MM?IhKsr#C0Nn;Z_WuX;$SLQih`+i-GZ7w(7OJrsKM+}db`kiT`J z+U0nJWQm^G9Y4a!&tN6-NXl1K!|c?1$UN}B8p^+#3AI}u1OLQ8kmCQm3y8X30QLsd zAhR7*zt^_+Bl!@Q(t)x8!xeP-WWhZmLOs8p{2d7TJN~b%Ml|=fg;y--Fr%gvJbQv# zlWNIvBs0O+XBnwAty^32b?D(|ZO^GSt84amb!gzkFCppl_=u)%z_SkOyWSJwI^Suy;TIe(!EfQ*Y#w!D1K0n)(C7qdYyZ>`HwSQIp@SU9Qsw$DJ)YA_mVK^0MxLE7<>p zZiH>^+sfKtzxS^$D8NjgjJaCi%KcKwYOn}vabPpdcHb|HXqia+IrKNcUX!PK)1cy4BwVLWV{ ze^urd;VE@%qUHKqY~95h@WX^Srqs9d38{v>1av&V>j9vFT{Hd<5>93z1oZbyQ@}?SsdJ2E{0FbiKd}5BQn<8Gc&y zLB!7e;&p?&Ge+0a!*csSdN{$}9Cgeq;oYLAi|ygs{qieU!#C41=Xb+g3g#phxRTeC zYzBp4|exYrG}5I zVR&3voYPNOSwy z1+yMJHMflW&6EF7U^{%(Z${uZoWhw#>lo|&n_(rk++)Q3ot)bC@d+&2 zC7t;NUf1|--$U4IXYzC6`B3Tq@a_TJ%MiQp9v%-{*n1!TJjEh0RkuE7mIcIOfK$54T?VG``q zYtT~%`)24i#KSFbA5yB})iORhm*7>e7QYeJ6N>*43)jYon-Xr$lKyobzW2^B{XTqT zz~oOPEKu_8M-@DC{tW319Am+0O88bvCS52j?Npy$2?t0FF`gvie^vTPdo5*_!yA>$*$=`}avOe@!4E&Y$%rz~j$?{m}nL*&P$Us5(& z!=4FytO*N=*KFDWd#ljiDTdeeepTEK-$zTb2p*VZQ!|9W*KgZR_>$lqO?QtFYbz|{cacO`gR+*q8QyfEeu_9>WB2t;4Vas(r$>BW`CYnss<6l2XE%uS7J5H@ zR|ytnj$NpS&D|R+CkS-9!g^?c&`yAN|UO2ccK z;;zK~6mHzyEeb20DyklUL&*O9LU32*Q7)o9h!rv(2$I87tB1`NXrTzWL+muNGf)8YAS%rIhOg428a-ZH^U3SZYUz`h+nw-7T)=(~*}j|0{Aoi^au~mvAe`B>ptuN&o^r_%g9Ya9Yc9awu6?3N!v-JR$#Zan zs*!^XT*TU>`3+Y08htGX+ZvY~{0t8qBl#%6p_GcuX}GEN>Q^OLfG_jaJD84A9i$2$ ztIApZ7N*_ya#7iQaTZGON zIAHxR>gQp#^dQ&6+J_{l;Ya&w$u+Q#;w`Fe{`}FYgf~4jr=Gu~U|y#b?qQ;*jtO+0 z@zyDZC3kvL<1_C*PR@t-(sNVeXFT;M=fEvT+o<8W?&CU{gzr^Ry(e-dISn?e9-w}n z5%~|~6!?t!Wor2MOLpXBn7#NU)dPWHmAO0|s&%j)3Q>fwV>|MyAa7ru-_50ZFx}|dxmRYfh8sEhx zQ@5eUFYxIlABMlaY@wQSBRBaVd|T@XHNH*28nO@kZ_K1>JXpsI);xEH8ehol0@)p2 z=9HnvPqz&xJHukG!_@GNF|#@jaMJMxs#^`$k!@gYhjD89*j)HQwubrI;;7*(&v=k` zz#NRnsqUePA#aCi^X^d3?=GC8V+acdv{S=%tY&p|VIMjvsuh`*$y?#|i&>f~xZm8* zwB>3MR^J#Y4|DT(_-eom6^A-E!biTP%d5fxB|ms2;76rd-xc8?*=lcLxZJ6)Y7@-c z@~4CkF80j}mVx_1l9$)P5y2zcQn2^HfH5b$?(RJraaihQN)l0D;>v#8Bn(?K^SvgX zcecKF=LO*B(M~c%eUC}E#(V>889s811czO#V_y$jET%RP<@MHq*<4)1)B6cV*plL~g~;D#%4hD- z!JfyiMiKeno~nm_G_d*sju(Tl*zE>|bnxOcGg)ExYL;{GyGKi{WyoBS2Nw|Lr@D353Q)=7Paw|oxX^#nHCTYr2Se#cF_ z^&u=06w>?-4p`EXs)c_J>|Fa6R-S*vPLu}=A2~e7;A59+7K!rm%8}~wA^7x-+IK|x zC!*B9q93MXtmq`lua=6?ojouqTfLene|ny1XLiAUd(Ks)!*Pl-y-#4bz$=aZFvGR7vlrojsOLJ4aN<;Nj0ODtfzgyayxr_Z#UG3z zu84o)!2pkmgmJ!x{UdYgIxyDAl(BkiEBwW5K`aJdzwDM+0qe{)UN`}pqaha$w|{*; zsRN&p306D-{~N3PH%3YA0>HYf63h)C-VLBmUB#-Rq@YG-dOY2wHv5`<=;tSm@b$7Q z=nBaqqTd2-vW|NkzfQdXy~|EC!zF>PWD#XtZ3`Tto8#B31yJN1tApKt3un%~kh zqKeLi9l=jb3@|)wq+e)Kf`{U2Zuyn6{~Y)xrr!NK!sS_y>Pf^byDj@?4reG&*t+64 ze1~Vook&Xvyg zhP^+(U(p6Pmg_Qv!xvU|cD#kpULtQf54ZYotzU$lVxs2CIThCiOz;XeY0vI##e zfY}CDy;`>!)tg|q)M{8%u54Nt_LY}3t%FrZ=L)LeRP8hyB3#>DW#o_+eqR;qa5p^V zcEPqDwzX__B>YCkluYdN*YPa;=s5grVy4_0W-wSR{{+|V>F>3L-`VZU_zlZnczkO= zJjB`~&VVa=x~)bw2M!LFw`GId*MwXAh8^9ar@3I!RW+V^+880~u@Mx34>Ui_vxCjH z%s-WfS&r$Ky?{MijASg~Y1*5%pWsoyNkPJwcjm0-+lr}>^*^HY;XJ)|U0ZnEWOCLB zmWj@j5^5EGDE0uG}#lde#qwyN>y(&NVJh)gQ?)zr= z1`e}eA2xxLZ)p#IhIeP&aZ!XDm$tc6;CtKLCuCsK0ok7ibg(R3sGXGz@7{NK#WnZ^ zQ`HZ`*7QCJ#J*@2at4=(-+zkS7fP^(ur>>aMhZPDKhZg zic_zN&;P#PWta@_y{~n_4EBmo4T^*-?&w~(fzKu758r?-4nItFg{{R3Q*Xm|){@qd zaOa;~+E)1A6L&m^^&qj&RdMItE$0w^d|^B0 zE%st?^m}{j?XZu83$q)1p=>XYF5IMZekua~F<=x*e4lad6MEV3Ig|C#Qt(}t>8pM4 z);(VM3;my(1i)c$58Cd49~&8&K7=<5 zHK?k=e&jYD1wC{xx?YLahBpb0?b=3oe_k&UKAW3K?*sGPJ~yiXSNPp&sf42vN^^+o z$5|NA#jKBs3z>_q;&4vGlRCcYRA#nMmkNXGMe7DjKB0WDfXJ@WPPPa0D{`ny|>$gUpEUdQIqSq7tmlMjL z3lZ~ziQ-iq3tN7pXvOjhV$!qH-xU{_1W%O;^(DNR3;j3juhdk?oTVz&aZ(XvBUib) zaJdv(I?O~!KFLz#_;=xp`rm~w>Z%cPf~=%SA3?XH$sL^a4^}HoA~y zJ;>$Avx*gybtdosXYS07-1%S3e=%rl;z)5gG9=hW=LwEbW*sMa!G8AFb*{lD)5l3d z@KPHac@5ku=1#eL8^kn}a>b1@IEP zBn@2}wmZ4@L#M7Bjm77slpi;d`^WN-S0nYS!hcazZ{Y4$xu!$p#$5Z|NX@Aj|JgIB zL*(91$cvV`IlmQm>b9s5Kde@6~({fBN~4PMl}9jHY_zKt{0Y^6aQ$Sw#2zG zQCMSkpBx>YaMl0uMmoAivPAC}pInc1)Y_7YB6T9Wic+kF04piw|8D+B;+AES^+R+m zT)9J|gi#;yaqoxcA7K>wf2NcD@A)GhxL*HRnOmlR$wB`J>3yqf&Ks3I{;NiE6rAxk z?J4BAC_U{Ajeo(rxX7{YutRIL`*%3FVP?Puj+505{RBJaJri?+wYW&v-oqSrPKO-e z#NL3?*YNNOraD{LKKf_-5NzM|j&V2q!A@(~c5&~DE;!NFF5Lod({dJQ zg;}oge>8(@3g2ip!at*at8Irbvsmq_hsQ45i7afQ6 z6_zAeaHK3r1-5#!By|~9VRL_@2*<_;>z{{Rg0{)Y!Y(^OtSqqOkhwUVMs-RGe1J1c<#!6Tz zEJTb2uIPK-<_w3&dWx@teO2tE2*34a5NCkH`FH3Nj%@CgpoQ~S@GLvQN#;2c%g>0; z*lRU}MVK#1F2b37qJe}W|cC_#}^W9^sd} zE2O94F|wBzVWuq=((m9qx`Tp*hit`U-oUZVuZQ=*dh5hwM&UFT!E1ywy@zB5;r`F! zy9u+9hGhESi96R=2=DGNk$VBBPo}jwz~ee5aviYtyTK^J*L6voTH$S{$Mpzn$5P~) z;TE$liaqRG(Juc47JF|~L%2ZkfP6jtp}p%E;rN=J3N^6kwwp`Dyk_O99Z`wuJ)A_1O3fxhoz)m>yVUkKR+QC7PGOI#a0M>yV7 zQSB1^!?aJAFpqPU+6CC;Ss!ILjBO^=qF{xSj~@`$-Frsk3|#BIaE$PmId#oY_|4~e zWx`4ws+uR^2WIY{cEJ&&nVXNnx(aUjglpr{Hy?&&`jR{eQ+jr59fVcZCW;WYcipMw z1AmPezd*a4zyl}ouDXA1bXYx(wVH5JRv~ZeZji|jH8q;xt zdq3om?l>UFaLt23XYPG!~aT#Ls?xqd*JKp&q+i{ z(Xl+N6Z{yR!NOdmMZ^!?Cq(9fZ@83^KElE`#J4?1Npg^3Ey*7KSF%tW|Jc5Y82?vS zqE=elno4?fJ$MY9Zt#l`3p!oDo8J?VB~K#4;rw-`RVr2BjoTzZg;xdbj? zn^0zlvt%@ml);DiYp<_^mWAKHpQ^6hZAiv+!5V(4}q`woMvXwWFgtN9T zt98Q{KK<^x2>TV&CBA^KO3m*|giCl=eSZn(By1Z>g$+*lTlT^)HaEIt!P9xM#eJ~- z#O1g7F#X1))&1~+SI_p962}j;`V7Ff_g;RghTm|}J{yFEo45lW!(T&h$s$ zTR383@#Hit(zp7{Bz$*_*8D5{=h42EAK-9pTE3s~rmYqt(=f+8$H!%O#?Nc>N0{W8 z@_>Ffo|#0?ouA=Cjr^E3@DKSA-&uI8_aR44*zjk;*>7;Q_i0&P_z$V^#vHuBMZY8j zXVErR%){=QoX;g;=M%&2KjHT~*QITO6Lx65Uxd|eh#gdg9Tay_mf*y=JK9@dR_5L7 zDe%BjIkO(z<9JYk26f!h>f@s(@Qr~qlNE5YNkp+FoS1yyjUG;qqz|=)cbe6lWQ0>S z*>*U?w#Ry|Fu}iGvk7^^S~RMKtKsfy-dSJR=bA+$Gd#T|_2FSySIT;X6)w}beK`o; zFMHqzJDeG@*EI}2?sSWV3)ac!j%W6DY0;xM!n8@SSfETGZC)rpPnYn zxSKoV1`*$KXBiLt9#vHR5oVdd8|J66GSbLGWiusw^NBnSM*YVf!?%o4y%;emx{r*tIXe#IJHet7GO zo#!{>T%TDq2`NJd0g{*4u-qRxX$Kh_P_`~Y(&iyrmp|GaA@k+bm3z9vma_;-)7oLJ@{ickIxhMg=VdR z0o*JzGSdQo&pUd@kcfX}%C8&765mzEa9Wq&r+!#dD&5b7IR0x`z&LzwU3L3*_*3PI zZ|`AUH$4S2c(`-N(JyfNUhXUA@RJSQbMtVEO8VRm*u`w(#4^mUQnT99ON_kx*W?py15cfy((ic4H@;py9_cEPTxjUjw+gRtAkZdm$BBS{RlS}-Ns z!dqHJJ;mX5XTq!qOYYj@AO|ZPPpBe%>37=>Re1KqB>_9wW>b-YHcS^h6+!scP}61u zc+<{5Q-nt%ycNvh{t9Uedw4#UBDotjPq;ccb3F4N8vp?t`B|I_7w^{`b8VwpCY|dIt(SoHiLdl+R zw_wIuGOYe<`iwVxeQ5h8Vl%aOEORzK@YdFu9#i;rQk2XAc(7n3#0uUze`47mcJ&Vt zvxRdXt$ukJ9_SJ4aD>-8apwlYY^*DexxmZ&4cb z5n4Y|TXPTY;FA`khgB82l?aO&+!kemeSZr(5I$+GCdLe#uV{=Q9KawY$`0R(ampvG zFqbL94JREe>LxtgtRl(-Z@;wTH(^n-ga|(zt>G+O0=Jf)6A^~bNUNC;_K8sv5r^}R zHU<#xHvTRm4S#zimO@xbt3p@~){w1zL|86eURV+SddXyp@Vv;Zuqtf&{SJF6oHSS> zqzQY6RjLzqYUC8whX2gAxe``0|0uK#J{z|F9N`DXA;Jc*y;)@m;R7=VgiPS96w`jf zWl!G=n!^Ln-(?iQ-?f~%cEL{#ufC~-ohCKU?T15o*=76SJgu3v2{6-XZQ3_5`~1yw z_u!3(m}c6D_y^xV_yuci=?$-eE9gi!n9#?&W!3C5+K+43IpF@v3lNus?PT0TJ>WBY+J6!* zdT9FoI&8gM{ZI@pIdHtC2)-{kpCbdmo}TP|0$(QA#puE={)0z;!JEds>RjMfN0!YL zm_ID|m>=90m8<*{rufpgMZ(k(2n_I3M<7(GZF~)-zXK7^{&GMF&bLB5wa|kpNpG@1-1RJ{&)OeojU$cL+?Pspa1^+HwFHi z0{=~c|E9qIBPmd^L70{F!4+e>|2gx9cXu3Vc?W7C|C9S`=-KM*LhsSPT>pQM72d*@ z6aS8g^ZlfgfS8}!@9d$mW5yWHHZ^tRIk-nfj_Ta_z@3}Y+lyg0868y5!s2|KT}Q#=K`KaJKQET`_c=`7sQ^qHCPlk7_} z7h&tBU89%bnhP8q)TMmTBpbTnZv?{MDdu=T}oJHm7~qSy7n z?*eBe2oDbHa}UEJsebdZuy~Lu#{}HT^Qw_>{I7>>AK@2G{z-&|Hf?2|gLmF~;z5|i z9JzW4rr+qKOn6Chk%`U{{RoxwD+secrD0qHpU!uGaS_f9ctpnqXAe$h5OzQDjD`=^ z@UT8a*w1Y%NeqtP@QF;xX_Bu7OHbxI1mu=mK1@ zvnxs)_SbUuIS)6kWw$YaUu~?2IR`7PW|A<6!!1RNqv6g~XTI-(huoCAqF|F^vj#^v z**=f*k3?Dpjpe_uaayfl40d5Rn89fcBsAy@Y!t2*tk_g+ZR%gzF zC*&WCgu=`9TZZ!Ctp`5pg}_1EH492%Ef#05Q?UEF`Jfv3U32=mU^w>xz3CIU(@QV! zB%FOeVtp&TDcH6x2yUm>ecufm(4>Ak0gFquR1LsG$`Y)EY0el&zJ?!jYbhRwU80P3 zPr+fy#XFC|c5l~O>DZK8Xwr?3NP79vwOn+ z9d=93@D9gAoBiOoZ&DSVVA=A!u18?;#C_g-;1P$M=#%iguUVQs+`i~}FB~o(Ki*>t zOY8afpM&j_FEZJ{Idr+CI5=B9olNA!1+vbhWH>?M3pHo1m6ai-!6i(z+lcTm3KLBZ zTwZKIjlb!|_;L}f!^lZ>#rKDl3iw~nemX9eQU@FC@}-8K+&DpLf(1TarP}jJ1E~W} zST#cZeWtWSlwP=D)ezM|T6vTac+q5*8sFdKJn0>*VyZ^<2BE-QU1VFEA~>uGoAKP=&jKYx)4wGeFYN|3%uv>05yK~&J5C8So6GV1y4a`Hbw!&^?%>gujYf$^z{^98G{pVa;C{pZ0zB)fma`XLKO0aJ4|h;bZ1;p?`=S`G!`qo=J`?w| zcherLbojN%ri6WPGV`IEx$ts9y1Ebiio<5A7`|4^HAp-!KWq1^5-)vlX^IXdo~OJ1 z+F^C@uaPzp;(0R)wtU_Q7kcSDC7#DOmqph-hliFUe2M4TlxEFpev~ZZek0FCT0?_lOw|`6Q!6vjqrxF{>Mc5KvU_dr37nC zSa=cT$yzCuC{5T^^V%w+{Neq!?FAW@qOjg2%BQg=R(@l+^`+fTqI~W6%D3MFmgDJO zAj)qk$>}m1_{0I;8$^BMZTURK2@X<9AQSba{1~wjXE=l*8n5z3eq!U^;RWY?JJC+q z%yFuocnQaC)m^5Ea5=~#3Tt`ss1813QkKp*h2UR58LTW z@TuQ!#>sGl##R4h_(9dyX2Q}&X0B=Qx)Z#HSK*`6t1NQh1gCqAgoXEnZ7qiPL^$bR zgY|9d)}&^B)#kKn({@qW|;8>^JBt!Yk!eCU}N_lofP7DvaCus z9MRBzI2E=%8g~8FPE>P6sQdrXKO`}w;;;Yy{5J*un*#q$f&Zq!|5s8V+Fq2EbO{rZ zIsbDXf939~*Izu)BvP#>r60l?^rcJm`!TI;;?{kgfTt*O|JPdwRq%!X9dRg3_-PaGW;+D$yXA3y0^TpqeKZ3;v`OzCKzTogbi$ljS|Zkc;Jt#TuJ2?&PzkdZc24xdtn89ETH%RpF@f)4x?^!)p2E*LBW7v*QRzyLxnBdv zT+I~Vfw#y-U#^0OKcZ(3Q^LQMiqDS0rSuQ?%)m4|&c)8b99t}25oYGWc`QknaTe#Uf`^kFvA$mA?VP>wH4IAOer?q@*;j4o7$y;GT z{;*YYu-zB^esfszgY{)2nB8gfYFju~_vxKz_#BIq?jHC`=(~hV@VL+>9T&J-S)J_) z{7iYnfIDn+xjQBaK4Shgzze2Ny2^MRR-kWQ;{z8IibvjnOGDn|9Dx5lS^u6W>R=6a zYW}ULp@}(}ihoy(DE?g|qOL^UG+$9wh0HP;ufLNlNcLj>==QOseFfbIve56>=YCym z_&YT7KU*_vUUU~>C0!*t5QG0?amc@6qJK~9zmCIyPwxMIw)f!K{&&Q`m~n2s=lJ$u z*LJ~$;T#M!*(FpQ-Um|(-J+ktqYtB$FT?6D-GKP8(6rnmxOC=ZskS6@m30gn_-#g z`af6Ui0~Y52Y90w&2$lb(B_9`0n8EaVcrFI9L!CrfX!)%#Z!y{^Qwd@bj z;Ey`b+yRH{(ez8h%2H#8Cg7C$XWyLQAW^o}63$r3yRwUv1$%W=vf9HjlK08Y@Q%1s zlZW7k12Uf%;EXW4mISzLwEDn?S~LKgCJG1Omz!8ybzvK=`%5e?nCS6*Sl%8!>1~iv6$_2tE=iaQL;CS-sbF3jSe>u;=d+^dEX)FTX7Jl#81G=1m5xM@ShgA zq3O(|GW=%fw&*0BeUYD&di+G0%t!dt=BXom2w(f7q;e6i?^=wPf@u_KYw7FodA4_l zM#HH-D=b7{rt>8pg|KOP=bQ>Wo^^S24ECSy_t_6;KAKwm4ZGBcdxyYLyfwVP;Nv~j zcrl4tKE>^sb&^+@Y=*aohDdk*JNKN z-0*A4;}@(wH=>gV*SMD|s=K4ZjZMk225zf8xitW0WH8#=2tUcsOuL2|7?D<>JfaM(K0z1K5(;C@);bpk6Y{tTyyVo zW{_?$<+@ zmD##g@F6C4C%@TFxX6OmR~>$;6qiv1pL`WjWe;D8?` zU!&&^KY5eTL66Dq4_~lzuS4B~HtnRz7Gm$Xr=Zh!ux0Ej$rcs&s*Y)oUr|+-h4(({_pS=d?Env zHr?TD2k&F@&y0e7`;GP5VavKO&l~W8=QUpPsLL>vE#y|gMwIz>bNF9Q{x1iomL-_n zPYnDkQs1bmqNGS2|5v1L{I8<;ck!Cy-$?~gZ$o!rEudYF~|7mCkN%-^MpZ})7|Bq52MO2KHREDMz2`?S` zUvHUuP2X@|QW3qS*4ocD>0|Xq>dqG0(-`%#x?cVv{6CvU&yXhnj)-WS@1sM^pt1Z=G>jdN?vDW!?=oJpIs_0ls`iR__qZUU?{v z5q=y(i3^3Fj9EQd3F~^tk6eTmN%V6}umi_OnN)b1LJ?aHn?4UYkq3`4RoJY7_3bR1 ztKemZlyl6m^qbcl&2aUiWep4At>vyg@Wt2aA6elm*9?l@z)$OE`Pt$A#&y4E;T@L; zEI44BFL#V6u#`<=I468(=42AH60$S4(`8)nrFW~}Y=BE2X-#s&%2z)rY=q~u?YP&$ z4ep^K8t}-gwMOgVPZWzbL->^^eGm_PCv|+?ZrEa~DW4bqu(H&1FFfp8IlKX;4b&|S zfLC{%VBv?IN;yeku&KN+SpY8Goh%Row;QbdAPjp*-`so^{%%M+DGn!Ce7DPjC)@Un zOT*7-A04|3N3vim3Ap7@c>Du+EvxTa^b zF5xS#Gl%7pq=Wh30jK$DEBKbF|8*%iP0hAqH~gtKuUrMLYtt>UhuzK%caz~6fxu!X zIC5zCvl(14e<#lkj!I8lV-HJRoXhrvAB?`(=n0oq+hy#BjmJU^0$`>J`LqM@1}Vk8 zAu!|ikdy%U%(PGVd06K7+oV9ab;sb%B$&o^#g!m9SA^z420X~1bvXpyms;Le1g}*z zxDXDX_Hmu7h6{OB-|2{zoW z@+KN~+`OdMLWCB38!h**)8NG#X z)dj8p1z+mh9`z13eEE`gD;MA*O}#@7aC_BR z@n7)AGz*J;aNw2Gf)z(c=6MPWSTIH z$B0L@-Gcu_*_W+=uO)XC6v0~4(H-<~SZvGr2k=EJyBS9K&78@;7TEvtHpW%(pGytK zuVBND0I@Z&QD=?BBrG$dzl{Yh_NHb04!?eJ+>s5QkN7!3QbYbzZx+M>|L9+6Vu4%F zUrXYG>$*N>@xe)Y{-tZ-E*_DHjqv`dz|QsX=6P{<4LHFvbeb1Fww$7C2ybbvVC08u zOSy%2!Q0vHhzi2;WyU0TSY~G_Ss32-;>-pg_$Z%fycoPjA$E5lT&Qg#Ee+?g-iQr@ zQ@W%d$-!q>+?!+IO$;J-O7MZ8AIr&bQ|5-xYOvkAFwIQ3;r*USE%;swDWC{et(V{@ z!&&`C`S)S7b(-b+@DX~$i3V7ZbFY~R{GrmAuM;MR8ND@!ixN3(24RImM}n;3xrXzx zlW+$E-j)LIpkaD83pYtb-q{1oRGgyxhI{*Vl3n4PC+P~b>KMve5m!-D|@xiOY#f~=fL;5eLgJW}E; z?C8|F`ULEJE&j?Cn8fGsf^f5p?4NY_baqv85Imc=;C+`k-i+JnB>dC!Om`g||5jf# z7^Y!exAi%^5HRzB@MfmW+(Fp?C*R#uFdJVe=M787#Q|{v?!Y}vF*NMPxt0gan!S{c2$|%C7Rv#_HVW+NJi8}Dp z8)8_Bgn7}o{V3)z=ZA|Ugl{&5?RSJLIma?1;Oy#4y*}_+n-lxb!oiX<{nVWja2e$Cuvv3`z@iyu?4!;o&d2<6kxRB2q2`fCU z?6?Kr9pQAj0;ls8RA<7=2`aa5!99*u+1aqtHvXAH__goq_&ad=A1#F%SksCpqyWD3 zHT~#QIC7S4UokA7xLnf%?~SzBRRZrnm`QpAyZ0Q|DTi6=SH`}D>s=MQ33smUF!}=T z99+*;4d>@F(f%fmr+l!jgSW*$c)+k3&FRjJ8;@aGX{|sGSYe&xSQ9+^DL`HTPUG4n z+6uSH$WKed(;4f%I^b88?zdIpz`>ck7qI!IHO|{$&idTXeQ?!}#`Wg#m}sKn5S*3w z{e>OetFk|E432wsC)NXQ@3p!A7M?rRs(%Q!U0=2I9-g$HTMmW`XfBaI!n(#su0_Ff zNfmzxOF!nBPlWSKeg1qR!sYy}vfy!s$IG8#t+t+`5_s^Y@$wh=1gpxLN3gi%I?60; zTylM12Q1u^zWf#L7x~gK3^(ulP5B1XYMSOwz%6{~)2>{E!3|I-d+C#7l^Gt zw{{EiS8oaj@p+~gRPNDhqpSYK6QyoAYRd|Rt+3howv|ue?0~3~clkAK;jFuj~1+i14=~-a06cw0b`F!!j|W-2!CP50?L&Wkn02$ka^F zo$%*0S|RvL)0HxzZ76TzPj_vEmD5-`5?~9u@~JJbx3(W61%8)t{Ifl*QLXx(UJvuV z?u~D$fSEeFCX->`+`YL&@V}PHzZMC)1C$k1L^gWj1%N85!~y^{1!{*fMD#1GDNwhx zQN`B(5dKqz%xyw>5N-REtl+Vb?V8q1$4(Xx3F%TcyhF7AaWY~@8)XGm>i9oR0cn*8 z{`&9F|C>`F?x+MSDP8DqgO-Mtal-~9OVWxCP7-NiG@IpRHI_xu>~P6>f^6A=W`|)K zdI{~_c8=!!XM;8bUo!toU&V%+{}c1+@$dIk;qLf^N4s$3p{&D-XJL;A1A!i}8he)X z1^5aRr&1tnx2xbqINU3kI+qA9tCSQ)!;iz41Zv@F_djWQaQs1=iE;RFu+WM$Set>@ zXb!$?&9W^9{w_-6@CV-UsN{YjjcGX4YRfmC3?USp8ReSaGGerr=##xZM`+| z{Z|JtSjE9wxBSbA^F0n_9(oGjyTSH^1NPor_+=O_xI{L2gB1RJgqwK|{+Hq(?{=JK zPDcf+Z@zvI;qSYvy98l|O3MR_@X`Knl+AE^&JPWJobQ$J9&Zo$Yq^}ZHT*Jpkueai zj<3lQgudhC36V2&|^sIVTI+75SoDXC$F_pD4?J_LVTeVfw(meSa|RYLKWK6hc9lDpSqwA$xHxtRj$}EphUoz+n3Z)Gec`+u=TS{~ z_L@?TC!C>}MRx+W__Hu;4iDcE;?ISBQjQ-tgbQ1oyGG$W9?#c=9RzxmR@b6`I%V}L zEtn-CBlb9Kaiuez7T)*e1y2y%aasM@6uzIpmE`2}aN_I+p;vI|o;t=nc(Ri%uN5v* zO;dOc({FbgYJ$zR7wnec^H-POw8Mt?m*VN`(EqM+ZFw4YwNzdBjd1;utv^=a_jMNd zw}>FT^lX2iG+b$5xKj+yaP%Xqz#nb1;+f&o%5S@N!xEAD=Q-h*zhz^+;FW7P^Ah1! zhTrl-;h>US(;1xqNs!axRrt(e0r>}9nE7S248BU@Zq`NotsR!FkKkf=N%noP)X;e7 z5d6a6!BR3TAJVdU1Xih!NhyQ#GB&+>1HU^UH`xojL*-(Hho?bZSeHT-vVVYR1`9RU&}hMt@bDcui%`{^GI9D`RB(|!no#VtE{Xh$N?aIVI)_9@ts60}$auc)y8vKOYTeo-|HTlu?K z%EQAOnsqv0wUVSfTDal1*1A48J$0;n7}q;hJfO1$o>cVfuYo)D&*+!J^lzu#vthRV zlwXx_%!sK@B0T%1n57>M7gE-`2FutCsJ(?%ztCQ|1Sj8ozwjCUNUL+e9bQA5*|7{W zRb}ev!w#JZEF37ol*Lo7sKLtOA-RN)Uk+vGgem%&bqw&yDoF`i*xI@3$0Cj|RtXgQ zNZh~o>k1ZN-c{><--Q!SU--d=_%9h+WD?o_Py|y4%5|qaE*%ZdIMNt z@alF+IMGP0e=BU?5Tv9A^T^z;G=^pS$Hw$wi=(u;#PjGdEfVAe3zXRC>BFKE7q7U( zUGffGEq-dRi+LbsVY;KjjAas{0&oA zAPcclc%QwhCL31Y30I3-LrZs>ypeABZeGAeY7J5GZw*mVHD-@ED=CSXhVY;3nEusL z-92SYVjR(XYm_{->lAH2;lG=d|41S*=m zfnU0F4fMm>_g1gkZ2-rs)lU9j?A>=b*Zu!Le0z_CkSK&w%HHEmN`uOltQHkAl0=l5 ztTKzrijt90DY7$KL{y?8C1sRIR=Dm@eH_2v=e)juUe|Gr<8++I>Aan<@qT-a=Xg99 zY@KDN5CA7C`pxvh#eIHW`S9>EC%QMVV#eHNsx4^7Uwib~W7z#xmWmxbTz@0J9=5bB zIoA%OzfAHI__v{|=`wwEfss3A_yS%#FsPsp^9;=vcf+$w=h$4}3$qs4A7CTHae>ua zF%IJB)I)Oq%3ur2K)AhfdwBya`$+Q3)or*EZ|$Vp;OruqY2NJ^4_|-Ik{mzf>lfz+ z>ofCZ4#JWawv#M7P)~nb=AY+&mT+1?bI1ZM7 zyCUBZe$1yr?__{t%+ph9&*5f~tIxR&@%@vQyHCJ({ioy4!7nc=2{Pi2Bpordehh0L zHKJjGc@)?8?8iv7=?}kW>ELfIlaAYWAzO76%q6}Y@av>A+|<_V^9SdvE;qp27)*=kmhaYx!`o05m&^4APaq44<{x;r!Qu7^H9JK)79B!VymSD*nRvT*UnJNw7Jt|5s(kJup?*xuBPDKYdM*H5^#A z*4!%1~u~G>U#~WGI0Ms)-!iutK>2ZHCXBmt!@Dv;zsXhOOE&7f2RzdI^bP$ z6h7l9XZr=dxm5q47wq`@?9F-jU?g`G`99$fOn(_!q1e+Wp(zR5YJIA6f$42thVj7( z{uMUqu`|@sE%@`ExGa_bEu|d&V4rpbj`&Dbo`da+DQPIz`r4cDLcL${KlOfP(zvgvphCI< z;Md=p{zBgXMFrKr|5NGj|5R3@thl0csUcRBven11`ixo*4IK|<-MIZn)qARc>;DVu z|H;cJ{jLAgQnAcY)BeA?^80_|cZ>60!%206WN`mn8-8z1{tfR2JU*WH=M5WCdphb6 z6;+7gbb(K`n=zfqfA_Pygzva`#NwWB)_F|&;|yn~Idv}L$g<^EhKLm-_UXjKbJEeh z#7w3$l*@2QRLN^%`3eJ_tMI!oDNl&)6prbn!46&wRm72|KXfwTUdv5)i4WVjQ?g-! zJvq0CIm|~Xd9Z$b%vEB3e{P*ZxNggXi^NlI2X*ekxhrmk5>H&2r96PW0xp~)ejFyO zQw~eo+;bt;rMT);!{G(X4-g-${h?C_515%*5(i4VQy#-rSJie9Z+#X+c@8V^r&5Tw zEnTY90#8LfQXu~BuwSPg=9Mg#BK|oxP3eHycP9!FKhYJ`c?WC#DCHs+v3Anwf%WNx z7>Tz~26aBebVa-~ez5IE4a!$I=#n3La(y|E<6;qXJLDtsm>~R`(EnZ#9Ow~QC7odNj2CvliIZRv;D?#yuvx80c5;Jvg zqIkm}f9LKd7HhAjoQ5fmb$Y}{-mvSOg`IB5suNQOKGY6|Wt*8d5U)Pdp&bUZ9u^TL z{*Dy|BjKmVz4?f3-=8_=Dcq+gcA*vy6Lh0(fy-RJpRR|aKkIwFf`j6|9eoVHn|bo36aKzD*orLa@hUcL zPY>+ncV=rd?6xNFX&)?DD5u;CUlF2FAB5Rsnpd~ODc@e_kHDLDK3)0-Hm5Zg{tf3i zAEN1mm9MRhnSp&A_Km!Q&ne2$P$Q+M$nbV|!x7i$PB6fw4qQ#WaQv)!A1hq@C%>!@ zjuL1yx8;0=ZAz{`UIy!L+H!dizG8Gsbp_0lIUV{P)-3nV6M@IwCp>?`TyGeK zCEtXpp8RH3f=?9L5NrEf6H&Do=_uJ}yw8^BwyH|@Jn1x>#vBVL- zcsjnH8k6L^UQe1YT4K6?0?<2^-rxc0>lduBKzgD#9X)aIf+JN&MV`z>+&B$YiE zJi@E8P61A{%dp{v+u0iYi38(2Yy{xAP3|5qSR*9}i;R zA6-`BaN*v@I^z5ZU#qonw_ZE161<9iXpc0U*xBPi9M5UJM;87w6!4H(=bE*JB3#)O z#ik5jKK0yO6=pe^WJ8>Vi#(@orhqL?7Uv$jL8tgBe1j8U3qo5>)kH{ z7g)e$CWP2FD$c+iHvPcYL2R;4e)|b{DnWRy2JH4aecLJc^}fv0#G9nuw)w%;;)>10 zOrjI|0kG&NwUwIit++$_A#ga=S$E>LR$Be@FehI~EwQ4I$CfBKmy*h(1rI24>BYc3 z%ft>7yD#tFlmHLQNtP1Ft~cD21YekuV$p_M*FDs|1|N8XUjJ~Yqq|NzEIKulPrQ%P zP050T8|UVTcQY7LZo!Ksslg09 zuKTEM0ncO>jF2m6!?C=^2VT6QSGAtC7Q*pVyF~M_J&rZZ{RzxFSgKSHTLhc$?uF-D z3pj-F`7V|rD_7n^R_Auzm4w{djan>a;U`~CSe}B9zP8Izfz#Iy1Qo%jtCzl$hff!| z_mdE}Jxi&x8l1pTQCkdq6)A7Gg*$#XMn8Z{aziz};f!dii%D>cq=d+ISki~>Itkeg zrzH>GgBf{8AXd2o{CN4L*#zR}8@0yu0}jX4iOuC#}I+uy)B!Xb|wV2RKm-#)V4 z$L*LUeCv|B7=I3mnvsTeov?oR&RtS)W9O05Y4~BQmH#$)rOFYPIaoL9Xt;A!pebe2!xpa8!xtstUzJE=L=IvLowO@B8AKa0WmfQhno?pFt zH9QvVphC7Q&uU1J#ofK)liTXydCq4ijNu@Y`H;Kt`?;nNvUu<1>s$|?!ye2WH_2j# zR5m;J48wAj!y7-qG?%Wa%)z2A#<4%)Y}G(37Ni)P&JR8(i^0BWkjPyPzwFChK^ALu z$kMadh7F4Le;0w#>n<%@l9%c|wd`v?Zi24D&Ne89nZwmOg{x7>w|EHz9OvNG2d>p_2{rUF_ z{Cfrdy#oJ_UxD+M>rgTyon8JJlv`Vol@(@#a#bL`fr2Z#JS;h2Ac7`Ww`&`cq)=}8 z@1Wdf{3VM=Ot+Z0zQ>*;S5>4R;tY|jQ*3YHp113-+S_8V>4tkF-(e{^;o3YnqxpDU zKg=JvgySVVd~7Ix~aiX)Au>n{7nj={SxP0rQ9IqH|K z_3hESSIp4tExfgBV7ohPmho(G3SK{wk{Skk8J(V{MM?8Re9M8=`!G;^IzWySzH(QE z#S6acoU)EIW`5#J=?R8Q{c10e#!Si3+qWHH&%v!eN8oFhpFBJYTLlTHdBRKlF9apR z%UE&~qF}za&F6yOvdk=2(iSQkQJ+eCJI^Y9uH}tH);6C{% z&EjFW+lX1#0v_+?Nu7Z!Z=JqZ3Ex~7mdT7e7%1)?&4|Hqsyp@1vB5Scm;NGy>6REP z)XZc5BWWiE*)Z{d_&%Sl-{7t5^G>P5kriAueXy(;gLM*YKSm!(_8)Nk<0I&RO3tov z(Fyp3dWn(_e8a)gmTc!>XAn3AKj*iz6vOA~#SF9Oz>81R^V;DveXxYse6Aq2^S-gK zTom@UUvl?!G{QEujhaH3nBe>t_lpIvOuyF_ z7r4ZEi}4Luuv>qo5$>>j8kz&Ywz^S00ZYXlc$^M<#txnd#6$)m=X4#&{m|1opP2}! z*jM!&hUJ8+8(+gE4tL~O;8F3g&T)9>#P&Pn`RKn+^_>e78=RgE-$WY2@8v77Y=LE? zZ|BUyzf#lI9fes?Rb2=&`yoUoJW z?3d-3_+YR<&wB#zXLZ?pt{eRHoWk;EnB!OT^A`AoTei$?SfD)4n;t30fKXP|1$enC zefmmR%wy~F!|?P?)03-UyNMw!efXa#4Jmoa$%;xPU~s1V-?aqg6;&vF<-yyJA4#TI zX06%&;8y}Iof&22&*q7*`WF|9TyhJG2+GUJo!=lB^zq_2rNu=f zfzn|X>yp2YFZ7>;>%WsWT_kObM{wNXTw95+s%$O%Nn|VfZP0&IO@tqQHoUkCuFR5t zeGXRi-?#KCtS_5t;S2BHDw}D$4e{~hmYeQyw$(&RGJM~LBmF46^nK~*AbexfCk}FH zPNcq;_S}wsBIz#DZt%|FM!9;}&~e$-BXFo~;WRfIsCmv_?{$T@MYRrGhId_gc5FX6 zzV7|x1U%qV|IQyC-hXmvDH^cP|1oX}gcDGTwZwQru6+kysZ`WHj%h2fgiz?gn`Q~VSIGyMJ%-*PiF zIBWWziTa5KYqCoHx+LoD54{`j+_;mOHhc{sk;CW}4Pa zyzP_&4;qkpLoV79HxxKEI>8&Z%cwQLE!Qi@#$dI;km7Wh^;BoeNi--Y>m>Ii!yljY zFeSiBdKPrCaL$~0Sre@K?6bjPm{FEx*&OT{b63;|jxc+*Y&#m5+gma6ioPY)VH?AlB$d+#~3hsrO zJT=Y|dzQwkhoeE4(r_t~eBL*QHDZrp>xf^|q~BPma)}Wi8f;CY`lR&W^`Xtm=CF4o z<&X-jC7!^P56@_S$kBpnre-W>;KeubHACMY4K&y`WOKbI=erd8$vXvB`pir30;}^B ziTs4GU(tMg4*t`zGMXE2a{3QyZgRbZ!dP}`z`Pa?%W7b@BkBEy@F_$7&4cjbn{S>V zY_S^8TkzY5AEU5+bbj}gIvlV)y3iSZXY=X#Etvgx?rK-qMZzqx8vbWdXCbMQ`A<@% zxG-fr`T(H;SwS9s#FwA#k#}}GPTBl!>4x6iM>KRhDPjzA$F+AB{Y|lC7CMQ`$p%@j z<)jMzp9*6;x)05ts6Y_sA91+?ksI0^m`CHkQ!HMjSpVF!{f*IlO#_xmjQgR@gCz;F zHFD=j3kR!t6U7xC$?cjaEhOhXS}BiUJ9{c>J(xzUfHDkkbSkD&BKuqIqUd0W0r@4b zso3DFZB!Idm2)8aOwzl#2AUl+rw| zO?)Ew=lmG1a5aNJWeQ%r;)_>!Aq=4Mp9rYz!Xz`wDoPa2JCam3PiH9yuD`RbZa+>- zcb>whdFa~PL#}`8{|l4M$jPfNjQ?lYOND>_{rUF_{Cfrdy#oJUfq$>S{}optPINsd zRTfGO-2ZG`TdYtuZnmNd^hL$~jHNasM%65aa{jy25Q8tXc*No!xq*B3+(s3xIo>>; z2IU@(&Bc-YZV0vq6%~^H{*q3&S+>F@{F{E(!CspkHW$KHI}}VF!Z}GNdmqEQe(P4= zgBz+Td&glrt@r6^u-KB9`YP^dr;VkGBjXF6ZjWrWh3i&xoU?*63?rRS!0XkgY)HAZ z#qiIITktz-8Ou$u^!>E$g>ZgYwks)D`ns7g-GC2^U$^9h&8o+~l)+J~bXREL*JIt6nLIJS6>n1QDcFCmtyKjMiS&Es1V>fyq;G-UneI*P zgWDY&8n?q))dzeH;bvbk16_Eg(7{0i4sNxw5{GpUn7xvNtNhR3pTy_M&76Fw1~cV1 z(td?S9c^aF_8Gb~o1bubl((`NtQc^1I|q(u95l9G1~09CqTmhhh&lObA|2xiygx-J z!wQyLK7E9fLmT66z%$BGE1$sXTbgLd_Ou5-JQLv-`R&=|aOK(ZlSkmN1H4+b@R)tL zhz5MTlSQf-R<%47A_kwURkfdkbLVTLcwsjcZW-1SXq;Mg?k5wxhW1`^AGUYaGX+dw ze9}pW2UMToxm~|YhTu~=!NQ~PnPlN%q_14#&_u=&oV{(;nf|uzkF)vK;zN%rQj+npr{SG+7?yf);9B#a; z)dl`C<6UzL-WuP>7Y9?(2Gym(!uhi;KjGK$JjF?{_rsjd37Gxm*SR=&b5O*be@fvKc!B(LIHy3sf6~RM$GOioIkuH7i z&*5#>RC>;EhV|UTZdjYCRmurY)4Oza9KPGZt-KGm`ZJfr=8flPb@~x99*@@j>#N=H zuSxOMQm|8U2=9J4sydK`jOSyfqs}-Ef9APh%>u`?HF-UNKXOC{@x!~7D@5zz?9;_U zyzo7*0HY@O9_xWdR@nB3=$=~mSn!knPe|E9BIUhW;TfLrcRld)yTJk9V4niJyWikX z;zEvGrw~{8vHU(Aj?aE{rhPlCGVi7^g6(l0MaJgv$c1IiWISS?#)ItDaEJTQqt|fC zrMe9>INy?<+yi9%Vszf?V^gsDiYo^S;MS{d4_LAPP1^Toa$$xye#UBWXS}G<)GQxxJSk|&PBj_DE`bl?-qOO+15;fxa>i6h2k zr}n~@+gmQ{!jCSAJT!%aJ9!N@!K~#0{)X`IvrV@?u7Z8ty8m2;H%5Bb2*M@C23pr)gQvC` z%iwZ4L;rmE$ERfhTyQEYL)Cpa?(`RXHh4E{ICU*dJv+OQlPL@e2QhlmPHbK{9~D0%^>$+R@CA?3aG$k=dkaz* zTr~8R$`Q`__V@dbza2kw0&er+UD)p6zKJRrwsf^w@C$pRGqLckA6^UIJDob81|Msy zUNGIzZT|xJrS7}GR^Xzlgcoyq(|3Pls$j{bogL)*$Tiv@e*(*OR!)-Z8MCsqxD9rp zl@=i5f0OJQM&H9aZN+MQuw=w)#jmhm|7EKc@WIt5JxAe8YbIY2SllG$-XD1MV^uP_ z-wucECm2xD7-PTikUS4*$x6yxa4Ri;7kOUlI;l?y!Zz$*XUOv;Sx8+f4tIWX6HQsgg=PSXi#A3%u8x&aH8*Xo6YbVr%YcxcoWk(zwPkxWeycP;S}{fDZAi=j~iw! z;S80&3i3WYyO^hW05*CT_F)g4CED%p0w+l5QQ5(!8#5|T!ZEI`E6Mof#gbCN^DN2{ zcpUhyEG6C!?DdkH}zV*IIo5(@5@U*O9~-4p|RYtb~tE zUCuRt=>o?4#bC3aXYB96mrm~$7KN8wzQ{KXmq`Ve3czCHkILQ9P`>)*nv9wK=+anH zoErms8_mC;f;)CZtq?>*_=AuQ)lAsG{EAM)UAV$2aTN#r{Mf!ZLo{^PL~P(9``^ep zs1pE7EG?ECfUAp6a%RA8Ycxh%;Z1i;dB4L(Y3n9);F1+lsjg_q&a^7&J_rANKf#*< zhdNC$y24Ul@0NAL6QTuMY~lKB=Rcd#ki8}&LD(9$i>Ch&3|Fb2r_v|Bq*OwOhU>%$ z6&hXmXwF5DJ&bUO2r0SR$OBofSDVa%m3g` z1UP#9oQ8v9*%(IQ3`W*Vx@h>$K2;gn1s5L{4Y~v_+GrTec$R{yo95y|D@c-_8UFsIrfO6+~V7qMUFQ~n3WiY`8Quyc?#!MzR^31 zs7P@2o!}l=pKYfiiK1*UY!&?qpDG#^xC{GzyZiPFT<2hSxfWiwJ(RN_PO}f;YlSuW z-L?=@wiU&9!xve2P7_CQc5r`(D;npEiS2B+#QuTzFueLoJTc4+1J1<%DPd+Ar#RFC0NgywvJe8O*_kG_~8e(Y2t&SKIcr}pP6Yg z-(bu1LHhl$R`zKJ;+Q$!5I6X#fcrJ#E`u#JXJGEzE8B>@z1#z$;0qkfSO(xZEaP?s z))*PrCjO8sas4{{GLUcmFdSExEm;7I@^Zxegp)nfa!J%@!fbeY9De!IUbX@Lbew1Z zG(5h+>|QH;{^PGFbMVQu7S;E#QPZ#tEuN3y?3#+N@NLHzml@%U4XV1o;8SOx%(KGb zxjB#L;XCxKhnK+qO4)|2i1Ike3pew?@)m+Gm%-;bmn)Lz?*KcUxhPzeG%rH*bF@jp1=imjcf*vI<7yjWmf~06_rhy_ znPkYrf1K`mIKsYk8Z0WXm)7zL54bZcB0VzrQ;ICy7gh?}Yaq629OJ`?b8HvWEG-C{sxKb6=6_?g99doxZ?vILE(q z%mroH5>;yC>1m`u_)dok6CMA#_7?lL!|!_z`sKnjlbq!r;GtKWQwm|#K*c!{)k_FDR(TH|oPMP_ z28Va6d?p8|ICTqLJ@GpYR)?p>$bvlLEz zZ@~5fUN=9tkp~vA`mOgGEas@=Mgne z@W3aIt?-Y%COTQL#>w%!itt45Gxi4fN?>lFIGkM`7Wo2J|2RF+g7mC(=(T$v zJju1bRSOP_Wgk#Fi1d{+s!$Xz-<*@G3%mZlW3>$KQCi8i9Uie(D`A9BjZ{WE!paw? zrkUYgoT+<#U^$A^;W^|t@*DEh)8M0h7cvLnKL;;06~K?rH~lD*eu46O5c%!5x0sX+es;NiH(5bvu>`hI%g3xPX>zF5EC21>|92r7bmCZ;-33$s zp~ep_jQ>}bTll4_pg`eYIz|88Ns~hH%&-*oXrQ6}Ly!?#`X?_~_5>AFq%d zJ#>C^4jx*=@6`o==swbL1-4<0n0f}U4T@Zr3F~i|)yRX@D)@sM;iYbzCg)&dy$h5- zaL}Oj{xdM2n5x&V0u+xmCfg$5^X40}9brL*fYZcHbVIS%;Lm4Mg#BPtMv|VxYwwDA z9)_QrX5_TOw_F&f?O@YQH!@zslL`8VZQv2{hSDzhb%2(M6TW zL8^9rSX8og5 z4hQfGG;za6G}xR}V6OKuU-)5>l!K; zEsqK#{7~0clvu1#L6sKniuyGz2!CH)qB7r(>%mpiN?fM;LuCf$s5Z|bZVz=+nSj-~ zE}vcS=ZmUi@O)H;B{8$_lC`%}}P033P0Pwg2zX!Pn4@tbpo>Wy&E#L{HqoJnVmNAQ$wrU&sQnPK%RIAzvp z8*v_!xW+@6@9`fIVt#=v_4{y(g6brB9@^FFH15E{%~~zQC*#*?7Qh;hFWe*!%JbF8 zh3D(y&k*1J(5rD1zB~ECjM$=xTI)JoH*C9sxNCcWW-6R1u$rE@JNSX-6+YS6Vy6DY+1mj6#?5uozWm3o6pd~G-o<@ z6(_ifcm5pF4uV&`)Eis|-`GgeJ_9#-Y*f0GQ#T6dk%6pW&Plu&SpE|+UopiYv;Lh+RUmfAal=ktq7lq6)lVN=N zT5NyBSQtLqhW9UIdOvx8Y1*TO-oS$O`syS-kr5d!cm_x7RVmcM3FXq0P4KqoG5(ow zzK_ye7dgH!?#MxSrMguCIbJI=X+Rdfx7j7G9(H2cxPccgnY~n33`dO&7|_GD(joS7 zu$Pi)#uKEE4XS7A4dG~dkMD`FuE$jR>`QbGe#q7oW~TG1k;^q*y-z21Zm#1x#A5qIXU$T1zxA%hKs~mB7N6?!b3E)!*y`w6Blt#2Qx_s6i48W z-SI5<;U&A0-jBm?rXuWK!v>zslT41N@ISWCw?GA9fOdnt2<(&+u{j)8rLkBq4|mXU zA0LG^jZ2vA39nLuaG<5;tG3gzOu3l zey6Kq=?ypTxj66@4q+Y=kA}SuH!4lQ94!hV39#>*-{$LarO!XCO-X@UwQcH2fZ?X# zk;j$r;+=4P)GOTr7hJy)p^WW2H_S_afiK5zPJf5tG~ z(k;p5WSyE1yhg!&>M`uWK4b0y7deFf%7d3Li)-_Qhp1P_U534dln#*Ph#!xnxYN__e`*79s zy*pmGV7SA(yHDPb<8!$$*}<2zKCk}+=eJ)E8ACufyLg8#*HLtX=I0gLNqe>jvsKu!#1GI17r2A{XNB*X(NeLJPl4DT8d6(&pMx>;)-o`fI8{-!H| zMd~&vY;?l)QvET16YiF4C^3a+571`bfQ@Iey%OPLw=7m)fQ_g_A}iq?tL_M#hOa(w z`0^aiE_!by4M!N#1^poYPYOay$7RFdj1t@DikE`eo^?ODhYQv=`m|mH-Yzg)D+zBD zdXp*-3uW&$Q-Ti#ie6j^_goqKq66oAziF=k-*s_(Yzpri)m<$EE8fm7^MzTKwrwZp zTV+ID6$&3`QR=gUf7Mk+$HJlqPCY#g-+UHx<0kxSa@LeI7)%Y+e64~HS$ZBP*F#tM z4^=yS`0Q`JT=*SF^{roU?C*nbAHZAtH)+knkDf?4Ou|3BlbSe@BFcQ>|2hFT=#Huj z!zTO2c9M~J`43*UNyCSfo0oFJabxF-)L=g0FVzz8Vv4#D0KgdkG*bUx=-iKA3v>J_ z|DD!VReqtxUwN_q|DSD26n-zc2h=`KC_DdL3)(b`)SikW5$hr&n)08)Xba>2W#t#@ z|8!LRR4j#b5j6YoAOHCY-XhOQb&k}d{<*O9Y*ydile>|N&dpfWVCYGj&yzrfB;@vH zVlPvY|CtL2;(Uuo45*J&`C!j?N|;?5XYltL;`N2wc?Z{w!r`6Impj7umb8CpH9|e3 zfA*{??9F&R>5DNsJKcDZKt5^c;gXLRP5##>f5_#Ta5KeQ;lmid8QVFp-xAO;M>)t* zMmG>nx^Q}#i3L(AyPBzM@RPOICF)={F-84b@HdUzwjGuTbHoeR-6f37hbWe)NLlrVZWV z;A`*n8#CYwf`_Xk;UKZVYlHC9&Zij{;DoeC7i0D!tY#CMdltT$o1$_Xj{YX95ehTZ zbucx-D>=;Q@4-W%Ek$2pm)*%!Fdp#KGF3~fecbw?`_J0J0kh} zk*yJI6qvL^0RGV4`l%duPI2^d?`3%LPA=X-^3*A-ib{>(sgqk+z8F(VEQFin7KRWh z%3+Ef4v|A6(l^2=I|}9UKku)lq3xlFD|lX*QB7JXPAID`j2%*u36_+@gG9lDgn``u zwG5v@&~0@|C1l1Pohe_n5tTdihVQ^-WO7@Cf{d^H^FdmU2Wjz$#XXeV-7LtmNydrn zWfUCoSh`&EA>3^k`C9^hl&B_pK^et?!&!Fh@Wc2oF9zVSp7N`|@P|jXDU|A}AYObu zpr{>|;}v&JhPmI}RcwTtjrQJFS4DiEPb>2Z>^+_wdkTK-!CoH(um5WOz63VYqi0u# zUli)Sn}Az>E-RveAAS^kvqTLQB5A7k6Sy&wK_^0t;r+kl#roklZwh_3s^h_q$g?I7 zR@X=<6Q2eOfO*1th42q^c0(_Cs>IMO8Lrffy?6~Ss`on?3%|>j3|7)ac9F8~xpe1Yrm)67J=i+^%LCI)1dANT_Y%MDNlA8r_lA5hUJnZ&)*7vc6RPcoCE%Ke zhqF51&q_=$g^~%UWHt~joqgL>Ho=p-efj3#Xq9sQ zy>QT!S;-WfcF3UNDEzHAM&m2&@RR3O8?5$ji|u=ObloxDKd{9c#o#u0&aPC5`UalV zL|%qx@F#htPoJ<|A>7le0`At;eAffNsC>$I58kB2D9(ia-J+l56~PRuyZbocpBZ8` zIWXU7QwQ13TsuRb3}5>GP?48xKVNk%7H;E@&9{VuC0N3OU<*m1&DQY7K7B7gnCh2_ zl_{K&A7N+ju9(Baa`0Ufg*j#T^V4xoeYh)c z)5-O4;EtFsU-)_3W(7gmZN109Ie3Z6&An3rc#_3hXcJ+b`xipS;0MR99nOOD*01vW z1#jlEnJj_d?Df=Q!tn(fBG=mB9*TVvJIoVzjqwj`Cdn~I3-8OSH4 z4KWA4I>Ysojf$!8n9&ij3-Dvfyw_2%-G}Yvrb_Tez^aALT(b=DdBWd zAseBs2=4+iMS7*^iLYDX1GiKYi{W(pnvRRGbsg=;D)`j!5|0YFT3>PeK79E2EY&pp zt}-$%1FkLE8q0YL-_Lo;)p$7TBd517+<9bIo-dqlQMEk^{$Ae{&joRB=!n zIsD$5N|znx<#j*W2P+u;PFVsMJ=^tID+d)P({~=LVS0J=w4!Rf*IIOs=UIRo?O!I>Cuue@?x!J_qxyv9zp) zElu89w!_^NPBF587PDi85l1fW=bMq_2XIVbQIaTpY-#iMGPswk_@qAk^O4kkvH+Is zR^y-hU|RwAII^Hr{-KL*@x(&~(qG|K)K?!>!TI5TEE(|qe4dRN_QF?IYddkmZtNL3 z%kogrclm8g3y-Q=vzx$@+(IT#alAc;+`SWUaX4GW9XM&H@q8LwCO~1%g6(L+Pffv# zDVs5)Td51G_BO9(pOOX8PCC3;eHK2F{auJGSatj4-Le?C?}o(gXK*w1H=aV+FJk#| zvLINZTlsCi+jzbPxYXXj$$>`i{Na7+W!V#O{KZdcvvAe6Z+FP|XR_MkuTX%BXgZad zKD@_zK5{3#mvXGi3|7&XEb@XIT_a6wVg5%v?@Qq5^c}nQzshG|knM{p zb=QWK6nA(bXHXKA8kSX6MX5q*q4i2hc3~+iMHN*Q3U`srwkc{Jie5CE(AuYhG;|si z5w7{2&iq^dW)eyZ{Z$nd!wqFPsp3ev)aV~G_KBVym6}N)YeN3bEEbb~amQ_xW#RwK zBz7Q^IJt&7XBI~+?y-1=q0d2yFVCZSajcSqIuY^4oGrOku-Y`&{nc>mo3HK(aN0VB z=Ud_9DLGUQaDgJ-D++wie~v{4KH{@O-59P294ujm1Gx@9aD&xKSIGSgLwqO1cI`EI z$kb%%JNWm5nD!3X&9}KV3f3>#E+lXXFA-zr17leC^YBLz*iWU>-U!xY=L^t;!}d(v z*M#}%4X+!+pT-_fD!`_VetXZryrV9M*ml3a8ro2Z6p4z`$bS-Rhr@wY7OuLfFWDCqZ9qc(4ioc)L zr^^ZM-N{ei0q=dhXO(CYq6@ouds5*S6*pEW!kltD8k6CfRq?J-utgOm=rDXdeS1SC zJSllr&=lU_B%4o@jP6AGye-=Bgv!_KL$E{OYgbv=ST=w@AGZI_ZmMd9 z=;pBf{OV__`d2Z?B(XJ(4%=VxY?nR^Thww5(7UZ?cVpN; zRMPJn{A~YZg+6@u-inbf_<`*waUHncszhZ3ZtdcKr3+`iPS9PFrmd6C_)MJ%RweBR;f-I?S4zP`UWvBB@bHb5ee$r+6T$ti@ILpX z{?)K+XJoxG{2;_BkRKk>);5%eeFZLJ&-6bH^T~cl}LI|IYuX{CEC;no=r; z3HnfBtNoA3TL*`na9&we!e+?)X%v)#CNMy_1pp z`9BK+s`!H!_gvg#&X3L`HVmbCfvTTv&(P0lF4wdC95ODR#aEW*Q`>RioFudKw${mQ&u55b3 zy^oR5&zu$T575^6+H|7&BHV4C!z%}0Y>6$fgl$h}rHjM#Z&E4pun1FQ{&HCCerDZZ zBLZT=`xKdB9^Ef5nqfNC%~tVeQF-aYX9l*M zT8YnBv5+)nh0AtOKc<2s*!5(o;PQm@=}zo#dG$c_$9iN8^y@MjV6k7a$DhDfXL*8) z;J7#SqF3QZbmI9H@OX*#&DHP;$q{i^3*? ziTwxR7h&!X&YVH*qfTgq1?DngKdc4UNM#!IJi^qvS4*kx`=j*R&LLI=Cu;5j4-OO0OAZSX_J=Tts8gD0_d*CDZM@DDn=ffhK(=9h^FTvq$F zx(p6vHhsSawsz_0DS(&0ysYu#^#6L2UDfq2#KQUMEG>_)eY{y#Cdn7Ir$cT$U*PE{ zHNRi^AfQ@i{ACuVwP2{rgLORULq=i0oHZ3r@Cy2epPmtCxU3U{1-9N3uYup3I#SNcL?WC=Rm1w1!Sm9uU4PL~-QZAjeu)A*HS7QHp ziT)Y9ku;)}d&&1*a{K77F<3ljX*2QS8{3x2M5%*GMyG;|ZX9tWStXLEDJU)ky76m4 zvJ0!5I6gb)domNO78ATV8e0gmMKCq?chY~zz{dDy-JRwmUwF-6#;nQcv@ltOlmQ%xT{N?RbVJ4oKE-v`< z!{t^x;qb#bta@idBH;p?k&6ZXBzI^SE|UDg<;8L9PI10P&XExa7g6#D3^2Agq&b@Ub^Hdxu< zPu56@D@fbt2k$Pw6m=YC-ZAsm7H(BIkZ=Mn{pQ9?507@8ZgYTTs81ixD?&A8kGC6H zv&Fb?RkabE@$ifH3i$aLWo`_0cd0p+i!?BA_q!YC;LiA=+%bHAZd(4HPpIqnZ`x@~ z*4WW>zUeOnZ{0mw(f}V$qSY!yEJ5b#5A9m`g6y=P5xkLGW8)LJ#9f7X0I>;cwc*fJ)?s2H)29eFt({lvQ7><8e@(cz3IW|_z33_p(rU%=y~R82GuY_2v+=|CFg|1YG;8!#Nms6|Vms3>)t^j1Pk6C-iGh z!80qCcm~3!cI>Nlhu^6heU~CJ@2=46Z1!mJ|VH8V+q%I$nl5#*`Doz>0&## z6F>Xt(`*FC?eacD+fo{={3jYY%%GabNc2Oe?(dM0Y!}pVhM2CvaJz2)5?Xl&se09^up#>X<`=qO5M;i0bbT{>3b^KpJUV3%kT?UuPw>2=!D9O zD{$;?%C&fStK!^Gl72Y*uN;qt2e$pHCu?j8u9`PE4`1*cNV*P-Ds^X)*reFK9+w;N zbXC4oeO+4uB@U6zUsQg z+7TYqDT*O$aD{5Fy}J*-%#eNPK78y98=EyO@i9fU4Bn$Sa=;wUYfWIUfX~^rKio~W zcSL@yf)gHX;NAf{$2*mf_b>hxx6@{LpkaS_J$!fXRIN4~z-eVm*67MN>t3z~b00I3 zdj>yxRPXVBuy^NCHGY4*z)yn)m858(d63dPPdb$nNl7!3R8)iv(WHSi&^$>((IiQz zsA!-RQdAmMB#KlhDLVK4JbkTuf8TZgxc}U>?p^C=t?!rD+Iv6eoagLkKYM@n-p^5m zz2BKKHN#J?7dFYmU-nbGiQnvcGnXi~2Hwq~S@atI_{85&9R5(J5ZVEcFu1-Fgk9rS z+jhaf?roOggPVPXWr*MK+R&>Ox(rtI;+-e1f7#>0-R$sTTn2!jJ|VkBnhTvIO;t@Qzd)&J;pR}Pc73nY zbY?X@qX$*w^w}jP^PLMFA(e&cSyf{HKLbM@g>90V1AqPZ&wtOrf6u`GUuPhEgC-~C z90g;Uf3L9UyC)kO_YAXKR$JvNnlXOYpY!3#Kp;A*TPZmE-(#7z_}DRF<3V!&OHPJA z+XTj6pslja-6?~T{kJ;YT4C`FXLn6l-u_o)0lZVlNYxRxzFWh82HtV-i`FH$gU_MM zAHMk3(z5}U-sJ9_0UtRRRp)*Q$6KG>7zL;KwB0F&4ZJ=@Hp3=w5~u7k(H6I6nSO&i z=)RtghJRn}|H_2-_tS{`NG9CMIW!v#?;Cr3p%2zwd~ZGvHr}XS@EiX7$=$bH7*NJ@ zQ=SQXUQ0!Kk$0e>H#~Df=UNb)_)=Cs9M*ko)^`ln-K(8&7Jlg9d6sxRyC^!qM<2p^k%#4iO)TMqWL z!poSY(|KY2^9T4w;R}f;2SWx3sqIWsog7+OX*5QR) z60TTe!WvsT--*I;+_gOM@OWpzXDWQmaAYtEX3>e{MMVdWAo{s?&>GfbPq@iwi!(p;R@oEYk><^O6OLt%FNlY| z_t1_q*kSUiYAWFl>^sdac?CXoJ4UTkiIAK%UWu^8{@ zWBmh)r2|H7>AFMW@Xaf=cb>wt9(M*e!Aj>#kMTO{8_4mtY}g8`CtWOh1^@j-JzB9e zj_r8A<%cDGkxtk6jOqu^P$mpr;O8e)g+|~|_D9o>@VbHMlCL}Ptb>jAXSHGDd`>>% z$<#YGEfyDmoBBK-UV@`3(=EKPR=9@i1ngo#m0AU>E&cj~+X<7UJFZ;df{#sjoY8_Q zV&<9baBs))v$-%AyIdtF9O1t%;ssp1sy}KGti>v!KL8I$_xk+C`IwY`_US8JuwtF+ zCwSN9e2G!mQ^COaE&R1Zd7>JAbkX0p5}rNN`K}TkFzvIv4ZDPXd{hjJa~wEw67Kh2 zG+qkJo_2{OzONDeACjT)2#d*EK6uEIM!yzj-69-0bQ4jV;jm{?aDCIsJ&mw=T&J@& z{6p!&q7e9PB*$HE_{yE1DekaF-R>1@cA;16-Mn5L&iZ7v-Vkn6m0r&Y&peUYX9qw2 zWw&yo5HUG5S?N9SA0LUnJ1~#X{@RUj1I^3L7iP0r9&Zb8ulH&6gj1y7iul7G#&;O@ z!1q(_v8WqSrJ!LY8`yd4T+bPJsq4=;4w#NPk}U`Bza`P!cmpl%a@LpxSU85iD-Mpl z>VNDuyd}M@dVmV*)oiZpk-v#^hOvH^UNmYl*J9HGE!sX7on^TE6&( zYAbk;ZN%pR`0d0-Pks2(59*^#_=xrTuUp_sB}SfLI6Ge3;t(ul^Qg)NKAAeyR02nR zIB`-N)}zY&Xo7QoTq_cXH5B{k%isi;qbut2@hlK;pYXe|`n5#^$#6`8c_@)zQ-W{v zet6}*O=TZp-a1VNbGR?0$$@z{q8LAR-sFWdvhH4Bg{x07&-UTUx3lwqFAUS2i*qlB zIg40_L*T8mn|tD5mngUCO!$=6qekNA?;Pu%>Mcdf@nV&o$4>a)EeX-9*HROcrAMz` zd13R0GWp})sw)3HuzexQsiH_ezFk@CpTEe^r-~9D=6+OiFlyscDpetDt={@E#;sJv zHPvN)vQ}h=Uu$8)PfI<5U7eE>L$q;NK=B`K+=%-U`QPg>j`^pp^Oq?2IgU^Mw*~vp zHckU=+}{$N9ijJ;L{Qx-)}X*Dx7UECxqd=HRXoI@Z$!HDjxW` z#pCf+hFIX>XY_d~{5Eyx!;f&)r;Xu)@FM9i7Q2iv(jPv5m&iZ8{bKnHoOoX@Xd0ia zJz(Pd@%4z5{PRhbl1puCjd9JEgk;c=u2PzPOw|PQCTVNSI8c7>;~;+T2CJUd%cU=a z1=}pmj3~(8~?s21n(U)i!d@pWbW5eesx$h!Q3Dj4&K(c+79MA zrm$anBO)nJ-GAMHFIhck_k%4Ca<)B#mp8q6@EjIlx#imp-#KBR_KWaE{)WG$ zA7Ul)=dBDd@!W#-s)1iv-owqtcY3;E52a*_K3I$`KzW@7=GA7bf4_oHoXYEpgL(Ej z*}Q>u4J?0kz>5mhOuxXl8P4}`SRzuSOXO16(wX@?g zqlq@B+KPGw4o$hQRt`(HUhxvbLh{f2)a~=I&Af!13cTaFg8>)XxbY)BL0<3~hmON0 z@FX8~`VG9ydz>v4cG{UR+Xa{HPRu+DCudTHe#5(-bB!j$Qd8r?23F_|#KzuhfYs6u z3}nC&2hPXM!L$JfZtHD`6d$sAL;R$leO6ZEclel)S62|6ytC;|r8Uxb@w1k}2e;S0 zqC+3yYN+MjSMc8}@$VH#wh^ieZG_gsOo+-t0AOL}UsXks{0USQMRn@RW1$=m*wv|u zYQ|S7P7#cjRH;qND|DRr7Mg>F{r_6z`hNz>9tzt*hW+^K|HhwiCLK;n)cZ3Bbhto=$q!zi z>d|r?7BuQ$$b_w5+NBJ`dIy-tuE15Y6Tdg$3fdYu!$qWDw6*1rgX;#hIkI4XABQ&r zxPrbFSWi{MB}NPe7vV!|8dYDwC+Nfb)c%v~t$JA6>3vr|ELd1(R!5XSqJHr+9I#B^E+6h4 ze2}J!8hhK|bioB!*;_)y5H)tbgw}fozS_3+axgp>>h|jxy!M36>UQ{>{@Se`@aUuP z!#b$ZU8QT+?uGf7Pp-HI+i!Bq^oHM*o{wihuXiK;#mfP3TLnM6F>H9J%OVI49=$Df z5ZV*td+)tlDza|6s3-A^wymhYVm(*FRN~R%m+BbUCjI+LIEpLbZ;1v`{e6Xr z=>PQudOpVy>YkPeRKi(l%v@uz0`-7HDQuOJ{!T;>H7kD0&2o75h&$^w*ql}z`U>V7 z`4d+I^L#%<9fJoIo09tB9?#m?U+{@0-64AP4g?N2IMd-BJuvUi=70w_yAM%df5p#* z{BW`7t*?x5LzHBmUuIhA&j$e(sqUlM+3 zAFdDz_h>uR5_Z}BB0dh@nXQ>G1uuJ&@gozyyhJH>HSAz3VSf`o>}zmb8fKIXsHlMN z_HNt02EO1?C)h-!i)k3jz!v@7VV&@FOuV!#{MKOmhhcbg-Eq#fu=$odrcc9rIBo`0PgOOJ;EYD{%*9*hM0P<^U(xETyWzZ3CZ~oM8=j={hwyCGxb47o4Q} zYOglzWV6ToI4m9R#-}gjID6(;?0yIc>d@lm8h}2qU^L1*z8ujK4IU`!yz?rx}_1Bo~YkUX@URN z@DgF&khgHbS~oJiODPC@3Fd>} z2Hho{QoVt0HJsb@j?BM*J~vnm7M>a;)3sgr>DI#>ipxkxey0zyf@f@GNuRv!O4$uB z+bc(2fnzCef)B!F5zb_~Tz)Mj2$oF_ApI*=J>(qRd;K=~dKKxj4gajq{apUBamWUN3GfXaRq}S3chgD|}Ct z-vk$J-Ez4XKF!;!ZwxzLbJhC@E4tTOQejrxKYf#MwF9S(6+ z5oShTI`w({X`()j7{)#1f=`9!B@^{^Kty5JDtKGg*Bs*d_#OF@CIwfmP%9UMkLFEW zQi6-`3N{kgqu1)Bb-J*~E7xv5I6z6OA6K!#zV(Hlx#3*a;!{Ll-hRo{>=IZ;#$IFx zT(m}yeKFk7PFLd&U%YTea1k7=wszNHc%||od7^!JKix_5ht(8M*(l+5x2!)ll zy4@k#r?W44q+?+#gT2S6;m!k+!WnR0g}CN-`0m~D75VVfk)N~UaBv|zS1H^)DO>pk zW@Hs(se{uzM*~OTI~^15B#w9uki~HU<0m|>&0P8LN3=m_~6XY)wQsokz(X6xFO5xoEn_rUn6q^ zj_O$*V*;0|j#5)$hWyl|5P0aQr_O2E;uW8F4}84KkM{^%&@(x80Ar)4t{FRwVXH(+ z=`Gl3HpEjFmj598yB6-Jb#9V@V|@MAH^6I;$kU6%8y}CX)x}uqLZ9!W_X&vqD7EpM z!a-hJq6gswHUHi#u*tq9vp3-tO8tR-a7ytEaW1iSQ~7msekLXB1z*Rv?4u=}pv~9D_e`FYI z-SEHfti+vNO;b$P26uL4avhPn(!$nS<$qSuF8qa5;&!j1NG`NR&zg|(!s=UPbs`Pr zl&Ng;Q_YPPuc^zbdaYt>&N4jx`!BK!ptkUHTgqyYOSCvCaa8Pa`}e~Y@_%;NZhDHI z=1b=b+nX^4+?+cqi80IGap(9L%tHR>=zAS{00B2gtt+4X&z$D=$|-N)yk*j>bx<-X zb?)$r=NKJp@eCNl=k}gcmV^&Yx_vnT=M5aas{{{Twx|w*57D`t34}9SVjkRtExH9Z zT!CLRk99qO?L7k9GhzQ&tI+|t^P?N{ZTMd9DZlS1u77go5F9B?X}$q-hF+}lgJVMtmb`-}>~++y!X+OW zht}bn8R%q-cmn_ZrhYBSwCjZ3sh713ke)ieZHfjT$g-)Dhj&(WvT`6o7%82q#Rgw4 z{Ik~-ws6V523js>Jyk8a+H526yCqPb6ahI_+YH zMewzg#Zi{<%@q^-%3=1fCzN@7|KHygDAd47(JYJuaH1rO_A{9Kd1GHK%-bXWychPF zZz?K>mB z*%S84JI-$kiwGU`CDP*>3u7JN#G6HP`ta?fP4|h2af`;<#WHXqW&YZJqI~O%yvZh9 z!TC(NesJuChyEX70X_GOX!zQeWo*32Z=H2ixgP%g(`SqkcH&A27=i`j*@f;Pe@U<6 zZDO5UnQHg)EI1`q^tT?o==$h)f4DM{C(a4Jx?5b(96t9djl~nzl38!C8n%8OG(*_A zM^$I25qG4khqn;<3pw^MRKli$ibqW0V|s_aT!mG-+Yea6d%d6i2!a)N^GP3q_xYr6 z-vM7=$Ko3Z|Nc?cQ-&+LGCA4h2t3s7S{w}DaD6@F2b*iiUw4Gxu%`(V>u&zG0DrG| zqJ>gHL{MB=Q4v8w6?L)&(j=epud1T4Fw!SiN2)24gArAAa+fy#Ql)b7*}9#&986uW zP}0+xnncfdf~qiQ=(tjQVN9Yzh68a-V*a0z!GOZS%-YTziGTd}&wtOrf6u`G`5A~X z*2g}Cf1+Fe5l^>1pySN_3fH?#Vf3Xwv~v%7zk5|6YI|J2hp+nIcXS=RKQxDfcTJIx&Yb zJ?NnfFIxQZYBc=FF@BH_{(Y&qVP_Y{T#To)Iv$!BoU*sF$$}5F%e3aee;$|kJck$i zvF(kAr4oYop6$jRo>O9@6>Om0C;l21;by5{3(LQ*s9)cM=N){0Vf?%bb6&rGX~e;{ zoU_$g@B@+K(SxuT)BU;txaD{4XTx5st7j0i-3Wgk(#(#AFD1?%UkPUh%O6OByOur| z6@ZU_ba?avPIwrx;d3P(co#peDS|UezpGH|B24Y*v_Akp(v{r90-L^OJM9lM`zsDz z!HCwjnDhc_ru z;V5+l26uRk|G71_?U=(YcTC(2FK;(~;|90Xl^Q6*(gDw|^T2c2A@z&k-vVQI>0zZ0 zvuZ!_`J7UscFeV5&ak{la2mD^7T}|sNKtx5YHG>IfvrX)d+b`^H`OlF7KOKegx5VEZ z*QXo9_TdN`t2kwe5~dqge$}{$6}mh%H@?6UVmx!fFnf8c&oHb~dGY3Ncun4wkSB0* z?Ptby85l`8?f+E>^SW{WGQEVS1(a)7OTnUS-=t(RF&5?!m1@9#84drG?~1Use)hU* z*eHIZVgk%o!n`yEo-J@wZGby;^8>tLZFUWrudpxYrmqfgy+~KrES$f{=DaQZK(AOe z_cB(TW*V6>L|>Gg#qM1FVyD%$pTX`?~&iGi>|2eG4<(;}n0Q9lmwqtHL69#fp!Hy>Mwy&{782 z|K$<3_we#<4U-f&a?6OS4u&%$5R zrq|BH4~kzOn}J(io|$62f{_MA&~6&GIp|!)2Ip_G(D(uKU!OR$49=QNBenDDO^F1lM;z;AiCt+q});e+K`p zxqoXcIYWa{k^{mr>Kfz_No(QRXzFU3RL;R`#;lFiRKs%KH9G{x=^0t5B6`nlhaMy^ zOwB9|gVfc}9M{DhC2__4dq0@eRPXTz`nae6bLBAq+OKfe6Pv;RxgP$r^_9XE^S4BQ z?b`#Nk+UXh>g_Tr#_7e;cY5K$8^1UY!7}_^pWnc@9{$*rEci@X%uFw+f^`^gffL zhgA{g>k>J?!lfNg8>ZoRmG9^Ty%3pg%qsl@|K?XT7l-MdI@hl;#W>wnrfv-!e$&)T z36A}`8CbO9$VVEnRUG_J7F}#|H1JwXvFl zCrYMRbNnh^g_ia=l@mJ z)lgq4W4MK|7+vd^!Y!m|^myfGnCcvoD+gGgf;I&iAU z&s`tjmxXgQ1GwA6k@_CibQ&o#fz{XN>k(UKUzA@j1)zr-C z0f((D6mNppx1XCi0Qau4n5&1wx*Fg6z&CuhzOI9-@*s@$7-C2h?__~_)U@`1* zqG5e9OxI?6|0bOGGeRyMW>%h!D}WigHm$e}Z%x0nFBe|#v6*rW)-E|jy#_nqT5XX7 z)6<0HvSAxWPU%9J-}t4c4rcF!ZVEVUVyzMZnxuorR*mTl+ zq8oPi3ceTx?+$Qm9Dt{@dIrwI0jEr|hT){|>QZN5fmvn$FR<6n1g}uoQoO+W8$55t zRD23fIiD&w11pX1o(h1=sTwP1;hA@yZx6b{Dc|!V$cyeRJRvS=VD{U=z2l!;P@B ze{Yo=Y`%u|Qv=*pMVWGhr=MJ4?uIYdthuENcTf`dO~B0!S+R2Pjzj^sfpYa24P9DR z>!e-ra90|?oG{W$MVZarV0DINXLiF6vn*pQVAcNnmx%hC75ZLdBRn3^I!M&N46|GA zRM>i{OUrxMU+~C?BFwsRaH9&|@2&u~q-F5@z4@9=u+zh`SK|n6WF%JZ-U6o$6+9k+ zZE8;3JOzhd`c&{1?hJPsCeFvU&u8_Xz{i84@~^-HF@n4A!)vxJc6bUKTXJ2@h9go- z1v_D<+wayU5b2XMdN1K4HtnY2@b?2_b;!3^0;Y?x7_ov&G7_to!gHxJW*+dZiuo&a zu!Bb+dk8ES*VFe7p_j3eK;LTE$DMDc6>heV4t)u)Hd^+m26jHjfB7l=ZT0p;6|e`7 z(aRI?Vds{w*I?rhH#R!JA49F8gJFrn4*zZNonvw7?(iQ8lYx!!64q^Bx5H(dH zBDEZr_IUlUJg>Q2c(+{88RGnH*eb*p1ONV56jTJSRy`hZ3|_mz?#4ZMO{}Z>4p{BY zyDhEocx|S;4jg75HrNQ~=6qct2ltuqB)x*)%4o$Bp_H?^e2>1vHqJc7+;EB3<{`dp zY-;b~ogqA57SnkG{yRATRpYuj1>WZ0s?LXWrEt|xU*P(qH(OT2!N<;XNnOMJ!RK*{ zBs}}|kbwa#KK<(r@%l_pY^Wz;pXJI%%5cbchHKHV<|U;};{9p{eXYrWuc+O5u^VQQ zeshL+Am!?x_64Wl%eJ2HHebj2bF6uG8CH7kD;)-B%%xl-LUU70;;qT>^3zZM+=5wG zhn#u{GydM4RSmDsc)W86?ytOj_Yr)O{ZgEH4wmZ-2>85&mG|o2_Jz$%EDHKy5sxRc zkKt>J?xhaHm43^jrr^dl8Cwdz|H;OVJ;J%T|M9EFEQWJW_G%u4z{6ZtS_*K`D+i?k*!W6) zq#V3zWN-Ti*e<=uSrKmit#;xwoU>@Jh6;Ru<1zmv{9tgLLml>VX{nlq*Hb%(i28i< zZie%3_zq`$g$^vuxQ3oS4_m1$1QOQ4NvSG1%y8o}1`h-Hi1fTE2fWXuQhz;st2APC z8T@?Icm;92oQQ~z;e)5R6vj5f^!AQQLh$wcPj#E&pAzq1iNnSvKIs;)quS{4HE^ch zj>BZ=)#x_A0$iZA!qf)ov@Mzy!~-Sw=3Ei7hhJRM+ocUpBu7m-!YZTm^i?ho!~5A%kMD(#O~lK)7qfNUk2%5TtV=jTGD=i#eP=KPo7 zy*GZZX3EDyDSC8N2rFfLky--xsw~?=*z32u)Czc~DcwH8G4ghjLNKRhXDDH_mR1QV zSbnG>gYXBVT5)-p+v{=};j_mZ#MR+l!g(!(KlvGouY(JU(nbk~S*nPczykb-7iGeS zQi?>k!0+{4gb63yyewi1hZyK+6BbrmDdGe-($-oN_Bqck><%xzz;uvsrfr0fHb%c$6+*#fMb049+CHyu*Y57}t z&)D=&!a`ppc!ptX!}euYV5{0J?ysqU$i>CA1a=TkOeP!|9lLY|+;AiM7U4Uu!#IWDxn=vC2p7iEb4tNe z>TW}XHTZvW5Dx_iS2LjzZd`GkLmmD#q0fI6ej0I#gLvTU1`jJ`!U0E*vYWsnW~%0d zbK1VMZGr1YkGYQ4uW~7-!o>x+^$Z{;V@_JfDqyD$E=xR;6ut!v zm4qK?y`+B#GY(zpAnd7FPgf704>>zYIQ7y7x@K6ZH=HdSKGnXC(gAPc@|Gg}H@NS~ z;7uh=4DvOw@Yspi4x~)(_0dozH~BBDKUT&5MsmJ^Tos_IvG9~6WpZt@sus2biO#5{ zAOB`d)u`22e098#@eoz^{k|R9dgmFh(O5N-+0wkPi4x zo4)|(T85DxV9TXTfF~Lw$@0_J?o*=R-R(5ezulhGLSVhM$I1LnSMw=;@G4m~(q1K% zw8QWuUm@w}zz;MJ*u&VGEPsE(m(B%V94AYrSDc8ZIKr;Z*`$w7uAx}Lcc)ZH%QfoJ zHo_}*E*vlAeGbJCzVzV?dAtvcD`;Bq*!y|Xs$z~bCD=4Kj_!(U+nctN^gt83&vt5f!-~UsDvKTH(drqcrP*bPT!Kt|$$aJfs zCd%wnT)XvR2E_MqziEo{4X$>2N#%tjf7^(@WE{2>yBv_aUDX@E@UyF*9mhFcDu zBh$|xkfXeU*J@~!>FTvcv_?4Gwv6Y=YSoB}oS<>C+704y#J&GfA8aUekxU;l zuAun9Ph9m#TlsX-JYdG42GYf*Y!nxG@^vU#-fYW0iX;4S&qDsica~DDV1v$u>mwmZ zp0*J#5#CRhw__TlGlcKDM3Cjriqa_B@MkIynf}yOkfID{d0rvS5LZuI1H0H8leR3B zpoqbzXXwc9`{J-YMF4&^^@uzkSEDbD3r;d$IKS@`L}{$Bp2v4GU8(aCg&sbzVvs!E zVDS&iY(171Bfv^r|3{U1DBobQ26-}l@x_O<5%}TK4l@1clpy6DoX(z1rq}P^M(Ku= z+-%77)8~)ST48hk52SzRUZQJ)Gqx;zzi0U#QflGN7Xr!hytm)d?!)^OmXM$4)))_^ z1inMpLY6n%b&OH~r*OoR`RV0EDOvEnd2P~7u7I{BXmJ4VkWa zAdAKY+Z79v1fFgUruR%}1Gif>;Y}JLwS}J=!GfH}##g zRDUUD6lP{z`2H>z^w0+3g1CkDHpV`h)&axD%@j@laD6ilHH#d>_4R;UH8eyMv3-w1kAca-~+sfyY>D??f^Y>xTYn#dPmvn>a zN??BW5Hh`i?*^p+rtvM@KcikbQ?lTRPlIH7@*z#hpy{?90osid}1C3*FKTB5DpJB|I7)5x85t+9|=EJZTsU3 zPfYY1oP$4?p0)OYZ`mwa9t%4!R=l|%F6BHu5)YsKBfe-aeBb|FWfGhlX1H@Vyk}HD zAr;=`q;YRMtP+~(mJY8A8RW2qKPB>LUxG&;q`F$dCj*Q)uE3fRCn`3<`}ZDxmjxSC z6!93tYn3Ub*WtC2EBEWehSerfd2qG9)e|lFWoi4)8}K3PDFIdZx=@>P5$x4G<|7Zk za8qHr4R49N_)HqEmMZAJ126iPBrXnryTDph3ZMH}?k@;$dYx)p z3ConcU$YEm;p&%p1iN?%g|Wj=OTFi7;Kn@0ZYG#M|I+Kn@Rf~J6*@T3PcF9s{*~?% zIai14N6py332wi5aBvEKV0V4X3z$VIRciva31F0Hf!+C&<457=O|Cy*!95#IM~C3R zgx+WEaAE?pK|j1(nd5RNyyehjY8Tx5TK8xVoO?@g@-=+J`o)H~u$y#_X*2A0Vfm^- z*wSM7>QnfeanAUAc2iLKFrj8o=Q0Ci8bvieCWtF((79DXcyo+FO|sroj2X-65vT)eKP&F zts-3%TwA_~^yJZllu-CB%U#l9!jCC_@P~v%vOFcIPWc!tc`7$|60g_7+Z1&WE?KkK z{|7w(g=x?O*1sTO@e@v)GS_y6Pi!m`n}w&(QsbRqxeX&ff5YRx5u^67k$Au}8oXAp zfw~R8JeqQu{sCfn-Yse7a4X-e&m#Ds9pmH%cw)m2Qx@1UZplVN!dD-yWP{C@HDA?* zMUQGvaKP1t7iQGqFcI&%rEpk(tc4O>`Ql0%H@wKZCr=h;aZugA0#4J~Ns)q;^G{Lv zVYeM6BO-9G{jWm81vhsx^22M7-ra6OWJ2VO750w?E+>#bYF9wQJRU$52+{@+77)}*uVND*+T1JllwaD?mvWgb5 z{-2(qjk4$p{`cQM|2+f$Jp=#WpMkKA2Aq^|3Kp};{_8OVR|G9%Q|YJ%8#|%`b} zm{68p^Jo3}ieF$BdJ#T677$bD=~c@7*LYG+Wf1|GsLZx z)2_&78NmjdM5~%%jU|O!lwl>IRFx6<4wpQe5PV2_wKxTz*j=|Q zJc~2*S$TuMIPCfT`%E9ayv6;s1srIY{NOd58t4-23MZZ@x4Q!42ZIc+z<U0W4w>3%Xhm-24(azX8irvLi?;lK9WvL`K}F>lk$o9 zF0nB2Kem_ry(|1(l>T=#`FC;pe}0!4_*Vbs{F`HU-|nTXSl)if?q+`320vQO{nQQ~{XFK|+=4X>Q_olxVaxNs=`O$)=VTI=!zG8j8T?_7 zZRs;}xKBya_j{^@f zY{)EIk~TF2Q`pB^reLm@!Zn0*_eHgj!RLyY&l7gOsnYoYjtR}PBOI+@)!hegkx~*T zeB{}o-gcOG#e=Uzzjd?L_5K%d;$$*o4R=btuV{l`iFI3Az>>SPKDEM{*UIxY!U3C4aKD6k+^Q+-;grp#)aURe z&!|CtIKCjk{uz;9dE}fX%*(#?Q6qfq>?=(rxP48I#8Wu<9n;Vnc;!mT$a>hP!8KM4 zR$Z4p{uqwZGS^uN9}?8Ise_+uR)6G%E6Ydj*T8qR1ryof{g?KMRKpF8jQWdU({+^a zhw#P^$3Ophjx`n+i${rZSSsV|lxbMcKyhmYY*T#Sa2!6J{j`)Aw|#hXpTbuR_YScAAU0Ura@+|Z#+kWxZkO-E)-$T_3*q+;8_jOQ8!o>zCB~&| zKbHNx4pZpEZsx=0-&(E`8^F06+PR5wtP|H=^Hexk;qb{En5jZ^HWqH)Ci(Un%(nmJ z^$3`!HgA14{3Cb!))1KK%ld+=@FMT0e@?;$t-qEK!pp=f1mX@ZjN{zMAmzJ)#nXDIa#7Q-q%dd?(`q^I>nU zNW+otH|dEu!AY)`5)ruK1+z~IEaABGDIYv!^u0eBwwi70T?!xPEV3ZH=Hli_7T8qE z;}H>euw81wLRUG-0Z zMGcG`2H`h{oIc0HHS$acx?q!j21mk;uL46_;Y*A)jd3uy?^JRlta>a+i}0(O&>J=I zDX9awM4W_i@9d-daG0DdFX8k3zuRuZyJzXbW8kneM@RDE-qh-;^RV37FMo&)z&9V=lG|2Oqvu#5}; zVQyGj`mP$`6Qm<8DOt#eLRFE{;aq;*F-!l_jPCp9Cb6M<~uCS zap7AqoF^CG@fmhh_T5eR+gp~;@8LbtdacB~K!02?tp~p0!LSe)oSUI^!?iUF^R|(y zizu()hxZoZ_V0zZQkr1NZcj2U$8`J?tp?WVUx)*GNKMV(hjZ@Plj*m(LMSD$-c@n( z`2I!NlmfVqzM72VnROJ?uEO^#WXb#o0wXBt@CxCDd6tSF$+UR*hxbAp@mFH@d?b8e z@qIFX2dfEPC>&I@Fi+$;vyI{h4-SZv>E69xXounU&V{&S_%&^cC!7+#5cdyXypgsW zzIX8+nSWaO0L=mBoLHC_els^qv4Z_w7mjzfiX7;WPIrVz6P^!aSXrkR?R`J~6iNdalcy zXV5gQ66Wu}iH!ufLx<=U}- zkKbhRY5taYeDYm!26C)uf00vwBm6BPeD7iD1iY+nU04><2b#+U6@t*G^sg*Uhiz?= zZbZYIRooYc!trxK9@|f$Pw?*kpcWjwG(h-LFy>Zytglrb#k$caLAQJ0TM=*G6v9Qy zTHWtM&?al@>G;9E$x;O=q3Bb0S+#n>NA|sV!WM?jFdfGF*1SELd7FKv5Sa4HRnIGx*{k@7ad4xTgvTEVhGxic^IOA~5C<^8bDqu9BZu z^^U|nxb?y2Be1l{zN`7Ljxu*6@p&xG%@|ChaL=uCufGDvIF0!3f^)A_WL$;wd9I4? zhr14%t7XE@Px-@lMq^CCv~+(8+&F#ke%Lw0W;+<217UxG&t)UZmD`AP?W!G-k72n-0}qMU?+fOvWQ{>h$aGzI0OrdO_t^kV?eBR%?Vme>J z+V5<6-C-vypC;p2%)K&N=xu^uxZmI9ABR}|;fqqruzyuK=Udopr^ypxB3(uRs>SjT<%eT4@6w#U~Cj-W3x7?`Cwi8l8! zix*29zP~~r{glpsUpWQ1a{irj@z%QS;}e)wv`qggfs*0YtlJXc$W3o&tYN3>TODWN zdmnR0cEcL^N5qt1^{zmsFnCGuteX`4W0la)Xn67Fj%M08?jEP*jW5C-YenvN!HIHD zV@qH^A7P75IDS)+>q~fbh(b;~yj_e_w-c6@U++>28~n;B`3^s6@#IN|S0A(XqHD)! zk!gf20p>Z`R=xzj+c$FU0&LP{&MFFb+5Sl;((4q<#g$-R6VbFlSX`;US&t}RIKIXL z4nL(#Sr6w4)!0pZ#b_ij`^6?$fo}KnO8C;=?Bfowl?*p$34B$X*TV%qbNC*|Sy*rT z$~rH2JsZ1+Elg*|d`f~Zh|eERfY;e%QUYM+qZ`&6Fi0W9q=;qP({_ za=@y$>roX4kc?5>8nfaEI_P-LJSBSjF#-C=a|``NDxG z@Qq~iSi%vaW6K)hJEm;{yzo0Ov8WgD{f~T^E8s;pY$96WFGqz$2ydyj4{wK6vRty4 z!v{N}Pj|t^ReL1~KdX%i?SogIypcy7-?F$Sco3czE0!VrKiIqPupIx#Z~QHlkx*%q z6e6^j=+e~Gprs)V(%z+^snRY{8cK-}ku+5(Efo2*!>zKLCz)=f9QiLT_&0Q8?@6nnk8(^NViB9D2 z@p<0X$q+t9o#e0z{-C?&IX$efv%zjPELyitf$*g*FKy^xy$|Yj>*2x8i`ELtbeaji;vU;X2PZ!gv|@!vsN#MzYafc=x>ar!}x|R)e7+TxG&&LU^(`MPC%QG-d2w z4QsTd>Po`a3)<#{=f5@`I|%DVYWLH^nQxo555sqgW6uy4dA_8n0KaOB8Ki+Xg^?Xq zhLvKQ>|VQ;W{d%t3`0~ z_R}Ug@SEY869sUf+oNZ>u*~exmiutWC&8V0aM}55-M8VZLgyp$;l-I(**S3dL;ha{ za3x!a)pg?d)#r{C!4Hf)`O@JmkE!BP_!`UJp=7xCwLE79JWywPKM|JG`yKEUKE=i4 z6bGw3lKEN<$J8_kMZx8pqEzZ&wTQ}zFjymW&HYBWy3wsD820?G$It?|FXejs!>X>U zJX_(DmOl>q!1)(!I@@3mvmP=}_%YdlOgp?S{#J$uoOSf1%SZSLy~Pf~zr}`~x?!<( zkM6s}l(BRUeMJ0$JTb!b=8bmWiTI{pE8Jjpu@c)KFg5ROCBoTRm1lp!+CdgguJBzR zb?b4s&9p$5a2~U*)ikWmdH0hGY#hvIIS)UWx3?lpm9gKP3?+c0Zo7Us!~0sw&8Xlo zJyTc0h77k)(!%kw_VZ3~WkizkIyn46b}-?#oDic8aEqfA9bqM-2t!7Av3^T}BW%jc zV!#6Liaf?ncq`>9eGd4l-l;4H_yp6~F)r9X=gJWbMqKnw&mTJF`N>VUaJS^lbsyXhs*Ep z`)&wd*iD+yfqe|PXiQ*QQ{4#-n50_2!wijLHj587L6mh2dq?CEhULnkGSOGK6$~92X+OF!e4LX|8{{tc&_)h zJd7zDcl9r+!hLU=ezwDXTe!`n;OSwWgomG5Bt6tx>*${!QYKC*~?&76UzAm@cuz1wP~2W!faX|Ubz#qrTwN2 z;pM^!R##6an(>yWFN*%zsu;hPS5O!Gvzdz6@@{7MpUVq(iTzoCT}(iN#QZUg?`8Kh zlH{0l?~-~F)f$qp^Qh-3N4Y=6m*w@;MIB_;SbXgIPUuk<2B-d~ zfwAe5+t+>P{(eTR@rl82hM+zwOt_e78RB|F^l--91)@dX&83SY!SKuZ$I(y zmpzuX~vctdV;pbZ?Hd#?WkeDT&g=`i>nS#aD0m+`+Imp zRqxOO+&?00z~YW6x&tRpGG$^(h)p-r_rp|^M~md)p1Ae^S=g;QQm_X8nQ8q{0VWMO zA0?JRX>xyKU<5PF=-Y3&js~e6y(diJSV8a0M`6C>bkTb7*tMV~cX-dXd_yzX+(l8e z96rE2RqPMT1vMm(!6$bH`Xs>f?jAc2<4(@$@=(cuo5{rm zXESVYYTmE_7ON%yu@M%r{IZw;Yot}%Z-FHleS$sUQy!`prQsl*qDN=o=x-8r>if#uvwB#GDS`*>g#VJf?^!;Ns< zg;$-LaPyXc;u83QSHi3{{O;Sv$3?J!?uGZ~;H!J`OK-r|DfH!mu)XmPbsu=UZ|}=+ z*r;}xQWNgq`Zehp{J8jb1Q+bN>ZVBxyk3WM>sr|P*!$=~c(Gu+B(cQP=J}&B?_tvo zl1Fl3weB+;KEmW(uWb|HHuEm;nH{pABH22X^i33w#J zHGDUJ?1RIObvS6jdTmdg)Zi_1h9*j|<9x8aG5lz+n;o&F(P@^Y2}jtiY~7J`_$o)6 zZYbPcbNP8T>}B5984p*QQl&=0E*{)ocj1(X$=MJ%JtM012`oxU7fQT-$i!Cim$2ez zcK=wo!t>erx3Fnnu8$_{q3>As6?Stym3;u7DG2l#gXg7$50T&kkt;W-y-kpK(3!sXFx^x@1QB0SkJFb}@<-ZQX4f9L__bZ>Mtnc)@_JGNumyRjHSztGe;WosaO-H9YhjvQ$4g0xHm+;yye9~-=qQ?2As%Xz36 zy21n{#gUCl{wEt<9_cSCCb7H=nb@CQ$Rw8+{udMcvsVe40Z5#FC)%~1+#t#ClRYrl zeujc_l*G-d_mIT!<4*=EMvN~N6cHx+|0yZflCAckrpB*-KmY%|0->Hq{Vo z(6F;BX_8SkGmw!D-8(kmXod-@ZF(NidU(75lp z1mEHO`dJP>p!&k{CHydh(t`r-Ff3Q(d5q--`;!%)AiiVu;$;&!ROaHtbl8H%kt+}O zxpj~79vuJb26rDEaH3A-J)CS?_Clx#L%A9o2S;JmYwWiYVQE)wTjKrr6^&hJg4JoX z&4~BgcW%*iO)-XgmWX&}!^a}GRrA8-dxn>x9i{E#H0~!3xx$(`0KHG301{QpI^SB!9UeVrsAGXr_{m~L; zh-k=u31@e|y%h-y9vNX6hF9*4$_}?H=2$ts?E>faw+L5!F~}AKD`n{Cb-}&reb19& z1>NqCGm>R~pV z(t0mI27V{+uq_K7;N#{|g|%}tc0PtF0+rl}<>K~`6m0V05pI@U#LD;XMH<2hu&uHX zr2~98V~0%`Y!R&_7yu83rq`vzV&fk_r^5%F^lXUZn*^uV=fXXqx5nh)8*A*8OJPm( zt3AAMYHCz$6KurXQ#^?8&ogb%ryFkS6{~882fKuJ4#D4iljHN@l_wB+g{UZ|84MH4 z#jLafi;$SNn(`U3ej^FVcnvxz2~tpXleX=(z8@>mO5_5FLF58L;u05Jl<3JKF>ZqC z?{O2|m9}KfXHfAbGs@`nLT^Aw{V(u9g z@EJx7;RTnrISi*b8tMX zoe$BE(%)P5`7nITE94h79LO2RW9o{pKH=6$qTk9wqB4RGo^`Ddy$MV6kT$ouU>3sA z^jc>)VS@khHF)m>Q*nCOs-CVc5}xv|$REcilhgEk?*`}lt2z+<+@t-@@jcEcR&V%q zw-J83&hFbyn2x>vSqc2X>175td{cR&i5+atMjc(@gi*QEQJ-bu{Xf^gc86E4oNwy6 z^~SL9hqO2agh%Y-@8*ZEh5h_#4QJnzjAn#=jn(~KU^}}Q;RZ*{el3*UkPAP|V&8HH zzM{k)O}xJD7fTZb`1H@cKic6}9lg#B@O|bd59i@*wVCPf4j8qo>(C}?`skg)TRaI~M2o5~S^*Ine znfZXd44x4;ZIOjfM3K~qaR&#wN7qopv9}M}6XOilC+C^)+oG5x5*673Po~A67_dgM z@2G;!FIZLV&R`+@^6H(&?{E(NCzcfWppaW~Cw$oVX;ut;?7-oPVmLX+@r@@;#{J7T z19ofYO(nsJo4@{whN)*fPkzN6yFvWK7h)WS?Wg9${ji2_c^P32!$OD8@Y=WgcL&2_ zli7Pe!U4BlTnvHTQfZmn;KIBZOJdx{$`df#r%wupiAPO9hPUNqyDi()8(g5Kxnz_B_`rRQB$o!#!BGE#IUh2s2J{k|tvVP{mdHD3clV_fuz#r*0(%XTlm|wm|g0YmHCH z;0!Eq;&%ht0uD{v}2X8a_ycSPsx3+iQb~xt_KiMW&tbRXfH++WEV%fEuU&-u;du-${@H_Ww2UrvG_W>%#8!`(lRE~vmN1LrD3 z;2o#896S!k&(U^WB;p4d`Vpy`BiDi3Nl*!U_-SPE=$V~a~FoVMxGuUuGt3kN9;j@HjIDu!7- za@VHAa%;BMynv-Y-}*rK#)qqW+u#R>a&KINi{tdJ^upXT?amo6WAdA&QF!GJm2CW` zJOR7(?DZhR*`>WtQ6SakcUq%Qyx!w$(Vq11>u+Ha#QXgapB2pkAEKjWy#gO+D$VAD zOTL88B*32Qt4hRREx8Y!v9P}{+iN*kqa&<53iizt{iX@Cm9I%7u3v_&_JT3ISv|@( z6c#FTWUztfBR7}?!-Wwve4fPdHa;=|aB0o_q7QsZ{aXX^{S@!tzw{E`F10V*1I{8D z-lcTM{X5u|Z33J6ow*^$4?ITHR%att;)hwuB`nVuW)mhbKs&tT^@bZois?K1TX65gmBYj<^ChdniFF1o|^ zCZ4{;>t&g%O232mkQIxGAnonRwpcvm6&+gonbnW-`DW8jkk2&SUh@bM;!nI}QnQj=}c_ z7buDNI~@H+KKo;|;#uvSPk6n}-H)VK2cR+VR#MAnSoMHpmka#m)YV@9Ft2S zaH~qCV;TH+O23@TV2=Ma>@O87~|;oAW3!3}<+E<-4UkOuzf5*h_fjOlPiE*f5ijG)qS}YY=Cy z71{)UXnx)mmM?A<%RD* zy4^|G?a2WfVfgtwiJhC^d&eyJ55rTn`A&@R#2NWv4OsXA>B(mJ)LonVMzEcADGe(u zX{+LN7Vc(M(_x2mPB;pA!UoB;X`Jw;TJ`Y|cxuCmfvs@XDQ9f>jjT-XgD^K-@1^09 z2_L#_cb*5feCi}|AO4Z`qkaecJxFb~9G=Y$V&a3lJ15H;VD2yu(>+A|d-{GK;7F6E z+`X{P;IBhpVfj(svBLqJgIj1}cZ+FlaCfuqXP(y{jcf}t@-GZ?0 z{#PUhI8IqLl5ogE%#&^K#usO#2&)!yNeRG{d&0g7z$eAd79E1kQ!5jeT~H>Z4v#lI zkRhBKfAHQ3_#j#KPvZTs*>B`O3r`zNCKJBw`7y^6-emShk#H2N)AmsKSIG45eQ=7k zWPA(}KmYtqB0NSRd7&15)}UHIglpWaxxsz}MTH)X0#W!&Jq=qhoD`qJN1L9;Z}^k&{*vwRi+2_iLFn7hwfI#{#Ao-)@au-p##~>%K4Dv2o{FlW zpP0E=m-v3x=jF5I!oM~uULwB#;yl{luIiZ9DL_|2e7{0eO3@PN%MXhC;VuFH-q$PY z3tw#!IVuUuJB=vwp>O}qZSgC_^$Z`-;R%2lhP#7^Cq6(x{`8iKK_!4xZjrBj}bB+A20l8e{+F9 zo1P2*DR>AA|0!c&YeRf0x zUN+g{1P`?_=nuh4)Q4%kV9%{4WW@Ovnp|Z3VZG@c@hDLKP{6AcYoqN zbPuMq+%%s88_9g>xdxBBU7WrOj~m|$i-U)+3XdhjJUtKigJ4VD%fm@9mxA4ESJ*>k z;}CIvwF?j2t>9~NH@;qm!v}5I_2FYY+`WnLY{C8tW%%_OgRV>PWb3^70XSQSwmkv9 zAyG`b7mj8QYm0|%yyx?_zz4dyUK5|^J+Ft>1~`M{(i96vSeH$b!)%ju^)c{VSXk=V zY1CWupFWR&A)KV;cBX83xlvZ(${_hs`Y?1rhkA zwD{?3aL*IU8++h{D4EGTIN7tllN&zqV4M1V*kIlJ##M0O5%yCx@Fm%b*)l6M#H&mu z{D3dmF>j29J@xJ`3WVbI@);e2;a9cQ>DI78;t|qu`1iLv>~CRF1NBiEc!SUCjbGul zstm<5cmh1AbcATaaKH2_QGA1sCGe|lfG@W^^duU}r=%NI1!2i{N!@&Sdx`rxU3ldQ z+Izd_O#pm8BKTo3!b_VPPtk>=-5@Vlt`S}wb~ToQ={fG_Y{Kie`RqBJ3@7SJ^>M;H zdk(AAz`;uAUD%2D_e-Z?6s{S+YqJ|3+jZ&m`ip2dr#?!j1c&ABw%QFpP;E-^gk!_0 z$9>>SeWjxQ@P*{V=aOK>rrlN!@TV6l;aqs4%_Q1*U0}L4XU;=#YD`0%GrT44=jwmSYlFu5yf{NK zkE$Cp{CL(}j2ouv_K@5H>(}34;DX1u4A1kx>$_i#aKcM%txxyBjo{q%u@SE=u%fO}p%zaIog(5kE@gPS=e+%Cf8 zSx>$%;QCFS-WDATXX^XT&B134A4|l+8fS;93D4P^&tHMR(z0aDzygbd&B<`j>g?l0 ze9?6pVO8+&k>T6l;r*^vy;Arle?9CEjfZuve%N=LhxSj7EheH+alb z=y@s3aEo7w175ik3N3|`M$v0YslgLhiSUqk#n88~2($W@c=+!2bo+Gp_QsKeuJAEl ztxvk}AYX94Is8Wb=bJCMlivOCdZ-Q8ZlI&fhVwK0ie%vE+1 zNL+Ne{!f-nwib6ke*Mq+iL}u`<2Y(S41f1V`f*i3DUiN-iUIR z(v0@bmcO4%6MUqV7%N9ilf>jRaRynNwW1pk;}93)`4_N|^)SP1Ga6_1JC0Ih{{LorE=_Y)J zW?%0o*j#?^lLGj{5$@1gc=y>P-4b|5aa9e)4Rq;D(o^Nb-u-XylEZC^rBoSkVXPt< z9b7~nniB@E*1laR4^!I@x>&-kq(NpASb1djfeDRHXCNbMUFpTgurbEw*{}l1*&!|*9bFty)A+*f}eU5pC{qw z@!%TxZC++C@p-o;8!E2PL8kQS!8$M4C(dKK94J#AVqwbdPtSAVWG z0B??dd0iK7$k;wcY{rzzV=Z_H{uQ`g+7+hkmpIHp#DDd5G7s))u|3QUv&8Qn>V$Qo zJm1R0NhesMXJOYlDN0S)F-Y2Ub1vTh^tE(lc&7BxBsbi;X)~=Pe806ej1{()ZQsEQ zuiC_Nd<%SXAmuI#9N$#`Sqhfj+P-lk%xLTvkqHa6k{x7#`MZ4H?YoVUj0J){VsOX) zW=#cnWYf@;2%LBCQiuVZIPd7g2TS%!nOnkCc2w+a@I<}b_cQR2r~U_Ocw6TNepUF& zl{@K8SmdsC_bpcw_+k0?pSR#a6=SzpINp_g$rCOcrlGtGr@m~yY7b|#?cVbWzI6RC zt1WCLb~#e&4$@h#(`Vh_jU}J^Nw9^>X*NyRAzSH#CM+_z%0m>syGdW$5;nM&HnkT{ zFVL2cgLMb48xo85`D7Un--IhKhjhrmjVi9=*Wi&Fc7Anu)f7WdG3-_~eb52Eqts~m z1?HoXV+n^-X8c9T@8bQ*Pjp1V0V-XLhv5zn!zvFr>V(@pb+{>K)9%yo*qw1BFF4IS z?4l7IZEr+X3!iImT&Dx~KdXKF1wPqF`m9XE&;Ksh4_}z%IL!y^mVOF2m4_CP-<1a# z;F%*Ij5Xm?rlYchNNJvCJsCd%ALQideFw9*?YIyD(>zEMX@GxvY!AqT6Q4I7Z-i}C zujPM-t*+*W{)V|?Esw9dheG6fx$V=in62;{1=ucs`oIv(E_CFaIXv3*knAJ;YTS{v z6pnXGoT-Ib@9bmhgq8149DM=rxq8)@;XZ2ICTwNRaEn2DyCSUOR-WDqH|Vw3n8JQL z-8S~X`zCW{?!c6`#Y-LVU90AtayWQ~vW5uXLsojb2+n&`=MR%LbY-zooc zz9_-XPbDZRf$UH4&z?Af;zFd&uAkTL(q2cBH_P(1e0-CFs+`1i-8*I--Sa=$;PN#c-%KyHFwA)m5Na2lFTiY8{Hl#D4L>lTsC6%cgx|GcYI0Up0D<0j15}&!9IiV$Vb)J2Afz^{MhnL`o**^;kVWEe2?5r>jeIO*l zJq=c(I}+*#)14e1jDb_gYnZOUN+u3_d|{fC<~0@Y{k`8#pN17O3XiwJ%@2QXItstD z4xrV#7=)x>3tQ*F` zPzmeo%wNS0t3)SL&}E>?z0D|$3I6yZ=`1l$J%vpE{5qJ_^7;+2HE|NpE#^7gDdVcz zjThkz*NUoM*tqU-z#TZ->IrifyrhyhGzKpU)~J4k-Md&n660(WpDa-l;iN!PL8yU}Ai!SDJzXp3Q%##Fb{aMgOt&zUgW#XvrBShj|9TO=GS ze|^@EVsT&0+Z9 zb&8Wl@XC`Z;;^e;02ca^HBOY?8jqf&-vxUxPS$+G@heYmlEt^+Tv%pe#(y`$|1KRa zrzYY^O^8zVa?_WHsE`newIV=&O6n0wNM3lymB&R?B`bgZNlCx^65;%6Xu0P? zO7eGoa;bgvSO8M(+-9StH$`agd%;^h@*SlWrwq*t1AnI^hmn%3#8^49wX!=l5@%@k zVEHnNm|MJQ=i`tr2hf+_62Td4&T88c>FlEn1ubbfcftCsKGIFjj`Rz>@bz;t@n7H< zQ!%OAVOG)0L1hn-Pq3YTu@~-&zUfg1Te;R2?1Oh1oZ@~3`&Vx3*bf^Ww|@B^z9(h5 zBmkdp*zWNe-pN?SD+K2@yK{YmO{sq>3&Yu}z750hgsz|i@%}a)7I&M3M^{B9iozFO zgl}1duQZjGh{2z%5nm)MZ{H3@g`^RUU#3IBz;0fa}wR z`ek6rB`!8OcwTI#I2g5Zg@F&aB{j%`wv`6;Z@I>6|<8tuC(q0xr_%es2w>%s# zJonrTW~6GkrU08~RM}d?CaZd@72%E_9?VYg&Yf~YN^p-j^)pX6hWiG+GR(XGiLF0; zuXUA}3j9Hzku{8PUA}=T{4!i{M>rgClQl#Qjw#IP>xJJ%WPa3y0~0<|Nk2kPp!kf| zggE}dPUTT}NOG^O4bC{mE<-ot6TbrGR_ouQckUz%aOs*{&$6~de*5@4896-eFCZcU zt564t64o`V5R!(ESNWYGyp|@<~+FQkz@M$;Z?Y40EqS7_O@%10Ky23pM3$277)F0>cffYF$ zXo&Y4xhID$7(QQ~Bt`hLfNNdgL$RXtI=btKgNCqUcc&iSTmDB*x88fs{$$Pa#Kaxiwx~SaNx9KmqaP`oEZn z5Q$2ROd`Cbi?r#{o&(iypHp30`Rh+|wLJb`L`>q(_$M!$!E&;GBOe`j*4_>e0x zR*rb~aAZ?2&Jf?fTkQp69E`arOD&AbYQgvXBA6xYaqm!T{5aKiBDk^}aGz{mww5EY!c_t|%%a&U%L` z;TQA#9|XjZiSbol&W0QL`Wt=3(L$eMRqX;>KE15i3=4SA)R@4v%E7P9CD7Z+hwS0?VL7r2aJ0kFga#~| zxnVpDUVBw{U<)kcG1vGE*0uiidnz5pyDgJ-?_kpQ$dn0qA}V%`n-p5)2duq{VMSG; zLv!#Q$IiX+@M3y&N5BD8_Tn$yv4Yc>D*T^Hqd4r<8ab4PM%5P=%Xkl=`}T;sg()*m< z7AY+qu=p?!XBI5jv=rlf7>(F>>e$Y~(h-fDnPu^MKW*48V0Jr)bE0ziJREBo^Tw?_`d~2{`(kwNnqwRA7;436J-#zh5Jd;``^}%yd|Isz`}K0aG*T4brc{9!+14 z3c|g5gOo||M)xY}DwwJ)Zy^HyTGgI!u844rxy*2Q|EbakLvUQCT0}5>Xcw0#j}rPM zfBT&z-tURZTQ;`v_GIgwZt#20k$ZPxvpn7oEBLaEv7F=)jO?l_^fZBG4rR`|z~M!2 zot5A>0vi0SFo~-;PXo?4fAR~NGP>Kt>Nm;3K8szk8{wIkZM*is9OkEU+2FzIz)322 zv8APQAH0y`cxEOAjVqbF{-&_;B|5V)*i&xJVjUbOE^GA*b_i=tU{XQx*-Q0a2kaaq ztm6Tn2?%}L37@>m`XU)_JKbR443jZZ(iFkovc{U_u#<@$c{zN}()@h^tkS)=rv>&r z&>e9Nj&~wgU89P|(bW3TU|7qyTG$*G8zHwi4L?42i}VF1XN$B~f!BQfHYu!*Leo!G zyVbB6d&)#2{K;&~@8PSc-c8AWa#GN zJ`&6!A6l>mz8&%Vj0n7)JV3}2p2?#>OF@_+t2+i(uo-vhLCTZF<7Jl)?;jN3*9<4q zbsW!yPafnGsE37XOwX3WnL69)@4|1Tc5oNN{eeY_$%Ow($&d+)2}pADpG77t{Ab-A z;XkAAgiuT1PPWq_zuZm|aXq_k{*EkVCrLond^SV9oXB<&kEkUGi%W*@*Zj};p1&tt zII>VPKC;JXi37XiYy&XNJ<;!gbSwtAIwncC#{Hdu661STj*Lh=|0EJe?&l8;(niev zQ=LyQ!I$2=x7-KQsWEF`fvGl~3jFAg2@Wc1b#ZXS<(m5jFwMXh+Z6Z$gGC`XOwDK2 zoecZ&J1n@IM=uSXo}n)+%fuzx?}wp{VGLEy@JSzXW>;8iX~EwSZn*nEhYr5o)1+hu zPpVS;wE1Eg{v!uVb>JAQ0R|bksyAV^JRHT>XEuhRr*=Jtl!|bPb^fE<@b|R0Cp6(b z!Lpyl;X4T>mR|6WarKTV3|-C5ElrJp%U+c#2*Bri6($Sd*S6$Kffx!q;d;O8Da=Gm z!z>H4OeQnc!H(n8cj>&4fK>XfD}|N97zA#3qE=wNhgm zE4V$kP)8QwJ4puB#&8PH$mb(4M|Xb!F%(~yaeO0jKA-)v{%J1gtytICeF8pf^ZuGM z{K7%r&=P)hHCtX5RyB?b_krIC5Q>1><0*_Wx zlUKo-wH5ns!>4+S7OG*|))zXeuuqz5dj*_s7bUR^uJcP4BEseJo|8{IA;H(b!q*CK zeoME|49ogPlM&Zbcdo@T2p%iB`RpZpOVZxP5Z1k!medUYl417P0CUB)Bz%V(_l(pG zJ7UO27@PkPtX&&3^%)*2tO)-N$C7FA-h#V#-g$HgPv9C`=ew4$;DlI=CLFrA!I%%G ztJry72acp7*P@4a88ZZ(fJ^#gD5o6oJpJkk^@f#&Gaod-v}q?qUudh94YSrk#^GyQ9J=(n7Auf&i69S%tQeZvGpNqTsj1TWQmmC2GM3*258%j)PAXy%ID_j8*DZ(P!RkjFERf}_oXIq5K%B^e8svA^OCtPo z&kLdR$YNT*+zTYEVo-aw96qe6d|4KDdHKp^ml~4rCHWX#cw=M^of90#fSAGePq5r3pR=J9v&3U4`n%SYrW>iMGFRaaqd^&S007MXuT@ntGp&R5u*4A%#@S>J`v3HP5T z3hU8F3n_`&n6+;>6L)E1#GJxKJL3JdeSPa}2A6xb8b-lW!yO!#wU7cgDtvhWYq*N_ zebB}r63QfsZ?IHmuyuhB>cs+%8uYjW&YJAW=!Weto;^#LqwS!5*fF%|+2{7`Bf?)# z?(KtD?ijA?U(Qt=NB_>=nVrP!&UGHQI5z0w`mL8!CE}NyuUN+pyKG?5UIUlu-V%z2 z^9Kiy_v7`w*7zJ6fd>bN9bUtOgB;$vdT3YJExx`6jxyIjLZOd#iwj@gJ%f*#&6bzI zJ!`z|O5q1>`fkAnm?F3*DuB3tjhpyu4;tbLeZ1M@BFsYJy*mh=S-qjb1t#lhzQlI| zBQg)?N+`n{$aQEo8zDu%|E^IQKD#jb=>yzn%$-hx*^6GSQ8Xs{(+=;}fS)v9t~r7y z=isH|p2YXN@+5uf&{{{l{_+!ydjBaDIYdR4Yx<(1e|q|t*Uu3V{8R52T^_wBB8hSc z^YsT8?Kl)k2ezH@YVe|_pz0;<>CP%1`ndVeW4&De$IMTn{!hO42pRtU`}ubT{#}88 zSK!|j_;&^VU4egB;NKPacLn}kf&YhBAY4@YKMi_+@6%eukmp7$ZF6aB8Ts@XG$W)W z%5@jM)mAJM@Wy{t>uSrF?pp*BSy2j+osP}Xpm_- zkiZRBuUAUVhgntAUJYX|a;B~(l^M)AZ$DQ9Z$4HDN9ACVDsIsftFvV^YvJ0^J*K?c8;O1McO)Bt}N~JTzoCMa_C#4R;!>5j@)xr7^`^xvj4e!fm zj^O>}jL043f(1zQX7O-6d8hqaSou_X(pxxWXBusKd%}OLzHvhKfc0N${o;hh_Wi-6<0X?^$!nn}qY1bM;X~!D2_E%T(YV)2g0O zc2#a;JO&GyH?s#z$ulFhH zNrM#pHQlqy2oA}(BQ6M2TP3}*gFmD(F)+eLTE$yK;k^!u2k79^r0z9$;b&5%78~Im zsZoAQaA*6Ql5KE?PA2^hJQd@v0;EE>XGa|)(ruRywr9^&I7*SK9n^I7u&862!LarItBfJYiMYnM!*R&g^=Ujcc0={|wlfh@=c&?u#FJX?9Zk;ap&aM1Tg)D z%5c2!qd6U{B0pr+$(=A*Z&3oGyh*;x&BYlK!$(+e*Rs7{|{e*tHL_;WCg^) zJ2X)L*O-I>3I{S0@|#Bkug?ggqbQiB!{VI_npJmgBBi^jM9e@f)!u#ZPlM|hMqz%opJ&6+rBrmIa%#ORrj*B;haZ7WFPF_S!w>jcs7|0u z$@7cCeNOnEiNl2ybP=hfx>D`%yLGEuGtfoDAIq?Z%MAmp^MrWV;8iIPwPj(GjaOc-Mi)=( z`+f+I!0Rrp(J_S2Q3Q&O!Rr1@G8*vM2?2>I_=wqt-Lmj&()|N-uwQ8+lNg+ITk+5m ztQK*Dd^fC=JSjtoZ&3MKSs*9;+;^J_6)Zx2LzEt#XbSaS1;4S1`A!B`QjgrFhS$D2 zm-5>VueVdLiw6G6u|vHNZu}5SzZ$lRVjz15A8ni^t$`iMVkq0-?$^o{gx@piJ#T<_ zCEe_zhpP*81FGONS`|x-@cQT54nKyk-sa$9gPr&1E!}~O$5lyN;kue{{y8v3a+M5W zvx`+^NiZ`R_hG^e`e*&a;e4`?<>Qan94GgNn>AUM9arh$Zws@_S}uF{UJG&)c%T35 z^7%H;(FYub*^JGW9sYcX>@a*Fx^DS+srV#+A>w!g!DS!Ww@AhVe{_4e?ClG8{Fz{y z{hZ6bW0OKo15b+;FQ4z@6$$@oTa*BL$CtzVsrkqU;h6TgWf#8t;NJ<4*pn@r+wCxU z6MX7;=(4vc{PwSaH?>bKpD)?%)Y5&}ZeDWP6}IGFcj3&WM3XJJ9`mQq1YU!M!-Kil z;Ry|@i}A4RPySwJ_)@GxTnL<6{w|BS{?mI`CwsyL`TkZL;r2wE>o%}|mGiFk@KXKL zTPASS&*4Gh`+RcQ`hgbABHVa~xF6D=%9P5(3TZd(iTf$#IQvs^n0M_ zIQ`j=-wU`OpY7Z5iW8Q2omwyp@2LIqVFO%i=;Kb@&p*G-bW_3}+6ofm@XUnbw@Di; z-CS8a`wNyT-7z`uc9qqdFwNus5gu4;p8FW#!DLta&F~COGl}rx()wRCa7+F#e!|>ipB-k-;(C)c zu@atd3K$uL^9FBI5za{BbovNedcFG94}beL@~a8{b?^Qs!ioD694q0{^Rab=OAe0w z%7-N~!XFVn!9nGG9cJ5*l1VsdYU^kMtkP%|MOasm+BpR7XgcRfc()tJm^+-mJ;su7 z)gwvgGq6RCu@>RqHT1s?;FFK#4-rl;dF-SLSIF+=Bb;|lZ%i7lb24Tk%(i*I%RadG zh!O?i7`yksx59VJgnstHdRHV|Ho_{tV(o-`612xD;4e|yF9`Dtx&sD{6(imBR7bW@dz~-fbVh1K$ki z(IC91D9kkt_NNw*BAoNPXfhgpRTsodnDUd3n?G#(Re2L(JMPj6C-|hY{$elOfBuY{ zIs8FQbAWLC#<0m_u;zrvJHkF?Bkqc@*`e@fgd2RWO^U+@b_M4Veyx4poeyreIDeI} zWFX%(8~nci*#*K8H#>NWVL=N<(C7jtXIh&mX3` z;ppslqJ$kX<~>_s<|XRwgqieqPCtkBa!1z_7O~{;DumBm;hF7$my|1~a$rxD(yRjJDx4g*upo1E?y>lykxJJ2~7I! z9Z0z8_VL-HaPZ$n>m^*GH89r+-+rIo zLU`m>ws#X8F!Z35unt%3Tm^hatL7%*_c_hJ`LM^BL;~SUITQ<-F#VX=dBVm)BR&Z* zlZ?78;jTS-^T9CxE=xVaCJ(mzy2H`NiE@NH6?^7S!=a-0_Yr==waL!_&bT+tPIzI> z#YLh)MklSKY` zJ+eTD1h;eKZ03Z`nHo2W!3;ZQ#JJ%n23DcxOoP_0d~u!*Hwg zIaWnDDno;=4^H9VC#4Fz#JDuJ!wZc;hMI6AYv)Cx1#6IewfAv&CncqF4Qwc4nqml- z+_<-zXo1?O!dPw!tA@AN5-nVX^fn(Y;7J;#&>Yy)W^~>f{?hNNkP2H=AK`R>kNE7M zj)zN@77x0@$J%pWgu==EkBz+GfPEi>ec?5y*8BRwo(JL;T;No%nzSIe>tZdXHLM`J zr6LS=aFuy(Ld4I=>578oD?9>@!KU*|i}CPug|Ms&d|R@3YZAOtV_B)8EZ6u2x%pi% zgh%Ypf)`@T;{dR^&hmm60)O@a5EEKn8xWHMNGxjGcBGNgNIPnU9C%&3AVZ~6>90zq$F9}47ZNdSwov+LRyrE5 zIPP|Q2**O1x8wSVg--svS(g!~T{-ee$L^oRkvqBPUS0h^VxFiy{`e?7Q@ixO2jK}b zg|CTuk7I9C$k?%z3a?p~-@R0fGfUAdmxudOUL03~E8T*2Yr&=r-v5ieI}fMod*DZJ zNJSx2NHT;HGS9;~6-`o6WQ4v`^4g~${#G?+4EOs0$_QRWH>MVZPNMI}{r!3G^W1y$e4eNC`mD9ju=d((?=?62-@+)9^6+G1m`+Yp;NDF%izU*Zc80f9 z3Pmfz_c!uhj)cp0kJ$9zz!JWDVvQfd_p6IlZoqZlHAH6M?`cg&m*6;#ii7iTnD!Q0 zRe0$aYS5H=@WA}}PpawhiwjhI_JmzWrHtlY>pv<7YE_Mu@`R_gDNDhL=)U`1Bs~TEb}dQj`@G|3z5?hLX{h zDA^wSUPy_{$SEnyD#^)`mz6Ij1l>+0Gxen@w6wA# zzo#%*q*QU(<)X0M$35m;gNLsDss6`0kv<#COON&ceP+#YW|xjwicvApWABM~a8V85 zu)q;rJNBuz!~OT1?`Xj}9EX{H!8?0;>{r27{kp@NcX7L`tDqk3!~E@1b}d&}{w=Az z7Ph^+<-Kqo8f0e~QX}D@yS0G@aB-g%vjNN-e)1R3Jv1QaJuwl6X)2O3+~Kj6`{+mC zp}8RaB=39JCZ}V41x7xR5ML}85n7d$V2Z25NhbR}7M7M^YlDpiHujFKkh z3lLvur4<*v-Lu_E9lp6jR(lO>sp%H#0zdz7Fkqn>;oy_{kGGcN zajQPkqW=x1I9#<=UtWRR(*-4geE55rhUj{D?P!3~9r)6VTLb&y9%@DoSJroDmss`VH9Bxy4}8po>36Ws48m2fn=gNY4W#W&df^KX-6z*qqWZ0}_rOb7 zBE5uN5zekwohpXMwVZU#;830$(YIkv&T55nSW4T|Ckei>Sz!1n{41_ueLT!i&D2s4 zm)~LYiiSs4*WS&7!*mn!?cuJzfh$Gu7E6l>S8@0NqZ1%ITD!y`mo$wJtiBsk|6doq~_@4f6>32(VGm`;2@$=jVw zv+$Z(%@i9rO?USBswz~MgdcshhL4nvtyhItNBNSC;Ho}1y3=r;-ZvUa_z+FMcnYl3 z8u*W`Q$mbRCcGmLnhj3Oo$L$GhNzfe4)ZPwndA;D*0xOAe z7wMzq4G)`pc^-c5KRxXP=UxfFxes0{nI)z}pNhesu6^w`Ksd?0edAY@)W&=}R<43Q zIHzm#U|HUwJM{3U@pDga!Vf%$uL!`ex|Ca;;h2WJcRS#vlAiLog*%NW##>5D5qBCn zEcs9TBP}II7I?0zY|Lp-mbRx}ckGbjvb3dFf1a!;HyEta9JcW=GgegkFYVELclcB7 zy@bt79M(vgUc=Aac3;u6aK!zSl|Y;%QJW05Ha6pz2xOV%%vkI`d1GN z!?d}3gmX!T1#)n7O3BGN*eiJGH$8miP`khIEwm3P zJ#EO1!VSmuD*X{y+RBzK7TzsBYikQ13AE>RfZwaOkH*7+h9*Z(!NMBj=SpEiWho9R zIJoX=DrY=e)|s9i6Neja4jHP$bsI388>V}7>7WC=HM4h8AKuE`Kpg=`{I>5jf@|83 zTBO0HHTjNYm?L!;X#m!?;ajm4{&jtq$rQY9Sa66A4hS2P{s|u(&Y>%d!~k7}t!>mu zL4(1v@0{?kmd}S@2+yu6Q01iKuSVg(F@2x68oW zwgzmLgu}LXoK}JB!#|k(x{4ddf6kDNkj>$j@P+iT6g7k|)@}cNiSW;zZep-bQcKVt zc*pUz{A_S-m51+QIMTcJ!OJmxpUm%%rry93JznBHu)B8KPe-^gDf)Q}9GuyW5zUxF zadC%u6MWsJ`C?5N(x+8zZ!uiHyg>XIETgYu76)Ja(2>mq&sc_?J_{?YT5oVS6z|Wz z!NUNySm0e30ne*Xj_!u(XPKFeVbh5VZ#*^z;R@WP~e_+Stob#*WHZ-?(x4*hn5YyEE|?}o#! z?X)iwE~7^`cU#D5So@a9 z<>>(20M5R6ehgOko48a0`=4T4uMao8yIVnmsS{>=L}1z9Q!G#YaXsv8F<%2`1ole0 z!c77TR)obmEjC-iZQH5jyG)Kq8XOkg!}Yf@R`9Px8`XW;fcd}sr%v2T7H&<)%QK$OopA0s@D;A z$}csUK{z2~llpS_y{eQpaXzdhLu$XH zgpG>caF?+87l&PAaLngCCgS{k$+_0@2Ii|c<4E|8(Y#&@T=3bVo$#GaeVq^Cu`OR^ zS>S8qbf))U)ArIB!i5*l^(VvGn@`RX-Z1j+R1{nzZEVO2zb;zw(GONB<*y(dKl1sE zD}1!PjE40hTV+=9q>(tB#Fk!OWKO>MX}m z#?(#6n!*DI<16`2VBR|q>vd_^NWn_K50-LV*3JjVSmltlEU|2r4k=|deB+pk>le6E zJM=LFOy#xq-72(U8fdj!P2rx-Hj*-Ok!a)maNTkmZpn@N_z?DIi*y}_d(KHZY(g7z z;=w1fZSZq1$1V%l@@rn<1Ni*22K_YHKiOR|4yJWFWcM0YDuv}3;-$7VTDrtQjeEp|J1n~@VuQfu&0DU4opHer`!ZNM4*=4Zc^r2@T zagVR_B#1uWU|J@}R=9t#{&Ipn8o+jQ+zf^7n{&Bz9B>biK51tHv#4INBPKFm zel&4R68@!O@b(k@&?nuAAMTj>&LMaP_rQmuOk8k<#QoDQFpXCBS87-_?$ir;NAy9z ziOeIOVdcc-c6Py+#1jo7V9RlX;PuWJC3x5<-3UI%_bK@xJnANyvLW?%`Uoe=Y$a(y2{shZ|5>F~_RlCS znLj&*p|y%Ax@Dy0$lM1Cemc*6B#UGx-3*`Uq#a&*MJcS6{%q4HqZGho#7uRCs8amh zW_3A4G4R`dBqk$wrTYXb15;78K zaKzKv3!iNcVo1jbbw(kw|JnPfcEi!bG#6swN3r5!-{3VzzID67rOoe2>vZwReMRrM zAuPD>Xrd)-tL2g{0_z7FUuuQ5+peBk28$jk79#247Vf0wGmPx&oG|dx8|FE>Q~C)! zof0CF1-or3H}`|RR8+!q{cvkQD@GvU5cHWj7A7-Hk273BuMxlYAW0KY5G9Mp%e3NTre>ErwDAN#Tnz9_c$ z?EpObmMMh+F78;|plyIa06)9~VfB5!AI6Cz~)e+d;^RN!_h#dGT z+RO>IGdD?4hnLPwPE_+N4tTsiU+*NsCEjoA$U}Aw7`!S^1Haif6D$w6Tr4Zj!{=4x zPicO^nPmI?%q0%GSO70^aT@dW{9gS8d?i_!LH8_)QtuC~4=G z3i!rVueFEZrJO;@o>GDmtf{@FwbiwaRPq^uly4ZHnCi@9ks)5_tvhe4{ey-TnD zWJt>BL0KiKU|~b71H+FD`S+-2I!AS#F_dPP9ywBFW8_m;A3yiH5;Pq>aU_w;|Gq!$ zL55s9;zsuK^)`s9%Ae`7fOj}SwmWbdE+Lf*8KAuDJJ!Ih54Xl>BwmBretoafhJD4u zm5!mj%ij3?xH_!sD$+g*S6E!#y$NPInOt=g<=;uG1!iI~Z6%M$1Ui&|OW*9EkH5tb z9FVLsEP(L(m<4MtlyCVw@{-$NuR2e`izvUiVs~c~pRd;%Gqn-r(anQTJc42N0P}tk zCoJo?xN|ZSR!g$qnF<$nJX6Sq`2sucke$(Jvd^G73;ylf9Fhdz{yO#P9-O^7;Xbir zpBB6Kz9zVO`MsG`*yhx0i$PdKgk6(e;l&sv} z1Pj?7-|-Y~bx5D*az%zb61Zmqt_W{7-vz(fE_`AE{?S>Up9+T$+_fOKv>p3(bu-5~ z^qJo6OkD$?98OMt0keOIi6E?={8?v>8|uhnG!5%vP8oMGS@`=;s~%A}i?7IfFD#fD z7IX-XlC~|jh0}jeXxPEE^}99?!S9rWdsE<|?b@BcVJoNY%n#t8Jr&Ay$SKkb(w`r} z?#V(b3~<8H1(jOZ=}~L%YIrL{iQ7~7#NsXqCU~Ovd~rSe_TCPCPMBph%KI72<;A45 z4(?^3Q)+`5>ce?>;jg6dFCB2Co#Ag{N48EAs=RJETeIZF7T`5chrGAJ zX}SA;|AK#Ppb^;xZxNS$NrfxQ`=#}>eQ;4JO$IHjKojVy2giGkonH>`iYeJ(2+Pn> zAE1YyahTSd!3jsHHm-&X{oKx+fRiqdd}o4RRL65z!xOqe%`9-l@5U+z*yu!ZB0GF5 zXXum*T=+TgEGJw-zRc_iKbZ;KwGIxfNi6k+jcd=W=Y{)|smuf5i6ru8KG^8XmX#r} ziLp_wAbhfTTYd!09m{YmhSdTUl`n7$;}It9iqIB(_Pb5jRCr@{H6$Fh{*r>T|h88G^zT-4yDl4_}h zA__=F1wn}wC?I7hyRG9N%A{+hKW%}SJxyl&v@Ewo&Wz04H?oHMrzvgC(kn_isrYA? zb$NvV&LjW1asSJ#BfG0|+W%Wv`${xtAWrEgqRd{-md969~=Lx%!iY$8Vm&RoVRkKpHv91;(y>GiE=Pgl-($J`lEZf0$dbFzka7BAkr11lE~FE@hOX=Q^3P;TBj zA|xybQwt2;Ie>Dp@7cX1I=C}HAm#f+-$`154(j|TYAee)}dC?}WB zTupVnMgdHwI#>M~;TPCC6va@^1+8YzdjtPql+(HgM<=de8h{lCU3zaLAMV+yD8`D< zb6)W|Zx+sdNB5TSQqEAff4^NErsPX`L5bx=bM&7lNg3=6_^1CN99b#7`y7|$}pNCy!^lU&)>nEnJS3< zUmjw$>eE)dLXFLuPQJ+tIfSnKnzLpWQh;x?RWUsY=jli|fY>v>1mrLFJA7|iAJ zP1z7WzUgS%C-~BcaG)MMb>QCeU-15Z;|U$OKUgWz1jhzK{+VH4+a?vp|r3sj*~Hg8RE`tEr)%NFV~cXkKVq~8ii|JSSgT| z7XEmsJV+1MHglDIPFpq>#;7*i>I9e4haazl<)=S*y1*aTXB~O~#|<)zM8mrlzr8Gi zck*`BjKkkN=T}{T(=Vs^hoWp?)}P$79gg&7>u~1o ztv3>VifdowTZnKdT&R?!(GE{b>Rj=IQ>%S<9>tkf5m@E76aM?m6M0=(R#0Ly^1A$= zypG*4iH;a~B8yk1WRl$*$sCW~s6R;bU1qZMiju{Z|8&GC%Y|$@%1jkQWVpZA_7>ET z?tJEnX7kY;|LlwZW3y2Pmcz7rga6KOddP4~G3y#a=DuD*!{C_}->&1xoOSgQ#CADC ziP}m(E}$pByyw8Wq4taQmemw!@ox-9#+mrC=wb3ZFOFK2i=ZrMQBzP2U}ObH;397sAhNc`_6U)8{DFQRDbGZ=Oq?g;__k zytv^rhXpF+;B)2G%9`+v4{6Icz&_0#XAi>CJ(kw=u&k!iFd5#*7pc5}?T~uZ=sdQ; zGq>GZKEi=3)_M_BK&aEAPd$U>tScPU;qA-T)fdCId!%;j!c(H`3>r>%K?9Msytw#_;4_wR1*r=9%Ud8SvVDPZ{^XqM}~R)x`C| zZ>ymJFQ{K%?1R6|RqJdd?D=kb6RtN`&#Mn;VQvBQ4HB^Y$IKNo{z#9LBhNKp=JAfA z7x2|_4ewA`&(z-j4*bN~lAHmTZa;n45xyMi8{G->nY$Ys5XT2dIjEpKNHfa3sty}v zddD1qeb~*~;S)S#a5;Dp7PzPXZ3MQJHe4fz z@+R%oSV=p~C3u;`0uI~jW||Ar8V7v54AW*+R1({4f7y~2bQ_M66YREueeLqj{epGg z?z$`k*T3g<5kmPkM_rxG4h!v9Q#XWb>W{!!1+(BW!vfecS$XTl6ooTQjEu+*SJTbizqqj3`>y{x;&{}@=V}Y z3PEdg;N`0eCrI#RpX1!OU_O@acg*l;beZ)Pcn4#sLneA^HxA~u&R_`XE&E8~%$Yxu`Pv3c)6tzKR&Cs{{>xk% z{BrZ?K3RCW;{&@ZJoiyPpB8?zeOuKoSn{dk+kMC>)(gVw(@44B8>~#kVP4Bf?Jk(l z#>|)lo((g6UID+|@NE+d%q%Lqr4WAkrQqHQnD#(TZZ7QWMXgF~_^p@QFOdO%Dr0>5 z6=s@xk#`&3s?d610-pWZDHRWApY47<0Q*hW6~w~czxhsf!)r2!Wg_8LA=atq@Ptmr z{SbH|U*F>?%vV~ka2ZzntWWy@cBCsWy#U*L$AlNcro3;I&cjib^=q?WuN?mhSNK!f z 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⪼GkDN{u%k&-FJ{oHz=`+L{-{r&HE{jTf2uTOux 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 z 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%0DnD9rj`m8s{sv7)Jyj>Imhi_FIX5!fRu`+n1 z3>=+CIIJV2SuQE_1P@g+d{S0MvZ}!;rQa~O9m9{OQLhv?n=?mM6wW3MUMYr)^Hx zJ_Y$E1-z1uq$8L_pAA11USUdctPGAS-WL=Chi_FgJyo1`urk9{Wu>C48hlm!sVBJ? zmbOu4)Q7j5vUahQ3}=;E9NU*-5DBKs%GfhdX85eEV{lZ>bXuX1wh_gB7+xzYgI(3&wqosC$>FMI_^t5r zO+NOx7LF_47ZjpCe5)EfSA4AadsbN)bzSil5@SwOy8I2-m6dm_YVchdIJ(sFld8da zrPs$+CM%=fE4@Cp;@}$Af+*vw!OV76HN$;nWmBt~>AylDYa|xZ;J`8jG8<|9q-t7&4yEewpwqa#(V;ML)GWbc= z3_q5YBCTq0WEnWNvr{2x*ch#>WmPj=SyoQ8s==4VuSFr+z&BoYi86h(*yBW@n%Q7> zknc)pB(vVQ7#YT_gNQdf-Nf;&FM_gl7LIMskyt2mn$-x4OF&YznmJX!*h*_wGp86A zqY#bYTg|N1an@N+YG#dI7O&&$2KrY9x*)$h6yjQZs~JldWZM@jgD%K%I93Tbe5)Dg zg6xS{2QH}@Ll?Bp_E0mXE{Hs}Co`vo!+UG6P+Zw~In{ZwR%7 zE~w*p1PW$H#?S?=Q-;)xp$l4PpQst=g6vxBOdmC4=z`YyOKJwXAj4U?(`p8~Anywb z(HDG&GE^7TcM&F3S)rv2>Sx#_@Z=0-(0aR%nlY6@6w;B2A+XO<26-?6D}*wrH^ZaV zP^e}MZP0r2keV^IK@_qYF@}aVXuW($%|IK}?~f^J=(d`HHfY=*?M)Sg-1z!}P*l_0KWpbYBIj_vIDNzFhR)URcepOrxw)ceBbC2JSDpwSn4ACUy^ z1G*r?S#RA^GtdQDSxkS;EX6?;8`Nv$O-C^fraD;1wR>7-se>#s0@40z##9H* z41iFEs)KrkERF!Js2S*k`e5^L4_XFw(C7`*v7?BM z5AFzF9y~L+DmXJZCYTBy66}Pt06z)58rT`w7Pu^s3oH*z3yenee`Uaja{>4GU-UoV z-|GK^e}n%-|5X1l|6%@aewXh%-!8=d-|f4`cb;#pZ;o#Q-WAx_SK%vBK3CpQ9#?iK zo0T(^mC6j|NW3?&N(mzJ|6}jV-iN$5doT57yi2^tdPjJ}-tJzv=X=k4p65LGAnyNs z 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 fCr;lUYLZ?~o_~1R!`$)fR`{ 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 Date: Tue, 27 Dec 2022 20:04:53 +0000 Subject: [PATCH 10/54] add 'RoutingScheme' subclass of AbstractNetwork to set MC/diffusive related variables --- src/troute-network/troute/AbstractNetwork.py | 68 ++--- .../troute/HYFeaturesNetwork.py | 67 ++--- src/troute-network/troute/RoutingScheme.py | 240 ++++++++++++++++++ 3 files changed, 286 insertions(+), 89 deletions(-) create mode 100644 src/troute-network/troute/RoutingScheme.py diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index dc5e2f72d..530eeb267 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -27,21 +27,11 @@ class AbstractNetwork(ABC): "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", "_qlateral", "_break_segments", "_coastal_boundary_depth_df", "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", - "_refactored_reaches", "_unrefactored_topobathy_df", "_segment_index"] + "_refactored_reaches", "_unrefactored_topobathy_df", "_segment_index", + "supernetwork_parameters", "waterbody_parameters","data_assimilation_parameters", + "restart_parameters", "compute_parameters", "verbose", "showtiming", "break_points"] - def __init__( - self, - compute_parameters, - waterbody_parameters, - restart_parameters, - break_points=None, - verbose=False, - showtiming=False - ): - - global __verbose__, __showtiming__ - __verbose__ = verbose - __showtiming__ = showtiming + def __init__(self,): self._independent_networks = None self._reverse_network = None @@ -62,17 +52,17 @@ def __init__( """ self._break_segments = set() - if break_points: - if break_points["break_network_at_waterbodies"]: + if self.break_points: + if self.break_points["break_network_at_waterbodies"]: self._break_segments = self._break_segments | set(self.waterbody_connections.values()) - if break_points["break_network_at_gages"]: + if self.break_points["break_network_at_gages"]: self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) - self.build_diffusive_domain(compute_parameters) + #self.build_diffusive_domain(compute_parameters) - self.create_independent_networks(waterbody_parameters) + self.create_independent_networks() - self.initial_warmstate_preprocess(break_points["break_network_at_waterbodies"],restart_parameters) + self.initial_warmstate_preprocess()) def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): @@ -211,16 +201,16 @@ def independent_networks(self): """ if self._independent_networks is None: # STEP 2: Identify Independent Networks and Reaches by Network - if __showtiming__: + if self.showtiming: start_time = time.time() - if __verbose__: + if self.verbose: print("organizing connections into reaches ...") self._independent_networks = reachable_network(self.reverse_network) - if __verbose__: + if self.verbose: print("reach organization complete") - if __showtiming__: + if self.showtiming: print("... in %s seconds." % (time.time() - start_time)) return self._independent_networks @@ -328,26 +318,6 @@ def link_gage_df(self): link_gage_df.index.name = 'link' return link_gage_df - @property - def diffusive_network_data(self): - return self._diffusive_network_data - - @property - def topobathy_df(self): - return self._topobathy_df - - @property - def refactored_diffusive_domain(self): - return self._refactored_diffusive_domain - - @property - def refactored_reaches(self): - return self._refactored_reaches - - @property - def unrefactored_topobathy_df(self): - return self._unrefactored_topobathy_df - @property @abstractmethod def waterbody_connections(self): @@ -645,14 +615,14 @@ def build_diffusive_domain(self, compute_parameters): for us in trib_segs: self._connections[us] = [] - def create_independent_networks(self, waterbody_parameters): + def create_independent_networks(self,): LOG.info("organizing connections into reaches ...") start_time = time.time() gage_break_segments = set() wbody_break_segments = set() - break_network_at_waterbodies = waterbody_parameters.get( + break_network_at_waterbodies = self.waterbody_parameters.get( "break_network_at_waterbodies", False ) @@ -678,7 +648,7 @@ def create_independent_networks(self, waterbody_parameters): LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) - def initial_warmstate_preprocess(self, break_network_at_waterbodies, restart_parameters,): + def initial_warmstate_preprocess(self,): ''' Assemble model initial condition data: @@ -711,13 +681,15 @@ def initial_warmstate_preprocess(self, break_network_at_waterbodies, restart_par ----- ''' + restart_parameters = self.restart_parameters + # generalize waterbody ID's to be used with any network index_id = self.waterbody_dataframe.index.names[0] #---------------------------------------------------------------------------- # Assemble waterbody initial states (outflow and pool elevation #---------------------------------------------------------------------------- - if break_network_at_waterbodies: + if self.break_points['break_network_at_waterbodies']: start_time = time.time() LOG.info("setting waterbody initial states ...") diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index f5d3cd236..40d43fc4e 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -1,4 +1,4 @@ -from .AbstractNetwork import AbstractNetwork +from .RoutingScheme import RoutingScheme import pandas as pd import numpy as np import geopandas as gpd @@ -89,7 +89,7 @@ def node_key_func(x): return df -class HYFeaturesNetwork(AbstractNetwork): +class HYFeaturesNetwork(RoutingScheme): """ """ @@ -105,27 +105,23 @@ def __init__(self, """ """ - global __verbose__, __showtiming__ - __verbose__ = verbose - __showtiming__ = showtiming - if __verbose__: + self.supernetwork_parameters = supernetwork_parameters + self.waterbody_parameters = waterbody_parameters + self.data_assimilation_parameters = data_assimilation_parameters + self.restart_parameters = restart_parameters + self.compute_parameters = compute_parameters + self.verbose = verbose + self.showtiming = showtiming + + if self.verbose: print("creating supernetwork connections set") - if __showtiming__: + if self.showtiming: start_time = time.time() #------------------------------------------------ # Load Geo File #------------------------------------------------ - (self._dataframe, - self._flowpath_dict, - self._connections, - self._waterbody_df, - self._waterbody_types_df, - self._terminal_codes, - ) = hyfeature_prep.read_geo_file( - supernetwork_parameters, - waterbody_parameters, - ) + self.read_geo_file() #TODO Update for waterbodies and DA specific to HYFeatures... self._waterbody_connections = {} @@ -134,27 +130,20 @@ def __init__(self, self._link_lake_crosswalk = None - if __verbose__: + if self.verbose: print("supernetwork connections set complete") - if __showtiming__: + if self.showtiming: print("... in %s seconds." % (time.time() - start_time)) - break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) - streamflow_da = data_assimilation_parameters.get('streamflow_da', False) + break_network_at_waterbodies = self.waterbody_parameters.get("break_network_at_waterbodies", False) + streamflow_da = self.data_assimilation_parameters.get('streamflow_da', False) break_network_at_gages = False if streamflow_da: break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, + self.break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} - super().__init__( - compute_parameters, - waterbody_parameters, - restart_parameters, - break_points, - verbose=__verbose__, - showtiming=__showtiming__, - ) + super().__init__() # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't @@ -217,19 +206,15 @@ def gages(self): def waterbody_null(self): return np.nan #pd.NA - def read_geo_file( - self, - supernetwork_parameters, - waterbody_parameters, - ): + def read_geo_file(self,): - geo_file_path = supernetwork_parameters["geo_file_path"] + geo_file_path = self.supernetwork_parameters["geo_file_path"] file_type = Path(geo_file_path).suffix if( file_type == '.gpkg' ): self._dataframe = read_geopkg(geo_file_path) elif( file_type == '.json') : - edge_list = supernetwork_parameters['flowpath_edge_list'] + edge_list = self.supernetwork_parameters['flowpath_edge_list'] self._dataframe = read_json(geo_file_path, edge_list) else: raise RuntimeError("Unsupported file type: {}".format(file_type)) @@ -244,7 +229,7 @@ def read_geo_file( # ********** need to be included in flowpath_attributes ************* self._dataframe['alt'] = 1.0 #FIXME get the right value for this... - cols = supernetwork_parameters.get('columns',None) + cols = self.supernetwork_parameters.get('columns',None) if cols: self._dataframe = self.dataframe[list(cols.values())] @@ -269,7 +254,7 @@ def read_geo_file( self._dataframe = self.dataframe.sort_index() # numeric code used to indicate network terminal segments - terminal_code = supernetwork_parameters.get("terminal_code", 0) + terminal_code = self.supernetwork_parameters.get("terminal_code", 0) # There can be an externally determined terminal code -- that's this first value self._terminal_codes = set() @@ -289,8 +274,8 @@ def read_geo_file( ) #Load waterbody/reservoir info - if waterbody_parameters: - levelpool_params = waterbody_parameters.get('level_pool', None) + if self.waterbody_parameters: + levelpool_params = self.waterbody_parameters.get('level_pool', None) if not levelpool_params: # FIXME should not be a hard requirement raise(RuntimeError("No supplied levelpool parameters in routing config")) diff --git a/src/troute-network/troute/RoutingScheme.py b/src/troute-network/troute/RoutingScheme.py new file mode 100644 index 000000000..195c16dcd --- /dev/null +++ b/src/troute-network/troute/RoutingScheme.py @@ -0,0 +1,240 @@ +from .AbstractNetwork import AbstractNetwork +import logging +import yaml +import json +import xarray as xr +import pandas as pd + +from troute.nhd_network import reverse_network +from troute.nhd_network_utilities_v02 import organize_independent_networks, build_refac_connections + +LOG = logging.getLogger('') + +def read_diffusive_domain(domain_file): + ''' + Read diffusive domain data from .ymal or .json file. + + Arguments + --------- + domain_file (str or pathlib.Path): Path of diffusive domain file + + Returns + ------- + data (dict int: [int]): domain tailwater segments: list of segments in domain + (includeing tailwater segment) + + ''' + if domain_file[-4:] == "yaml": + with open(domain_file) as domain: + data = yaml.load(domain, Loader=yaml.SafeLoader) + else: + with open(domain_file) as domain: + data = json.load(domain) + + return data + +def read_netcdf(geo_file_path): + ''' + Open a netcdf file with xarray and convert to dataframe + + Arguments + --------- + geo_file_path (str or pathlib.Path): netCDF filepath + + Returns + ------- + ds.to_dataframe() (DataFrame): netCDF contents + + Notes + ----- + - When handling large volumes of netCDF files, xarray is not the most efficient. + + ''' + with xr.open_dataset(geo_file_path) as ds: + return ds.to_dataframe() + + +class RoutingScheme(AbstractNetwork): + """ + + """ + __slots__ = ["hybrid_params"] + + def __init__(self,): + """ + + """ + self.hybrid_params = self.compute_parameters.get("hybrid_parameters", False) + + self._diffusive_domain = None + self._diffusive_network_data = None + self._topobathy_df = pd.DataFrame() + self._unrefactored_topobathy_df = pd.DataFrame() + self._refactored_diffusive_domain = None + self._refactored_diffusive_network_data = None + self._refactored_reaches = {} + + # Determine whether to run hybrid routing from user input + run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) + domain_file = self.hybrid_params.get("diffusive_domain", None) + + if run_hybrid and domain_file: + #========================================================================== + # build diffusive domain data and edit MC domain data for hybrid simulation + self._diffusive_domain = read_diffusive_domain(domain_file) + self._diffusive_network_data = {} + + rconn_diff0 = reverse_network(self._connections) + + for tw in self._diffusive_domain: + mainstem_segs = self._diffusive_domain[tw]['links'] + # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is + # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. + upstream_boundary_mainstem_link = self._diffusive_domain[tw]['upstream_boundary_link_mainstem'] + if upstream_boundary_mainstem_link[0] in mainstem_segs: + mainstem_segs.remove(upstream_boundary_mainstem_link[0]) + + # ===== build diffusive network data objects ==== + self._diffusive_network_data[tw] = {} + + # add diffusive domain segments + self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs + + # diffusive domain tributary segments + trib_segs = [] + + for seg in mainstem_segs: + us_list = rconn_diff0[seg] + for u in us_list: + if u not in mainstem_segs: + trib_segs.append(u) + + self._diffusive_network_data[tw]['tributary_segments'] = trib_segs + # diffusive domain connections object + self._diffusive_network_data[tw]['connections'] = {k: self._connections[k] for k in (mainstem_segs + trib_segs)} + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, reaches, rconn_diff = organize_independent_networks( + self._diffusive_network_data[tw]['connections'], + set(trib_segs), + set(), + ) + + self._diffusive_network_data[tw]['rconn'] = rconn_diff + self._diffusive_network_data[tw]['reaches'] = reaches[tw] + + # RouteLink parameters + self._diffusive_network_data[tw]['param_df'] = self._dataframe.filter( + (mainstem_segs + trib_segs), + axis = 0, + ) + self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link + + # ==== remove diffusive domain segs from MC domain ==== + # drop indices from param_df + self._dataframe = self._dataframe.drop(mainstem_segs) + + # remove keys from connections dictionary + for s in mainstem_segs: + self._connections.pop(s) + + # update downstream connections of trib segs + for us in trib_segs: + self._connections[us] = [] + + super().__init__() + + + def diffusive_network_data(self,): + return self._diffusive_network_data + + def topobathy_df(self,): + if self._topobathy_df.empty(): + run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) + use_topobathy = self.hybrid_params.get('use_natl_xsections', False) + + if run_hybrid and use_topobathy: + run_refactored = self.hybrid_params.get('run_refactored_network', False) + + if run_refactored: + refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + self._topobathy_df = read_netcdf(refactored_topobathy_file).set_index('link') + else: + topobathy_file = self.hybrid_params.get("topobathy_domain", None) + self._topobathy_df = read_netcdf(topobathy_file).set_index('link') + self._topobathy_df.index = self._topobathy_df.index.astype(int) + + return self._topobathy_df + + def refactored_diffusive_domain(self,): + if not self._refactored_diffusive_domain: + run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) + run_refactored = self.hybrid_params.get('run_refactored_network', False) + + if run_hybrid and run_refactored: + refactored_domain_file = self.hybrid_params.get("refactored_domain", None) + + self._refactored_diffusive_domain = read_diffusive_domain(refactored_domain_file) + + return self._refactored_diffusive_domain + + def refactored_reaches(self,): + if not self._refactored_reaches: + run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) + run_refactored = self.hybrid_params.get('run_refactored_network', False) + + if run_hybrid and run_refactored: + refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + diffusive_parameters = {'geo_file_path': refactored_topobathy_file} + refactored_connections = build_refac_connections(diffusive_parameters) + + for tw in self._diffusive_domain: + + # list of stream segments of a single refactored diffusive domain + refac_tw = self.refactored_diffusive_domain[tw]['refac_tw'] + rlinks_tw = self.refactored_diffusive_domain[tw]['rlinks'] + refactored_connections_tw = {} + + # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections + # for a single refactored diffusive domain defined by a current tw. + for k in rlinks_tw: + if k in refactored_connections.keys() and k != refac_tw: + refactored_connections_tw[k] = refactored_connections[k] + + trib_segs = self.diffusive_network_data[tw]['tributary_segments'] + refactored_diffusive_network_data = {} + refactored_diffusive_network_data[refac_tw] = {} + refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs + refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw + + for k in trib_segs: + refactored_diffusive_network_data[refac_tw]['connections'][k] = [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( + refactored_diffusive_network_data[refac_tw]['connections'], + set(trib_segs), + set(), + ) + + self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] + refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] + refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] + + return self._refactored_reaches + + def unrefactored_topobathy_df(self,): + if self._unrefactored_topobathy_df.empty(): + run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) + use_topobathy = self.hybrid_params.get('use_natl_xsections', False) + run_refactored = self.hybrid_params.get('run_refactored_network', False) + + if run_hybrid and use_topobathy and run_refactored: + topobathy_file = self.hybrid_params.get("topobathy_domain", None) + self._unrefactored_topobathy_df = read_netcdf(topobathy_file).set_index('link') + self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) + + return self._unrefactored_topobathy_df + From f96e902952244beff3c6be6dd782fb783972635d Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Wed, 28 Dec 2022 15:52:23 +0000 Subject: [PATCH 11/54] bug fixes to get test HYfeatures with MC only run working --- src/troute-network/troute/AbstractNetwork.py | 8 +++----- src/troute-network/troute/HYFeaturesNetwork.py | 7 ++++--- src/troute-network/troute/RoutingScheme.py | 17 ++++++++++++----- src/troute-nwm/src/nwm_routing/__main__.py | 4 ++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 530eeb267..fa7e123fe 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -25,9 +25,7 @@ class AbstractNetwork(ABC): "_waterbody_types_df", "_waterbody_type_specified", "_independent_networks", "_reaches_by_tw", "_flowpath_dict", "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", - "_qlateral", "_break_segments", "_coastal_boundary_depth_df", - "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", - "_refactored_reaches", "_unrefactored_topobathy_df", "_segment_index", + "_qlateral", "_break_segments", "_segment_index", "supernetwork_parameters", "waterbody_parameters","data_assimilation_parameters", "restart_parameters", "compute_parameters", "verbose", "showtiming", "break_points"] @@ -62,7 +60,7 @@ def __init__(self,): self.create_independent_networks() - self.initial_warmstate_preprocess()) + self.initial_warmstate_preprocess() def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): @@ -305,7 +303,7 @@ def segment_index(self): """ # list of all segments in the domain (MC + diffusive) self._segment_index = self.dataframe.index - if self.diffusive_network_data: + if self._diffusive_network_data: for tw in self.diffusive_network_data: self._segment_index = self._segment_index.append( pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 40d43fc4e..183e4f5a6 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -45,6 +45,7 @@ def numeric_id(flowpath): toid = flowpath['toid'].split('-')[-1] flowpath['id'] = int(id) flowpath['toid'] = int(toid) + return flowpath def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): """ @@ -209,7 +210,7 @@ def waterbody_null(self): def read_geo_file(self,): geo_file_path = self.supernetwork_parameters["geo_file_path"] - + file_type = Path(geo_file_path).suffix if( file_type == '.gpkg' ): self._dataframe = read_geopkg(geo_file_path) @@ -218,11 +219,11 @@ def read_geo_file(self,): self._dataframe = read_json(geo_file_path, edge_list) else: raise RuntimeError("Unsupported file type: {}".format(file_type)) - + # Don't need the string prefix anymore, drop it mask = ~ self.dataframe['toid'].str.startswith("tnex") self._dataframe = self.dataframe.apply(numeric_id, axis=1) - + # make the flowpath linkage, ignore the terminal nexus self._flowpath_dict = dict(zip(self.dataframe.loc[mask].toid, self.dataframe.loc[mask].id)) diff --git a/src/troute-network/troute/RoutingScheme.py b/src/troute-network/troute/RoutingScheme.py index 195c16dcd..11d27a2cf 100644 --- a/src/troute-network/troute/RoutingScheme.py +++ b/src/troute-network/troute/RoutingScheme.py @@ -58,7 +58,10 @@ class RoutingScheme(AbstractNetwork): """ """ - __slots__ = ["hybrid_params"] + __slots__ = ["hybrid_params", "_diffusive_domain", "_coastal_boundary_depth_df", + "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", + "_refactored_diffusive_network_data", "_refactored_reaches", + "_unrefactored_topobathy_df",] def __init__(self,): """ @@ -145,12 +148,13 @@ def __init__(self,): super().__init__() - + @property def diffusive_network_data(self,): return self._diffusive_network_data + @property def topobathy_df(self,): - if self._topobathy_df.empty(): + if self._topobathy_df.empty: run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) use_topobathy = self.hybrid_params.get('use_natl_xsections', False) @@ -166,7 +170,8 @@ def topobathy_df(self,): self._topobathy_df.index = self._topobathy_df.index.astype(int) return self._topobathy_df - + + @property def refactored_diffusive_domain(self,): if not self._refactored_diffusive_domain: run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) @@ -179,6 +184,7 @@ def refactored_diffusive_domain(self,): return self._refactored_diffusive_domain + @property def refactored_reaches(self,): if not self._refactored_reaches: run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) @@ -225,8 +231,9 @@ def refactored_reaches(self,): return self._refactored_reaches + @property def unrefactored_topobathy_df(self,): - if self._unrefactored_topobathy_df.empty(): + if self._unrefactored_topobathy_df.empty: run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) use_topobathy = self.hybrid_params.get('use_natl_xsections', False) run_refactored = self.hybrid_params.get('run_refactored_network', False) diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index 225de164a..13527c705 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -77,14 +77,14 @@ def main_v04(argv): # perform initial warmstate preprocess. if showtiming: network_start_time = time.time() - + #if "ngen_nexus_file" in supernetwork_parameters: if supernetwork_parameters["geo_file_type"] == 'HYFeaturesNetwork': network = HYFeaturesNetwork(supernetwork_parameters, waterbody_parameters, data_assimilation_parameters, restart_parameters, - forcing_parameters, + compute_parameters, verbose=True, showtiming=showtiming) elif supernetwork_parameters["geo_file_type"] == 'NHDNetwork': From 7b73ab0263b9a707aee36fc0f0454ead01aa157d Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 30 Dec 2022 15:12:14 +0000 Subject: [PATCH 12/54] move build_qlateral_array function from AbstractNetwork to individual networks --- src/troute-network/troute/AbstractNetwork.py | 366 +----------------- src/troute-network/troute/DataAssimilation.py | 2 +- .../troute/HYFeaturesNetwork.py | 123 +++++- src/troute-network/troute/NHDNetwork.py | 79 ++++ src/troute-network/troute/RoutingScheme.py | 36 +- src/troute-nwm/src/nwm_routing/__main__.py | 10 +- 6 files changed, 241 insertions(+), 375 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index fa7e123fe..72afb0c44 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -1,14 +1,10 @@ from abc import ABC, abstractmethod from functools import partial import pandas as pd -import numpy as np -import pyarrow.parquet as pq from datetime import datetime, timedelta -from joblib import delayed, Parallel -import netCDF4 + import time import logging -import pathlib from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition from troute.nhd_network_utilities_v02 import organize_independent_networks, build_channel_initial_state, build_refac_connections @@ -27,7 +23,8 @@ class AbstractNetwork(ABC): "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", "_qlateral", "_break_segments", "_segment_index", "supernetwork_parameters", "waterbody_parameters","data_assimilation_parameters", - "restart_parameters", "compute_parameters", "verbose", "showtiming", "break_points"] + "restart_parameters", "compute_parameters", "forcing_parameters", + "hybrid_parameters", "verbose", "showtiming", "break_points"] def __init__(self,): @@ -55,15 +52,13 @@ def __init__(self,): self._break_segments = self._break_segments | set(self.waterbody_connections.values()) if self.break_points["break_network_at_gages"]: self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) - - #self.build_diffusive_domain(compute_parameters) self.create_independent_networks() self.initial_warmstate_preprocess() - def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): + def assemble_forcings(self, run,): """ Assemble model forcings. Forcings include hydrological lateral inflows (qlats) and coastal boundary depths for hybrid runs @@ -91,13 +86,13 @@ def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernet """ # Unpack user-specified forcing parameters - dt = forcing_parameters.get("dt", None) - qts_subdivisions = forcing_parameters.get("qts_subdivisions", None) - qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) - qlat_file_index_col = forcing_parameters.get("qlat_file_index_col", "feature_id") - qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") - qlat_file_gw_bucket_flux_col = forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") - qlat_file_terrain_runoff_col = forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") + dt = self.forcing_parameters.get("dt", None) + qts_subdivisions = self.forcing_parameters.get("qts_subdivisions", None) + qlat_input_folder = self.forcing_parameters.get("qlat_input_folder", None) + qlat_file_index_col = self.forcing_parameters.get("qlat_file_index_col", "feature_id") + qlat_file_value_col = self.forcing_parameters.get("qlat_file_value_col", "q_lateral") + qlat_file_gw_bucket_flux_col = self.forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") + qlat_file_terrain_runoff_col = self.forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") # TODO: find a better way to deal with these defaults and overrides. @@ -117,21 +112,27 @@ def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernet # Place holder, if reading qlats from a file use this. # TODO: add an option for reading qlat data from BMI/model engine + start_time = time.time() + LOG.info("Creating a DataFrame of lateral inflow forcings ...") + from_file = True if from_file: self.build_qlateral_array( run, - cpu_pool, - supernetwork_parameters, ) + + LOG.debug( + "lateral inflow DataFrame creation complete in %s seconds." \ + % (time.time() - start_time) + ) #--------------------------------------------------------------------- # Assemble coastal coupling data [WIP] #--------------------------------------------------------------------- # Run if coastal_boundary_depth_df has not already been created: if self._coastal_boundary_depth_df.empty: - coastal_boundary_elev_files = forcing_parameters.get('coastal_boundary_input_file', None) - coastal_boundary_domain_files = hybrid_parameters.get('coastal_boundary_domain', None) + coastal_boundary_elev_files = self.forcing_parameters.get('coastal_boundary_input_file', None) + coastal_boundary_domain_files = self.hybrid_parameters.get('coastal_boundary_domain', None) if coastal_boundary_elev_files: #start_time = time.time() @@ -405,213 +406,6 @@ def astype(self, type, columns=None): else: self._dataframe = self._dataframe.astype(type) - - def build_diffusive_domain(self, compute_parameters): - """ - - """ - hybrid_params = compute_parameters.get("hybrid_parameters", False) - if hybrid_params: - # switch parameters - # if run_hybrid = False, run MC only - # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc - # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric - # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric - run_hybrid = hybrid_params.get('run_hybrid_routing', False) - use_topobathy = hybrid_params.get('use_natl_xsections', False) - run_refactored = hybrid_params.get('run_refactored_network', False) - - # file path parameters of non-refactored hydrofabric defined by RouteLink.nc - domain_file = hybrid_params.get("diffusive_domain", None) - topobathy_file = hybrid_params.get("topobathy_domain", None) - - # file path parameters of refactored hydrofabric for diffusive wave channel routing - refactored_domain_file = hybrid_params.get("refactored_domain", None) - refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) - #------------------------------------------------------------------------- - # for non-refactored hydofabric defined by RouteLink.nc - # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. - if run_hybrid and domain_file: - - LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - diffusive_domain = nhd_io.read_diffusive_domain(domain_file) - - if use_topobathy and topobathy_file: - - LOG.debug('Natural cross section data on original hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - self._topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - - # TODO: Request GID make comID variable an integer in their product, so - # we do not need to change variable types, here. - self._topobathy_df.index = self._topobathy_df.index.astype(int) - - else: - self._topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - self._diffusive_network_data = {} - - else: - diffusive_domain = None - self._diffusive_network_data = None - self._topobathy_df = pd.DataFrame() - LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') - self._unrefactored_topobathy_df = pd.DataFrame() - #------------------------------------------------------------------------- - # for refactored hydofabric - if run_hybrid and run_refactored and refactored_domain_file: - - LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - self._refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) - - if use_topobathy and refactored_topobathy_file: - - LOG.debug('Natural cross section data of refactored hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - self._topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) - - # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy - # for crosswalking water elevations between non-refactored and refactored hydrofabrics. - self._unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) - - else: - self._topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - refactored_diffusive_network_data = {} - - else: - self._refactored_diffusive_domain = None - refactored_diffusive_network_data = None - self._refactored_reaches = {} - LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') - - else: - diffusive_domain = None - self._diffusive_network_data = None - self._topobathy_df = pd.DataFrame() - self._unrefactored_topobathy_df = pd.DataFrame() - self._refactored_diffusive_domain = None - refactored_diffusive_network_data = None - self._refactored_reaches = {} - LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') - - #============================================================================ - # build diffusive domain data and edit MC domain data for hybrid simulation - - # - if diffusive_domain: - rconn_diff0 = reverse_network(self._connections) - self._refactored_reaches = {} - - for tw in diffusive_domain: - mainstem_segs = diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - self._diffusive_network_data[tw] = {} - - # add diffusive domain segments - self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - self._diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - self._diffusive_network_data[tw]['connections'] = {k: self._connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = organize_independent_networks( - self._diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - self._diffusive_network_data[tw]['rconn'] = rconn_diff - self._diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - self._diffusive_network_data[tw]['param_df'] = self._dataframe.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - if self._refactored_diffusive_domain: - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = build_refac_connections(diffusive_parameters) - - # list of stream segments of a single refactored diffusive domain - refac_tw = self._refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = self._refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k]= [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] - else: - self._refactored_reaches={} - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - self._dataframe = self._dataframe.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - self._connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - self._connections[us] = [] def create_independent_networks(self,): @@ -817,121 +611,3 @@ def _try_parsing_date(text): "channel initial states complete in %s seconds."\ % (time.time() - start_time) ) - - def build_qlateral_array(self, run, cpu_pool, supernetwork_parameters,): - - start_time = time.time() - LOG.info("Creating a DataFrame of lateral inflow forcings ...") - - # TODO: set default/optional arguments - qts_subdivisions = run.get("qts_subdivisions", 1) - nts = run.get("nts", 1) - qlat_input_folder = run.get("qlat_input_folder", None) - qlat_input_file = run.get("qlat_input_file", None) - - geo_file_type = supernetwork_parameters.get('geo_file_type') - - if qlat_input_folder: - qlat_input_folder = pathlib.Path(qlat_input_folder) - if "qlat_files" in run: - qlat_files = run.get("qlat_files") - qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] - elif "qlat_file_pattern_filter" in run: - qlat_file_pattern_filter = run.get( - "qlat_file_pattern_filter", "*CHRT_OUT*" - ) - qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) - - qlat_file_index_col = run.get( - "qlat_file_index_col", "feature_id" - ) - - if geo_file_type=='NHDNetwork': - # Parallel reading of qlateral data from CHRTOUT - with Parallel(n_jobs=cpu_pool) as parallel: - jobs = [] - for f in qlat_files: - jobs.append( - delayed(nhd_io.get_ql_from_chrtout) - #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) - #delayed(nhd_io.get_ql_from_csv) - (f) - ) - ql_list = parallel(jobs) - - # get feature_id from a single CHRTOUT file - with netCDF4.Dataset(qlat_files[0]) as ds: - idx = ds.variables[qlat_file_index_col][:].filled() - - # package data into a DataFrame - qlats_df = pd.DataFrame( - np.stack(ql_list).T, - index = idx, - columns = range(len(qlat_files)) - ) - - qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] - elif geo_file_type=='HYFeaturesNetwork': - dfs=[] - for f in qlat_files: - df = read_file(f).set_index(['feature_id']) - dfs.append(df) - - # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids - nexuses_lateralflows_df = pd.concat(dfs, axis=1) - - # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids - qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) - for k,v in self.downstream_flowpath_dict.items() ), axis=1 - ).T - qlats_df.columns=range(len(qlat_files)) - qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] - - # The segment_index has the full network set of segments/flowpaths. - # Whereas the set of flowpaths that are downstream of nexuses is a - # subset of the segment_index. Therefore, all of the segments/flowpaths - # that are not accounted for in the set of flowpaths downstream of - # nexuses need to be added to the qlateral dataframe and padded with - # zeros. - all_df = pd.DataFrame( np.zeros( (len(self.segment_index), len(qlats_df.columns)) ), index=self.segment_index, - columns=qlats_df.columns ) - all_df.loc[ qlats_df.index ] = qlats_df - qlats_df = all_df.sort_index() - - elif qlat_input_file: - qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) - else: - qlat_const = run.get("qlat_const", 0) - qlats_df = pd.DataFrame( - qlat_const, - index=self.segment_index, - columns=range(nts // qts_subdivisions), - dtype="float32", - ) - - # TODO: Make a more sophisticated date-based filter - max_col = 1 + nts // qts_subdivisions - if len(qlats_df.columns) > max_col: - qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) - - if not self.segment_index.empty: - qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] - - LOG.debug( - "lateral inflow DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - self._qlateral = qlats_df - - - -def read_file(file_name): - extension = file_name.suffix - if extension=='.csv': - df = pd.read_csv(file_name) - elif extension=='.parquet': - df = pq.read_table(file_name).to_pandas().reset_index() - df.index.name = None - - return df \ No newline at end of file diff --git a/src/troute-network/troute/DataAssimilation.py b/src/troute-network/troute/DataAssimilation.py index 195f0d75e..b5bb0a2c6 100644 --- a/src/troute-network/troute/DataAssimilation.py +++ b/src/troute-network/troute/DataAssimilation.py @@ -943,7 +943,7 @@ def update_for_next_loop( ) # USACE Reservoirs - if 3 in network._waterbody_types_dataframe['reservoir_type'].unique(): + if 3 in network.waterbody_types_dataframe['reservoir_type'].unique(): if self.reservoir_usace_df.empty and len(self.reservoir_usace_param_df.index) > 0: self._reservoir_usace_df = pd.DataFrame( data = np.nan, diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 183e4f5a6..74d299448 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -6,9 +6,9 @@ import os import json from pathlib import Path +import pyarrow.parquet as pq import troute.nhd_io as nhd_io #FIXME -import troute.hyfeature_preprocess as hyfeature_prep from troute.nhd_network import reverse_dict, extract_connections __verbose__ = False @@ -94,13 +94,16 @@ class HYFeaturesNetwork(RoutingScheme): """ """ - __slots__ = [] + __slots__ = ["_upstream_terminal"] + def __init__(self, supernetwork_parameters, waterbody_parameters, data_assimilation_parameters, - restart_parameters=None, - compute_parameters=None, + restart_parameters, + compute_parameters, + forcing_parameters, + hybrid_parameters, verbose=False, showtiming=False): """ @@ -111,6 +114,8 @@ def __init__(self, self.data_assimilation_parameters = data_assimilation_parameters self.restart_parameters = restart_parameters self.compute_parameters = compute_parameters + self.forcing_parameters = forcing_parameters + self.hybrid_parameters = hybrid_parameters self.verbose = verbose self.showtiming = showtiming @@ -269,6 +274,15 @@ def read_geo_file(self,): self.dataframe[~self.dataframe["downstream"].isin(self.dataframe.index)]["downstream"].values ) + #This is NEARLY redundant to the self.terminal_codes property, but in this case + #we actually need the mapping of what is upstream of that terminal node as well. + #we also only want terminals that actually exist based on definition, not user input + terminal_mask = ~self._dataframe["downstream"].isin(self._dataframe.index) + terminal = self._dataframe.loc[ terminal_mask ]["downstream"] + self._upstream_terminal = dict() + for key, value in terminal.items(): + self._upstream_terminal.setdefault(value, set()).add(key) + # build connections dictionary self._connections = extract_connections( self.dataframe, "downstream", terminal_codes=self.terminal_codes @@ -312,4 +326,105 @@ def read_geo_file(self,): #So make this default to 1 (levelpool) self._waterbody_types_df = pd.DataFrame(index=self.waterbody_dataframe.index) self._waterbody_types_df['reservoir_type'] = 1 + + def build_qlateral_array(self, run,): + + # TODO: set default/optional arguments + qts_subdivisions = run.get("qts_subdivisions", 1) + nts = run.get("nts", 1) + qlat_input_folder = run.get("qlat_input_folder", None) + qlat_input_file = run.get("qlat_input_file", None) + + if qlat_input_folder: + qlat_input_folder = Path(qlat_input_folder) + if "qlat_files" in run: + qlat_files = run.get("qlat_files") + qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] + elif "qlat_file_pattern_filter" in run: + qlat_file_pattern_filter = run.get( + "qlat_file_pattern_filter", "*CHRT_OUT*" + ) + qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) + + dfs=[] + for f in qlat_files: + df = read_file(f).set_index(['feature_id']) + dfs.append(df) + + # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids + nexuses_lateralflows_df = pd.concat(dfs, axis=1) + + # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids + qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) + for k,v in self.downstream_flowpath_dict.items() ), axis=1 + ).T + qlats_df.columns=range(len(qlat_files)) + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + ''' + #For a terminal nexus, we want to include the lateral flow from the catchment contributing to that nexus + #one way to do that is to cheat and put that lateral flow at the upstream...this is probably the simplest way + #right now. The other is to create a virtual channel segment downstream to "route" i.e accumulate into + #but it isn't clear right now how to do that with flow/velocity/depth requirements + #find the terminal nodes + for tnx, test_up in self._upstream_terminal.items(): + #first need to ensure there is an upstream location to dump to + pdb.set_trace() + for nex in test_up: + try: + #FIXME if multiple upstreams exist in this case then a choice is to be made as to which it goes into + #some cases the choice is easy cause the upstream doesn't exist, but in others, it may not be so simple + #in such cases where multiple valid upstream nexuses exist, perhaps the mainstem should be used? + pdb.set_trace() + qlats_df.loc[up] += nexuses_lateralflows_df.loc[tnx] + break #flow added, don't add it again! + except KeyError: + #this upstream doesn't actually exist on the network (maybe it is a headwater?) + #or perhaps the output file doesnt exist? If this is the case, this isn't a good trap + #but for now, add the flow to a known good nexus upstream of the terminal + continue + #TODO what happens if can't put the qlat anywhere? Right now this silently ignores the issue... + qlats_df.drop(tnx, inplace=True) + ''' + + # The segment_index has the full network set of segments/flowpaths. + # Whereas the set of flowpaths that are downstream of nexuses is a + # subset of the segment_index. Therefore, all of the segments/flowpaths + # that are not accounted for in the set of flowpaths downstream of + # nexuses need to be added to the qlateral dataframe and padded with + # zeros. + all_df = pd.DataFrame( np.zeros( (len(self.segment_index), len(qlats_df.columns)) ), index=self.segment_index, + columns=qlats_df.columns ) + all_df.loc[ qlats_df.index ] = qlats_df + qlats_df = all_df.sort_index() + + elif qlat_input_file: + qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) + else: + qlat_const = run.get("qlat_const", 0) + qlats_df = pd.DataFrame( + qlat_const, + index=self.segment_index, + columns=range(nts // qts_subdivisions), + dtype="float32", + ) + + # TODO: Make a more sophisticated date-based filter + max_col = 1 + nts // qts_subdivisions + if len(qlats_df.columns) > max_col: + qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) + + if not self.segment_index.empty: + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + self._qlateral = qlats_df + +def read_file(file_name): + extension = file_name.suffix + if extension=='.csv': + df = pd.read_csv(file_name) + elif extension=='.parquet': + df = pq.read_table(file_name).to_pandas().reset_index() + df.index.name = None + + return df \ No newline at end of file diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index 06b614405..eb47e81d7 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -363,3 +363,82 @@ def read_geo_file( self._waterbody_df = pd.DataFrame() self._usgs_lake_gage_crosswalk = None self._usace_lake_gage_crosswalk = None + + def build_qlateral_array(self, run, cpu_pool): + + # TODO: set default/optional arguments + qts_subdivisions = run.get("qts_subdivisions", 1) + nts = run.get("nts", 1) + qlat_input_folder = run.get("qlat_input_folder", None) + qlat_input_file = run.get("qlat_input_file", None) + + if qlat_input_folder: + qlat_input_folder = pathlib.Path(qlat_input_folder) + if "qlat_files" in run: + qlat_files = run.get("qlat_files") + qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] + elif "qlat_file_pattern_filter" in run: + qlat_file_pattern_filter = run.get( + "qlat_file_pattern_filter", "*CHRT_OUT*" + ) + qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) + + qlat_file_index_col = run.get( + "qlat_file_index_col", "feature_id" + ) + + # Parallel reading of qlateral data from CHRTOUT + with Parallel(n_jobs=cpu_pool) as parallel: + jobs = [] + for f in qlat_files: + jobs.append( + delayed(nhd_io.get_ql_from_chrtout) + #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) + #delayed(nhd_io.get_ql_from_csv) + (f) + ) + ql_list = parallel(jobs) + + # get feature_id from a single CHRTOUT file + with netCDF4.Dataset(qlat_files[0]) as ds: + idx = ds.variables[qlat_file_index_col][:].filled() + + # package data into a DataFrame + qlats_df = pd.DataFrame( + np.stack(ql_list).T, + index = idx, + columns = range(len(qlat_files)) + ) + + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + elif qlat_input_file: + qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) + else: + qlat_const = run.get("qlat_const", 0) + qlats_df = pd.DataFrame( + qlat_const, + index=self.segment_index, + columns=range(nts // qts_subdivisions), + dtype="float32", + ) + + # TODO: Make a more sophisticated date-based filter + max_col = 1 + nts // qts_subdivisions + if len(qlats_df.columns) > max_col: + qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) + + if not self.segment_index.empty: + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + self._qlateral = qlats_df + +def read_file(file_name): + extension = file_name.suffix + if extension=='.csv': + df = pd.read_csv(file_name) + elif extension=='.parquet': + df = pq.read_table(file_name).to_pandas().reset_index() + df.index.name = None + + return df \ No newline at end of file diff --git a/src/troute-network/troute/RoutingScheme.py b/src/troute-network/troute/RoutingScheme.py index 11d27a2cf..049b0b84f 100644 --- a/src/troute-network/troute/RoutingScheme.py +++ b/src/troute-network/troute/RoutingScheme.py @@ -67,8 +67,6 @@ def __init__(self,): """ """ - self.hybrid_params = self.compute_parameters.get("hybrid_parameters", False) - self._diffusive_domain = None self._diffusive_network_data = None self._topobathy_df = pd.DataFrame() @@ -78,8 +76,8 @@ def __init__(self,): self._refactored_reaches = {} # Determine whether to run hybrid routing from user input - run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) - domain_file = self.hybrid_params.get("diffusive_domain", None) + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + domain_file = self.hybrid_parameters.get("diffusive_domain", None) if run_hybrid and domain_file: #========================================================================== @@ -155,17 +153,17 @@ def diffusive_network_data(self,): @property def topobathy_df(self,): if self._topobathy_df.empty: - run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) - use_topobathy = self.hybrid_params.get('use_natl_xsections', False) + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) if run_hybrid and use_topobathy: - run_refactored = self.hybrid_params.get('run_refactored_network', False) + run_refactored = self.hybrid_parameters.get('run_refactored_network', False) if run_refactored: - refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + refactored_topobathy_file = self.hybrid_parameters.get("refactored_topobathy_domain", None) self._topobathy_df = read_netcdf(refactored_topobathy_file).set_index('link') else: - topobathy_file = self.hybrid_params.get("topobathy_domain", None) + topobathy_file = self.hybrid_parameters.get("topobathy_domain", None) self._topobathy_df = read_netcdf(topobathy_file).set_index('link') self._topobathy_df.index = self._topobathy_df.index.astype(int) @@ -174,11 +172,11 @@ def topobathy_df(self,): @property def refactored_diffusive_domain(self,): if not self._refactored_diffusive_domain: - run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) - run_refactored = self.hybrid_params.get('run_refactored_network', False) + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + run_refactored = self.hybrid_parameters.get('run_refactored_network', False) if run_hybrid and run_refactored: - refactored_domain_file = self.hybrid_params.get("refactored_domain", None) + refactored_domain_file = self.hybrid_parameters.get("refactored_domain", None) self._refactored_diffusive_domain = read_diffusive_domain(refactored_domain_file) @@ -187,11 +185,11 @@ def refactored_diffusive_domain(self,): @property def refactored_reaches(self,): if not self._refactored_reaches: - run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) - run_refactored = self.hybrid_params.get('run_refactored_network', False) + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + run_refactored = self.hybrid_parameters.get('run_refactored_network', False) if run_hybrid and run_refactored: - refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + refactored_topobathy_file = self.hybrid_parameters.get("refactored_topobathy_domain", None) diffusive_parameters = {'geo_file_path': refactored_topobathy_file} refactored_connections = build_refac_connections(diffusive_parameters) @@ -234,12 +232,12 @@ def refactored_reaches(self,): @property def unrefactored_topobathy_df(self,): if self._unrefactored_topobathy_df.empty: - run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) - use_topobathy = self.hybrid_params.get('use_natl_xsections', False) - run_refactored = self.hybrid_params.get('run_refactored_network', False) + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) + run_refactored = self.hybrid_parameters.get('run_refactored_network', False) if run_hybrid and use_topobathy and run_refactored: - topobathy_file = self.hybrid_params.get("topobathy_domain", None) + topobathy_file = self.hybrid_parameters.get("topobathy_domain", None) self._unrefactored_topobathy_df = read_netcdf(topobathy_file).set_index('link') self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index 13527c705..235f7cf3b 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -85,6 +85,8 @@ def main_v04(argv): data_assimilation_parameters, restart_parameters, compute_parameters, + forcing_parameters, + hybrid_parameters, verbose=True, showtiming=showtiming) elif supernetwork_parameters["geo_file_type"] == 'NHDNetwork': @@ -127,7 +129,7 @@ def main_v04(argv): parity_sets = [] # Create forcing data within network object for first loop iteration - network.assemble_forcings(run_sets[0], forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool) + network.assemble_forcings(run_sets[0],) # Create data assimilation object from da_sets for first loop iteration # TODO: Add data_assimilation for hyfeature network @@ -240,11 +242,7 @@ def main_v04(argv): network.new_t0(dt,nts) # update forcing data - network.assemble_forcings(run_sets[run_set_iterator + 1], - forcing_parameters, - hybrid_parameters, - supernetwork_parameters, - cpu_pool) + network.assemble_forcings(run_sets[run_set_iterator + 1],) # get reservoir DA initial parameters for next loop iteration data_assimilation.update_for_next_loop( From b3822a195b4680f115fb2f45d82b6658622a80bf Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 30 Dec 2022 18:35:16 +0000 Subject: [PATCH 13/54] bug fixes to get NHDNetwork working with new configuration --- src/troute-network/troute/NHDNetwork.py | 116 +++++++++------------ src/troute-nwm/src/nwm_routing/__main__.py | 2 +- 2 files changed, 48 insertions(+), 70 deletions(-) diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index eb47e81d7..e93d47f15 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -1,10 +1,14 @@ -from .AbstractNetwork import AbstractNetwork +from .RoutingScheme import RoutingScheme import troute.nhd_io as nhd_io import troute.nhd_preprocess as nhd_prep import pandas as pd +import numpy as np import time import pathlib from collections import defaultdict +import netCDF4 +from joblib import delayed, Parallel +import pyarrow.parquet as pq from troute.nhd_network import reverse_dict, extract_waterbody_connections, gage_mapping, extract_connections, replace_waterbodies_connections @@ -12,7 +16,7 @@ __verbose__ = True #FIXME pass verbosity -class NHDNetwork(AbstractNetwork): +class NHDNetwork(RoutingScheme): """ """ @@ -24,77 +28,55 @@ class NHDNetwork(AbstractNetwork): def __init__( self, supernetwork_parameters, - waterbody_parameters=None, - restart_parameters=None, - forcing_parameters=None, - compute_parameters=None, - data_assimilation_parameters=None, - preprocessing_parameters=None, + waterbody_parameters, + restart_parameters, + forcing_parameters, + compute_parameters, + data_assimilation_parameters, + hybrid_parameters, verbose=False, showtiming=False, ): """ """ - global __verbose__, __showtiming__ - __verbose__ = verbose - __showtiming__ = showtiming - if __verbose__: + self.supernetwork_parameters = supernetwork_parameters + self.waterbody_parameters = waterbody_parameters + self.data_assimilation_parameters = data_assimilation_parameters + self.restart_parameters = restart_parameters + self.compute_parameters = compute_parameters + self.forcing_parameters = forcing_parameters + self.hybrid_parameters = hybrid_parameters + self.verbose = verbose + self.showtiming = showtiming + + if self.verbose: print("creating supernetwork connections set") - if __showtiming__: + if self.showtiming: start_time = time.time() #------------------------------------------------ # Load Geo Data #------------------------------------------------ - self.read_geo_file( - supernetwork_parameters, - waterbody_parameters, - data_assimilation_parameters, - ) - ''' - ( - self._dataframe, - self._connections, - self._terminal_codes, - self._waterbody_df, - self._waterbody_types_df, - self._waterbody_type_specified, - self._waterbody_connections, - self._link_lake_crosswalk, - self._gages, - self._usgs_lake_gage_crosswalk, - self._usace_lake_gage_crosswalk, - ) = nhd_prep.read_geo_file( - supernetwork_parameters, - waterbody_parameters, - data_assimilation_parameters, - ) - ''' - if __verbose__: + self.read_geo_file() + + if self.verbose: print("supernetwork connections set complete") - if __showtiming__: + if self.showtiming: print("... in %s seconds." % (time.time() - start_time)) - break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) - streamflow_da = data_assimilation_parameters.get('streamflow_da', False) + break_network_at_waterbodies = self.waterbody_parameters.get("break_network_at_waterbodies", False) + streamflow_da = self.data_assimilation_parameters.get('streamflow_da', False) break_network_at_gages = False if streamflow_da: break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, + self.break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} self._flowpath_dict = {} - super().__init__( - compute_parameters, - waterbody_parameters, - restart_parameters, - break_points, - verbose=__verbose__, - showtiming=__showtiming__, - ) + super().__init__() # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't @@ -141,12 +123,7 @@ def usace_lake_gage_crosswalk(self): #def wbody_conn(self): # return self._waterbody_connections - def read_geo_file( - self, - supernetwork_parameters, - waterbody_parameters, - data_assimilation_parameters - ): + def read_geo_file(self,): ''' Construct network connections network, parameter dataframe, waterbody mapping, and gage mapping. This is an intermediate-level function that calls several @@ -167,7 +144,7 @@ def read_geo_file( # crosswalking dictionary between variables names in input dataset and # variable names recognized by troute.routing module. - cols = supernetwork_parameters.get( + cols = self.supernetwork_parameters.get( 'columns', { 'key' : 'link', @@ -189,10 +166,10 @@ def read_geo_file( ) # numeric code used to indicate network terminal segments - terminal_code = supernetwork_parameters.get("terminal_code", 0) + terminal_code = self.supernetwork_parameters.get("terminal_code", 0) # read parameter dataframe - self._dataframe = nhd_io.read(pathlib.Path(supernetwork_parameters["geo_file_path"])) + self._dataframe = nhd_io.read(pathlib.Path(self.supernetwork_parameters["geo_file_path"])) # select the column names specified in the values in the cols dict variable self._dataframe = self.dataframe[list(cols.values())] @@ -201,8 +178,8 @@ def read_geo_file( self._dataframe = self.dataframe.rename(columns=reverse_dict(cols)) # handle synthetic waterbody segments - synthetic_wb_segments = supernetwork_parameters.get("synthetic_wb_segments", None) - synthetic_wb_id_offset = supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) + synthetic_wb_segments = self.supernetwork_parameters.get("synthetic_wb_segments", None) + synthetic_wb_id_offset = self.supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) if synthetic_wb_segments: # rename the current key column to key32 key32_d = {"key":"key32"} @@ -218,10 +195,10 @@ def read_geo_file( self._dataframe = self.dataframe.set_index("key").sort_index() # get and apply domain mask - if "mask_file_path" in supernetwork_parameters: + if "mask_file_path" in self.supernetwork_parameters: data_mask = nhd_io.read_mask( - pathlib.Path(supernetwork_parameters["mask_file_path"]), - layer_string=supernetwork_parameters.get("mask_layer_string", None), + pathlib.Path(self.supernetwork_parameters["mask_file_path"]), + layer_string=self.supernetwork_parameters.get("mask_layer_string", None), ) data_mask = data_mask.set_index(data_mask.columns[0]) self._dataframe = self.dataframe.filter(data_mask.index, axis=0) @@ -260,7 +237,7 @@ def read_geo_file( self._dataframe = self.dataframe.astype("float32") - break_network_at_waterbodies = waterbody_parameters.get( + break_network_at_waterbodies = self.waterbody_parameters.get( "break_network_at_waterbodies", False ) @@ -281,7 +258,7 @@ def read_geo_file( if break_network_at_waterbodies: # Read waterbody parameters from LAKEPARM file - level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) + level_pool_params = self.waterbody_parameters.get('level_pool', defaultdict(list)) self._waterbody_df = nhd_io.read_lakeparm( level_pool_params['level_pool_waterbody_parameter_file_path'], level_pool_params.get("level_pool_waterbody_id", 'lake_id'), @@ -299,7 +276,7 @@ def read_geo_file( self._waterbody_types_df = pd.DataFrame() # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True - reservoir_da = data_assimilation_parameters.get( + reservoir_da = self.data_assimilation_parameters.get( 'reservoir_da', {} ) @@ -323,7 +300,7 @@ def read_geo_file( usgs_hybrid = False # check if RFC-type reservoirs are set to true - rfc_params = waterbody_parameters.get('rfc') + rfc_params = self.waterbody_parameters.get('rfc') if rfc_params: rfc_forecast = rfc_params.get( 'reservoir_rfc_forecasts', @@ -364,13 +341,14 @@ def read_geo_file( self._usgs_lake_gage_crosswalk = None self._usace_lake_gage_crosswalk = None - def build_qlateral_array(self, run, cpu_pool): + def build_qlateral_array(self, run,): # TODO: set default/optional arguments qts_subdivisions = run.get("qts_subdivisions", 1) nts = run.get("nts", 1) qlat_input_folder = run.get("qlat_input_folder", None) qlat_input_file = run.get("qlat_input_file", None) + cpu_pool = self.compute_parameters.get('cpu_pool', 1) if qlat_input_folder: qlat_input_folder = pathlib.Path(qlat_input_folder) diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index 235f7cf3b..cf6bc29cb 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -96,7 +96,7 @@ def main_v04(argv): forcing_parameters, compute_parameters, data_assimilation_parameters, - preprocessing_parameters, + hybrid_parameters, verbose=True, showtiming=showtiming, ) From 327854cfa38147d481858165c53692277ea1a0d2 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Thu, 5 Jan 2023 22:22:24 +0000 Subject: [PATCH 14/54] modified routing classes to be separate from network classes --- src/troute-network/troute/AbstractNetwork.py | 69 +++- src/troute-network/troute/AbstractRouting.py | 352 ++++++++++++++++++ .../troute/HYFeaturesNetwork.py | 4 +- src/troute-network/troute/NHDNetwork.py | 4 +- 4 files changed, 420 insertions(+), 9 deletions(-) create mode 100644 src/troute-network/troute/AbstractRouting.py diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 72afb0c44..e48671cc5 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -2,6 +2,7 @@ from functools import partial import pandas as pd from datetime import datetime, timedelta +from collections import defaultdict import time import logging @@ -9,6 +10,7 @@ from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition from troute.nhd_network_utilities_v02 import organize_independent_networks, build_channel_initial_state, build_refac_connections import troute.nhd_io as nhd_io +from .AbstractRouting import * LOG = logging.getLogger('') @@ -21,10 +23,10 @@ class AbstractNetwork(ABC): "_waterbody_types_df", "_waterbody_type_specified", "_independent_networks", "_reaches_by_tw", "_flowpath_dict", "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", - "_qlateral", "_break_segments", "_segment_index", + "_qlateral", "_break_segments", "_segment_index", "_coastal_boundary_depth_df", "supernetwork_parameters", "waterbody_parameters","data_assimilation_parameters", "restart_parameters", "compute_parameters", "forcing_parameters", - "hybrid_parameters", "verbose", "showtiming", "break_points"] + "hybrid_parameters", "verbose", "showtiming", "break_points", "_routing"] def __init__(self,): @@ -52,6 +54,8 @@ def __init__(self,): self._break_segments = self._break_segments | set(self.waterbody_connections.values()) if self.break_points["break_network_at_gages"]: self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) + + self.initialize_routing_scheme() self.create_independent_networks() @@ -304,10 +308,10 @@ def segment_index(self): """ # list of all segments in the domain (MC + diffusive) self._segment_index = self.dataframe.index - if self._diffusive_network_data: - for tw in self.diffusive_network_data: + if self._routing.diffusive_network_data: + for tw in self._routing.diffusive_network_data: self._segment_index = self._segment_index.append( - pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) + pd.Index(self._routing.diffusive_network_data[tw]['mainstem_segs']) ) return self._segment_index @@ -347,6 +351,27 @@ def coastal_boundary_depth_df(self): """ return self._coastal_boundary_depth_df + @property + def diffusive_network_data(self): + return self._routing.diffusive_network_data + + @property + def topobathy_df(self): + return self._routing.topobathy_df + + @property + def refactored_diffusive_domain(self): + return self._routing.refactored_diffusive_domain + + @property + def refactored_reaches(self): + return self._routing.refactored_reaches + + @property + def unrefactored_topobathy_df(self): + return self._routing.unrefactored_topobathy_df + + def set_synthetic_wb_segments(self, synthetic_wb_segments, synthetic_wb_id_offset): """ @@ -406,7 +431,41 @@ def astype(self, type, columns=None): else: self._dataframe = self._dataframe.astype(type) + def initialize_routing_scheme(self,): + ''' + + ''' + # Get user inputs from configuration file + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) + run_refactored = self.hybrid_parameters.get('run_refactored_network', False) + + routing_type = [run_hybrid, use_topobathy, run_refactored] + + _routing_scheme_map = { + MCOnly: [False, False, False], + SimpleHybridDiffusive: [True, False, False], + HybridNatlXSectionNonRefactored: [True, True, False], + HybridNatlXSectionRefactored: [True, True, True], + } + + # Default to MCOnly routing + routing_scheme = MCOnly + # Check user input to determine the routing scheme + for key, value in _routing_scheme_map.items(): + if value==routing_type: + routing_scheme = key + + routing = routing_scheme(self.hybrid_parameters) + + ( + self._dataframe, + self._connections + ) = routing.update_routing_domain(self.dataframe, self.connections) + + self._routing = routing + def create_independent_networks(self,): LOG.info("organizing connections into reaches ...") diff --git a/src/troute-network/troute/AbstractRouting.py b/src/troute-network/troute/AbstractRouting.py new file mode 100644 index 000000000..2190acc76 --- /dev/null +++ b/src/troute-network/troute/AbstractRouting.py @@ -0,0 +1,352 @@ +from abc import ABC, abstractmethod +import logging +import yaml +import json +import xarray as xr +import pandas as pd + +from troute.nhd_network import reverse_network +from troute.nhd_network_utilities_v02 import organize_independent_networks, build_refac_connections + +LOG = logging.getLogger('') + +def read_diffusive_domain(domain_file): + ''' + Read diffusive domain data from .ymal or .json file. + + Arguments + --------- + domain_file (str or pathlib.Path): Path of diffusive domain file + + Returns + ------- + data (dict int: [int]): domain tailwater segments: list of segments in domain + (includeing tailwater segment) + + ''' + if domain_file[-4:] == "yaml": + with open(domain_file) as domain: + data = yaml.load(domain, Loader=yaml.SafeLoader) + else: + with open(domain_file) as domain: + data = json.load(domain) + + return data + +def read_netcdf(geo_file_path): + ''' + Open a netcdf file with xarray and convert to dataframe + + Arguments + --------- + geo_file_path (str or pathlib.Path): netCDF filepath + + Returns + ------- + ds.to_dataframe() (DataFrame): netCDF contents + + Notes + ----- + - When handling large volumes of netCDF files, xarray is not the most efficient. + + ''' + with xr.open_dataset(geo_file_path) as ds: + return ds.to_dataframe() + + +class AbstractRouting(ABC): + """ + + """ + __slots__ = ["hybrid_params", "_diffusive_domain", "_coastal_boundary_depth_df", + "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", + "_refactored_diffusive_network_data", "_refactored_reaches", + "_unrefactored_topobathy_df",] + + def __init__(self): + """ + + """ + self._diffusive_domain = None + self._diffusive_network_data = None + self._topobathy_df = pd.DataFrame() + self._unrefactored_topobathy_df = pd.DataFrame() + self._refactored_diffusive_domain = None + self._refactored_diffusive_network_data = None + self._refactored_reaches = {} + + @abstractmethod + def update_routing_domain(self, dataframe, connections): + pass + + @property + @abstractmethod + def diffusive_network_data(self): + pass + + @property + @abstractmethod + def topobathy_df(self): + pass + + @property + @abstractmethod + def refactored_diffusive_domain(self): + pass + + @property + @abstractmethod + def refactored_reaches(self): + pass + + @property + @abstractmethod + def unrefactored_topobathy_df(self): + pass + + +class MCOnly(AbstractRouting): + + def __init__(self, hybrid_params): + self.hybrid_params = hybrid_params + + super().__init__() + + def update_routing_domain(self, dataframe, connections): + return dataframe, connections, self._diffusive_domain + + @property + def diffusive_network_data(self): + self._diffusive_network_data = None + return self._diffusive_network_data + + @property + def topobathy_df(self): + self._topobathy_df = pd.DataFrame() + return self._topobathy_df + + @property + def refactored_diffusive_domain(self): + self._refactored_diffusive_domain = None + return self._refactored_diffusive_domain + + @property + def refactored_reaches(self): + self._refactored_reaches = {} + return self._refactored_reaches + + @property + def unrefactored_topobathy_df(self): + self._unrefactored_topobathy_df = pd.DataFrame() + return self._unrefactored_topobathy_df + + +class SimpleHybridDiffusive(AbstractRouting): + + def __init__(self, hybrid_params): + self.hybrid_params = hybrid_params + + super().__init__() + + def update_routing_domain(self, dataframe, connections): + #========================================================================== + # build diffusive domain data and edit MC domain data for hybrid simulation + domain_file = self.hybrid_params.get("diffusive_domain", None) + self._diffusive_domain = read_diffusive_domain(domain_file) + self._diffusive_network_data = {} + + rconn_diff0 = reverse_network(connections) + + for tw in self._diffusive_domain: + mainstem_segs = self._diffusive_domain[tw]['links'] + # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is + # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. + upstream_boundary_mainstem_link = self._diffusive_domain[tw]['upstream_boundary_link_mainstem'] + if upstream_boundary_mainstem_link[0] in mainstem_segs: + mainstem_segs.remove(upstream_boundary_mainstem_link[0]) + + # ===== build diffusive network data objects ==== + self._diffusive_network_data[tw] = {} + + # add diffusive domain segments + self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs + + # diffusive domain tributary segments + trib_segs = [] + + for seg in mainstem_segs: + us_list = rconn_diff0[seg] + for u in us_list: + if u not in mainstem_segs: + trib_segs.append(u) + + self._diffusive_network_data[tw]['tributary_segments'] = trib_segs + # diffusive domain connections object + self._diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, reaches, rconn_diff = organize_independent_networks( + self._diffusive_network_data[tw]['connections'], + set(trib_segs), + set(), + ) + + self._diffusive_network_data[tw]['rconn'] = rconn_diff + self._diffusive_network_data[tw]['reaches'] = reaches[tw] + + # RouteLink parameters + self._diffusive_network_data[tw]['param_df'] = dataframe.filter( + (mainstem_segs + trib_segs), + axis = 0, + ) + self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link + + # ==== remove diffusive domain segs from MC domain ==== + # drop indices from param_df + dataframe = dataframe.drop(mainstem_segs) + + # remove keys from connections dictionary + for s in mainstem_segs: + connections.pop(s) + + # update downstream connections of trib segs + for us in trib_segs: + connections[us] = [] + + return dataframe, connections + + @property + def diffusive_network_data(self): + return self._diffusive_network_data + + @property + def topobathy_df(self): + self._topobathy_df = pd.DataFrame() + return self._topobathy_df + + @property + def refactored_diffusive_domain(self): + self._refactored_diffusive_domain = None + return self._refactored_diffusive_domain + + @property + def refactored_reaches(self): + self._refactored_reaches = {} + return self._refactored_reaches + + @property + def unrefactored_topobathy_df(self): + self._unrefactored_topobathy_df = pd.DataFrame() + return self._unrefactored_topobathy_df + + +class HybridNatlXSectionNonRefactored(SimpleHybridDiffusive): + + def __init__(self, hybrid_params): + + super().__init__(hybrid_params = hybrid_params) + + @property + def diffusive_network_data(self): + return self._diffusive_network_data + + @property + def topobathy_df(self): + if self._topobathy_df.empty: + topobathy_file = self.hybrid_params.get("topobathy_domain", None) + self._topobathy_df = read_netcdf(topobathy_file).set_index('link') + self._topobathy_df.index = self._topobathy_df.index.astype(int) + return self._topobathy_df + + @property + def refactored_diffusive_domain(self): + self._refactored_diffusive_domain = None + return self._refactored_diffusive_domain + + @property + def refactored_reaches(self): + self._refactored_reaches = {} + return self._refactored_reaches + + @property + def unrefactored_topobathy_df(self): + self._unrefactored_topobathy_df = pd.DataFrame() + return self._unrefactored_topobathy_df + + +class HybridNatlXSectionRefactored(SimpleHybridDiffusive): + + def __init__(self, hybrid_params): + + super().__init__(hybrid_params = hybrid_params) + + @property + def diffusive_network_data(self): + return self._diffusive_network_data + + @property + def topobathy_df(self): + if self._topobathy_df.empty: + refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + self._topobathy_df = read_netcdf(refactored_topobathy_file).set_index('link') + return self._topobathy_df + + @property + def refactored_diffusive_domain(self): + if not self._refactored_diffusive_domain: + refactored_domain_file = self.hybrid_params.get("refactored_domain", None) + self._refactored_diffusive_domain = read_diffusive_domain(refactored_domain_file) + return self._refactored_diffusive_domain + + @property + def refactored_reaches(self): + if not self._refactored_reaches: + refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + diffusive_parameters = {'geo_file_path': refactored_topobathy_file} + refactored_connections = build_refac_connections(diffusive_parameters) + + for tw in self._diffusive_domain: + + # list of stream segments of a single refactored diffusive domain + refac_tw = self.refactored_diffusive_domain[tw]['refac_tw'] + rlinks_tw = self.refactored_diffusive_domain[tw]['rlinks'] + refactored_connections_tw = {} + + # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections + # for a single refactored diffusive domain defined by a current tw. + for k in rlinks_tw: + if k in refactored_connections.keys() and k != refac_tw: + refactored_connections_tw[k] = refactored_connections[k] + + trib_segs = self.diffusive_network_data[tw]['tributary_segments'] + refactored_diffusive_network_data = {} + refactored_diffusive_network_data[refac_tw] = {} + refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs + refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw + + for k in trib_segs: + refactored_diffusive_network_data[refac_tw]['connections'][k] = [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( + refactored_diffusive_network_data[refac_tw]['connections'], + set(trib_segs), + set(), + ) + + self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] + refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] + refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] + return self._refactored_reaches + + @property + def unrefactored_topobathy_df(self): + if self._unrefactored_topobathy_df.empty: + topobathy_file = self.hybrid_params.get("topobathy_domain", None) + self._unrefactored_topobathy_df = read_netcdf(topobathy_file).set_index('link') + self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) + return self._unrefactored_topobathy_df + + diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 74d299448..fdd4931d1 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -1,4 +1,4 @@ -from .RoutingScheme import RoutingScheme +from .AbstractNetwork import AbstractNetwork import pandas as pd import numpy as np import geopandas as gpd @@ -90,7 +90,7 @@ def node_key_func(x): return df -class HYFeaturesNetwork(RoutingScheme): +class HYFeaturesNetwork(AbstractNetwork): """ """ diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index e93d47f15..77caf44c6 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -1,4 +1,4 @@ -from .RoutingScheme import RoutingScheme +from .AbstractNetwork import AbstractNetwork import troute.nhd_io as nhd_io import troute.nhd_preprocess as nhd_prep import pandas as pd @@ -16,7 +16,7 @@ __verbose__ = True #FIXME pass verbosity -class NHDNetwork(RoutingScheme): +class NHDNetwork(AbstractNetwork): """ """ From 54b971440f6ec2b5b526607f8c569af29cfb496f Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Tue, 10 Jan 2023 20:14:31 +0000 Subject: [PATCH 15/54] edited names of routing objects, consolidated inputs to nwm_route into fewer inputs --- src/troute-network/troute/AbstractNetwork.py | 6 +- src/troute-network/troute/AbstractRouting.py | 36 +------- src/troute-nwm/src/nwm_routing/__main__.py | 95 ++------------------ src/troute-routing/troute/routing/compute.py | 72 ++++++++------- 4 files changed, 55 insertions(+), 154 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index e48671cc5..eb497531a 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -444,9 +444,9 @@ def initialize_routing_scheme(self,): _routing_scheme_map = { MCOnly: [False, False, False], - SimpleHybridDiffusive: [True, False, False], - HybridNatlXSectionNonRefactored: [True, True, False], - HybridNatlXSectionRefactored: [True, True, True], + MCwithDiffusive: [True, False, False], + MCwithDiffusiveNatlXSectionNonRefactored: [True, True, False], + MCwithDiffusiveNatlXSectionRefactored: [True, True, True], } # Default to MCOnly routing diff --git a/src/troute-network/troute/AbstractRouting.py b/src/troute-network/troute/AbstractRouting.py index 2190acc76..be10b4eb8 100644 --- a/src/troute-network/troute/AbstractRouting.py +++ b/src/troute-network/troute/AbstractRouting.py @@ -113,35 +113,30 @@ def __init__(self, hybrid_params): super().__init__() def update_routing_domain(self, dataframe, connections): - return dataframe, connections, self._diffusive_domain + return dataframe, connections @property def diffusive_network_data(self): - self._diffusive_network_data = None return self._diffusive_network_data @property def topobathy_df(self): - self._topobathy_df = pd.DataFrame() return self._topobathy_df @property def refactored_diffusive_domain(self): - self._refactored_diffusive_domain = None return self._refactored_diffusive_domain @property def refactored_reaches(self): - self._refactored_reaches = {} return self._refactored_reaches @property def unrefactored_topobathy_df(self): - self._unrefactored_topobathy_df = pd.DataFrame() return self._unrefactored_topobathy_df -class SimpleHybridDiffusive(AbstractRouting): +class MCwithDiffusive(AbstractRouting): def __init__(self, hybrid_params): self.hybrid_params = hybrid_params @@ -241,16 +236,12 @@ def unrefactored_topobathy_df(self): return self._unrefactored_topobathy_df -class HybridNatlXSectionNonRefactored(SimpleHybridDiffusive): +class MCwithDiffusiveNatlXSectionNonRefactored(MCwithDiffusive): def __init__(self, hybrid_params): super().__init__(hybrid_params = hybrid_params) - @property - def diffusive_network_data(self): - return self._diffusive_network_data - @property def topobathy_df(self): if self._topobathy_df.empty: @@ -258,33 +249,14 @@ def topobathy_df(self): self._topobathy_df = read_netcdf(topobathy_file).set_index('link') self._topobathy_df.index = self._topobathy_df.index.astype(int) return self._topobathy_df - - @property - def refactored_diffusive_domain(self): - self._refactored_diffusive_domain = None - return self._refactored_diffusive_domain - - @property - def refactored_reaches(self): - self._refactored_reaches = {} - return self._refactored_reaches - - @property - def unrefactored_topobathy_df(self): - self._unrefactored_topobathy_df = pd.DataFrame() - return self._unrefactored_topobathy_df -class HybridNatlXSectionRefactored(SimpleHybridDiffusive): +class MCwithDiffusiveNatlXSectionRefactored(MCwithDiffusive): def __init__(self, hybrid_params): super().__init__(hybrid_params = hybrid_params) - @property - def diffusive_network_data(self): - return self._diffusive_network_data - @property def topobathy_df(self): if self._topobathy_df.empty: diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index cf6bc29cb..15a357c98 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -170,42 +170,18 @@ def main_v04(argv): route_start_time = time.time() run_results = nwm_route( - network.connections, - network.reverse_network, - network.waterbody_connections, - network._reaches_by_tw, ## check: def name is different from return self._ .. + network, + data_assimilation, parallel_compute_method, compute_kernel, subnetwork_target_size, cpu_pool, - network.t0, ## check if t0 is being updated dt, nts, qts_subdivisions, - network.independent_networks, - network.dataframe, - network.q0, - network._qlateral, - data_assimilation.usgs_df, - data_assimilation.lastobs_df, - data_assimilation.reservoir_usgs_df, - data_assimilation.reservoir_usgs_param_df, - data_assimilation.reservoir_usace_df, - data_assimilation.reservoir_usace_param_df, - data_assimilation.assimilation_parameters, assume_short_ts, return_courant, - network.waterbody_dataframe, - waterbody_parameters, - network.waterbody_types_dataframe, - network.waterbody_type_specified, - network.diffusive_network_data, - network.topobathy_df, - network.refactored_diffusive_domain, - network.refactored_reaches, subnetwork_list, - network.coastal_boundary_depth_df, - network.unrefactored_topobathy_df, ) # returns list, first item is run result, second item is subnetwork items @@ -1031,42 +1007,18 @@ def _handle_args_v03(argv): return parser.parse_args(argv) def nwm_route( - downstream_connections, - upstream_connections, - waterbodies_in_connections, - reaches_bytw, + network, + data_assimilation, parallel_compute_method, compute_kernel, subnetwork_target_size, cpu_pool, - t0, dt, nts, qts_subdivisions, - independent_networks, - param_df, - q0, - qlats, - usgs_df, - lastobs_df, - reservoir_usgs_df, - reservoir_usgs_param_df, - reservoir_usace_df, - reservoir_usace_param_df, - da_parameter_dict, assume_short_ts, return_courant, - waterbodies_df, - waterbody_parameters, - waterbody_types_df, - waterbody_type_specified, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, subnetwork_list, - coastal_boundary_depth_df, - unrefactored_topobathy_df, ): ################### Main Execution Loop across ordered networks @@ -1083,35 +1035,17 @@ def nwm_route( start_time_mc = time.time() results = compute_nhd_routing_v02( - downstream_connections, - upstream_connections, - waterbodies_in_connections, - reaches_bytw, + network, + data_assimilation, compute_kernel, parallel_compute_method, subnetwork_target_size, # The default here might be the whole network or some percentage... cpu_pool, - t0, dt, nts, qts_subdivisions, - independent_networks, - param_df, - q0, - qlats, - usgs_df, - lastobs_df, - reservoir_usgs_df, - reservoir_usgs_param_df, - reservoir_usace_df, - reservoir_usace_param_df, - da_parameter_dict, assume_short_ts, return_courant, - waterbodies_df, - waterbody_parameters, - waterbody_types_df, - waterbody_type_specified, subnetwork_list, ) LOG.debug("MC computation complete in %s seconds." % (time.time() - start_time_mc)) @@ -1120,7 +1054,7 @@ def nwm_route( results = results[0] # run diffusive side of a hybrid simulation - if diffusive_network_data: + if network.diffusive_network_data: start_time_diff = time.time() ''' # retrieve MC-computed streamflow value at upstream boundary of diffusive mainstem @@ -1144,23 +1078,12 @@ def nwm_route( results.extend( compute_diffusive_routing( results, - diffusive_network_data, + network, + data_assimilation, cpu_pool, - t0, dt, nts, - q0, - qlats, qts_subdivisions, - usgs_df, - lastobs_df, - da_parameter_dict, - waterbodies_df, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - coastal_boundary_depth_df, - unrefactored_topobathy_df, ) ) LOG.debug("Diffusive computation complete in %s seconds." % (time.time() - start_time_diff)) diff --git a/src/troute-routing/troute/routing/compute.py b/src/troute-routing/troute/routing/compute.py index a5673817c..f1130362f 100644 --- a/src/troute-routing/troute/routing/compute.py +++ b/src/troute-routing/troute/routing/compute.py @@ -218,38 +218,41 @@ def _prep_reservoir_da_dataframes(reservoir_usgs_df, reservoir_usgs_param_df, re return reservoir_usgs_df_sub, reservoir_usgs_df_time, reservoir_usgs_update_time, reservoir_usgs_prev_persisted_flow, reservoir_usgs_persistence_update_time, reservoir_usgs_persistence_index, reservoir_usace_df_sub, reservoir_usace_df_time, reservoir_usace_update_time, reservoir_usace_prev_persisted_flow, reservoir_usace_persistence_update_time, reservoir_usace_persistence_index, waterbody_types_df_sub def compute_nhd_routing_v02( - connections, - rconn, - wbody_conn, - reaches_bytw, + network, + data_assimilation, compute_func_name, parallel_compute_method, subnetwork_target_size, cpu_pool, - t0, dt, nts, qts_subdivisions, - independent_networks, - param_df, - q0, - qlats, - usgs_df, - lastobs_df, - reservoir_usgs_df, - reservoir_usgs_param_df, - reservoir_usace_df, - reservoir_usace_param_df, - da_parameter_dict, assume_short_ts, return_courant, - waterbodies_df, - waterbody_parameters, - waterbody_types_df, - waterbody_type_specified, subnetwork_list, ): + connections = network.connections + rconn = network.reverse_network + wbody_conn = network.waterbody_connections + reaches_bytw = network.reaches_by_tailwater + t0 = network.t0 + independent_networks = network.independent_networks + param_df = network.dataframe + q0 = network.q0 + qlats = network.qlateral + usgs_df = data_assimilation.usgs_df + lastobs_df = data_assimilation.lastobs_df + reservoir_usgs_df = data_assimilation.reservoir_usgs_df + reservoir_usgs_param_df = data_assimilation.reservoir_usgs_param_df + reservoir_usace_df = data_assimilation.reservoir_usace_df + reservoir_usace_param_df = data_assimilation.reservoir_usace_param_df + da_parameter_dict = data_assimilation.assimilation_parameters + waterbodies_df = network.waterbody_dataframe + waterbody_parameters = network.waterbody_parameters + waterbody_types_df = network.waterbody_types_dataframe + waterbody_type_specified = network.waterbody_type_specified + da_decay_coefficient = da_parameter_dict.get("da_decay_coefficient", 0) param_df["dt"] = dt param_df = param_df.astype("float32") @@ -1124,25 +1127,28 @@ def compute_nhd_routing_v02( def compute_diffusive_routing( results, - diffusive_network_data, + network, + data_assimilation, cpu_pool, - t0, dt, nts, - q0, - qlats, qts_subdivisions, - usgs_df, - lastobs_df, - da_parameter_dict, - waterbodies_df, - topobathy, - refactored_diffusive_domain, - refactored_reaches, - coastal_boundary_depth_df, - unrefactored_topobathy, ): + diffusive_network_data = network.diffusive_network_data + t0 = network.t0 + q0 = network.q0 + qlats = network.qlateral + usgs_df = data_assimilation.usgs_df + lastobs_df = data_assimilation.lastobs_df + da_parameter_dict = data_assimilation.assimilation_parameters + waterbodies_df = network.waterbody_dataframe + topobathy = network.topobathy_df + refactored_diffusive_domain = network.refactored_diffusive_domain + refactored_reaches = network.refactored_reaches + coastal_boundary_depth_df = network.coastal_boundary_depth_df + unrefactored_topobathy = network.unrefactored_topobathy_df + results_diffusive = [] for tw in diffusive_network_data: # <------- TODO - by-network parallel loop, here. trib_segs = None From 527b5bdf30cb5129d4488983e6ad064fd94ced13 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Mon, 23 Jan 2023 17:44:51 +0000 Subject: [PATCH 16/54] edits to diffusive routing --- src/troute-network/troute/AbstractNetwork.py | 4 +++- src/troute-network/troute/AbstractRouting.py | 3 +++ .../troute/routing/diffusive_utils.py | 21 +++++++++++-------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index eb497531a..bbe75fe5f 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -151,7 +151,9 @@ def assemble_forcings(self, run,): #LOG.debug( # "coastal boundary elevation observation DataFrame creation complete in %s seconds." \ # % (time.time() - start_time) - #) + #) + else: + self._coastal_boundary_depth_df = pd.DataFrame() def new_q0(self, run_results): """ diff --git a/src/troute-network/troute/AbstractRouting.py b/src/troute-network/troute/AbstractRouting.py index be10b4eb8..7ff5a46f9 100644 --- a/src/troute-network/troute/AbstractRouting.py +++ b/src/troute-network/troute/AbstractRouting.py @@ -179,6 +179,9 @@ def update_routing_domain(self, dataframe, connections): # diffusive domain connections object self._diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} + # make sure that no downstream link below tw + self._diffusive_network_data[tw]['connections'][tw] = [] + # diffusive domain reaches and upstream connections. # break network at tributary segments _, reaches, rconn_diff = organize_independent_networks( diff --git a/src/troute-routing/troute/routing/diffusive_utils.py b/src/troute-routing/troute/routing/diffusive_utils.py index 053124b1b..9c9ca6071 100644 --- a/src/troute-routing/troute/routing/diffusive_utils.py +++ b/src/troute-routing/troute/routing/diffusive_utils.py @@ -993,15 +993,15 @@ def fp_coastal_boundary_input_map( dsbd_option -- (int) 1 or 2 for coastal boundary depth data or normal depth data, respectively nts_db_g -- (int) number of coastal boundary input data timesteps dbcd_g -- (float) coastal boundary input data time series [m] - """ - - date_time_obj1 = datetime.strptime(coastal_boundary_depth_df.columns[1], '%Y-%m-%d %H:%M:%S') - date_time_obj0 = datetime.strptime(coastal_boundary_depth_df.columns[0], '%Y-%m-%d %H:%M:%S') - dt_db_g = (date_time_obj1 - date_time_obj0).total_seconds() - nts_db_g = int((tfin_g - t0_g) * 3600.0 / dt_db_g) + 1 # include initial time 0 to the final time - dbcd_g = np.ones(nts_db_g) + """ - if not coastal_boundary_depth_df.empty: + if not coastal_boundary_depth_df.empty: + date_time_obj1 = datetime.strptime(coastal_boundary_depth_df.columns[1], '%Y-%m-%d %H:%M:%S') + date_time_obj0 = datetime.strptime(coastal_boundary_depth_df.columns[0], '%Y-%m-%d %H:%M:%S') + dt_db_g = (date_time_obj1 - date_time_obj0).total_seconds() + nts_db_g = int((tfin_g - t0_g) * 3600.0 / dt_db_g) + 1 # include initial time 0 to the final time + dbcd_g = np.ones(nts_db_g) + dt_timeslice = timedelta(minutes=dt_db_g/60.0) tfin = t0 + dt_timeslice*(nts_db_g-1) timestamps = pd.date_range(t0, tfin, freq=dt_timeslice) @@ -1043,8 +1043,11 @@ def fp_coastal_boundary_input_map( dbcd_g[:] = dbcd_df_interpolated.loc[tw].values else: + dt_db_g = 3600.0 # by default in sec + nts_db_g = int((tfin_g - t0_g) * 3600.0 / dt_db_g) + 1 # include initial time 0 to the final time + dbcd_g = np.ones(nts_db_g) dsbd_option = 2 # instead, use normal depth as the downstream boundary condition - dbcd_g[:] = 0.0 + dbcd_g[:] = 0.0 return dt_db_g, dsbd_option, nts_db_g, dbcd_g From f33f2a71c4a4a29966289c57433b5884d1fb98f8 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Mon, 23 Jan 2023 18:11:13 +0000 Subject: [PATCH 17/54] temporary fix for writing CHRTOUT files in serial rather than parallel --- src/troute-network/troute/nhd_io.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/troute-network/troute/nhd_io.py b/src/troute-network/troute/nhd_io.py index c278d5a9a..94427efc3 100644 --- a/src/troute-network/troute/nhd_io.py +++ b/src/troute-network/troute/nhd_io.py @@ -773,19 +773,28 @@ def write_chrtout( LOG.debug("Writing t-route data to %d CHRTOUT files" % (nfiles_to_write)) start = time.time() - with Parallel(n_jobs=cpu_pool) as parallel: - - jobs = [] + try: + with Parallel(n_jobs=cpu_pool) as parallel: + + jobs = [] + for i, f in enumerate(chrtout_files[:nfiles_to_write]): + + s = time.time() + variables = { + varname: (qtrt[:,i], dim, attrs) + } + jobs.append(delayed(write_to_netcdf)(f, variables)) + #LOG.debug("Writing %s." % (f)) + + parallel(jobs) + except: for i, f in enumerate(chrtout_files[:nfiles_to_write]): - s = time.time() variables = { - varname: (qtrt[:,i], dim, attrs) + varname: (qtrt[:i], dim, attrs) } - jobs.append(delayed(write_to_netcdf)(f, variables)) + write_to_netcdf(f, variables) LOG.debug("Writing %s." % (f)) - - parallel(jobs) LOG.debug("Writing t-route data to %d CHRTOUT files took %s seconds." % (nfiles_to_write, (time.time() - start))) From d5fbb148657a7bfd323863102b8100cad02b7c72 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Wed, 25 Jan 2023 21:41:48 +0000 Subject: [PATCH 18/54] minor updates to previous push --- src/troute-network/troute/AbstractNetwork.py | 16 +++++++++------- src/troute-network/troute/AbstractRouting.py | 2 +- src/troute-network/troute/HYFeaturesNetwork.py | 15 ++++++++------- src/troute-network/troute/NHDNetwork.py | 1 + src/troute-network/troute/nhd_io.py | 1 + 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index bbe75fe5f..a77053342 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -10,7 +10,7 @@ from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition from troute.nhd_network_utilities_v02 import organize_independent_networks, build_channel_initial_state, build_refac_connections import troute.nhd_io as nhd_io -from .AbstractRouting import * +from .AbstractRouting import MCOnly, MCwithDiffusive, MCwithDiffusiveNatlXSectionNonRefactored, MCwithDiffusiveNatlXSectionRefactored LOG = logging.getLogger('') @@ -20,7 +20,7 @@ class AbstractNetwork(ABC): """ __slots__ = ["_dataframe", "_waterbody_connections", "_gages", "_terminal_codes", "_connections", "_waterbody_df", - "_waterbody_types_df", "_waterbody_type_specified", + "_waterbody_types_df", "_waterbody_type_specified", "_link_gage_df", "_independent_networks", "_reaches_by_tw", "_flowpath_dict", "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", "_qlateral", "_break_segments", "_segment_index", "_coastal_boundary_depth_df", @@ -36,6 +36,7 @@ def __init__(self,): self._q0 = None self._t0 = None self._qlateral = None + self._link_gage_df = None #qlat_const = forcing_parameters.get("qlat_const", 0) #FIXME qlat_const """ Figure out a good way to default initialize to qlat_const/c @@ -239,11 +240,11 @@ def reaches_by_tailwater(self): @property def waterbody_dataframe(self): - return self._waterbody_df.sort_index() + return self._waterbody_df @property def waterbody_types_dataframe(self): - return self._waterbody_types_df.sort_index() + return self._waterbody_types_df @property def waterbody_type_specified(self): @@ -319,9 +320,10 @@ def segment_index(self): @property def link_gage_df(self): - link_gage_df = pd.DataFrame.from_dict(self._gages) - link_gage_df.index.name = 'link' - return link_gage_df + if self._link_gage_df is None: + self._link_gage_df = pd.DataFrame.from_dict(self._gages) + self._link_gage_df.index.name = 'link' + return self._link_gage_df @property @abstractmethod diff --git a/src/troute-network/troute/AbstractRouting.py b/src/troute-network/troute/AbstractRouting.py index 7ff5a46f9..ddeab6162 100644 --- a/src/troute-network/troute/AbstractRouting.py +++ b/src/troute-network/troute/AbstractRouting.py @@ -108,7 +108,7 @@ def unrefactored_topobathy_df(self): class MCOnly(AbstractRouting): def __init__(self, hybrid_params): - self.hybrid_params = hybrid_params + self.hybrid_params = None super().__init__() diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index fdd4931d1..c34bc4b67 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -3,7 +3,6 @@ import numpy as np import geopandas as gpd import time -import os import json from pathlib import Path import pyarrow.parquet as pq @@ -54,10 +53,10 @@ def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=Non for level-pool reservoir computation. """ def node_key_func(x): - return int(x[3:]) - if os.path.splitext(parm_file)[1]=='.gpkg': + return int( x.split('-')[-1] ) + if Path(parm_file).suffix=='.gpkg': df = gpd.read_file(parm_file, layer="lake_attributes").set_index('id') - elif os.path.splitext(parm_file)[1]=='.json': + elif Path(parm_file).suffix=='.json': df = pd.read_json(parm_file, orient="index") df.index = df.index.map(node_key_func) @@ -75,11 +74,11 @@ def read_ngen_waterbody_type_df(parm_file, lake_index_field="wb-id", lake_id_mas # layer, but as of now (Nov 22, 2022) there doesn't seem to be a differentiation # between USGS reservoirs, USACE reservoirs, or RFC reservoirs... def node_key_func(x): - return int(x[3:]) + return int( x.split('-')[-1] ) - if os.path.splitext(parm_file)[1]=='.gpkg': + if Path(parm_file).suffix=='.gpkg': df = gpd.read_file(parm_file, layer="crosswalk").set_index('id') - elif os.path.splitext(parm_file)[1]=='.json': + elif Path(parm_file).suffix=='.json': df = pd.read_json(parm_file, orient="index") df.index = df.index.map(node_key_func) @@ -306,6 +305,7 @@ def read_geo_file(self,): self.waterbody_dataframe.reset_index() .drop_duplicates(subset=lake_id) .set_index(lake_id) + .sort_index() ) try: @@ -319,6 +319,7 @@ def read_geo_file(self,): self.waterbody_types_dataframe.reset_index() .drop_duplicates(subset=lake_id) .set_index(lake_id) + .sort_index() ) except ValueError: diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index 77caf44c6..f5f47289e 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -270,6 +270,7 @@ def read_geo_file(self,): self.waterbody_dataframe.reset_index() .drop_duplicates(subset="lake_id") .set_index("lake_id") + .sort_index() ) # Declare empty dataframe diff --git a/src/troute-network/troute/nhd_io.py b/src/troute-network/troute/nhd_io.py index 94427efc3..710d5b87a 100644 --- a/src/troute-network/troute/nhd_io.py +++ b/src/troute-network/troute/nhd_io.py @@ -356,6 +356,7 @@ def read_reservoir_parameter_file( df1 = (df1.reset_index() .drop_duplicates(subset="lake_id") .set_index("lake_id") + .sort_index() ) # recode to levelpool (1) for reservoir DA types set to false From bc715229c23049e81ec1a6842bdcf4a96118969f Mon Sep 17 00:00:00 2001 From: shorvath-noaa <103054653+shorvath-noaa@users.noreply.github.com> Date: Wed, 25 Jan 2023 15:22:52 -0700 Subject: [PATCH 19/54] Delete RoutingScheme.py --- src/troute-network/troute/RoutingScheme.py | 245 --------------------- 1 file changed, 245 deletions(-) delete mode 100644 src/troute-network/troute/RoutingScheme.py diff --git a/src/troute-network/troute/RoutingScheme.py b/src/troute-network/troute/RoutingScheme.py deleted file mode 100644 index 049b0b84f..000000000 --- a/src/troute-network/troute/RoutingScheme.py +++ /dev/null @@ -1,245 +0,0 @@ -from .AbstractNetwork import AbstractNetwork -import logging -import yaml -import json -import xarray as xr -import pandas as pd - -from troute.nhd_network import reverse_network -from troute.nhd_network_utilities_v02 import organize_independent_networks, build_refac_connections - -LOG = logging.getLogger('') - -def read_diffusive_domain(domain_file): - ''' - Read diffusive domain data from .ymal or .json file. - - Arguments - --------- - domain_file (str or pathlib.Path): Path of diffusive domain file - - Returns - ------- - data (dict int: [int]): domain tailwater segments: list of segments in domain - (includeing tailwater segment) - - ''' - if domain_file[-4:] == "yaml": - with open(domain_file) as domain: - data = yaml.load(domain, Loader=yaml.SafeLoader) - else: - with open(domain_file) as domain: - data = json.load(domain) - - return data - -def read_netcdf(geo_file_path): - ''' - Open a netcdf file with xarray and convert to dataframe - - Arguments - --------- - geo_file_path (str or pathlib.Path): netCDF filepath - - Returns - ------- - ds.to_dataframe() (DataFrame): netCDF contents - - Notes - ----- - - When handling large volumes of netCDF files, xarray is not the most efficient. - - ''' - with xr.open_dataset(geo_file_path) as ds: - return ds.to_dataframe() - - -class RoutingScheme(AbstractNetwork): - """ - - """ - __slots__ = ["hybrid_params", "_diffusive_domain", "_coastal_boundary_depth_df", - "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", - "_refactored_diffusive_network_data", "_refactored_reaches", - "_unrefactored_topobathy_df",] - - def __init__(self,): - """ - - """ - self._diffusive_domain = None - self._diffusive_network_data = None - self._topobathy_df = pd.DataFrame() - self._unrefactored_topobathy_df = pd.DataFrame() - self._refactored_diffusive_domain = None - self._refactored_diffusive_network_data = None - self._refactored_reaches = {} - - # Determine whether to run hybrid routing from user input - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - domain_file = self.hybrid_parameters.get("diffusive_domain", None) - - if run_hybrid and domain_file: - #========================================================================== - # build diffusive domain data and edit MC domain data for hybrid simulation - self._diffusive_domain = read_diffusive_domain(domain_file) - self._diffusive_network_data = {} - - rconn_diff0 = reverse_network(self._connections) - - for tw in self._diffusive_domain: - mainstem_segs = self._diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = self._diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - self._diffusive_network_data[tw] = {} - - # add diffusive domain segments - self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - self._diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - self._diffusive_network_data[tw]['connections'] = {k: self._connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = organize_independent_networks( - self._diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - self._diffusive_network_data[tw]['rconn'] = rconn_diff - self._diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - self._diffusive_network_data[tw]['param_df'] = self._dataframe.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - self._dataframe = self._dataframe.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - self._connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - self._connections[us] = [] - - super().__init__() - - @property - def diffusive_network_data(self,): - return self._diffusive_network_data - - @property - def topobathy_df(self,): - if self._topobathy_df.empty: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) - - if run_hybrid and use_topobathy: - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_refactored: - refactored_topobathy_file = self.hybrid_parameters.get("refactored_topobathy_domain", None) - self._topobathy_df = read_netcdf(refactored_topobathy_file).set_index('link') - else: - topobathy_file = self.hybrid_parameters.get("topobathy_domain", None) - self._topobathy_df = read_netcdf(topobathy_file).set_index('link') - self._topobathy_df.index = self._topobathy_df.index.astype(int) - - return self._topobathy_df - - @property - def refactored_diffusive_domain(self,): - if not self._refactored_diffusive_domain: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_hybrid and run_refactored: - refactored_domain_file = self.hybrid_parameters.get("refactored_domain", None) - - self._refactored_diffusive_domain = read_diffusive_domain(refactored_domain_file) - - return self._refactored_diffusive_domain - - @property - def refactored_reaches(self,): - if not self._refactored_reaches: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_hybrid and run_refactored: - refactored_topobathy_file = self.hybrid_parameters.get("refactored_topobathy_domain", None) - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = build_refac_connections(diffusive_parameters) - - for tw in self._diffusive_domain: - - # list of stream segments of a single refactored diffusive domain - refac_tw = self.refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = self.refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - trib_segs = self.diffusive_network_data[tw]['tributary_segments'] - refactored_diffusive_network_data = {} - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k] = [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] - - return self._refactored_reaches - - @property - def unrefactored_topobathy_df(self,): - if self._unrefactored_topobathy_df.empty: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_hybrid and use_topobathy and run_refactored: - topobathy_file = self.hybrid_parameters.get("topobathy_domain", None) - self._unrefactored_topobathy_df = read_netcdf(topobathy_file).set_index('link') - self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) - - return self._unrefactored_topobathy_df - From 11ca4ac156f3d91c5be05ac40eae3f503638a39a Mon Sep 17 00:00:00 2001 From: shorvath-noaa <103054653+shorvath-noaa@users.noreply.github.com> Date: Fri, 27 Jan 2023 11:59:54 -0700 Subject: [PATCH 20/54] Delete nhd_preprocess.py --- src/troute-network/troute/nhd_preprocess.py | 1148 ------------------- 1 file changed, 1148 deletions(-) delete mode 100644 src/troute-network/troute/nhd_preprocess.py diff --git a/src/troute-network/troute/nhd_preprocess.py b/src/troute-network/troute/nhd_preprocess.py deleted file mode 100644 index 2422bac84..000000000 --- a/src/troute-network/troute/nhd_preprocess.py +++ /dev/null @@ -1,1148 +0,0 @@ -import time -import pathlib -import logging -from datetime import datetime -from collections import defaultdict - -import pandas as pd -import numpy as np -import xarray as xr - -import troute.nhd_network_utilities_v02 as nnu -import troute.nhd_network as nhd_network -import troute.nhd_io as nhd_io - -LOG = logging.getLogger('') - -def read_geo_file(supernetwork_parameters, waterbody_parameters, data_assimilation_parameters): - ''' - Construct network connections network, parameter dataframe, waterbody mapping, - and gage mapping. This is an intermediate-level function that calls several - lower level functions to read data, conduct network operations, and extract mappings. - - Arguments - --------- - supernetwork_parameters (dict): User input network parameters - - Returns: - -------- - connections (dict int: [int]): Network connections - param_df (DataFrame): Geometry and hydraulic parameters - wbodies (dict, int: int): segment-waterbody mapping - gages (dict, int: int): segment-gage mapping - - ''' - - # crosswalking dictionary between variables names in input dataset and - # variable names recognized by troute.routing module. - cols = supernetwork_parameters.get( - 'columns', - { - 'key' : 'link', - 'downstream': 'to', - 'dx' : 'Length', - 'n' : 'n', - 'ncc' : 'nCC', - 's0' : 'So', - 'bw' : 'BtmWdth', - 'waterbody' : 'NHDWaterbodyComID', - 'gages' : 'gages', - 'tw' : 'TopWdth', - 'twcc' : 'TopWdthCC', - 'alt' : 'alt', - 'musk' : 'MusK', - 'musx' : 'MusX', - 'cs' : 'ChSlp', - } - ) - - # numeric code used to indicate network terminal segments - terminal_code = supernetwork_parameters.get("terminal_code", 0) - - # read parameter dataframe - param_df = nhd_io.read(pathlib.Path(supernetwork_parameters["geo_file_path"])) - - # select the column names specified in the values in the cols dict variable - param_df = param_df[list(cols.values())] - - # rename dataframe columns to keys in the cols dict variable - param_df = param_df.rename(columns=nhd_network.reverse_dict(cols)) - - # handle synthetic waterbody segments - synthetic_wb_segments = supernetwork_parameters.get("synthetic_wb_segments", None) - synthetic_wb_id_offset = supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) - if synthetic_wb_segments: - # rename the current key column to key32 - key32_d = {"key":"key32"} - param_df = param_df.rename(columns=key32_d) - # create a key index that is int64 - # copy the links into the new column - param_df["key"] = param_df.key32.astype("int64") - # update the values of the synthetic reservoir segments - fix_idx = param_df.key.isin(set(synthetic_wb_segments)) - param_df.loc[fix_idx,"key"] = (param_df[fix_idx].key + synthetic_wb_id_offset).astype("int64") - - # set parameter dataframe index as segment id number, sort - param_df = param_df.set_index("key").sort_index() - - # get and apply domain mask - if "mask_file_path" in supernetwork_parameters: - data_mask = nhd_io.read_mask( - pathlib.Path(supernetwork_parameters["mask_file_path"]), - layer_string=supernetwork_parameters.get("mask_layer_string", None), - ) - data_mask = data_mask.set_index(data_mask.columns[0]) - param_df = param_df.filter(data_mask.index, axis=0) - - # map segment ids to waterbody ids - wbodies = {} - if "waterbody" in cols: - wbodies = nhd_network.extract_waterbody_connections( - param_df[["waterbody"]] - ) - param_df = param_df.drop("waterbody", axis=1) - - # map segment ids to gage ids - gages = {} - if "gages" in cols: - gages = nhd_network.gage_mapping(param_df[["gages"]]) - param_df = param_df.drop("gages", axis=1) - - # There can be an externally determined terminal code -- that's this first value - terminal_codes = set() - terminal_codes.add(terminal_code) - # ... but there may also be off-domain nodes that are not explicitly identified - # but which are terminal (i.e., off-domain) as a result of a mask or some other - # an interior domain truncation that results in a - # otherwise valid node value being pointed to, but which is masked out or - # being intentionally separated into another domain. - terminal_codes = terminal_codes | set( - param_df[~param_df["downstream"].isin(param_df.index)]["downstream"].values - ) - - # build connections dictionary - connections = nhd_network.extract_connections( - param_df, "downstream", terminal_codes=terminal_codes - ) - param_df = param_df.drop("downstream", axis=1) - - param_df = param_df.astype("float32") - - break_network_at_waterbodies = waterbody_parameters.get( - "break_network_at_waterbodies", False - ) - - # if waterbodies are being simulated, adjust the connections graph so that - # waterbodies are collapsed to single nodes. Also, build a mapping between - # waterbody outlet segments and lake ids - if break_network_at_waterbodies: - connections, link_lake_crosswalk = nhd_network.replace_waterbodies_connections( - connections, wbodies - ) - else: - link_lake_crosswalk = None - - #============================================================================ - # Retrieve and organize waterbody parameters - - waterbody_type_specified = False - if break_network_at_waterbodies: - - # Read waterbody parameters from LAKEPARM file - level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) - waterbodies_df = nhd_io.read_lakeparm( - level_pool_params['level_pool_waterbody_parameter_file_path'], - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - wbodies.values() - ) - - # Remove duplicate lake_ids and rows - waterbodies_df = ( - waterbodies_df.reset_index() - .drop_duplicates(subset="lake_id") - .set_index("lake_id") - ) - - # Declare empty dataframe - waterbody_types_df = pd.DataFrame() - - # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True - reservoir_da = data_assimilation_parameters.get( - 'reservoir_da', - {} - ) - - if reservoir_da: - usgs_hybrid = reservoir_da.get( - 'reservoir_persistence_usgs', - False - ) - usace_hybrid = reservoir_da.get( - 'reservoir_persistence_usace', - False - ) - param_file = reservoir_da.get( - 'gage_lakeID_crosswalk_file', - None - ) - else: - param_file = None - usace_hybrid = False - usgs_hybrid = False - - # check if RFC-type reservoirs are set to true - rfc_params = waterbody_parameters.get('rfc') - if rfc_params: - rfc_forecast = rfc_params.get( - 'reservoir_rfc_forecasts', - False - ) - param_file = rfc_params.get('reservoir_parameter_file',None) - else: - rfc_forecast = False - - if (param_file and reservoir_da) or (param_file and rfc_forecast): - waterbody_type_specified = True - ( - waterbody_types_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk - ) = nhd_io.read_reservoir_parameter_file( - param_file, - usgs_hybrid, - usace_hybrid, - rfc_forecast, - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - reservoir_da.get('crosswalk_usgs_gage_field', 'usgs_gage_id'), - reservoir_da.get('crosswalk_usgs_lakeID_field', 'usgs_lake_id'), - reservoir_da.get('crosswalk_usace_gage_field', 'usace_gage_id'), - reservoir_da.get('crosswalk_usace_lakeID_field', 'usace_lake_id'), - wbodies.values(), - ) - else: - waterbody_type_specified = True - waterbody_types_df = pd.DataFrame(data = 1, index = waterbodies_df.index, columns = ['reservoir_type']) - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - else: - # Declare empty dataframes - waterbody_types_df = pd.DataFrame() - waterbodies_df = pd.DataFrame() - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - return ( - param_df, - connections, - terminal_codes, - waterbodies_df, - waterbody_types_df, - waterbody_type_specified, - wbodies, - link_lake_crosswalk, - gages, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk) - - -def build_nhd_network(supernetwork_parameters,waterbody_parameters, - preprocessing_parameters,compute_parameters, - data_assimilation_parameters): - - # Build routing network data objects. Network data objects specify river - # network connectivity, channel geometry, and waterbody parameters. - if preprocessing_parameters.get('use_preprocessed_data', False): - - # get data from pre-processed file - ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) = unpack_nhd_preprocess_data( - preprocessing_parameters - ) - else: - - # build data objects from scratch - ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) = nhd_network_preprocess( - supernetwork_parameters, - waterbody_parameters, - preprocessing_parameters, - compute_parameters, - data_assimilation_parameters, - ) - - return (connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df - ) - - -def nhd_network_preprocess( - supernetwork_parameters, - waterbody_parameters, - preprocessing_parameters, - compute_parameters, - data_assimilation_parameters, -): - ''' - Creation of routing network data objects. Logical ordering of lower-level - function calls that build individual network data objects. - - Arguments - --------- - supernetwork_parameters (dict): user input data re network extent - waterbody_parameters (dict): user input data re waterbodies - preprocessing_parameters (dict): user input data re preprocessing - compute_parameters (dict): user input data re compute configuration - data_assimilation_parameters (dict): user input data re data assimilation - - Returns - ------- - connections (dict of int: [int]): {segment id: [downsteram adjacent segment ids]} - param_df (DataFrame): Hydraulic geometry and roughness parameters, by segment - wbody_conn (dict of int: int): {segment id: associated lake id} - waterbodies_df (DataFrame): Waterbody (reservoir) parameters - waterbody_types_df (DataFrame): Waterbody type codes (1 - levelpool, 2 - USGS, 3 - USACE, 4 - RFC) - break_network_at_waterbodies (bool): If True, waterbodies occpy reaches of their own - waterbody_type_specified (bool): If True, more than just levelpool waterbodies exist - link_lake_crosswalk (dict of int: int): {lake id: outlet segment id} - independent_networks (dict of int: {int: [int]}): {tailwater id: {segment id: [upstream adjacent segment ids]}} - reaches_bytw (dict of int: [[int]]): {tailwater id: list or reach lists} - rconn (dict of int: [int]): {segment id: [upstream adjacent segment ids]} - pd.DataFrame.from_dict(gages) (DataFrame): Gage ids and corresponding segment ids at which they are located - diffusive_network_data (dict or None): Network data objects for diffusive domain - topobathy_df (DataFrame): Natural cross section data for diffusive domain - - Notes - ----- - - waterbody_type_specified is likely an excessive return and can be removed and inferred from the - contents of waterbody_types_df - - The values of the link_lake_crosswalk dictionary are the downstream-most segments within - the waterbody extent to which waterbody data are written. They are NOT the first segments - downsteram of the waterbody - ''' - - #============================================================================ - # Establish diffusive domain for MC/diffusive hybrid simulations - - hybrid_params = compute_parameters.get("hybrid_parameters", False) - if hybrid_params: - # switch parameters - # if run_hybrid = False, run MC only - # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc - # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric - # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric - run_hybrid = hybrid_params.get('run_hybrid_routing', False) - use_topobathy = hybrid_params.get('use_natl_xsections', False) - run_refactored = hybrid_params.get('run_refactored_network', False) - - # file path parameters of non-refactored hydrofabric defined by RouteLink.nc - domain_file = hybrid_params.get("diffusive_domain", None) - topobathy_file = hybrid_params.get("topobathy_domain", None) - - # file path parameters of refactored hydrofabric for diffusive wave channel routing - refactored_domain_file = hybrid_params.get("refactored_domain", None) - refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) - #------------------------------------------------------------------------- - # for non-refactored hydofabric defined by RouteLink.nc - # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. - if run_hybrid and domain_file: - - LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - diffusive_domain = nhd_io.read_diffusive_domain(domain_file) - - if use_topobathy and topobathy_file: - - LOG.debug('Natural cross section data on original hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - - # TODO: Request GID make comID variable an integer in their product, so - # we do not need to change variable types, here. - topobathy_df.index = topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - diffusive_network_data = {} - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') - unrefactored_topobathy_df = pd.DataFrame() - #------------------------------------------------------------------------- - # for refactored hydofabric - if run_hybrid and run_refactored and refactored_domain_file: - - LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) - - if use_topobathy and refactored_topobathy_file: - - LOG.debug('Natural cross section data of refactored hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) - - # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy - # for crosswalking water elevations between non-refactored and refactored hydrofabrics. - unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - unrefactored_topobathy_df.index = unrefactored_topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - refactored_diffusive_network_data = {} - - else: - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - unrefactored_topobathy_df = pd.DataFrame() - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') - #============================================================================ - # Build network connections graph, assemble parameter dataframe, - # establish segment-waterbody, and segment-gage mappings - LOG.info("creating network connections graph") - start_time = time.time() - - connections, param_df, wbody_conn, gages = nnu.build_connections( - supernetwork_parameters, - ) - - link_gage_df = pd.DataFrame.from_dict(gages) - link_gage_df.index.name = 'link' - break_network_at_waterbodies = waterbody_parameters.get( - "break_network_at_waterbodies", False - ) - - # if streamflow DA, then break network at gages - break_network_at_gages = False - streamflow_da = data_assimilation_parameters.get('streamflow_da', False) - if streamflow_da: - break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - - if not wbody_conn: - # Turn off any further reservoir processing if the network contains no - # waterbodies - break_network_at_waterbodies = False - - # if waterbodies are being simulated, adjust the connections graph so that - # waterbodies are collapsed to single nodes. Also, build a mapping between - # waterbody outlet segments and lake ids - if break_network_at_waterbodies: - connections, link_lake_crosswalk = nhd_network.replace_waterbodies_connections( - connections, wbody_conn - ) - else: - link_lake_crosswalk = None - - LOG.debug("network connections graph created in %s seconds." % (time.time() - start_time)) - - #============================================================================ - # Retrieve and organize waterbody parameters - - waterbody_type_specified = False - if break_network_at_waterbodies: - - # Read waterbody parameters from LAKEPARM file - level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) - waterbodies_df = nhd_io.read_lakeparm( - level_pool_params['level_pool_waterbody_parameter_file_path'], - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - wbody_conn.values() - ) - - # Remove duplicate lake_ids and rows - waterbodies_df = ( - waterbodies_df.reset_index() - .drop_duplicates(subset="lake_id") - .set_index("lake_id") - ) - - # Declare empty dataframe - waterbody_types_df = pd.DataFrame() - - # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True - reservoir_da = data_assimilation_parameters.get( - 'reservoir_da', - {} - ) - - if reservoir_da: - usgs_hybrid = reservoir_da.get( - 'reservoir_persistence_usgs', - False - ) - usace_hybrid = reservoir_da.get( - 'reservoir_persistence_usace', - False - ) - param_file = reservoir_da.get( - 'gage_lakeID_crosswalk_file', - None - ) - else: - param_file = None - usace_hybrid = False - usgs_hybrid = False - - # check if RFC-type reservoirs are set to true - rfc_params = waterbody_parameters.get('rfc') - if rfc_params: - rfc_forecast = rfc_params.get( - 'reservoir_rfc_forecasts', - False - ) - param_file = rfc_params.get('reservoir_parameter_file',None) - else: - rfc_forecast = False - - if (param_file and reservoir_da) or (param_file and rfc_forecast): - waterbody_type_specified = True - ( - waterbody_types_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk - ) = nhd_io.read_reservoir_parameter_file( - param_file, - usgs_hybrid, - usace_hybrid, - rfc_forecast, - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - reservoir_da.get('crosswalk_usgs_gage_field', 'usgs_gage_id'), - reservoir_da.get('crosswalk_usgs_lakeID_field', 'usgs_lake_id'), - reservoir_da.get('crosswalk_usace_gage_field', 'usace_gage_id'), - reservoir_da.get('crosswalk_usace_lakeID_field', 'usace_lake_id'), - wbody_conn.values(), - ) - else: - waterbody_type_specified = True - waterbody_types_df = pd.DataFrame(data = 1, index = waterbodies_df.index, columns = ['reservoir_type']) - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - else: - # Declare empty dataframes - waterbody_types_df = pd.DataFrame() - waterbodies_df = pd.DataFrame() - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - #============================================================================ - # build diffusive domain data and edit MC domain data for hybrid simulation - - # - if diffusive_domain: - rconn_diff0 = nhd_network.reverse_network(connections) - refactored_reaches = {} - - for tw in diffusive_domain: - mainstem_segs = diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - diffusive_network_data[tw] = {} - - # add diffusive domain segments - diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = nnu.organize_independent_networks( - diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - diffusive_network_data[tw]['rconn'] = rconn_diff - diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - diffusive_network_data[tw]['param_df'] = param_df.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - if refactored_diffusive_domain: - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = nnu.build_refac_connections(diffusive_parameters) - - # list of stream segments of a single refactored diffusive domain - refac_tw = refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k]= [refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = nnu.organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = diffusive_network_data[tw]['upstream_boundary_link'] - else: - refactored_reaches={} - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - param_df = param_df.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - connections[us] = [] - - #============================================================================ - # Identify Independent Networks and Reaches by Network - LOG.info("organizing connections into reaches ...") - start_time = time.time() - gage_break_segments = set() - wbody_break_segments = set() - if break_network_at_waterbodies: - wbody_break_segments = wbody_break_segments.union(wbody_conn.values()) - - if break_network_at_gages: - gage_break_segments = gage_break_segments.union(gages['gages'].keys()) - - independent_networks, reaches_bytw, rconn = nnu.organize_independent_networks( - connections, - wbody_break_segments, - gage_break_segments, - ) - - LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) - - if preprocessing_parameters.get('preprocess_only', False): - - LOG.debug("saving preprocessed network data to disk for future use") - # todo: consider a better default than None - destination_folder = preprocessing_parameters.get('preprocess_output_folder', None) - if destination_folder: - - output_filename = preprocessing_parameters.get( - 'preprocess_output_filename', - 'preprocess_output' - ) - - outputs = {} - outputs.update( - {'connections': connections, - 'param_df': param_df, - 'wbody_conn': wbody_conn, - 'waterbodies_df': waterbodies_df, - 'waterbody_types_df': waterbody_types_df, - 'break_network_at_waterbodies': break_network_at_waterbodies, - 'waterbody_type_specified': waterbody_type_specified, - 'link_lake_crosswalk': link_lake_crosswalk, - 'independent_networks': independent_networks, - 'reaches_bytw': reaches_bytw, - 'rconn': rconn, - 'link_gage_df': link_gage_df, - 'usgs_lake_gage_crosswalk': usgs_lake_gage_crosswalk, - 'usace_lake_gage_crosswalk': usace_lake_gage_crosswalk, - 'diffusive_network_data': diffusive_network_data, - 'topobathy_data': topobathy_df, - } - ) - try: - np.save( - pathlib.Path(destination_folder).joinpath(output_filename), - outputs - ) - except: - LOG.critical('Canonot find %s. Aborting preprocessing routine' % pathlib.Path(destination_folder)) - quit() - - LOG.debug( - "writing preprocessed network data to %s"\ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - LOG.critical( - "Preprocessed network data written to %s aborting preprocessing sequence" \ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - quit() - - else: - LOG.critical( - "No destination folder specified for preprocessing. Please specify preprocess_output_folder in configuration file. Aborting preprocessing routine" - ) - quit() - - return ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) - -def unpack_nhd_preprocess_data(preprocessing_parameters): - - preprocess_filepath = preprocessing_parameters.get('preprocess_source_file',None) - if preprocess_filepath: - try: - inputs = np.load(pathlib.Path(preprocess_filepath),allow_pickle='TRUE').item() - except: - LOG.critical('Canonot find %s' % pathlib.Path(preprocess_filepath)) - quit() - - connections = inputs.get('connections',None) - param_df = inputs.get('param_df',None) - wbody_conn = inputs.get('wbody_conn',None) - waterbodies_df = inputs.get('waterbodies_df',None) - waterbody_types_df = inputs.get('waterbody_types_df',None) - break_network_at_waterbodies = inputs.get('break_network_at_waterbodies',None) - waterbody_type_specified = inputs.get('waterbody_type_specified',None) - link_lake_crosswalk = inputs.get('link_lake_crosswalk', None) - independent_networks = inputs.get('independent_networks',None) - reaches_bytw = inputs.get('reaches_bytw',None) - rconn = inputs.get('rconn',None) - gages = inputs.get('link_gage_df',None) - usgs_lake_gage_crosswalk = inputs.get('usgs_lake_gage_crosswalk',None) - usace_lake_gage_crosswalk = inputs.get('usace_lake_gage_crosswalk',None) - diffusive_network_data = inputs.get('diffusive_network_data',None) - topobathy_df = inputs.get('topobathy_data',None) - refactored_diffusive_domain = inputs.get('refactored_diffusive_domain',None) - refactored_reaches = inputs.get('refactored_reaches',None) - unrefactored_topobathy_df = inputs.get('unrefactored_topobathy',None) - - else: - LOG.critical("use_preprocessed_data = True, but no preprocess_source_file is specified. Aborting the simulation.") - quit() - - return ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - gages, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) - - -def nhd_initial_warmstate_preprocess( - break_network_at_waterbodies, - restart_parameters, - data_assimilation_parameters, - segment_index, - waterbodies_df, - link_lake_crosswalk, -): - - ''' - Assemble model initial condition data: - - waterbody inital states (outflow and pool elevation) - - channel initial states (flow and depth) - - initial time - - Arguments - --------- - - break_network_at_waterbodies (bool): If True, waterbody initial states will - be appended to the waterbody parameter - dataframe. If False, waterbodies will - not be simulated and the waterbody - parameter datataframe wil not be changed - - restart_parameters (dict): User-input simulation restart - parameters - - data_assimilation_parameters (dict): User-input data assimilation - parameters - - segment_index (Pandas Index): All segment IDs in the simulation - doamin - - waterbodies_df (Pandas DataFrame): Waterbody parameters - - link_lake_crosswalk (dict): Crosswalking between lake ids and the link - id of the lake outlet segment - - Returns - ------- - - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial - states (outflow and pool elevation) - - q0 (Pandas DataFrame): Initial flow and depth states for each - segment in the model domain - - t0 (datetime): Datetime of the model initialization - - Notes - ----- - ''' - - #---------------------------------------------------------------------------- - # Assemble waterbody initial states (outflow and pool elevation - #---------------------------------------------------------------------------- - - if break_network_at_waterbodies: - - start_time = time.time() - LOG.info("setting waterbody initial states ...") - - # if a lite restart file is provided, read initial states from it. - if restart_parameters.get("lite_waterbody_restart_file", None): - - waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( - restart_parameters['lite_waterbody_restart_file'] - ) - - # read waterbody initial states from WRF-Hydro type restart file - elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): - waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( - restart_parameters["wrf_hydro_waterbody_restart_file"], - restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], - restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", 'lake_id'), - restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], - restart_parameters.get( - "wrf_hydro_waterbody_crosswalk_filter_file_field_name", - 'NHDWaterbodyComID' - ), - ) - - # if no restart file is provided, default initial states - else: - # TODO: Consider adding option to read cold state from route-link file - waterbodies_initial_ds_flow_const = 0.0 - waterbodies_initial_depth_const = -1e9 - # Set initial states from cold-state - waterbodies_initial_states_df = pd.DataFrame( - 0, - index=waterbodies_df.index, - columns=[ - "qd0", - "h0", - ], - dtype="float32", - ) - # TODO: This assignment could probably by done in the above call - waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const - waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const - waterbodies_initial_states_df["index"] = range( - len(waterbodies_initial_states_df) - ) - - waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on="lake_id" - ) - - LOG.debug( - "waterbody initial states complete in %s seconds."\ - % (time.time() - start_time)) - start_time = time.time() - - #---------------------------------------------------------------------------- - # Assemble channel initial states (flow and depth) - # also establish simulation initialization timestamp - #---------------------------------------------------------------------------- - start_time = time.time() - LOG.info("setting channel initial states ...") - - # if lite restart file is provided, the read channel initial states from it - if restart_parameters.get("lite_channel_restart_file", None): - - q0, t0 = nhd_io.read_lite_restart( - restart_parameters['lite_channel_restart_file'] - ) - t0_str = None - - # build initial states from user-provided restart parameters - else: - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - - # get initialization time from restart file - if restart_parameters.get("wrf_hydro_channel_restart_file", None): - channel_initial_states_file = restart_parameters[ - "wrf_hydro_channel_restart_file" - ] - t0_str = nhd_io.get_param_str( - channel_initial_states_file, - "Restart_Time" - ) - else: - t0_str = "2015-08-16_00:00:00" - - # convert timestamp from string to datetime - t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") - - # get initial time from user inputs - if restart_parameters.get("start_datetime", None): - t0_str = restart_parameters.get("start_datetime") - - def _try_parsing_date(text): - for fmt in ( - "%Y-%m-%d_%H:%M", - "%Y-%m-%d_%H:%M:%S", - "%Y-%m-%d %H:%M", - "%Y-%m-%d %H:%M:%S", - "%Y/%m/%d %H:%M", - "%Y/%m/%d %H:%M:%S" - ): - try: - return datetime.strptime(text, fmt) - except ValueError: - pass - LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') - quit() - - t0 = _try_parsing_date(t0_str) - else: - if t0_str == "2015-08-16_00:00:00": - LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') - else: - LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) - - LOG.debug( - "channel initial states complete in %s seconds."\ - % (time.time() - start_time) - ) - start_time = time.time() - - return waterbodies_df, q0, t0 - # TODO: This returns a full dataframe (waterbodies_df) with the - # merged initial states for waterbodies, but only the - # initial state values (q0; not merged with the channel properties) - # for the channels -- - # That is because that is how they are used downstream. Need to - # trace that back and decide if there is one of those two ways - # that is optimal and make both returns that way. - - -def nhd_forcing( - run, - forcing_parameters, - hybrid_parameters, - segment_index, - cpu_pool, - t0, - coastal_boundary_depth_df, -): - """ - Assemble model forcings. Forcings include hydrological lateral inflows (qlats) - and coastal boundary depths for hybrid runs - - Aguments - -------- - - run (dict): List of forcing files pertaining to a - single run-set - - forcing_parameters (dict): User-input simulation forcing parameters - - hybrid_parameters (dict): User-input simulation hybrid parameters - - segment_index (Int64): Reach segment ids - - cpu_pool (int): Number of CPUs in the process-parallel pool - - Returns - ------- - - qlats_df (Pandas DataFrame): Lateral inflow data, indexed by - segment ID - - coastal_bounary_depth_df (Pandas DataFrame): Coastal boundary water depths, - indexed by segment ID - - Notes - ----- - - """ - - # Unpack user-specified forcing parameters - dt = forcing_parameters.get("dt", None) - qts_subdivisions = forcing_parameters.get("qts_subdivisions", None) - qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) - qlat_file_index_col = forcing_parameters.get("qlat_file_index_col", "feature_id") - qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") - qlat_file_gw_bucket_flux_col = forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") - qlat_file_terrain_runoff_col = forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") - - - # TODO: find a better way to deal with these defaults and overrides. - run["t0"] = run.get("t0", t0) - run["nts"] = run.get("nts") - run["dt"] = run.get("dt", dt) - run["qts_subdivisions"] = run.get("qts_subdivisions", qts_subdivisions) - run["qlat_input_folder"] = run.get("qlat_input_folder", qlat_input_folder) - run["qlat_file_index_col"] = run.get("qlat_file_index_col", qlat_file_index_col) - run["qlat_file_value_col"] = run.get("qlat_file_value_col", qlat_file_value_col) - run["qlat_file_gw_bucket_flux_col"] = run.get("qlat_file_gw_bucket_flux_col", qlat_file_gw_bucket_flux_col) - run["qlat_file_terrain_runoff_col"] = run.get("qlat_file_terrain_runoff_col", qlat_file_terrain_runoff_col) - - #--------------------------------------------------------------------------- - # Assemble lateral inflow data - #--------------------------------------------------------------------------- - - start_time = time.time() - LOG.info("Creating a DataFrame of lateral inflow forcings ...") - - # Place holder, if reading qlats from a file use this. - # TODO: add an option for reading qlat data from BMI/model engine - from_file = True - if from_file: - qlats_df = nnu.build_qlateral_array( - run, - cpu_pool, - segment_index, - ) - - LOG.debug( - "lateral inflow DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - #--------------------------------------------------------------------- - # Assemble coastal coupling data [WIP] - #--------------------------------------------------------------------- - # Run if coastal_boundary_depth_df has not already been created: - if coastal_boundary_depth_df.empty: - coastal_boundary_elev_files = forcing_parameters.get('coastal_boundary_input_file', None) - coastal_boundary_domain_files = hybrid_parameters.get('coastal_boundary_domain', None) - - if coastal_boundary_elev_files: - start_time = time.time() - LOG.info("creating coastal dataframe ...") - - coastal_boundary_domain = nhd_io.read_coastal_boundary_domain(coastal_boundary_domain_files) - coastal_boundary_depth_df = nhd_io.build_coastal_ncdf_dataframe( - coastal_boundary_elev_files, - coastal_boundary_domain, - ) - - LOG.debug( - "coastal boundary elevation observation DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - return qlats_df, coastal_boundary_depth_df From 9d9a5a2a767613777a41ed64c3ea2982bcf291bf Mon Sep 17 00:00:00 2001 From: shorvath-noaa <103054653+shorvath-noaa@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:00:11 -0700 Subject: [PATCH 21/54] Delete hyfeature_preprocess.py --- .../troute/hyfeature_preprocess.py | 949 ------------------ 1 file changed, 949 deletions(-) delete mode 100644 src/troute-network/troute/hyfeature_preprocess.py diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py deleted file mode 100644 index 5ab0a5bec..000000000 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ /dev/null @@ -1,949 +0,0 @@ -import time -import pathlib -import logging -from datetime import datetime -from collections import defaultdict -from pathlib import Path -import os - -import pandas as pd -import numpy as np -import xarray as xr -import geopandas as gpd - -import troute.nhd_network_utilities_v02 as nnu -import troute.nhd_network as nhd_network -import troute.nhd_io as nhd_io -from troute.nhd_network import reverse_dict -import troute.hyfeature_network_utilities as hnu - -LOG = logging.getLogger('') - -def read_geo_file( - supernetwork_parameters, - waterbody_parameters, -): - - geo_file_path = supernetwork_parameters["geo_file_path"] - - file_type = Path(geo_file_path).suffix - if( file_type == '.gpkg' ): - dataframe = read_geopkg(geo_file_path) - elif( file_type == '.json') : - edge_list = supernetwork_parameters['flowpath_edge_list'] - dataframe = read_json(geo_file_path, edge_list) - else: - raise RuntimeError("Unsupported file type: {}".format(file_type)) - - # Don't need the string prefix anymore, drop it - mask = ~ dataframe['toid'].str.startswith("tnex") - dataframe = dataframe.apply(numeric_id, axis=1) - - # make the flowpath linkage, ignore the terminal nexus - flowpath_dict = dict(zip(dataframe.loc[mask].toid, dataframe.loc[mask].id)) - - # ********** need to be included in flowpath_attributes ************* - dataframe['alt'] = 1.0 #FIXME get the right value for this... - - cols = supernetwork_parameters.get('columns',None) - - if cols: - dataframe = dataframe[list(cols.values())] - # Rename parameter columns to standard names: from route-link names - # key: "link" - # downstream: "to" - # dx: "Length" - # n: "n" # TODO: rename to `manningn` - # ncc: "nCC" # TODO: rename to `mannningncc` - # s0: "So" # TODO: rename to `bedslope` - # bw: "BtmWdth" # TODO: rename to `bottomwidth` - # waterbody: "NHDWaterbodyComID" - # gages: "gages" - # tw: "TopWdth" # TODO: rename to `topwidth` - # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` - # alt: "alt" - # musk: "MusK" - # musx: "MusX" - # cs: "ChSlp" # TODO: rename to `sideslope` - dataframe = dataframe.rename(columns=reverse_dict(cols)) - dataframe.set_index("key", inplace=True) - dataframe = dataframe.sort_index() - - # numeric code used to indicate network terminal segments - terminal_code = supernetwork_parameters.get("terminal_code", 0) - - # There can be an externally determined terminal code -- that's this first value - terminal_codes = set() - terminal_codes.add(terminal_code) - # ... but there may also be off-domain nodes that are not explicitly identified - # but which are terminal (i.e., off-domain) as a result of a mask or some other - # an interior domain truncation that results in a - # otherwise valid node value being pointed to, but which is masked out or - # being intentionally separated into another domain. - terminal_codes = terminal_codes | set( - dataframe[~dataframe["downstream"].isin(dataframe.index)]["downstream"].values - ) - - # build connections dictionary - connections = nhd_network.extract_connections( - dataframe, "downstream", terminal_codes=terminal_codes - ) - - #Load waterbody/reservoir info - if waterbody_parameters: - levelpool_params = waterbody_parameters.get('level_pool', None) - if not levelpool_params: - # FIXME should not be a hard requirement - raise(RuntimeError("No supplied levelpool parameters in routing config")) - - lake_id = levelpool_params.get("level_pool_waterbody_id", "wb-id") - waterbody_df = read_ngen_waterbody_df( - levelpool_params["level_pool_waterbody_parameter_file_path"], - lake_id, - ) - - # Remove duplicate lake_ids and rows - waterbody_df = ( - waterbody_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - - try: - waterbody_types_df = read_ngen_waterbody_type_df( - levelpool_params["reservoir_parameter_file"], - lake_id, - #self.waterbody_connections.values(), - ) - # Remove duplicate lake_ids and rows - waterbody_types_df =( - waterbody_types_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - - except ValueError: - #FIXME any reservoir operations requires some type - #So make this default to 1 (levelpool) - waterbody_types_df = pd.DataFrame(index=waterbody_df.index) - waterbody_types_df['reservoir_type'] = 1 - - return dataframe, flowpath_dict, connections, waterbody_df, waterbody_types_df, terminal_codes - -def build_hyfeature_network(supernetwork_parameters, - waterbody_parameters, -): - - geo_file_path = supernetwork_parameters["geo_file_path"] - cols = supernetwork_parameters["columns"] - terminal_code = supernetwork_parameters.get("terminal_code", 0) - - break_network_at_waterbodies = supernetwork_parameters.get("break_network_at_waterbodies", False) - break_network_at_gages = supernetwork_parameters.get("break_network_at_gages", False) - break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, - "break_network_at_gages": break_network_at_gages} - - file_type = Path(geo_file_path).suffix - if( file_type == '.gpkg' ): - dataframe = hyf_network.read_geopkg(geo_file_path) - elif( file_type == '.json') : - edge_list = supernetwork_parameters['flowpath_edge_list'] - dataframe = hyf_network.read_json(geo_file_path, edge_list) - else: - raise RuntimeError("Unsupported file type: {}".format(file_type)) - - # Don't need the string prefix anymore, drop it - mask = ~ dataframe['toid'].str.startswith("tnex") - dataframe = dataframe.apply(hyf_network.numeric_id, axis=1) - - # make the flowpath linkage, ignore the terminal nexus - flowpath_dict = dict(zip(dataframe.loc[mask].toid, dataframe.loc[mask].id)) - waterbody_types_df = pd.DataFrame() - waterbody_df = pd.DataFrame() - waterbody_type_specified = False - - # ********** need to be included in flowpath_attributes ************* - # FIXME once again, order here can hurt....to hack `alt` in, either need to - # put it as a column in the config, or do this AFTER the super constructor - # otherwise the alt column gets sliced out... - dataframe['alt'] = 1.0 #FIXME get the right value for this... - - #Load waterbody/reservoir info - #For ngen HYFeatures, the reservoirs to be simulated - #are determined by the lake.json file - #we limit waterbody_connections to only the flowpaths - #that coincide with a lake listed in this file - #see `waterbody_connections` - if waterbody_parameters: - # FIXME later, DO ALL LAKE PARAMS BETTER - levelpool_params = waterbody_parameters.get('level_pool', None) - if not levelpool_params: - # FIXME should not be a hard requirement - raise(RuntimeError("No supplied levelpool parameters in routing config")) - - lake_id = levelpool_params.get("level_pool_waterbody_id", "wb-id") - waterbody_df = read_ngen_waterbody_df( - levelpool_params["level_pool_waterbody_parameter_file_path"], - lake_id, - #self.waterbody_connections.values() - ) - - # Remove duplicate lake_ids and rows - waterbody_df = ( - waterbody_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - waterbody_df["qd0"] = 0.0 - waterbody_df["h0"] = -1e9 - - try: - waterbody_types_df = read_ngen_waterbody_type_df( - levelpool_params["reservoir_parameter_file"], - lake_id, - #self.waterbody_connections.values(), - ) - # Remove duplicate lake_ids and rows - waterbody_types_df =( - waterbody_types_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - - except ValueError: - #FIXME any reservoir operations requires some type - #So make this default to 1 (levelpool) - waterbody_types_df = pd.DataFrame(index=waterbody_df.index) - waterbody_types_df['reservoir_type'] = 1 - - return (dataframe, - flowpath_dict, - waterbody_types_df, - waterbody_df, - waterbody_type_specified, - cols, - terminal_code, - break_points, - ) - -def hyfeature_hybrid_routing_preprocess( - connections, - param_df, - wbody_conn, - gages, - preprocessing_parameters, - compute_parameters, - waterbody_parameters, -): - ''' - Creation of routing network data objects. Logical ordering of lower-level - function calls that build individual network data objects. - - Arguments - --------- - supernetwork_parameters (dict): user input data re network extent - waterbody_parameters (dict): user input data re waterbodies - preprocessing_parameters (dict): user input data re preprocessing - compute_parameters (dict): user input data re compute configuration - data_assimilation_parameters (dict): user input data re data assimilation - - Returns - ------- - connections (dict of int: [int]): {segment id: [downsteram adjacent segment ids]} - param_df (DataFrame): Hydraulic geometry and roughness parameters, by segment - wbody_conn (dict of int: int): {segment id: associated lake id} - waterbodies_df (DataFrame): Waterbody (reservoir) parameters - waterbody_types_df (DataFrame): Waterbody type codes (1 - levelpool, 2 - USGS, 3 - USACE, 4 - RFC) - break_network_at_waterbodies (bool): If True, waterbodies occpy reaches of their own - waterbody_type_specified (bool): If True, more than just levelpool waterbodies exist - link_lake_crosswalk (dict of int: int): {lake id: outlet segment id} - independent_networks (dict of int: {int: [int]}): {tailwater id: {segment id: [upstream adjacent segment ids]}} - reaches_bytw (dict of int: [[int]]): {tailwater id: list or reach lists} - rconn (dict of int: [int]): {segment id: [upstream adjacent segment ids]} - pd.DataFrame.from_dict(gages) (DataFrame): Gage ids and corresponding segment ids at which they are located - diffusive_network_data (dict or None): Network data objects for diffusive domain - topobathy_df (DataFrame): Natural cross section data for diffusive domain - - Notes - ----- - - waterbody_type_specified is likely an excessive return and can be removed and inferred from the - contents of waterbody_types_df - - The values of the link_lake_crosswalk dictionary are the downstream-most segments within - the waterbody extent to which waterbody data are written. They are NOT the first segments - downsteram of the waterbody - ''' - - #============================================================================ - # Establish diffusive domain for MC/diffusive hybrid simulations - - hybrid_params = compute_parameters.get("hybrid_parameters", False) - if hybrid_params: - # switch parameters - # if run_hybrid = False, run MC only - # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc - # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric - # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric - run_hybrid = hybrid_params.get('run_hybrid_routing', False) - use_topobathy = hybrid_params.get('use_natl_xsections', False) - run_refactored = hybrid_params.get('run_refactored_network', False) - - # file path parameters of non-refactored hydrofabric defined by RouteLink.nc - domain_file = hybrid_params.get("diffusive_domain", None) - topobathy_file = hybrid_params.get("topobathy_domain", None) - - # file path parameters of refactored hydrofabric for diffusive wave channel routing - refactored_domain_file = hybrid_params.get("refactored_domain", None) - refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) - #------------------------------------------------------------------------- - # for non-refactored hydofabric defined by RouteLink.nc - # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. - if run_hybrid and domain_file: - - LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - diffusive_domain = nhd_io.read_diffusive_domain(domain_file) - - if use_topobathy and topobathy_file: - - LOG.debug('Natural cross section data on original hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - - # TODO: Request GID make comID variable an integer in their product, so - # we do not need to change variable types, here. - topobathy_df.index = topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - diffusive_network_data = {} - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') - unrefactored_topobathy_df = pd.DataFrame() - #------------------------------------------------------------------------- - # for refactored hydofabric - if run_hybrid and run_refactored and refactored_domain_file: - - LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) - - if use_topobathy and refactored_topobathy_file: - - LOG.debug('Natural cross section data of refactored hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) - - # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy - # for crosswalking water elevations between non-refactored and refactored hydrofabrics. - unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - unrefactored_topobathy_df.index = unrefactored_topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - refactored_diffusive_network_data = {} - - else: - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - unrefactored_topobathy_df = pd.DataFrame() - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') - - #============================================================================ - # build diffusive domain data and edit MC domain data for hybrid simulation - - # - if diffusive_domain: - rconn_diff0 = nhd_network.reverse_network(connections) - refactored_reaches = {} - - for tw in diffusive_domain: - mainstem_segs = diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - diffusive_network_data[tw] = {} - - # add diffusive domain segments - diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = nnu.organize_independent_networks( - diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - diffusive_network_data[tw]['rconn'] = rconn_diff - diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - diffusive_network_data[tw]['param_df'] = param_df.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - if refactored_diffusive_domain: - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = nnu.build_refac_connections(diffusive_parameters) - - # list of stream segments of a single refactored diffusive domain - refac_tw = refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k]= [refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = nnu.organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = diffusive_network_data[tw]['upstream_boundary_link'] - else: - refactored_reaches={} - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - param_df = param_df.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - connections[us] = [] - - #============================================================================ - # Identify Independent Networks and Reaches by Network - LOG.info("organizing connections into reaches ...") - start_time = time.time() - gage_break_segments = set() - wbody_break_segments = set() - - break_network_at_waterbodies = waterbody_parameters.get( - "break_network_at_waterbodies", False - ) - - # if streamflow DA, then break network at gages - break_network_at_gages = False - - if break_network_at_waterbodies: - wbody_break_segments = wbody_break_segments.union(wbody_conn.values()) - - if break_network_at_gages: - gage_break_segments = gage_break_segments.union(gages['gages'].keys()) - - independent_networks, reaches_bytw, rconn = nnu.organize_independent_networks( - connections, - wbody_break_segments, - gage_break_segments, - ) - - LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) - # FIXME: Make this commented out alive - ''' - if preprocessing_parameters.get('preprocess_only', False): - - LOG.debug("saving preprocessed network data to disk for future use") - # todo: consider a better default than None - destination_folder = preprocessing_parameters.get('preprocess_output_folder', None) - if destination_folder: - - output_filename = preprocessing_parameters.get( - 'preprocess_output_filename', - 'preprocess_output' - ) - - outputs = {} - outputs.update( - {'connections': connections, - 'param_df': param_df, - 'wbody_conn': wbody_conn, - 'waterbodies_df': waterbodies_df, - 'waterbody_types_df': waterbody_types_df, - 'break_network_at_waterbodies': break_network_at_waterbodies, - 'waterbody_type_specified': waterbody_type_specified, - 'link_lake_crosswalk': link_lake_crosswalk, - 'independent_networks': independent_networks, - 'reaches_bytw': reaches_bytw, - 'rconn': rconn, - 'link_gage_df': link_gage_df, - 'usgs_lake_gage_crosswalk': usgs_lake_gage_crosswalk, - 'usace_lake_gage_crosswalk': usace_lake_gage_crosswalk, - 'diffusive_network_data': diffusive_network_data, - 'topobathy_data': topobathy_df, - } - ) - try: - np.save( - pathlib.Path(destination_folder).joinpath(output_filename), - outputs - ) - except: - LOG.critical('Canonot find %s. Aborting preprocessing routine' % pathlib.Path(destination_folder)) - quit() - - LOG.debug( - "writing preprocessed network data to %s"\ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - LOG.critical( - "Preprocessed network data written to %s aborting preprocessing sequence" \ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - quit() - - else: - LOG.critical( - "No destination folder specified for preprocessing. Please specify preprocess_output_folder in configuration file. Aborting preprocessing routine" - ) - quit() - ''' - return(independent_networks, - reaches_bytw, - rconn, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) - -def hyfeature_initial_warmstate_preprocess( - # break_network_at_waterbodies, - restart_parameters, - # data_assimilation_parameters, - segment_index, - # waterbodies_df, - # link_lake_crosswalk, -): - - ''' - Assemble model initial condition data: - - waterbody inital states (outflow and pool elevation) - - channel initial states (flow and depth) - - initial time - - Arguments - --------- - - break_network_at_waterbodies (bool): If True, waterbody initial states will - be appended to the waterbody parameter - dataframe. If False, waterbodies will - not be simulated and the waterbody - parameter datataframe wil not be changed - - restart_parameters (dict): User-input simulation restart - parameters - - data_assimilation_parameters (dict): User-input data assimilation - parameters - - segment_index (Pandas Index): All segment IDs in the simulation - doamin - - waterbodies_df (Pandas DataFrame): Waterbody parameters - - link_lake_crosswalk (dict): Crosswalking between lake ids and the link - id of the lake outlet segment - - Returns - ------- - - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial - states (outflow and pool elevation) - - q0 (Pandas DataFrame): Initial flow and depth states for each - segment in the model domain - - t0 (datetime): Datetime of the model initialization - - Notes - ----- - ''' - - #---------------------------------------------------------------------------- - # Assemble waterbody initial states (outflow and pool elevation - #---------------------------------------------------------------------------- - ''' - if break_network_at_waterbodies: - - start_time = time.time() - LOG.info("setting waterbody initial states ...") - - # if a lite restart file is provided, read initial states from it. - if restart_parameters.get("lite_waterbody_restart_file", None): - - waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( - restart_parameters['lite_waterbody_restart_file'] - ) - - # read waterbody initial states from WRF-Hydro type restart file - elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): - waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( - restart_parameters["wrf_hydro_waterbody_restart_file"], - restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], - restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", 'lake_id'), - restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], - restart_parameters.get( - "wrf_hydro_waterbody_crosswalk_filter_file_field_name", - 'NHDWaterbodyComID' - ), - ) - - # if no restart file is provided, default initial states - else: - # TODO: Consider adding option to read cold state from route-link file - waterbodies_initial_ds_flow_const = 0.0 - waterbodies_initial_depth_const = -1e9 - # Set initial states from cold-state - waterbodies_initial_states_df = pd.DataFrame( - 0, - index=waterbodies_df.index, - columns=[ - "qd0", - "h0", - ], - dtype="float32", - ) - # TODO: This assignment could probably by done in the above call - waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const - waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const - waterbodies_initial_states_df["index"] = range( - len(waterbodies_initial_states_df) - ) - - waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on="lake_id" - ) - - LOG.debug( - "waterbody initial states complete in %s seconds."\ - % (time.time() - start_time)) - start_time = time.time() - ''' - - #---------------------------------------------------------------------------- - # Assemble channel initial states (flow and depth) - # also establish simulation initialization timestamp - #---------------------------------------------------------------------------- - start_time = time.time() - LOG.info("setting channel initial states ...") - - # if lite restart file is provided, the read channel initial states from it - if restart_parameters.get("lite_channel_restart_file", None): - # FIXME: Change it for hyfeature! - ''' - q0, t0 = nhd_io.read_lite_restart( - restart_parameters['lite_channel_restart_file'] - ) - t0_str = None - ''' - # when a restart file for hyfeature is provied, then read initial states from it. - elif restart_parameters.get("hyfeature_channel_restart_file", None): - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - channel_initial_states_file = restart_parameters["hyfeature_channel_restart_file"] - df = pd.read_csv(channel_initial_states_file) - t0_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") - t0 = datetime.strptime(t0_str,"%Y-%m-%d_%H:%M:%S") - - # build initial states from user-provided restart parameters - else: - # FIXME: Change it for hyfeature! - ''' - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - - # get initialization time from restart file - if restart_parameters.get("wrf_hydro_channel_restart_file", None): - channel_initial_states_file = restart_parameters[ - "wrf_hydro_channel_restart_file" - ] - t0_str = nhd_io.get_param_str( - channel_initial_states_file, - "Restart_Time" - ) - else: - t0_str = "2015-08-16_00:00:00" - - # convert timestamp from string to datetime - t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") - ''' - # get initial time from user inputs - if restart_parameters.get("start_datetime", None): - t0_str = restart_parameters.get("start_datetime") - - def _try_parsing_date(text): - for fmt in ( - "%Y-%m-%d_%H:%M", - "%Y-%m-%d_%H:%M:%S", - "%Y-%m-%d %H:%M", - "%Y-%m-%d %H:%M:%S", - "%Y/%m/%d %H:%M", - "%Y/%m/%d %H:%M:%S" - ): - try: - return datetime.strptime(text, fmt) - except ValueError: - pass - LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') - quit() - - t0 = _try_parsing_date(t0_str) - else: - if t0_str == "2015-08-16_00:00:00": - LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') - else: - LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) - - LOG.debug( - "channel initial states complete in %s seconds."\ - % (time.time() - start_time) - ) - start_time = time.time() - - return ( - #waterbodies_df, - q0, - t0, - ) - # TODO: This returns a full dataframe (waterbodies_df) with the - # merged initial states for waterbodies, but only the - # initial state values (q0; not merged with the channel properties) - # for the channels -- - # That is because that is how they are used downstream. Need to - # trace that back and decide if there is one of those two ways - # that is optimal and make both returns that way. - -def hyfeature_forcing( - run, - forcing_parameters, - hybrid_parameters, - nexus_to_upstream_flowpath_dict, - segment_index, - cpu_pool, - t0, - coastal_boundary_depth_df, -): - """ - Assemble model forcings. Forcings include hydrological lateral inflows (qlats) - and coastal boundary depths for hybrid runs - - Aguments - -------- - - run (dict): List of forcing files pertaining to a - single run-set - - forcing_parameters (dict): User-input simulation forcing parameters - - hybrid_parameters (dict): User-input simulation hybrid parameters - - segment_index (Int64): Reach segment ids - - cpu_pool (int): Number of CPUs in the process-parallel pool - Returns - ------- - - qlats_df (Pandas DataFrame): Lateral inflow data, indexed by - segment ID - - coastal_bounary_depth_df (Pandas DataFrame): Coastal boundary water depths, - indexed by segment ID - - Notes - ----- - - """ - - # Unpack user-specified forcing parameters - dt = forcing_parameters.get("dt", None) - qts_subdivisions = forcing_parameters.get("qts_subdivisions", None) - nexus_input_folder = forcing_parameters.get("nexus_input_folder", None) - qlat_file_index_col = forcing_parameters.get("qlat_file_index_col", "feature_id") - qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") - qlat_file_gw_bucket_flux_col = forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") - qlat_file_terrain_runoff_col = forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") - - - # TODO: find a better way to deal with these defaults and overrides. - run["t0"] = run.get("t0", t0) - run["nts"] = run.get("nts") - run["dt"] = run.get("dt", dt) - run["qts_subdivisions"] = run.get("qts_subdivisions", qts_subdivisions) - run["nexus_input_folder"] = run.get("nexus_input_folder", nexus_input_folder) - run["qlat_file_index_col"] = run.get("qlat_file_index_col", qlat_file_index_col) - run["qlat_file_value_col"] = run.get("qlat_file_value_col", qlat_file_value_col) - run["qlat_file_gw_bucket_flux_col"] = run.get("qlat_file_gw_bucket_flux_col", qlat_file_gw_bucket_flux_col) - run["qlat_file_terrain_runoff_col"] = run.get("qlat_file_terrain_runoff_col", qlat_file_terrain_runoff_col) - - #--------------------------------------------------------------------------- - # Assemble lateral inflow data - #--------------------------------------------------------------------------- - - start_time = time.time() - LOG.info("Creating a DataFrame of lateral inflow forcings ...") - - # Place holder, if reading qlats from a file use this. - # TODO: add an option for reading qlat data from BMI/model engine - from_file = True - if from_file: - qlats_df = hnu.build_qlateral_array( - run, - cpu_pool, - nexus_to_upstream_flowpath_dict, - segment_index, - ) - - LOG.debug( - "lateral inflow DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - #--------------------------------------------------------------------- - # Assemble coastal coupling data [WIP] - #--------------------------------------------------------------------- - # Run if coastal_boundary_depth_df has not already been created: - if coastal_boundary_depth_df.empty: - coastal_boundary_elev_files = forcing_parameters.get('coastal_boundary_input_file', None) - coastal_boundary_domain_files = hybrid_parameters.get('coastal_boundary_domain', None) - - if coastal_boundary_elev_files: - start_time = time.time() - LOG.info("creating coastal dataframe ...") - - coastal_boundary_domain = nhd_io.read_coastal_boundary_domain(coastal_boundary_domain_files) - coastal_boundary_depth_df = nhd_io.build_coastal_ncdf_dataframe( - coastal_boundary_elev_files, - coastal_boundary_domain, - ) - - LOG.debug( - "coastal boundary elevation observation DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - return qlats_df, coastal_boundary_depth_df - -def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): - """ - Reads .gpkg or lake.json file and prepares a dataframe, filtered - to the relevant reservoirs, to provide the parameters - for level-pool reservoir computation. - """ - def node_key_func(x): - return int(x[3:]) - if os.path.splitext(parm_file)[1]=='.gpkg': - df = gpd.read_file(parm_file, layer="lake_attributes").set_index('id') - elif os.path.splitext(parm_file)[1]=='.json': - df = pd.read_json(parm_file, orient="index") - - df.index = df.index.map(node_key_func) - df.index.name = lake_index_field - - if lake_id_mask: - df = df.loc[lake_id_mask] - return df - -def read_ngen_waterbody_type_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): - """ - """ - #FIXME: this function is likely not correct. Unclear how we will get - # reservoir type from the gpkg files. Information should be in 'crosswalk' - # layer, but as of now (Nov 22, 2022) there doesn't seem to be a differentiation - # between USGS reservoirs, USACE reservoirs, or RFC reservoirs... - def node_key_func(x): - return int(x[3:]) - - if os.path.splitext(parm_file)[1]=='.gpkg': - df = gpd.read_file(parm_file, layer="crosswalk").set_index('id') - elif os.path.splitext(parm_file)[1]=='.json': - df = pd.read_json(parm_file, orient="index") - - df.index = df.index.map(node_key_func) - df.index.name = lake_index_field - if lake_id_mask: - df = df.loc[lake_id_mask] - - return df - -def read_geopkg(file_path): - flowpaths = gpd.read_file(file_path, layer="flowpaths") - attributes = gpd.read_file(file_path, layer="flowpath_attributes").drop('geometry', axis=1) - #merge all relevant data into a single dataframe - flowpaths = pd.merge(flowpaths, attributes, on='id') - - return flowpaths - -def read_json(file_path, edge_list): - dfs = [] - with open(edge_list) as edge_file: - edge_data = json.load(edge_file) - edge_map = {} - for id_dict in edge_data: - edge_map[ id_dict['id'] ] = id_dict['toid'] - with open(file_path) as data_file: - json_data = json.load(data_file) - for key_wb, value_params in json_data.items(): - df = pd.json_normalize(value_params) - df['id'] = key_wb - df['toid'] = edge_map[key_wb] - dfs.append(df) - df_main = pd.concat(dfs, ignore_index=True) - - return df_main - -def numeric_id(flowpath): - id = flowpath['id'].split('-')[-1] - toid = flowpath['toid'].split('-')[-1] - flowpath['id'] = int(id) - flowpath['toid'] = int(toid) - - return flowpath \ No newline at end of file From adb99038cb541d5b297b8c4196a429212dd07427 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 27 Jan 2023 19:03:51 +0000 Subject: [PATCH 22/54] more updates to address comments above --- src/troute-network/troute/AbstractNetwork.py | 144 ++++++++---------- src/troute-network/troute/AbstractRouting.py | 6 +- .../troute/HYFeaturesNetwork.py | 7 - src/troute-network/troute/NHDNetwork.py | 8 - 4 files changed, 63 insertions(+), 102 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index a77053342..9941f39ef 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -48,13 +48,20 @@ def __init__(self,): dtype="float32", ) """ + break_network_at_waterbodies = self.waterbody_parameters.get("break_network_at_waterbodies", False) + streamflow_da = self.data_assimilation_parameters.get('streamflow_da', False) + break_network_at_gages = False + if streamflow_da: + break_network_at_gages = streamflow_da.get('streamflow_nudging', False) + self.break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, + "break_network_at_gages": break_network_at_gages} + self._break_segments = set() - - if self.break_points: - if self.break_points["break_network_at_waterbodies"]: - self._break_segments = self._break_segments | set(self.waterbody_connections.values()) - if self.break_points["break_network_at_gages"]: - self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) + + if self.break_points["break_network_at_waterbodies"]: + self._break_segments = self._break_segments | set(self.waterbody_connections.values()) + if self.break_points["break_network_at_gages"]: + self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) self.initialize_routing_scheme() @@ -70,20 +77,11 @@ def assemble_forcings(self, run,): Aguments -------- - - run (dict): List of forcing files pertaining to a - single run-set - - forcing_parameters (dict): User-input simulation forcing parameters - - hybrid_parameters (dict): User-input simulation hybrid parameters - - supernetwork_parameters (dict): User-input simulation supernetwork parameters - - segment_index (Int64): Reach segment ids - - cpu_pool (int): Number of CPUs in the process-parallel pool + - run (dict): List of forcing files pertaining to a + single run-set Returns ------- - - qlats_df (Pandas DataFrame): Lateral inflow data, indexed by - segment ID - - coastal_bounary_depth_df (Pandas DataFrame): Coastal boundary water depths, - indexed by segment ID Notes ----- @@ -102,7 +100,6 @@ def assemble_forcings(self, run,): # TODO: find a better way to deal with these defaults and overrides. run["t0"] = run.get("t0", self.t0) - run["nts"] = run.get("nts") run["dt"] = run.get("dt", dt) run["qts_subdivisions"] = run.get("qts_subdivisions", qts_subdivisions) run["qlat_input_folder"] = run.get("qlat_input_folder", qlat_input_folder) @@ -513,24 +510,9 @@ def initial_warmstate_preprocess(self,): Arguments --------- - - break_network_at_waterbodies (bool): If True, waterbody initial states will - be appended to the waterbody parameter - dataframe. If False, waterbodies will - not be simulated and the waterbody - parameter datataframe wil not be changed - - restart_parameters (dict): User-input simulation restart - parameters - - segment_index (Pandas Index): All segment IDs in the simulation - doamin - - waterbodies_df (Pandas DataFrame): Waterbody parameters Returns ------- - - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial - states (outflow and pool elevation) - - q0 (Pandas DataFrame): Initial flow and depth states for each - segment in the model domain - - t0 (datetime): Datetime of the model initialization Notes ----- @@ -603,72 +585,70 @@ def initial_warmstate_preprocess(self,): #---------------------------------------------------------------------------- # Assemble channel initial states (flow and depth) # also establish simulation initialization timestamp + # 3 Restart Options: + # 1. From t-route generated lite restart file (network agnostic) + # 2. From wrf_hydro_restart file (valid for NHDNetwork only) + # 3. Cold start, requires user specified start datetime #---------------------------------------------------------------------------- start_time = time.time() LOG.info("setting channel initial states ...") # if lite restart file is provided, the read channel initial states from it if restart_parameters.get("lite_channel_restart_file", None): - # FIXME: Change it for hyfeature! self._q0, self._t0 = nhd_io.read_lite_restart( restart_parameters['lite_channel_restart_file'] ) - t0_str = None - - # when a restart file for hyfeature is provied, then read initial states from it. - elif restart_parameters.get("hyfeature_channel_restart_file", None): - self._q0 = build_channel_initial_state(restart_parameters, self.segment_index) - channel_initial_states_file = restart_parameters["hyfeature_channel_restart_file"] - df = pd.read_csv(channel_initial_states_file) - t0_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") - self._t0 = datetime.strptime(t0_str,"%Y-%m-%d_%H:%M:%S") + + elif restart_parameters.get("wrf_hydro_channel_restart_file", None): + self._q0 = nhd_io.get_channel_restart_from_wrf_hydro( + restart_parameters["wrf_hydro_channel_restart_file"], + restart_parameters["wrf_hydro_channel_ID_crosswalk_file"], + restart_parameters.get("wrf_hydro_channel_ID_crosswalk_file_field_name", 'link'), + restart_parameters.get("wrf_hydro_channel_restart_upstream_flow_field_name", 'qlink1'), + restart_parameters.get("wrf_hydro_channel_restart_downstream_flow_field_name", 'qlink2'), + restart_parameters.get("wrf_hydro_channel_restart_depth_flow_field_name", 'hlink'), + ) - # build initial states from user-provided restart parameters - else: - # FIXME: Change it for hyfeature! - self._q0 = build_channel_initial_state(restart_parameters, self.segment_index) - - # get initialization time from restart file - if restart_parameters.get("wrf_hydro_channel_restart_file", None): - channel_initial_states_file = restart_parameters[ - "wrf_hydro_channel_restart_file" - ] - t0_str = nhd_io.get_param_str( - channel_initial_states_file, + t0_str = nhd_io.get_param_str( + restart_parameters["wrf_hydro_channel_restart_file"], "Restart_Time" ) - else: - t0_str = "2015-08-16_00:00:00" - + # convert timestamp from string to datetime self._t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") - - # get initial time from user inputs - if restart_parameters.get("start_datetime", None): - t0_str = restart_parameters.get("start_datetime") + + else: + # Set cold initial state + # assume to be zero + # 0, index=connections.keys(), columns=["qu0", "qd0", "h0",], dtype="float32" + self._q0 = pd.DataFrame( + 0, index=self.segment_index, columns=["qu0", "qd0", "h0"], dtype="float32", + ) - def _try_parsing_date(text): - for fmt in ( - "%Y-%m-%d_%H:%M", - "%Y-%m-%d_%H:%M:%S", - "%Y-%m-%d %H:%M", - "%Y-%m-%d %H:%M:%S", - "%Y/%m/%d %H:%M", - "%Y/%m/%d %H:%M:%S" - ): - try: - return datetime.strptime(text, fmt) - except ValueError: - pass - LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') - quit() + # get initial time from user inputs + if restart_parameters.get("start_datetime", None): + t0_str = restart_parameters.get("start_datetime") - self._t0 = _try_parsing_date(t0_str) - else: - if t0_str == "2015-08-16_00:00:00": - LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') + def _try_parsing_date(text): + for fmt in ( + "%Y-%m-%d_%H:%M", + "%Y-%m-%d_%H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%d %H:%M:%S", + "%Y/%m/%d %H:%M", + "%Y/%m/%d %H:%M:%S" + ): + try: + return datetime.strptime(text, fmt) + except ValueError: + pass + LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') + quit() + + self._t0 = _try_parsing_date(t0_str) + else: - LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) + raise(RuntimeError("No start_datetime provided in config file for cold start.")) LOG.debug( "channel initial states complete in %s seconds."\ diff --git a/src/troute-network/troute/AbstractRouting.py b/src/troute-network/troute/AbstractRouting.py index ddeab6162..ab8310c45 100644 --- a/src/troute-network/troute/AbstractRouting.py +++ b/src/troute-network/troute/AbstractRouting.py @@ -107,7 +107,7 @@ def unrefactored_topobathy_df(self): class MCOnly(AbstractRouting): - def __init__(self, hybrid_params): + def __init__(self, _): self.hybrid_params = None super().__init__() @@ -220,22 +220,18 @@ def diffusive_network_data(self): @property def topobathy_df(self): - self._topobathy_df = pd.DataFrame() return self._topobathy_df @property def refactored_diffusive_domain(self): - self._refactored_diffusive_domain = None return self._refactored_diffusive_domain @property def refactored_reaches(self): - self._refactored_reaches = {} return self._refactored_reaches @property def unrefactored_topobathy_df(self): - self._unrefactored_topobathy_df = pd.DataFrame() return self._unrefactored_topobathy_df diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index c34bc4b67..e7d88fbdc 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -140,13 +140,6 @@ def __init__(self, if self.showtiming: print("... in %s seconds." % (time.time() - start_time)) - break_network_at_waterbodies = self.waterbody_parameters.get("break_network_at_waterbodies", False) - streamflow_da = self.data_assimilation_parameters.get('streamflow_da', False) - break_network_at_gages = False - if streamflow_da: - break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - self.break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, - "break_network_at_gages": break_network_at_gages} super().__init__() diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index f5f47289e..ceb1ec116 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -66,14 +66,6 @@ def __init__( if self.showtiming: print("... in %s seconds." % (time.time() - start_time)) - break_network_at_waterbodies = self.waterbody_parameters.get("break_network_at_waterbodies", False) - streamflow_da = self.data_assimilation_parameters.get('streamflow_da', False) - break_network_at_gages = False - if streamflow_da: - break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - self.break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, - "break_network_at_gages": break_network_at_gages} - self._flowpath_dict = {} super().__init__() From dcbb793c0245eb1a895d7af0b9c129135cae3401 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 27 Jan 2023 19:14:27 +0000 Subject: [PATCH 23/54] deleted files --- src/troute-network/troute/RoutingScheme.py | 245 ---- .../troute/hyfeature_preprocess.py | 949 -------------- src/troute-network/troute/nhd_preprocess.py | 1148 ----------------- 3 files changed, 2342 deletions(-) delete mode 100644 src/troute-network/troute/RoutingScheme.py delete mode 100644 src/troute-network/troute/hyfeature_preprocess.py delete mode 100644 src/troute-network/troute/nhd_preprocess.py diff --git a/src/troute-network/troute/RoutingScheme.py b/src/troute-network/troute/RoutingScheme.py deleted file mode 100644 index 049b0b84f..000000000 --- a/src/troute-network/troute/RoutingScheme.py +++ /dev/null @@ -1,245 +0,0 @@ -from .AbstractNetwork import AbstractNetwork -import logging -import yaml -import json -import xarray as xr -import pandas as pd - -from troute.nhd_network import reverse_network -from troute.nhd_network_utilities_v02 import organize_independent_networks, build_refac_connections - -LOG = logging.getLogger('') - -def read_diffusive_domain(domain_file): - ''' - Read diffusive domain data from .ymal or .json file. - - Arguments - --------- - domain_file (str or pathlib.Path): Path of diffusive domain file - - Returns - ------- - data (dict int: [int]): domain tailwater segments: list of segments in domain - (includeing tailwater segment) - - ''' - if domain_file[-4:] == "yaml": - with open(domain_file) as domain: - data = yaml.load(domain, Loader=yaml.SafeLoader) - else: - with open(domain_file) as domain: - data = json.load(domain) - - return data - -def read_netcdf(geo_file_path): - ''' - Open a netcdf file with xarray and convert to dataframe - - Arguments - --------- - geo_file_path (str or pathlib.Path): netCDF filepath - - Returns - ------- - ds.to_dataframe() (DataFrame): netCDF contents - - Notes - ----- - - When handling large volumes of netCDF files, xarray is not the most efficient. - - ''' - with xr.open_dataset(geo_file_path) as ds: - return ds.to_dataframe() - - -class RoutingScheme(AbstractNetwork): - """ - - """ - __slots__ = ["hybrid_params", "_diffusive_domain", "_coastal_boundary_depth_df", - "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", - "_refactored_diffusive_network_data", "_refactored_reaches", - "_unrefactored_topobathy_df",] - - def __init__(self,): - """ - - """ - self._diffusive_domain = None - self._diffusive_network_data = None - self._topobathy_df = pd.DataFrame() - self._unrefactored_topobathy_df = pd.DataFrame() - self._refactored_diffusive_domain = None - self._refactored_diffusive_network_data = None - self._refactored_reaches = {} - - # Determine whether to run hybrid routing from user input - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - domain_file = self.hybrid_parameters.get("diffusive_domain", None) - - if run_hybrid and domain_file: - #========================================================================== - # build diffusive domain data and edit MC domain data for hybrid simulation - self._diffusive_domain = read_diffusive_domain(domain_file) - self._diffusive_network_data = {} - - rconn_diff0 = reverse_network(self._connections) - - for tw in self._diffusive_domain: - mainstem_segs = self._diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = self._diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - self._diffusive_network_data[tw] = {} - - # add diffusive domain segments - self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - self._diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - self._diffusive_network_data[tw]['connections'] = {k: self._connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = organize_independent_networks( - self._diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - self._diffusive_network_data[tw]['rconn'] = rconn_diff - self._diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - self._diffusive_network_data[tw]['param_df'] = self._dataframe.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - self._dataframe = self._dataframe.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - self._connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - self._connections[us] = [] - - super().__init__() - - @property - def diffusive_network_data(self,): - return self._diffusive_network_data - - @property - def topobathy_df(self,): - if self._topobathy_df.empty: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) - - if run_hybrid and use_topobathy: - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_refactored: - refactored_topobathy_file = self.hybrid_parameters.get("refactored_topobathy_domain", None) - self._topobathy_df = read_netcdf(refactored_topobathy_file).set_index('link') - else: - topobathy_file = self.hybrid_parameters.get("topobathy_domain", None) - self._topobathy_df = read_netcdf(topobathy_file).set_index('link') - self._topobathy_df.index = self._topobathy_df.index.astype(int) - - return self._topobathy_df - - @property - def refactored_diffusive_domain(self,): - if not self._refactored_diffusive_domain: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_hybrid and run_refactored: - refactored_domain_file = self.hybrid_parameters.get("refactored_domain", None) - - self._refactored_diffusive_domain = read_diffusive_domain(refactored_domain_file) - - return self._refactored_diffusive_domain - - @property - def refactored_reaches(self,): - if not self._refactored_reaches: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_hybrid and run_refactored: - refactored_topobathy_file = self.hybrid_parameters.get("refactored_topobathy_domain", None) - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = build_refac_connections(diffusive_parameters) - - for tw in self._diffusive_domain: - - # list of stream segments of a single refactored diffusive domain - refac_tw = self.refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = self.refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - trib_segs = self.diffusive_network_data[tw]['tributary_segments'] - refactored_diffusive_network_data = {} - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k] = [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] - - return self._refactored_reaches - - @property - def unrefactored_topobathy_df(self,): - if self._unrefactored_topobathy_df.empty: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_hybrid and use_topobathy and run_refactored: - topobathy_file = self.hybrid_parameters.get("topobathy_domain", None) - self._unrefactored_topobathy_df = read_netcdf(topobathy_file).set_index('link') - self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) - - return self._unrefactored_topobathy_df - diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py deleted file mode 100644 index 5ab0a5bec..000000000 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ /dev/null @@ -1,949 +0,0 @@ -import time -import pathlib -import logging -from datetime import datetime -from collections import defaultdict -from pathlib import Path -import os - -import pandas as pd -import numpy as np -import xarray as xr -import geopandas as gpd - -import troute.nhd_network_utilities_v02 as nnu -import troute.nhd_network as nhd_network -import troute.nhd_io as nhd_io -from troute.nhd_network import reverse_dict -import troute.hyfeature_network_utilities as hnu - -LOG = logging.getLogger('') - -def read_geo_file( - supernetwork_parameters, - waterbody_parameters, -): - - geo_file_path = supernetwork_parameters["geo_file_path"] - - file_type = Path(geo_file_path).suffix - if( file_type == '.gpkg' ): - dataframe = read_geopkg(geo_file_path) - elif( file_type == '.json') : - edge_list = supernetwork_parameters['flowpath_edge_list'] - dataframe = read_json(geo_file_path, edge_list) - else: - raise RuntimeError("Unsupported file type: {}".format(file_type)) - - # Don't need the string prefix anymore, drop it - mask = ~ dataframe['toid'].str.startswith("tnex") - dataframe = dataframe.apply(numeric_id, axis=1) - - # make the flowpath linkage, ignore the terminal nexus - flowpath_dict = dict(zip(dataframe.loc[mask].toid, dataframe.loc[mask].id)) - - # ********** need to be included in flowpath_attributes ************* - dataframe['alt'] = 1.0 #FIXME get the right value for this... - - cols = supernetwork_parameters.get('columns',None) - - if cols: - dataframe = dataframe[list(cols.values())] - # Rename parameter columns to standard names: from route-link names - # key: "link" - # downstream: "to" - # dx: "Length" - # n: "n" # TODO: rename to `manningn` - # ncc: "nCC" # TODO: rename to `mannningncc` - # s0: "So" # TODO: rename to `bedslope` - # bw: "BtmWdth" # TODO: rename to `bottomwidth` - # waterbody: "NHDWaterbodyComID" - # gages: "gages" - # tw: "TopWdth" # TODO: rename to `topwidth` - # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` - # alt: "alt" - # musk: "MusK" - # musx: "MusX" - # cs: "ChSlp" # TODO: rename to `sideslope` - dataframe = dataframe.rename(columns=reverse_dict(cols)) - dataframe.set_index("key", inplace=True) - dataframe = dataframe.sort_index() - - # numeric code used to indicate network terminal segments - terminal_code = supernetwork_parameters.get("terminal_code", 0) - - # There can be an externally determined terminal code -- that's this first value - terminal_codes = set() - terminal_codes.add(terminal_code) - # ... but there may also be off-domain nodes that are not explicitly identified - # but which are terminal (i.e., off-domain) as a result of a mask or some other - # an interior domain truncation that results in a - # otherwise valid node value being pointed to, but which is masked out or - # being intentionally separated into another domain. - terminal_codes = terminal_codes | set( - dataframe[~dataframe["downstream"].isin(dataframe.index)]["downstream"].values - ) - - # build connections dictionary - connections = nhd_network.extract_connections( - dataframe, "downstream", terminal_codes=terminal_codes - ) - - #Load waterbody/reservoir info - if waterbody_parameters: - levelpool_params = waterbody_parameters.get('level_pool', None) - if not levelpool_params: - # FIXME should not be a hard requirement - raise(RuntimeError("No supplied levelpool parameters in routing config")) - - lake_id = levelpool_params.get("level_pool_waterbody_id", "wb-id") - waterbody_df = read_ngen_waterbody_df( - levelpool_params["level_pool_waterbody_parameter_file_path"], - lake_id, - ) - - # Remove duplicate lake_ids and rows - waterbody_df = ( - waterbody_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - - try: - waterbody_types_df = read_ngen_waterbody_type_df( - levelpool_params["reservoir_parameter_file"], - lake_id, - #self.waterbody_connections.values(), - ) - # Remove duplicate lake_ids and rows - waterbody_types_df =( - waterbody_types_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - - except ValueError: - #FIXME any reservoir operations requires some type - #So make this default to 1 (levelpool) - waterbody_types_df = pd.DataFrame(index=waterbody_df.index) - waterbody_types_df['reservoir_type'] = 1 - - return dataframe, flowpath_dict, connections, waterbody_df, waterbody_types_df, terminal_codes - -def build_hyfeature_network(supernetwork_parameters, - waterbody_parameters, -): - - geo_file_path = supernetwork_parameters["geo_file_path"] - cols = supernetwork_parameters["columns"] - terminal_code = supernetwork_parameters.get("terminal_code", 0) - - break_network_at_waterbodies = supernetwork_parameters.get("break_network_at_waterbodies", False) - break_network_at_gages = supernetwork_parameters.get("break_network_at_gages", False) - break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, - "break_network_at_gages": break_network_at_gages} - - file_type = Path(geo_file_path).suffix - if( file_type == '.gpkg' ): - dataframe = hyf_network.read_geopkg(geo_file_path) - elif( file_type == '.json') : - edge_list = supernetwork_parameters['flowpath_edge_list'] - dataframe = hyf_network.read_json(geo_file_path, edge_list) - else: - raise RuntimeError("Unsupported file type: {}".format(file_type)) - - # Don't need the string prefix anymore, drop it - mask = ~ dataframe['toid'].str.startswith("tnex") - dataframe = dataframe.apply(hyf_network.numeric_id, axis=1) - - # make the flowpath linkage, ignore the terminal nexus - flowpath_dict = dict(zip(dataframe.loc[mask].toid, dataframe.loc[mask].id)) - waterbody_types_df = pd.DataFrame() - waterbody_df = pd.DataFrame() - waterbody_type_specified = False - - # ********** need to be included in flowpath_attributes ************* - # FIXME once again, order here can hurt....to hack `alt` in, either need to - # put it as a column in the config, or do this AFTER the super constructor - # otherwise the alt column gets sliced out... - dataframe['alt'] = 1.0 #FIXME get the right value for this... - - #Load waterbody/reservoir info - #For ngen HYFeatures, the reservoirs to be simulated - #are determined by the lake.json file - #we limit waterbody_connections to only the flowpaths - #that coincide with a lake listed in this file - #see `waterbody_connections` - if waterbody_parameters: - # FIXME later, DO ALL LAKE PARAMS BETTER - levelpool_params = waterbody_parameters.get('level_pool', None) - if not levelpool_params: - # FIXME should not be a hard requirement - raise(RuntimeError("No supplied levelpool parameters in routing config")) - - lake_id = levelpool_params.get("level_pool_waterbody_id", "wb-id") - waterbody_df = read_ngen_waterbody_df( - levelpool_params["level_pool_waterbody_parameter_file_path"], - lake_id, - #self.waterbody_connections.values() - ) - - # Remove duplicate lake_ids and rows - waterbody_df = ( - waterbody_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - waterbody_df["qd0"] = 0.0 - waterbody_df["h0"] = -1e9 - - try: - waterbody_types_df = read_ngen_waterbody_type_df( - levelpool_params["reservoir_parameter_file"], - lake_id, - #self.waterbody_connections.values(), - ) - # Remove duplicate lake_ids and rows - waterbody_types_df =( - waterbody_types_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - - except ValueError: - #FIXME any reservoir operations requires some type - #So make this default to 1 (levelpool) - waterbody_types_df = pd.DataFrame(index=waterbody_df.index) - waterbody_types_df['reservoir_type'] = 1 - - return (dataframe, - flowpath_dict, - waterbody_types_df, - waterbody_df, - waterbody_type_specified, - cols, - terminal_code, - break_points, - ) - -def hyfeature_hybrid_routing_preprocess( - connections, - param_df, - wbody_conn, - gages, - preprocessing_parameters, - compute_parameters, - waterbody_parameters, -): - ''' - Creation of routing network data objects. Logical ordering of lower-level - function calls that build individual network data objects. - - Arguments - --------- - supernetwork_parameters (dict): user input data re network extent - waterbody_parameters (dict): user input data re waterbodies - preprocessing_parameters (dict): user input data re preprocessing - compute_parameters (dict): user input data re compute configuration - data_assimilation_parameters (dict): user input data re data assimilation - - Returns - ------- - connections (dict of int: [int]): {segment id: [downsteram adjacent segment ids]} - param_df (DataFrame): Hydraulic geometry and roughness parameters, by segment - wbody_conn (dict of int: int): {segment id: associated lake id} - waterbodies_df (DataFrame): Waterbody (reservoir) parameters - waterbody_types_df (DataFrame): Waterbody type codes (1 - levelpool, 2 - USGS, 3 - USACE, 4 - RFC) - break_network_at_waterbodies (bool): If True, waterbodies occpy reaches of their own - waterbody_type_specified (bool): If True, more than just levelpool waterbodies exist - link_lake_crosswalk (dict of int: int): {lake id: outlet segment id} - independent_networks (dict of int: {int: [int]}): {tailwater id: {segment id: [upstream adjacent segment ids]}} - reaches_bytw (dict of int: [[int]]): {tailwater id: list or reach lists} - rconn (dict of int: [int]): {segment id: [upstream adjacent segment ids]} - pd.DataFrame.from_dict(gages) (DataFrame): Gage ids and corresponding segment ids at which they are located - diffusive_network_data (dict or None): Network data objects for diffusive domain - topobathy_df (DataFrame): Natural cross section data for diffusive domain - - Notes - ----- - - waterbody_type_specified is likely an excessive return and can be removed and inferred from the - contents of waterbody_types_df - - The values of the link_lake_crosswalk dictionary are the downstream-most segments within - the waterbody extent to which waterbody data are written. They are NOT the first segments - downsteram of the waterbody - ''' - - #============================================================================ - # Establish diffusive domain for MC/diffusive hybrid simulations - - hybrid_params = compute_parameters.get("hybrid_parameters", False) - if hybrid_params: - # switch parameters - # if run_hybrid = False, run MC only - # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc - # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric - # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric - run_hybrid = hybrid_params.get('run_hybrid_routing', False) - use_topobathy = hybrid_params.get('use_natl_xsections', False) - run_refactored = hybrid_params.get('run_refactored_network', False) - - # file path parameters of non-refactored hydrofabric defined by RouteLink.nc - domain_file = hybrid_params.get("diffusive_domain", None) - topobathy_file = hybrid_params.get("topobathy_domain", None) - - # file path parameters of refactored hydrofabric for diffusive wave channel routing - refactored_domain_file = hybrid_params.get("refactored_domain", None) - refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) - #------------------------------------------------------------------------- - # for non-refactored hydofabric defined by RouteLink.nc - # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. - if run_hybrid and domain_file: - - LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - diffusive_domain = nhd_io.read_diffusive_domain(domain_file) - - if use_topobathy and topobathy_file: - - LOG.debug('Natural cross section data on original hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - - # TODO: Request GID make comID variable an integer in their product, so - # we do not need to change variable types, here. - topobathy_df.index = topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - diffusive_network_data = {} - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') - unrefactored_topobathy_df = pd.DataFrame() - #------------------------------------------------------------------------- - # for refactored hydofabric - if run_hybrid and run_refactored and refactored_domain_file: - - LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) - - if use_topobathy and refactored_topobathy_file: - - LOG.debug('Natural cross section data of refactored hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) - - # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy - # for crosswalking water elevations between non-refactored and refactored hydrofabrics. - unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - unrefactored_topobathy_df.index = unrefactored_topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - refactored_diffusive_network_data = {} - - else: - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - unrefactored_topobathy_df = pd.DataFrame() - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') - - #============================================================================ - # build diffusive domain data and edit MC domain data for hybrid simulation - - # - if diffusive_domain: - rconn_diff0 = nhd_network.reverse_network(connections) - refactored_reaches = {} - - for tw in diffusive_domain: - mainstem_segs = diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - diffusive_network_data[tw] = {} - - # add diffusive domain segments - diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = nnu.organize_independent_networks( - diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - diffusive_network_data[tw]['rconn'] = rconn_diff - diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - diffusive_network_data[tw]['param_df'] = param_df.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - if refactored_diffusive_domain: - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = nnu.build_refac_connections(diffusive_parameters) - - # list of stream segments of a single refactored diffusive domain - refac_tw = refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k]= [refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = nnu.organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = diffusive_network_data[tw]['upstream_boundary_link'] - else: - refactored_reaches={} - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - param_df = param_df.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - connections[us] = [] - - #============================================================================ - # Identify Independent Networks and Reaches by Network - LOG.info("organizing connections into reaches ...") - start_time = time.time() - gage_break_segments = set() - wbody_break_segments = set() - - break_network_at_waterbodies = waterbody_parameters.get( - "break_network_at_waterbodies", False - ) - - # if streamflow DA, then break network at gages - break_network_at_gages = False - - if break_network_at_waterbodies: - wbody_break_segments = wbody_break_segments.union(wbody_conn.values()) - - if break_network_at_gages: - gage_break_segments = gage_break_segments.union(gages['gages'].keys()) - - independent_networks, reaches_bytw, rconn = nnu.organize_independent_networks( - connections, - wbody_break_segments, - gage_break_segments, - ) - - LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) - # FIXME: Make this commented out alive - ''' - if preprocessing_parameters.get('preprocess_only', False): - - LOG.debug("saving preprocessed network data to disk for future use") - # todo: consider a better default than None - destination_folder = preprocessing_parameters.get('preprocess_output_folder', None) - if destination_folder: - - output_filename = preprocessing_parameters.get( - 'preprocess_output_filename', - 'preprocess_output' - ) - - outputs = {} - outputs.update( - {'connections': connections, - 'param_df': param_df, - 'wbody_conn': wbody_conn, - 'waterbodies_df': waterbodies_df, - 'waterbody_types_df': waterbody_types_df, - 'break_network_at_waterbodies': break_network_at_waterbodies, - 'waterbody_type_specified': waterbody_type_specified, - 'link_lake_crosswalk': link_lake_crosswalk, - 'independent_networks': independent_networks, - 'reaches_bytw': reaches_bytw, - 'rconn': rconn, - 'link_gage_df': link_gage_df, - 'usgs_lake_gage_crosswalk': usgs_lake_gage_crosswalk, - 'usace_lake_gage_crosswalk': usace_lake_gage_crosswalk, - 'diffusive_network_data': diffusive_network_data, - 'topobathy_data': topobathy_df, - } - ) - try: - np.save( - pathlib.Path(destination_folder).joinpath(output_filename), - outputs - ) - except: - LOG.critical('Canonot find %s. Aborting preprocessing routine' % pathlib.Path(destination_folder)) - quit() - - LOG.debug( - "writing preprocessed network data to %s"\ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - LOG.critical( - "Preprocessed network data written to %s aborting preprocessing sequence" \ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - quit() - - else: - LOG.critical( - "No destination folder specified for preprocessing. Please specify preprocess_output_folder in configuration file. Aborting preprocessing routine" - ) - quit() - ''' - return(independent_networks, - reaches_bytw, - rconn, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) - -def hyfeature_initial_warmstate_preprocess( - # break_network_at_waterbodies, - restart_parameters, - # data_assimilation_parameters, - segment_index, - # waterbodies_df, - # link_lake_crosswalk, -): - - ''' - Assemble model initial condition data: - - waterbody inital states (outflow and pool elevation) - - channel initial states (flow and depth) - - initial time - - Arguments - --------- - - break_network_at_waterbodies (bool): If True, waterbody initial states will - be appended to the waterbody parameter - dataframe. If False, waterbodies will - not be simulated and the waterbody - parameter datataframe wil not be changed - - restart_parameters (dict): User-input simulation restart - parameters - - data_assimilation_parameters (dict): User-input data assimilation - parameters - - segment_index (Pandas Index): All segment IDs in the simulation - doamin - - waterbodies_df (Pandas DataFrame): Waterbody parameters - - link_lake_crosswalk (dict): Crosswalking between lake ids and the link - id of the lake outlet segment - - Returns - ------- - - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial - states (outflow and pool elevation) - - q0 (Pandas DataFrame): Initial flow and depth states for each - segment in the model domain - - t0 (datetime): Datetime of the model initialization - - Notes - ----- - ''' - - #---------------------------------------------------------------------------- - # Assemble waterbody initial states (outflow and pool elevation - #---------------------------------------------------------------------------- - ''' - if break_network_at_waterbodies: - - start_time = time.time() - LOG.info("setting waterbody initial states ...") - - # if a lite restart file is provided, read initial states from it. - if restart_parameters.get("lite_waterbody_restart_file", None): - - waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( - restart_parameters['lite_waterbody_restart_file'] - ) - - # read waterbody initial states from WRF-Hydro type restart file - elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): - waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( - restart_parameters["wrf_hydro_waterbody_restart_file"], - restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], - restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", 'lake_id'), - restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], - restart_parameters.get( - "wrf_hydro_waterbody_crosswalk_filter_file_field_name", - 'NHDWaterbodyComID' - ), - ) - - # if no restart file is provided, default initial states - else: - # TODO: Consider adding option to read cold state from route-link file - waterbodies_initial_ds_flow_const = 0.0 - waterbodies_initial_depth_const = -1e9 - # Set initial states from cold-state - waterbodies_initial_states_df = pd.DataFrame( - 0, - index=waterbodies_df.index, - columns=[ - "qd0", - "h0", - ], - dtype="float32", - ) - # TODO: This assignment could probably by done in the above call - waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const - waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const - waterbodies_initial_states_df["index"] = range( - len(waterbodies_initial_states_df) - ) - - waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on="lake_id" - ) - - LOG.debug( - "waterbody initial states complete in %s seconds."\ - % (time.time() - start_time)) - start_time = time.time() - ''' - - #---------------------------------------------------------------------------- - # Assemble channel initial states (flow and depth) - # also establish simulation initialization timestamp - #---------------------------------------------------------------------------- - start_time = time.time() - LOG.info("setting channel initial states ...") - - # if lite restart file is provided, the read channel initial states from it - if restart_parameters.get("lite_channel_restart_file", None): - # FIXME: Change it for hyfeature! - ''' - q0, t0 = nhd_io.read_lite_restart( - restart_parameters['lite_channel_restart_file'] - ) - t0_str = None - ''' - # when a restart file for hyfeature is provied, then read initial states from it. - elif restart_parameters.get("hyfeature_channel_restart_file", None): - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - channel_initial_states_file = restart_parameters["hyfeature_channel_restart_file"] - df = pd.read_csv(channel_initial_states_file) - t0_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") - t0 = datetime.strptime(t0_str,"%Y-%m-%d_%H:%M:%S") - - # build initial states from user-provided restart parameters - else: - # FIXME: Change it for hyfeature! - ''' - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - - # get initialization time from restart file - if restart_parameters.get("wrf_hydro_channel_restart_file", None): - channel_initial_states_file = restart_parameters[ - "wrf_hydro_channel_restart_file" - ] - t0_str = nhd_io.get_param_str( - channel_initial_states_file, - "Restart_Time" - ) - else: - t0_str = "2015-08-16_00:00:00" - - # convert timestamp from string to datetime - t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") - ''' - # get initial time from user inputs - if restart_parameters.get("start_datetime", None): - t0_str = restart_parameters.get("start_datetime") - - def _try_parsing_date(text): - for fmt in ( - "%Y-%m-%d_%H:%M", - "%Y-%m-%d_%H:%M:%S", - "%Y-%m-%d %H:%M", - "%Y-%m-%d %H:%M:%S", - "%Y/%m/%d %H:%M", - "%Y/%m/%d %H:%M:%S" - ): - try: - return datetime.strptime(text, fmt) - except ValueError: - pass - LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') - quit() - - t0 = _try_parsing_date(t0_str) - else: - if t0_str == "2015-08-16_00:00:00": - LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') - else: - LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) - - LOG.debug( - "channel initial states complete in %s seconds."\ - % (time.time() - start_time) - ) - start_time = time.time() - - return ( - #waterbodies_df, - q0, - t0, - ) - # TODO: This returns a full dataframe (waterbodies_df) with the - # merged initial states for waterbodies, but only the - # initial state values (q0; not merged with the channel properties) - # for the channels -- - # That is because that is how they are used downstream. Need to - # trace that back and decide if there is one of those two ways - # that is optimal and make both returns that way. - -def hyfeature_forcing( - run, - forcing_parameters, - hybrid_parameters, - nexus_to_upstream_flowpath_dict, - segment_index, - cpu_pool, - t0, - coastal_boundary_depth_df, -): - """ - Assemble model forcings. Forcings include hydrological lateral inflows (qlats) - and coastal boundary depths for hybrid runs - - Aguments - -------- - - run (dict): List of forcing files pertaining to a - single run-set - - forcing_parameters (dict): User-input simulation forcing parameters - - hybrid_parameters (dict): User-input simulation hybrid parameters - - segment_index (Int64): Reach segment ids - - cpu_pool (int): Number of CPUs in the process-parallel pool - Returns - ------- - - qlats_df (Pandas DataFrame): Lateral inflow data, indexed by - segment ID - - coastal_bounary_depth_df (Pandas DataFrame): Coastal boundary water depths, - indexed by segment ID - - Notes - ----- - - """ - - # Unpack user-specified forcing parameters - dt = forcing_parameters.get("dt", None) - qts_subdivisions = forcing_parameters.get("qts_subdivisions", None) - nexus_input_folder = forcing_parameters.get("nexus_input_folder", None) - qlat_file_index_col = forcing_parameters.get("qlat_file_index_col", "feature_id") - qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") - qlat_file_gw_bucket_flux_col = forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") - qlat_file_terrain_runoff_col = forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") - - - # TODO: find a better way to deal with these defaults and overrides. - run["t0"] = run.get("t0", t0) - run["nts"] = run.get("nts") - run["dt"] = run.get("dt", dt) - run["qts_subdivisions"] = run.get("qts_subdivisions", qts_subdivisions) - run["nexus_input_folder"] = run.get("nexus_input_folder", nexus_input_folder) - run["qlat_file_index_col"] = run.get("qlat_file_index_col", qlat_file_index_col) - run["qlat_file_value_col"] = run.get("qlat_file_value_col", qlat_file_value_col) - run["qlat_file_gw_bucket_flux_col"] = run.get("qlat_file_gw_bucket_flux_col", qlat_file_gw_bucket_flux_col) - run["qlat_file_terrain_runoff_col"] = run.get("qlat_file_terrain_runoff_col", qlat_file_terrain_runoff_col) - - #--------------------------------------------------------------------------- - # Assemble lateral inflow data - #--------------------------------------------------------------------------- - - start_time = time.time() - LOG.info("Creating a DataFrame of lateral inflow forcings ...") - - # Place holder, if reading qlats from a file use this. - # TODO: add an option for reading qlat data from BMI/model engine - from_file = True - if from_file: - qlats_df = hnu.build_qlateral_array( - run, - cpu_pool, - nexus_to_upstream_flowpath_dict, - segment_index, - ) - - LOG.debug( - "lateral inflow DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - #--------------------------------------------------------------------- - # Assemble coastal coupling data [WIP] - #--------------------------------------------------------------------- - # Run if coastal_boundary_depth_df has not already been created: - if coastal_boundary_depth_df.empty: - coastal_boundary_elev_files = forcing_parameters.get('coastal_boundary_input_file', None) - coastal_boundary_domain_files = hybrid_parameters.get('coastal_boundary_domain', None) - - if coastal_boundary_elev_files: - start_time = time.time() - LOG.info("creating coastal dataframe ...") - - coastal_boundary_domain = nhd_io.read_coastal_boundary_domain(coastal_boundary_domain_files) - coastal_boundary_depth_df = nhd_io.build_coastal_ncdf_dataframe( - coastal_boundary_elev_files, - coastal_boundary_domain, - ) - - LOG.debug( - "coastal boundary elevation observation DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - return qlats_df, coastal_boundary_depth_df - -def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): - """ - Reads .gpkg or lake.json file and prepares a dataframe, filtered - to the relevant reservoirs, to provide the parameters - for level-pool reservoir computation. - """ - def node_key_func(x): - return int(x[3:]) - if os.path.splitext(parm_file)[1]=='.gpkg': - df = gpd.read_file(parm_file, layer="lake_attributes").set_index('id') - elif os.path.splitext(parm_file)[1]=='.json': - df = pd.read_json(parm_file, orient="index") - - df.index = df.index.map(node_key_func) - df.index.name = lake_index_field - - if lake_id_mask: - df = df.loc[lake_id_mask] - return df - -def read_ngen_waterbody_type_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): - """ - """ - #FIXME: this function is likely not correct. Unclear how we will get - # reservoir type from the gpkg files. Information should be in 'crosswalk' - # layer, but as of now (Nov 22, 2022) there doesn't seem to be a differentiation - # between USGS reservoirs, USACE reservoirs, or RFC reservoirs... - def node_key_func(x): - return int(x[3:]) - - if os.path.splitext(parm_file)[1]=='.gpkg': - df = gpd.read_file(parm_file, layer="crosswalk").set_index('id') - elif os.path.splitext(parm_file)[1]=='.json': - df = pd.read_json(parm_file, orient="index") - - df.index = df.index.map(node_key_func) - df.index.name = lake_index_field - if lake_id_mask: - df = df.loc[lake_id_mask] - - return df - -def read_geopkg(file_path): - flowpaths = gpd.read_file(file_path, layer="flowpaths") - attributes = gpd.read_file(file_path, layer="flowpath_attributes").drop('geometry', axis=1) - #merge all relevant data into a single dataframe - flowpaths = pd.merge(flowpaths, attributes, on='id') - - return flowpaths - -def read_json(file_path, edge_list): - dfs = [] - with open(edge_list) as edge_file: - edge_data = json.load(edge_file) - edge_map = {} - for id_dict in edge_data: - edge_map[ id_dict['id'] ] = id_dict['toid'] - with open(file_path) as data_file: - json_data = json.load(data_file) - for key_wb, value_params in json_data.items(): - df = pd.json_normalize(value_params) - df['id'] = key_wb - df['toid'] = edge_map[key_wb] - dfs.append(df) - df_main = pd.concat(dfs, ignore_index=True) - - return df_main - -def numeric_id(flowpath): - id = flowpath['id'].split('-')[-1] - toid = flowpath['toid'].split('-')[-1] - flowpath['id'] = int(id) - flowpath['toid'] = int(toid) - - return flowpath \ No newline at end of file diff --git a/src/troute-network/troute/nhd_preprocess.py b/src/troute-network/troute/nhd_preprocess.py deleted file mode 100644 index 2422bac84..000000000 --- a/src/troute-network/troute/nhd_preprocess.py +++ /dev/null @@ -1,1148 +0,0 @@ -import time -import pathlib -import logging -from datetime import datetime -from collections import defaultdict - -import pandas as pd -import numpy as np -import xarray as xr - -import troute.nhd_network_utilities_v02 as nnu -import troute.nhd_network as nhd_network -import troute.nhd_io as nhd_io - -LOG = logging.getLogger('') - -def read_geo_file(supernetwork_parameters, waterbody_parameters, data_assimilation_parameters): - ''' - Construct network connections network, parameter dataframe, waterbody mapping, - and gage mapping. This is an intermediate-level function that calls several - lower level functions to read data, conduct network operations, and extract mappings. - - Arguments - --------- - supernetwork_parameters (dict): User input network parameters - - Returns: - -------- - connections (dict int: [int]): Network connections - param_df (DataFrame): Geometry and hydraulic parameters - wbodies (dict, int: int): segment-waterbody mapping - gages (dict, int: int): segment-gage mapping - - ''' - - # crosswalking dictionary between variables names in input dataset and - # variable names recognized by troute.routing module. - cols = supernetwork_parameters.get( - 'columns', - { - 'key' : 'link', - 'downstream': 'to', - 'dx' : 'Length', - 'n' : 'n', - 'ncc' : 'nCC', - 's0' : 'So', - 'bw' : 'BtmWdth', - 'waterbody' : 'NHDWaterbodyComID', - 'gages' : 'gages', - 'tw' : 'TopWdth', - 'twcc' : 'TopWdthCC', - 'alt' : 'alt', - 'musk' : 'MusK', - 'musx' : 'MusX', - 'cs' : 'ChSlp', - } - ) - - # numeric code used to indicate network terminal segments - terminal_code = supernetwork_parameters.get("terminal_code", 0) - - # read parameter dataframe - param_df = nhd_io.read(pathlib.Path(supernetwork_parameters["geo_file_path"])) - - # select the column names specified in the values in the cols dict variable - param_df = param_df[list(cols.values())] - - # rename dataframe columns to keys in the cols dict variable - param_df = param_df.rename(columns=nhd_network.reverse_dict(cols)) - - # handle synthetic waterbody segments - synthetic_wb_segments = supernetwork_parameters.get("synthetic_wb_segments", None) - synthetic_wb_id_offset = supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) - if synthetic_wb_segments: - # rename the current key column to key32 - key32_d = {"key":"key32"} - param_df = param_df.rename(columns=key32_d) - # create a key index that is int64 - # copy the links into the new column - param_df["key"] = param_df.key32.astype("int64") - # update the values of the synthetic reservoir segments - fix_idx = param_df.key.isin(set(synthetic_wb_segments)) - param_df.loc[fix_idx,"key"] = (param_df[fix_idx].key + synthetic_wb_id_offset).astype("int64") - - # set parameter dataframe index as segment id number, sort - param_df = param_df.set_index("key").sort_index() - - # get and apply domain mask - if "mask_file_path" in supernetwork_parameters: - data_mask = nhd_io.read_mask( - pathlib.Path(supernetwork_parameters["mask_file_path"]), - layer_string=supernetwork_parameters.get("mask_layer_string", None), - ) - data_mask = data_mask.set_index(data_mask.columns[0]) - param_df = param_df.filter(data_mask.index, axis=0) - - # map segment ids to waterbody ids - wbodies = {} - if "waterbody" in cols: - wbodies = nhd_network.extract_waterbody_connections( - param_df[["waterbody"]] - ) - param_df = param_df.drop("waterbody", axis=1) - - # map segment ids to gage ids - gages = {} - if "gages" in cols: - gages = nhd_network.gage_mapping(param_df[["gages"]]) - param_df = param_df.drop("gages", axis=1) - - # There can be an externally determined terminal code -- that's this first value - terminal_codes = set() - terminal_codes.add(terminal_code) - # ... but there may also be off-domain nodes that are not explicitly identified - # but which are terminal (i.e., off-domain) as a result of a mask or some other - # an interior domain truncation that results in a - # otherwise valid node value being pointed to, but which is masked out or - # being intentionally separated into another domain. - terminal_codes = terminal_codes | set( - param_df[~param_df["downstream"].isin(param_df.index)]["downstream"].values - ) - - # build connections dictionary - connections = nhd_network.extract_connections( - param_df, "downstream", terminal_codes=terminal_codes - ) - param_df = param_df.drop("downstream", axis=1) - - param_df = param_df.astype("float32") - - break_network_at_waterbodies = waterbody_parameters.get( - "break_network_at_waterbodies", False - ) - - # if waterbodies are being simulated, adjust the connections graph so that - # waterbodies are collapsed to single nodes. Also, build a mapping between - # waterbody outlet segments and lake ids - if break_network_at_waterbodies: - connections, link_lake_crosswalk = nhd_network.replace_waterbodies_connections( - connections, wbodies - ) - else: - link_lake_crosswalk = None - - #============================================================================ - # Retrieve and organize waterbody parameters - - waterbody_type_specified = False - if break_network_at_waterbodies: - - # Read waterbody parameters from LAKEPARM file - level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) - waterbodies_df = nhd_io.read_lakeparm( - level_pool_params['level_pool_waterbody_parameter_file_path'], - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - wbodies.values() - ) - - # Remove duplicate lake_ids and rows - waterbodies_df = ( - waterbodies_df.reset_index() - .drop_duplicates(subset="lake_id") - .set_index("lake_id") - ) - - # Declare empty dataframe - waterbody_types_df = pd.DataFrame() - - # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True - reservoir_da = data_assimilation_parameters.get( - 'reservoir_da', - {} - ) - - if reservoir_da: - usgs_hybrid = reservoir_da.get( - 'reservoir_persistence_usgs', - False - ) - usace_hybrid = reservoir_da.get( - 'reservoir_persistence_usace', - False - ) - param_file = reservoir_da.get( - 'gage_lakeID_crosswalk_file', - None - ) - else: - param_file = None - usace_hybrid = False - usgs_hybrid = False - - # check if RFC-type reservoirs are set to true - rfc_params = waterbody_parameters.get('rfc') - if rfc_params: - rfc_forecast = rfc_params.get( - 'reservoir_rfc_forecasts', - False - ) - param_file = rfc_params.get('reservoir_parameter_file',None) - else: - rfc_forecast = False - - if (param_file and reservoir_da) or (param_file and rfc_forecast): - waterbody_type_specified = True - ( - waterbody_types_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk - ) = nhd_io.read_reservoir_parameter_file( - param_file, - usgs_hybrid, - usace_hybrid, - rfc_forecast, - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - reservoir_da.get('crosswalk_usgs_gage_field', 'usgs_gage_id'), - reservoir_da.get('crosswalk_usgs_lakeID_field', 'usgs_lake_id'), - reservoir_da.get('crosswalk_usace_gage_field', 'usace_gage_id'), - reservoir_da.get('crosswalk_usace_lakeID_field', 'usace_lake_id'), - wbodies.values(), - ) - else: - waterbody_type_specified = True - waterbody_types_df = pd.DataFrame(data = 1, index = waterbodies_df.index, columns = ['reservoir_type']) - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - else: - # Declare empty dataframes - waterbody_types_df = pd.DataFrame() - waterbodies_df = pd.DataFrame() - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - return ( - param_df, - connections, - terminal_codes, - waterbodies_df, - waterbody_types_df, - waterbody_type_specified, - wbodies, - link_lake_crosswalk, - gages, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk) - - -def build_nhd_network(supernetwork_parameters,waterbody_parameters, - preprocessing_parameters,compute_parameters, - data_assimilation_parameters): - - # Build routing network data objects. Network data objects specify river - # network connectivity, channel geometry, and waterbody parameters. - if preprocessing_parameters.get('use_preprocessed_data', False): - - # get data from pre-processed file - ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) = unpack_nhd_preprocess_data( - preprocessing_parameters - ) - else: - - # build data objects from scratch - ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) = nhd_network_preprocess( - supernetwork_parameters, - waterbody_parameters, - preprocessing_parameters, - compute_parameters, - data_assimilation_parameters, - ) - - return (connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df - ) - - -def nhd_network_preprocess( - supernetwork_parameters, - waterbody_parameters, - preprocessing_parameters, - compute_parameters, - data_assimilation_parameters, -): - ''' - Creation of routing network data objects. Logical ordering of lower-level - function calls that build individual network data objects. - - Arguments - --------- - supernetwork_parameters (dict): user input data re network extent - waterbody_parameters (dict): user input data re waterbodies - preprocessing_parameters (dict): user input data re preprocessing - compute_parameters (dict): user input data re compute configuration - data_assimilation_parameters (dict): user input data re data assimilation - - Returns - ------- - connections (dict of int: [int]): {segment id: [downsteram adjacent segment ids]} - param_df (DataFrame): Hydraulic geometry and roughness parameters, by segment - wbody_conn (dict of int: int): {segment id: associated lake id} - waterbodies_df (DataFrame): Waterbody (reservoir) parameters - waterbody_types_df (DataFrame): Waterbody type codes (1 - levelpool, 2 - USGS, 3 - USACE, 4 - RFC) - break_network_at_waterbodies (bool): If True, waterbodies occpy reaches of their own - waterbody_type_specified (bool): If True, more than just levelpool waterbodies exist - link_lake_crosswalk (dict of int: int): {lake id: outlet segment id} - independent_networks (dict of int: {int: [int]}): {tailwater id: {segment id: [upstream adjacent segment ids]}} - reaches_bytw (dict of int: [[int]]): {tailwater id: list or reach lists} - rconn (dict of int: [int]): {segment id: [upstream adjacent segment ids]} - pd.DataFrame.from_dict(gages) (DataFrame): Gage ids and corresponding segment ids at which they are located - diffusive_network_data (dict or None): Network data objects for diffusive domain - topobathy_df (DataFrame): Natural cross section data for diffusive domain - - Notes - ----- - - waterbody_type_specified is likely an excessive return and can be removed and inferred from the - contents of waterbody_types_df - - The values of the link_lake_crosswalk dictionary are the downstream-most segments within - the waterbody extent to which waterbody data are written. They are NOT the first segments - downsteram of the waterbody - ''' - - #============================================================================ - # Establish diffusive domain for MC/diffusive hybrid simulations - - hybrid_params = compute_parameters.get("hybrid_parameters", False) - if hybrid_params: - # switch parameters - # if run_hybrid = False, run MC only - # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc - # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric - # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric - run_hybrid = hybrid_params.get('run_hybrid_routing', False) - use_topobathy = hybrid_params.get('use_natl_xsections', False) - run_refactored = hybrid_params.get('run_refactored_network', False) - - # file path parameters of non-refactored hydrofabric defined by RouteLink.nc - domain_file = hybrid_params.get("diffusive_domain", None) - topobathy_file = hybrid_params.get("topobathy_domain", None) - - # file path parameters of refactored hydrofabric for diffusive wave channel routing - refactored_domain_file = hybrid_params.get("refactored_domain", None) - refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) - #------------------------------------------------------------------------- - # for non-refactored hydofabric defined by RouteLink.nc - # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. - if run_hybrid and domain_file: - - LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - diffusive_domain = nhd_io.read_diffusive_domain(domain_file) - - if use_topobathy and topobathy_file: - - LOG.debug('Natural cross section data on original hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - - # TODO: Request GID make comID variable an integer in their product, so - # we do not need to change variable types, here. - topobathy_df.index = topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - diffusive_network_data = {} - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') - unrefactored_topobathy_df = pd.DataFrame() - #------------------------------------------------------------------------- - # for refactored hydofabric - if run_hybrid and run_refactored and refactored_domain_file: - - LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) - - if use_topobathy and refactored_topobathy_file: - - LOG.debug('Natural cross section data of refactored hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) - - # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy - # for crosswalking water elevations between non-refactored and refactored hydrofabrics. - unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - unrefactored_topobathy_df.index = unrefactored_topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - refactored_diffusive_network_data = {} - - else: - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - unrefactored_topobathy_df = pd.DataFrame() - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') - #============================================================================ - # Build network connections graph, assemble parameter dataframe, - # establish segment-waterbody, and segment-gage mappings - LOG.info("creating network connections graph") - start_time = time.time() - - connections, param_df, wbody_conn, gages = nnu.build_connections( - supernetwork_parameters, - ) - - link_gage_df = pd.DataFrame.from_dict(gages) - link_gage_df.index.name = 'link' - break_network_at_waterbodies = waterbody_parameters.get( - "break_network_at_waterbodies", False - ) - - # if streamflow DA, then break network at gages - break_network_at_gages = False - streamflow_da = data_assimilation_parameters.get('streamflow_da', False) - if streamflow_da: - break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - - if not wbody_conn: - # Turn off any further reservoir processing if the network contains no - # waterbodies - break_network_at_waterbodies = False - - # if waterbodies are being simulated, adjust the connections graph so that - # waterbodies are collapsed to single nodes. Also, build a mapping between - # waterbody outlet segments and lake ids - if break_network_at_waterbodies: - connections, link_lake_crosswalk = nhd_network.replace_waterbodies_connections( - connections, wbody_conn - ) - else: - link_lake_crosswalk = None - - LOG.debug("network connections graph created in %s seconds." % (time.time() - start_time)) - - #============================================================================ - # Retrieve and organize waterbody parameters - - waterbody_type_specified = False - if break_network_at_waterbodies: - - # Read waterbody parameters from LAKEPARM file - level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) - waterbodies_df = nhd_io.read_lakeparm( - level_pool_params['level_pool_waterbody_parameter_file_path'], - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - wbody_conn.values() - ) - - # Remove duplicate lake_ids and rows - waterbodies_df = ( - waterbodies_df.reset_index() - .drop_duplicates(subset="lake_id") - .set_index("lake_id") - ) - - # Declare empty dataframe - waterbody_types_df = pd.DataFrame() - - # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True - reservoir_da = data_assimilation_parameters.get( - 'reservoir_da', - {} - ) - - if reservoir_da: - usgs_hybrid = reservoir_da.get( - 'reservoir_persistence_usgs', - False - ) - usace_hybrid = reservoir_da.get( - 'reservoir_persistence_usace', - False - ) - param_file = reservoir_da.get( - 'gage_lakeID_crosswalk_file', - None - ) - else: - param_file = None - usace_hybrid = False - usgs_hybrid = False - - # check if RFC-type reservoirs are set to true - rfc_params = waterbody_parameters.get('rfc') - if rfc_params: - rfc_forecast = rfc_params.get( - 'reservoir_rfc_forecasts', - False - ) - param_file = rfc_params.get('reservoir_parameter_file',None) - else: - rfc_forecast = False - - if (param_file and reservoir_da) or (param_file and rfc_forecast): - waterbody_type_specified = True - ( - waterbody_types_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk - ) = nhd_io.read_reservoir_parameter_file( - param_file, - usgs_hybrid, - usace_hybrid, - rfc_forecast, - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - reservoir_da.get('crosswalk_usgs_gage_field', 'usgs_gage_id'), - reservoir_da.get('crosswalk_usgs_lakeID_field', 'usgs_lake_id'), - reservoir_da.get('crosswalk_usace_gage_field', 'usace_gage_id'), - reservoir_da.get('crosswalk_usace_lakeID_field', 'usace_lake_id'), - wbody_conn.values(), - ) - else: - waterbody_type_specified = True - waterbody_types_df = pd.DataFrame(data = 1, index = waterbodies_df.index, columns = ['reservoir_type']) - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - else: - # Declare empty dataframes - waterbody_types_df = pd.DataFrame() - waterbodies_df = pd.DataFrame() - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - #============================================================================ - # build diffusive domain data and edit MC domain data for hybrid simulation - - # - if diffusive_domain: - rconn_diff0 = nhd_network.reverse_network(connections) - refactored_reaches = {} - - for tw in diffusive_domain: - mainstem_segs = diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - diffusive_network_data[tw] = {} - - # add diffusive domain segments - diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = nnu.organize_independent_networks( - diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - diffusive_network_data[tw]['rconn'] = rconn_diff - diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - diffusive_network_data[tw]['param_df'] = param_df.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - if refactored_diffusive_domain: - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = nnu.build_refac_connections(diffusive_parameters) - - # list of stream segments of a single refactored diffusive domain - refac_tw = refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k]= [refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = nnu.organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = diffusive_network_data[tw]['upstream_boundary_link'] - else: - refactored_reaches={} - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - param_df = param_df.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - connections[us] = [] - - #============================================================================ - # Identify Independent Networks and Reaches by Network - LOG.info("organizing connections into reaches ...") - start_time = time.time() - gage_break_segments = set() - wbody_break_segments = set() - if break_network_at_waterbodies: - wbody_break_segments = wbody_break_segments.union(wbody_conn.values()) - - if break_network_at_gages: - gage_break_segments = gage_break_segments.union(gages['gages'].keys()) - - independent_networks, reaches_bytw, rconn = nnu.organize_independent_networks( - connections, - wbody_break_segments, - gage_break_segments, - ) - - LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) - - if preprocessing_parameters.get('preprocess_only', False): - - LOG.debug("saving preprocessed network data to disk for future use") - # todo: consider a better default than None - destination_folder = preprocessing_parameters.get('preprocess_output_folder', None) - if destination_folder: - - output_filename = preprocessing_parameters.get( - 'preprocess_output_filename', - 'preprocess_output' - ) - - outputs = {} - outputs.update( - {'connections': connections, - 'param_df': param_df, - 'wbody_conn': wbody_conn, - 'waterbodies_df': waterbodies_df, - 'waterbody_types_df': waterbody_types_df, - 'break_network_at_waterbodies': break_network_at_waterbodies, - 'waterbody_type_specified': waterbody_type_specified, - 'link_lake_crosswalk': link_lake_crosswalk, - 'independent_networks': independent_networks, - 'reaches_bytw': reaches_bytw, - 'rconn': rconn, - 'link_gage_df': link_gage_df, - 'usgs_lake_gage_crosswalk': usgs_lake_gage_crosswalk, - 'usace_lake_gage_crosswalk': usace_lake_gage_crosswalk, - 'diffusive_network_data': diffusive_network_data, - 'topobathy_data': topobathy_df, - } - ) - try: - np.save( - pathlib.Path(destination_folder).joinpath(output_filename), - outputs - ) - except: - LOG.critical('Canonot find %s. Aborting preprocessing routine' % pathlib.Path(destination_folder)) - quit() - - LOG.debug( - "writing preprocessed network data to %s"\ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - LOG.critical( - "Preprocessed network data written to %s aborting preprocessing sequence" \ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - quit() - - else: - LOG.critical( - "No destination folder specified for preprocessing. Please specify preprocess_output_folder in configuration file. Aborting preprocessing routine" - ) - quit() - - return ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) - -def unpack_nhd_preprocess_data(preprocessing_parameters): - - preprocess_filepath = preprocessing_parameters.get('preprocess_source_file',None) - if preprocess_filepath: - try: - inputs = np.load(pathlib.Path(preprocess_filepath),allow_pickle='TRUE').item() - except: - LOG.critical('Canonot find %s' % pathlib.Path(preprocess_filepath)) - quit() - - connections = inputs.get('connections',None) - param_df = inputs.get('param_df',None) - wbody_conn = inputs.get('wbody_conn',None) - waterbodies_df = inputs.get('waterbodies_df',None) - waterbody_types_df = inputs.get('waterbody_types_df',None) - break_network_at_waterbodies = inputs.get('break_network_at_waterbodies',None) - waterbody_type_specified = inputs.get('waterbody_type_specified',None) - link_lake_crosswalk = inputs.get('link_lake_crosswalk', None) - independent_networks = inputs.get('independent_networks',None) - reaches_bytw = inputs.get('reaches_bytw',None) - rconn = inputs.get('rconn',None) - gages = inputs.get('link_gage_df',None) - usgs_lake_gage_crosswalk = inputs.get('usgs_lake_gage_crosswalk',None) - usace_lake_gage_crosswalk = inputs.get('usace_lake_gage_crosswalk',None) - diffusive_network_data = inputs.get('diffusive_network_data',None) - topobathy_df = inputs.get('topobathy_data',None) - refactored_diffusive_domain = inputs.get('refactored_diffusive_domain',None) - refactored_reaches = inputs.get('refactored_reaches',None) - unrefactored_topobathy_df = inputs.get('unrefactored_topobathy',None) - - else: - LOG.critical("use_preprocessed_data = True, but no preprocess_source_file is specified. Aborting the simulation.") - quit() - - return ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - gages, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) - - -def nhd_initial_warmstate_preprocess( - break_network_at_waterbodies, - restart_parameters, - data_assimilation_parameters, - segment_index, - waterbodies_df, - link_lake_crosswalk, -): - - ''' - Assemble model initial condition data: - - waterbody inital states (outflow and pool elevation) - - channel initial states (flow and depth) - - initial time - - Arguments - --------- - - break_network_at_waterbodies (bool): If True, waterbody initial states will - be appended to the waterbody parameter - dataframe. If False, waterbodies will - not be simulated and the waterbody - parameter datataframe wil not be changed - - restart_parameters (dict): User-input simulation restart - parameters - - data_assimilation_parameters (dict): User-input data assimilation - parameters - - segment_index (Pandas Index): All segment IDs in the simulation - doamin - - waterbodies_df (Pandas DataFrame): Waterbody parameters - - link_lake_crosswalk (dict): Crosswalking between lake ids and the link - id of the lake outlet segment - - Returns - ------- - - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial - states (outflow and pool elevation) - - q0 (Pandas DataFrame): Initial flow and depth states for each - segment in the model domain - - t0 (datetime): Datetime of the model initialization - - Notes - ----- - ''' - - #---------------------------------------------------------------------------- - # Assemble waterbody initial states (outflow and pool elevation - #---------------------------------------------------------------------------- - - if break_network_at_waterbodies: - - start_time = time.time() - LOG.info("setting waterbody initial states ...") - - # if a lite restart file is provided, read initial states from it. - if restart_parameters.get("lite_waterbody_restart_file", None): - - waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( - restart_parameters['lite_waterbody_restart_file'] - ) - - # read waterbody initial states from WRF-Hydro type restart file - elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): - waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( - restart_parameters["wrf_hydro_waterbody_restart_file"], - restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], - restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", 'lake_id'), - restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], - restart_parameters.get( - "wrf_hydro_waterbody_crosswalk_filter_file_field_name", - 'NHDWaterbodyComID' - ), - ) - - # if no restart file is provided, default initial states - else: - # TODO: Consider adding option to read cold state from route-link file - waterbodies_initial_ds_flow_const = 0.0 - waterbodies_initial_depth_const = -1e9 - # Set initial states from cold-state - waterbodies_initial_states_df = pd.DataFrame( - 0, - index=waterbodies_df.index, - columns=[ - "qd0", - "h0", - ], - dtype="float32", - ) - # TODO: This assignment could probably by done in the above call - waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const - waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const - waterbodies_initial_states_df["index"] = range( - len(waterbodies_initial_states_df) - ) - - waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on="lake_id" - ) - - LOG.debug( - "waterbody initial states complete in %s seconds."\ - % (time.time() - start_time)) - start_time = time.time() - - #---------------------------------------------------------------------------- - # Assemble channel initial states (flow and depth) - # also establish simulation initialization timestamp - #---------------------------------------------------------------------------- - start_time = time.time() - LOG.info("setting channel initial states ...") - - # if lite restart file is provided, the read channel initial states from it - if restart_parameters.get("lite_channel_restart_file", None): - - q0, t0 = nhd_io.read_lite_restart( - restart_parameters['lite_channel_restart_file'] - ) - t0_str = None - - # build initial states from user-provided restart parameters - else: - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - - # get initialization time from restart file - if restart_parameters.get("wrf_hydro_channel_restart_file", None): - channel_initial_states_file = restart_parameters[ - "wrf_hydro_channel_restart_file" - ] - t0_str = nhd_io.get_param_str( - channel_initial_states_file, - "Restart_Time" - ) - else: - t0_str = "2015-08-16_00:00:00" - - # convert timestamp from string to datetime - t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") - - # get initial time from user inputs - if restart_parameters.get("start_datetime", None): - t0_str = restart_parameters.get("start_datetime") - - def _try_parsing_date(text): - for fmt in ( - "%Y-%m-%d_%H:%M", - "%Y-%m-%d_%H:%M:%S", - "%Y-%m-%d %H:%M", - "%Y-%m-%d %H:%M:%S", - "%Y/%m/%d %H:%M", - "%Y/%m/%d %H:%M:%S" - ): - try: - return datetime.strptime(text, fmt) - except ValueError: - pass - LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') - quit() - - t0 = _try_parsing_date(t0_str) - else: - if t0_str == "2015-08-16_00:00:00": - LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') - else: - LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) - - LOG.debug( - "channel initial states complete in %s seconds."\ - % (time.time() - start_time) - ) - start_time = time.time() - - return waterbodies_df, q0, t0 - # TODO: This returns a full dataframe (waterbodies_df) with the - # merged initial states for waterbodies, but only the - # initial state values (q0; not merged with the channel properties) - # for the channels -- - # That is because that is how they are used downstream. Need to - # trace that back and decide if there is one of those two ways - # that is optimal and make both returns that way. - - -def nhd_forcing( - run, - forcing_parameters, - hybrid_parameters, - segment_index, - cpu_pool, - t0, - coastal_boundary_depth_df, -): - """ - Assemble model forcings. Forcings include hydrological lateral inflows (qlats) - and coastal boundary depths for hybrid runs - - Aguments - -------- - - run (dict): List of forcing files pertaining to a - single run-set - - forcing_parameters (dict): User-input simulation forcing parameters - - hybrid_parameters (dict): User-input simulation hybrid parameters - - segment_index (Int64): Reach segment ids - - cpu_pool (int): Number of CPUs in the process-parallel pool - - Returns - ------- - - qlats_df (Pandas DataFrame): Lateral inflow data, indexed by - segment ID - - coastal_bounary_depth_df (Pandas DataFrame): Coastal boundary water depths, - indexed by segment ID - - Notes - ----- - - """ - - # Unpack user-specified forcing parameters - dt = forcing_parameters.get("dt", None) - qts_subdivisions = forcing_parameters.get("qts_subdivisions", None) - qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) - qlat_file_index_col = forcing_parameters.get("qlat_file_index_col", "feature_id") - qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") - qlat_file_gw_bucket_flux_col = forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") - qlat_file_terrain_runoff_col = forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") - - - # TODO: find a better way to deal with these defaults and overrides. - run["t0"] = run.get("t0", t0) - run["nts"] = run.get("nts") - run["dt"] = run.get("dt", dt) - run["qts_subdivisions"] = run.get("qts_subdivisions", qts_subdivisions) - run["qlat_input_folder"] = run.get("qlat_input_folder", qlat_input_folder) - run["qlat_file_index_col"] = run.get("qlat_file_index_col", qlat_file_index_col) - run["qlat_file_value_col"] = run.get("qlat_file_value_col", qlat_file_value_col) - run["qlat_file_gw_bucket_flux_col"] = run.get("qlat_file_gw_bucket_flux_col", qlat_file_gw_bucket_flux_col) - run["qlat_file_terrain_runoff_col"] = run.get("qlat_file_terrain_runoff_col", qlat_file_terrain_runoff_col) - - #--------------------------------------------------------------------------- - # Assemble lateral inflow data - #--------------------------------------------------------------------------- - - start_time = time.time() - LOG.info("Creating a DataFrame of lateral inflow forcings ...") - - # Place holder, if reading qlats from a file use this. - # TODO: add an option for reading qlat data from BMI/model engine - from_file = True - if from_file: - qlats_df = nnu.build_qlateral_array( - run, - cpu_pool, - segment_index, - ) - - LOG.debug( - "lateral inflow DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - #--------------------------------------------------------------------- - # Assemble coastal coupling data [WIP] - #--------------------------------------------------------------------- - # Run if coastal_boundary_depth_df has not already been created: - if coastal_boundary_depth_df.empty: - coastal_boundary_elev_files = forcing_parameters.get('coastal_boundary_input_file', None) - coastal_boundary_domain_files = hybrid_parameters.get('coastal_boundary_domain', None) - - if coastal_boundary_elev_files: - start_time = time.time() - LOG.info("creating coastal dataframe ...") - - coastal_boundary_domain = nhd_io.read_coastal_boundary_domain(coastal_boundary_domain_files) - coastal_boundary_depth_df = nhd_io.build_coastal_ncdf_dataframe( - coastal_boundary_elev_files, - coastal_boundary_domain, - ) - - LOG.debug( - "coastal boundary elevation observation DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - return qlats_df, coastal_boundary_depth_df From 4f13af107a81f6bf3378401530a427559a53a714 Mon Sep 17 00:00:00 2001 From: Nels Date: Thu, 2 Feb 2023 14:19:42 -0700 Subject: [PATCH 24/54] Fix troute-nwm package setup.cfg (#603) Make sure the setup.cfg knows how to properly find the actual package under the `src` dir. Otherwise, a standard install (`pip install troute-nwm/`) won't actually install the module source required (`-e` works/ed cause it just created a link to the src dir, but for actual packaging, it doesn't bundle the package code without this in the config. For reference, check out [this useful reference on src/ layouts](https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#using-a-src-layout) --- src/troute-nwm/setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/troute-nwm/setup.cfg b/src/troute-nwm/setup.cfg index 8b01a0134..f547f46e8 100644 --- a/src/troute-nwm/setup.cfg +++ b/src/troute-nwm/setup.cfg @@ -22,3 +22,6 @@ install_requires= pyyaml toolz joblib + +[options.packages.find] + where=src From 89eea672d35f95ec31d2bba8596d8574629e5a5e Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 10 Feb 2023 15:26:05 +0000 Subject: [PATCH 25/54] removed loading nhd_preprocess.py as this file was removed --- src/troute-network/troute/NHDNetwork.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index ceb1ec116..71864b1d0 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -1,6 +1,5 @@ from .AbstractNetwork import AbstractNetwork import troute.nhd_io as nhd_io -import troute.nhd_preprocess as nhd_prep import pandas as pd import numpy as np import time From 651745f9a2dbefa9485a4495148f13c01635dc74 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 10 Feb 2023 16:32:48 +0000 Subject: [PATCH 26/54] reverse changes to compute_nhd_routing_v02 input arguments --- src/troute-nwm/src/nwm_routing/__main__.py | 95 ++++++++++++++++++-- src/troute-routing/troute/routing/compute.py | 72 +++++++-------- 2 files changed, 119 insertions(+), 48 deletions(-) diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index 15a357c98..b4f0d906b 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -170,18 +170,42 @@ def main_v04(argv): route_start_time = time.time() run_results = nwm_route( - network, - data_assimilation, + network.connections, + network.reverse_network, + network.waterbody_connections, + network.reaches_by_tailwater, parallel_compute_method, compute_kernel, subnetwork_target_size, cpu_pool, + network.t0, dt, nts, qts_subdivisions, + network.independent_networks, + network.dataframe, + network.q0, + network._qlateral, + data_assimilation.usgs_df, + data_assimilation.lastobs_df, + data_assimilation.reservoir_usgs_df, + data_assimilation.reservoir_usgs_param_df, + data_assimilation.reservoir_usace_df, + data_assimilation.reservoir_usace_param_df, + data_assimilation.assimilation_parameters, assume_short_ts, return_courant, + network.waterbody_dataframe, + waterbody_parameters, + network.waterbody_types_dataframe, + network.waterbody_type_specified, + network.diffusive_network_data, + network.topobathy_df, + network.refactored_diffusive_domain, + network.refactored_reaches, subnetwork_list, + network.coastal_boundary_depth_df, + network.unrefactored_topobathy_df, ) # returns list, first item is run result, second item is subnetwork items @@ -1007,18 +1031,42 @@ def _handle_args_v03(argv): return parser.parse_args(argv) def nwm_route( - network, - data_assimilation, + downstream_connections, + upstream_connections, + waterbodies_in_connections, + reaches_bytw, parallel_compute_method, compute_kernel, subnetwork_target_size, cpu_pool, + t0, dt, nts, qts_subdivisions, + independent_networks, + param_df, + q0, + qlats, + usgs_df, + lastobs_df, + reservoir_usgs_df, + reservoir_usgs_param_df, + reservoir_usace_df, + reservoir_usace_param_df, + da_parameter_dict, assume_short_ts, return_courant, + waterbodies_df, + waterbody_parameters, + waterbody_types_df, + waterbody_type_specified, + diffusive_network_data, + topobathy_df, + refactored_diffusive_domain, + refactored_reaches, subnetwork_list, + coastal_boundary_depth_df, + unrefactored_topobathy_df, ): ################### Main Execution Loop across ordered networks @@ -1035,17 +1083,35 @@ def nwm_route( start_time_mc = time.time() results = compute_nhd_routing_v02( - network, - data_assimilation, + downstream_connections, + upstream_connections, + waterbodies_in_connections, + reaches_bytw, compute_kernel, parallel_compute_method, subnetwork_target_size, # The default here might be the whole network or some percentage... cpu_pool, + t0, dt, nts, qts_subdivisions, + independent_networks, + param_df, + q0, + qlats, + usgs_df, + lastobs_df, + reservoir_usgs_df, + reservoir_usgs_param_df, + reservoir_usace_df, + reservoir_usace_param_df, + da_parameter_dict, assume_short_ts, return_courant, + waterbodies_df, + waterbody_parameters, + waterbody_types_df, + waterbody_type_specified, subnetwork_list, ) LOG.debug("MC computation complete in %s seconds." % (time.time() - start_time_mc)) @@ -1054,7 +1120,7 @@ def nwm_route( results = results[0] # run diffusive side of a hybrid simulation - if network.diffusive_network_data: + if diffusive_network_data: start_time_diff = time.time() ''' # retrieve MC-computed streamflow value at upstream boundary of diffusive mainstem @@ -1078,12 +1144,23 @@ def nwm_route( results.extend( compute_diffusive_routing( results, - network, - data_assimilation, + diffusive_network_data, cpu_pool, + t0, dt, nts, + q0, + qlats, qts_subdivisions, + usgs_df, + lastobs_df, + da_parameter_dict, + waterbodies_df, + topobathy_df, + refactored_diffusive_domain, + refactored_reaches, + coastal_boundary_depth_df, + unrefactored_topobathy_df, ) ) LOG.debug("Diffusive computation complete in %s seconds." % (time.time() - start_time_diff)) diff --git a/src/troute-routing/troute/routing/compute.py b/src/troute-routing/troute/routing/compute.py index f1130362f..a5673817c 100644 --- a/src/troute-routing/troute/routing/compute.py +++ b/src/troute-routing/troute/routing/compute.py @@ -218,41 +218,38 @@ def _prep_reservoir_da_dataframes(reservoir_usgs_df, reservoir_usgs_param_df, re return reservoir_usgs_df_sub, reservoir_usgs_df_time, reservoir_usgs_update_time, reservoir_usgs_prev_persisted_flow, reservoir_usgs_persistence_update_time, reservoir_usgs_persistence_index, reservoir_usace_df_sub, reservoir_usace_df_time, reservoir_usace_update_time, reservoir_usace_prev_persisted_flow, reservoir_usace_persistence_update_time, reservoir_usace_persistence_index, waterbody_types_df_sub def compute_nhd_routing_v02( - network, - data_assimilation, + connections, + rconn, + wbody_conn, + reaches_bytw, compute_func_name, parallel_compute_method, subnetwork_target_size, cpu_pool, + t0, dt, nts, qts_subdivisions, + independent_networks, + param_df, + q0, + qlats, + usgs_df, + lastobs_df, + reservoir_usgs_df, + reservoir_usgs_param_df, + reservoir_usace_df, + reservoir_usace_param_df, + da_parameter_dict, assume_short_ts, return_courant, + waterbodies_df, + waterbody_parameters, + waterbody_types_df, + waterbody_type_specified, subnetwork_list, ): - connections = network.connections - rconn = network.reverse_network - wbody_conn = network.waterbody_connections - reaches_bytw = network.reaches_by_tailwater - t0 = network.t0 - independent_networks = network.independent_networks - param_df = network.dataframe - q0 = network.q0 - qlats = network.qlateral - usgs_df = data_assimilation.usgs_df - lastobs_df = data_assimilation.lastobs_df - reservoir_usgs_df = data_assimilation.reservoir_usgs_df - reservoir_usgs_param_df = data_assimilation.reservoir_usgs_param_df - reservoir_usace_df = data_assimilation.reservoir_usace_df - reservoir_usace_param_df = data_assimilation.reservoir_usace_param_df - da_parameter_dict = data_assimilation.assimilation_parameters - waterbodies_df = network.waterbody_dataframe - waterbody_parameters = network.waterbody_parameters - waterbody_types_df = network.waterbody_types_dataframe - waterbody_type_specified = network.waterbody_type_specified - da_decay_coefficient = da_parameter_dict.get("da_decay_coefficient", 0) param_df["dt"] = dt param_df = param_df.astype("float32") @@ -1127,28 +1124,25 @@ def compute_nhd_routing_v02( def compute_diffusive_routing( results, - network, - data_assimilation, + diffusive_network_data, cpu_pool, + t0, dt, nts, + q0, + qlats, qts_subdivisions, + usgs_df, + lastobs_df, + da_parameter_dict, + waterbodies_df, + topobathy, + refactored_diffusive_domain, + refactored_reaches, + coastal_boundary_depth_df, + unrefactored_topobathy, ): - diffusive_network_data = network.diffusive_network_data - t0 = network.t0 - q0 = network.q0 - qlats = network.qlateral - usgs_df = data_assimilation.usgs_df - lastobs_df = data_assimilation.lastobs_df - da_parameter_dict = data_assimilation.assimilation_parameters - waterbodies_df = network.waterbody_dataframe - topobathy = network.topobathy_df - refactored_diffusive_domain = network.refactored_diffusive_domain - refactored_reaches = network.refactored_reaches - coastal_boundary_depth_df = network.coastal_boundary_depth_df - unrefactored_topobathy = network.unrefactored_topobathy_df - results_diffusive = [] for tw in diffusive_network_data: # <------- TODO - by-network parallel loop, here. trib_segs = None From 68fe63d866f79eaca876dbcf523bfaeb5222864e Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 10 Feb 2023 16:33:21 +0000 Subject: [PATCH 27/54] edit to work with redesigned initial_warmstate_preprocess function --- test/unit_test_hyfeature/unittest_hyfeature.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit_test_hyfeature/unittest_hyfeature.yaml b/test/unit_test_hyfeature/unittest_hyfeature.yaml index 5ecaeb3ca..1adcb0de8 100644 --- a/test/unit_test_hyfeature/unittest_hyfeature.yaml +++ b/test/unit_test_hyfeature/unittest_hyfeature.yaml @@ -45,7 +45,7 @@ compute_parameters: #wrf_hydro_waterbody_restart_file: restart/HYDRO_RST.2020-08-26_00:00_DOMAIN1 #wrf_hydro_waterbody_ID_crosswalk_file : domain/LAKEPARM_NWMv2.1.nc #wrf_hydro_waterbody_crosswalk_filter_file: domain/LAKEPARM_NWMv2.1.nc - hyfeature_channel_restart_file: restart/201512010000NEXOUT.csv + start_datetime: "2015-12-01_00:00:00" hybrid_parameters: run_hybrid_routing: True diffusive_domain : domain/coastal_domain.yaml From c2006d1ba8ac12377c0843f7762d98aa5af48015 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 10 Feb 2023 18:37:26 +0000 Subject: [PATCH 28/54] move build_forcing_sets() to AbstractNetwork. abstractnetwork_preprocess.py is no longer needed --- src/troute-network/troute/AbstractNetwork.py | 206 ++++++++++++++++++- src/troute-nwm/src/nwm_routing/__main__.py | 18 +- 2 files changed, 206 insertions(+), 18 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 9941f39ef..3c9bcc7fd 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -1,14 +1,18 @@ from abc import ABC, abstractmethod from functools import partial import pandas as pd +import numpy as np from datetime import datetime, timedelta -from collections import defaultdict +import os +import pathlib import time import logging +import pyarrow as pa +import pyarrow.parquet as pq from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition -from troute.nhd_network_utilities_v02 import organize_independent_networks, build_channel_initial_state, build_refac_connections +from troute.nhd_network_utilities_v02 import organize_independent_networks import troute.nhd_io as nhd_io from .AbstractRouting import MCOnly, MCwithDiffusive, MCwithDiffusiveNatlXSectionNonRefactored, MCwithDiffusiveNatlXSectionRefactored @@ -654,3 +658,201 @@ def _try_parsing_date(text): "channel initial states complete in %s seconds."\ % (time.time() - start_time) ) + + def build_forcing_sets(self,): + + forcing_parameters = self.forcing_parameters + supernetwork_parameters = self.supernetwork_parameters + + run_sets = forcing_parameters.get("qlat_forcing_sets", None) + qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) + nts = forcing_parameters.get("nts", None) + max_loop_size = forcing_parameters.get("max_loop_size", 12) + dt = forcing_parameters.get("dt", None) + + geo_file_type = supernetwork_parameters.get('geo_file_type') + + try: + qlat_input_folder = pathlib.Path(qlat_input_folder) + assert qlat_input_folder.is_dir() == True + except TypeError: + raise TypeError("Aborting simulation because no qlat_input_folder is specified in the forcing_parameters section of the .yaml control file.") from None + except AssertionError: + raise AssertionError("Aborting simulation because the qlat_input_folder:", qlat_input_folder,"does not exist. Please check the the nexus_input_folder variable is correctly entered in the .yaml control file") from None + + forcing_glob_filter = forcing_parameters.get("qlat_file_pattern_filter", "*.NEXOUT") + + if forcing_glob_filter=="nex-*": + print("Reformating qlat nexus files as hourly binary files...") + binary_folder = forcing_parameters.get('binary_nexus_file_folder', None) + qlat_files = qlat_input_folder.glob(forcing_glob_filter) + + #Check that directory/files specified will work + if not binary_folder: + raise(RuntimeError("No output binary qlat folder supplied in config")) + elif not os.path.exists(binary_folder): + raise(RuntimeError("Output binary qlat folder supplied in config does not exist")) + elif len(list(pathlib.Path(binary_folder).glob('*.parquet'))) != 0: + raise(RuntimeError("Output binary qlat folder supplied in config is not empty (already contains '.parquet' files)")) + + #Add tnx for backwards compatability + qlat_files_list = list(qlat_files) + list(qlat_input_folder.glob('tnx*.csv')) + #Convert files to binary hourly files, reset nexus input information + qlat_input_folder, forcing_glob_filter = nex_files_to_binary(qlat_files_list, binary_folder) + forcing_parameters["qlat_input_folder"] = qlat_input_folder + forcing_parameters["qlat_file_pattern_filter"] = forcing_glob_filter + + # TODO: Throw errors if insufficient input data are available + if run_sets: + #FIXME: Change it for hyfeature + ''' + # append final_timestamp variable to each set_list + qlat_input_folder = pathlib.Path(qlat_input_folder) + for (s, _) in enumerate(run_sets): + final_chrtout = qlat_input_folder.joinpath(run_sets[s]['qlat_files' + ][-1]) + final_timestamp_str = nhd_io.get_param_str(final_chrtout, + 'model_output_valid_time') + run_sets[s]['final_timestamp'] = \ + datetime.strptime(final_timestamp_str, '%Y-%m-%d_%H:%M:%S') + ''' + elif qlat_input_folder: + # Construct run_set dictionary from user-specified parameters + + # get the first and seconded files from an ordered list of all forcing files + qlat_input_folder = pathlib.Path(qlat_input_folder) + all_files = sorted(qlat_input_folder.glob(forcing_glob_filter)) + first_file = all_files[0] + second_file = all_files[1] + + # Deduce the timeinterval of the forcing data from the output timestamps of the first + # two ordered CHRTOUT files + if forcing_glob_filter=="*.CHRTOUT_DOMAIN1": + t1 = nhd_io.get_param_str(first_file, "model_output_valid_time") + t1 = datetime.strptime(t1, "%Y-%m-%d_%H:%M:%S") + t2 = nhd_io.get_param_str(second_file, "model_output_valid_time") + t2 = datetime.strptime(t2, "%Y-%m-%d_%H:%M:%S") + else: + df = read_file(first_file) + t1_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + t1 = datetime.strptime(t1_str,"%Y-%m-%d_%H:%M:%S") + df = read_file(second_file) + t2_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + t2 = datetime.strptime(t2_str,"%Y-%m-%d_%H:%M:%S") + + + dt_qlat_timedelta = t2 - t1 + dt_qlat = dt_qlat_timedelta.seconds + + # determine qts_subdivisions + qts_subdivisions = dt_qlat / dt + if dt_qlat % dt == 0: + qts_subdivisions = int(dt_qlat / dt) + # make sure that qts_subdivisions = dt_qlat / dt + forcing_parameters['qts_subdivisions']= qts_subdivisions + + # the number of files required for the simulation + nfiles = int(np.ceil(nts / qts_subdivisions)) + + # list of forcing file datetimes + #datetime_list = [t0 + dt_qlat_timedelta * (n + 1) for n in + # range(nfiles)] + # ** Correction ** Because qlat file at time t is constantly applied throughout [t, t+1], + # ** n + 1 should be replaced by n + datetime_list = [self.t0 + dt_qlat_timedelta * (n) for n in + range(nfiles)] + datetime_list_str = [datetime.strftime(d, '%Y%m%d%H%M') for d in + datetime_list] + + # list of forcing files + forcing_filename_list = [d_str + forcing_glob_filter[1:] for d_str in + datetime_list_str] + + # check that all forcing files exist + for f in forcing_filename_list: + try: + J = pathlib.Path(qlat_input_folder.joinpath(f)) + assert J.is_file() == True + except AssertionError: + raise AssertionError("Aborting simulation because forcing file", J, "cannot be not found.") from None + + # build run sets list + run_sets = [] + k = 0 + j = 0 + nts_accum = 0 + nts_last = 0 + while k < len(forcing_filename_list): + run_sets.append({}) + + if k + max_loop_size < len(forcing_filename_list): + run_sets[j]['qlat_files'] = forcing_filename_list[k:k + + max_loop_size] + else: + run_sets[j]['qlat_files'] = forcing_filename_list[k:] + + nts_accum += len(run_sets[j]['qlat_files']) * qts_subdivisions + if nts_accum <= nts: + run_sets[j]['nts'] = int(len(run_sets[j]['qlat_files']) + * qts_subdivisions) + else: + run_sets[j]['nts'] = int(nts - nts_last) + + final_qlat = qlat_input_folder.joinpath(run_sets[j]['qlat_files'][-1]) + if forcing_glob_filter=="*.CHRTOUT_DOMAIN1": + final_timestamp_str = nhd_io.get_param_str(final_qlat,'model_output_valid_time') + else: + df = read_file(final_qlat) + final_timestamp_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + + run_sets[j]['final_timestamp'] = \ + datetime.strptime(final_timestamp_str, '%Y-%m-%d_%H:%M:%S') + + nts_last = nts_accum + k += max_loop_size + j += 1 + + return run_sets + +def nex_files_to_binary(nexus_files, binary_folder): + for f in nexus_files: + # read the csv file + df = pd.read_csv(f, usecols=[1,2], names=['Datetime','qlat']) + + # convert and reformat datetime column + df['Datetime']= pd.to_datetime(df['Datetime']).dt.strftime("%Y%m%d%H%M") + + # reformat the dataframe + df['feature_id'] = get_id_from_filename(f) + df = df.pivot(index="feature_id", columns="Datetime", values="qlat") + df.columns.name = None + + for col in df.columns: + table_new = pa.Table.from_pandas(df.loc[:, [col]]) + + if not os.path.exists(f'{binary_folder}/{col}NEXOUT.parquet'): + pq.write_table(table_new, f'{binary_folder}/{col}NEXOUT.parquet') + + else: + table_old = pq.read_table(f'{binary_folder}/{col}NEXOUT.parquet') + table = pa.concat_tables([table_old,table_new]) + pq.write_table(table, f'{binary_folder}/{col}NEXOUT.parquet') + + nexus_input_folder = binary_folder + forcing_glob_filter = '*NEXOUT.parquet' + + return nexus_input_folder, forcing_glob_filter + +def get_id_from_filename(file_name): + id = os.path.splitext(file_name)[0].split('-')[1].split('_')[0] + return int(id) + +def read_file(file_name): + extension = file_name.suffix + if extension=='.csv': + df = pd.read_csv(file_name) + elif extension=='.parquet': + df = pq.read_table(file_name).to_pandas().reset_index() + df.index.name = None + + return df \ No newline at end of file diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index b4f0d906b..6d646864d 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -4,7 +4,6 @@ import asyncio import logging from datetime import datetime, timedelta -from collections import defaultdict from pathlib import Path import concurrent.futures @@ -25,12 +24,9 @@ from .output import nwm_output_generator from .log_level_set import log_level_set from troute.routing.compute import compute_nhd_routing_v02, compute_diffusive_routing -import troute.nhd_network as nhd_network + import troute.nhd_io as nhd_io import troute.nhd_network_utilities_v02 as nnu -import troute.routing.diffusive_utils as diff_utils -import troute.hyfeature_network_utilities as hnu -import troute.abstractnetwork_preprocess as abs_prep LOG = logging.getLogger('') @@ -106,17 +102,7 @@ def main_v04(argv): task_times['network_creation_time'] = network_end_time - network_start_time # Create run_sets: sets of forcing files for each loop - run_sets = abs_prep.build_forcing_sets( - supernetwork_parameters, - forcing_parameters, - network.t0 - ) - ''' - if supernetwork_parameters["geo_file_type"] == 'NHDNetwork': - run_sets = nnu.build_forcing_sets(forcing_parameters, network.t0) - elif supernetwork_parameters["geo_file_type"] == 'HYFeaturesNetwork': - run_sets = hnu.build_forcing_sets(forcing_parameters, network.t0) - ''' + run_sets = network.build_forcing_sets() # Create da_sets: sets of TimeSlice files for each loop if "data_assimilation_parameters" in compute_parameters: From f1be8cd1c9d4730afa02cf0d93aa881f691a1e30 Mon Sep 17 00:00:00 2001 From: shorvath-noaa <103054653+shorvath-noaa@users.noreply.github.com> Date: Fri, 10 Feb 2023 11:38:26 -0700 Subject: [PATCH 29/54] Delete abstractnetwork_preprocess.py --- .../troute/abstractnetwork_preprocess.py | 780 ------------------ 1 file changed, 780 deletions(-) delete mode 100644 src/troute-network/troute/abstractnetwork_preprocess.py diff --git a/src/troute-network/troute/abstractnetwork_preprocess.py b/src/troute-network/troute/abstractnetwork_preprocess.py deleted file mode 100644 index 25d76ffde..000000000 --- a/src/troute-network/troute/abstractnetwork_preprocess.py +++ /dev/null @@ -1,780 +0,0 @@ -import json -import pathlib -from functools import partial, reduce -from itertools import chain -from datetime import datetime, timedelta -from collections import defaultdict, deque -import logging -import os -import time - -import pandas as pd -import numpy as np -import netCDF4 -from joblib import delayed, Parallel -import pyarrow as pa -import pyarrow.parquet as pq - -import troute.nhd_io as nhd_io -import troute.nhd_network as nhd_network -import troute.nhd_network_utilities_v02 as nnu - - -LOG = logging.getLogger('') - -def build_diffusive_domain( - compute_parameters, - param_df, - connections, -): - - hybrid_params = compute_parameters.get("hybrid_parameters", False) - if hybrid_params: - # switch parameters - # if run_hybrid = False, run MC only - # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc - # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric - # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric - run_hybrid = hybrid_params.get('run_hybrid_routing', False) - use_topobathy = hybrid_params.get('use_natl_xsections', False) - run_refactored = hybrid_params.get('run_refactored_network', False) - - # file path parameters of non-refactored hydrofabric defined by RouteLink.nc - domain_file = hybrid_params.get("diffusive_domain", None) - topobathy_file = hybrid_params.get("topobathy_domain", None) - - # file path parameters of refactored hydrofabric for diffusive wave channel routing - refactored_domain_file = hybrid_params.get("refactored_domain", None) - refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) - #------------------------------------------------------------------------- - # for non-refactored hydofabric defined by RouteLink.nc - # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. - if run_hybrid and domain_file: - - LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - diffusive_domain = nhd_io.read_diffusive_domain(domain_file) - - if use_topobathy and topobathy_file: - - LOG.debug('Natural cross section data on original hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - - # TODO: Request GID make comID variable an integer in their product, so - # we do not need to change variable types, here. - topobathy_df.index = topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - diffusive_network_data = {} - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') - unrefactored_topobathy_df = pd.DataFrame() - #------------------------------------------------------------------------- - # for refactored hydofabric - if run_hybrid and run_refactored and refactored_domain_file: - - LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) - - if use_topobathy and refactored_topobathy_file: - - LOG.debug('Natural cross section data of refactored hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) - - # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy - # for crosswalking water elevations between non-refactored and refactored hydrofabrics. - unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - unrefactored_topobathy_df.index = unrefactored_topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - refactored_diffusive_network_data = {} - - else: - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - unrefactored_topobathy_df = pd.DataFrame() - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') - - #============================================================================ - # build diffusive domain data and edit MC domain data for hybrid simulation - - # - if diffusive_domain: - rconn_diff0 = nhd_network.reverse_network(connections) - refactored_reaches = {} - - for tw in diffusive_domain: - mainstem_segs = diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - diffusive_network_data[tw] = {} - - # add diffusive domain segments - diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = nnu.organize_independent_networks( - diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - diffusive_network_data[tw]['rconn'] = rconn_diff - diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - diffusive_network_data[tw]['param_df'] = param_df.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - if refactored_diffusive_domain: - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = nnu.build_refac_connections(diffusive_parameters) - - # list of stream segments of a single refactored diffusive domain - refac_tw = refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k]= [refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = nnu.organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = diffusive_network_data[tw]['upstream_boundary_link'] - else: - refactored_reaches={} - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - param_df = param_df.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - connections[us] = [] - - return ( - param_df, - connections, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df - ) - -def create_independent_networks( - waterbody_parameters, - connections, - wbody_conn, - gages = pd.DataFrame() #FIXME update default value when we update 'break_network_at_gages', - ): - - LOG.info("organizing connections into reaches ...") - start_time = time.time() - gage_break_segments = set() - wbody_break_segments = set() - - break_network_at_waterbodies = waterbody_parameters.get( - "break_network_at_waterbodies", False - ) - - # if streamflow DA, then break network at gages - #TODO update to work with HYFeatures, need to determine how we'll do DA... - break_network_at_gages = False - - if break_network_at_waterbodies: - wbody_break_segments = wbody_break_segments.union(wbody_conn.values()) - - if break_network_at_gages: - gage_break_segments = gage_break_segments.union(gages['gages'].keys()) - - independent_networks, reaches_bytw, rconn = nnu.organize_independent_networks( - connections, - wbody_break_segments, - gage_break_segments, - ) - - LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) - - return independent_networks, reaches_bytw, rconn - -def initial_warmstate_preprocess( - break_network_at_waterbodies, - restart_parameters, - segment_index, - waterbodies_df, - ): - - ''' - Assemble model initial condition data: - - waterbody inital states (outflow and pool elevation) - - channel initial states (flow and depth) - - initial time - - Arguments - --------- - - break_network_at_waterbodies (bool): If True, waterbody initial states will - be appended to the waterbody parameter - dataframe. If False, waterbodies will - not be simulated and the waterbody - parameter datataframe wil not be changed - - restart_parameters (dict): User-input simulation restart - parameters - - segment_index (Pandas Index): All segment IDs in the simulation - doamin - - waterbodies_df (Pandas DataFrame): Waterbody parameters - - Returns - ------- - - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial - states (outflow and pool elevation) - - q0 (Pandas DataFrame): Initial flow and depth states for each - segment in the model domain - - t0 (datetime): Datetime of the model initialization - - Notes - ----- - ''' - - # generalize waterbody ID's to be used with any network - index_id = waterbodies_df.index.names[0] - - #---------------------------------------------------------------------------- - # Assemble waterbody initial states (outflow and pool elevation - #---------------------------------------------------------------------------- - if break_network_at_waterbodies: - - start_time = time.time() - LOG.info("setting waterbody initial states ...") - - # if a lite restart file is provided, read initial states from it. - if restart_parameters.get("lite_waterbody_restart_file", None): - - waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( - restart_parameters['lite_waterbody_restart_file'] - ) - - # read waterbody initial states from WRF-Hydro type restart file - elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): - waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( - restart_parameters["wrf_hydro_waterbody_restart_file"], - restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], - restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", index_id), - restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], - restart_parameters.get( - "wrf_hydro_waterbody_crosswalk_filter_file_field_name", - 'NHDWaterbodyComID' - ), - ) - - # if no restart file is provided, default initial states - else: - # TODO: Consider adding option to read cold state from route-link file - waterbodies_initial_ds_flow_const = 0.0 - waterbodies_initial_depth_const = -1e9 - # Set initial states from cold-state - waterbodies_initial_states_df = pd.DataFrame( - 0, - index=waterbodies_df.index, - columns=[ - "qd0", - "h0", - ], - dtype="float32", - ) - # TODO: This assignment could probably by done in the above call - waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const - waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const - waterbodies_initial_states_df["index"] = range( - len(waterbodies_initial_states_df) - ) - - waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on=index_id - ) - - LOG.debug( - "waterbody initial states complete in %s seconds."\ - % (time.time() - start_time)) - start_time = time.time() - - #---------------------------------------------------------------------------- - # Assemble channel initial states (flow and depth) - # also establish simulation initialization timestamp - #---------------------------------------------------------------------------- - start_time = time.time() - LOG.info("setting channel initial states ...") - - # if lite restart file is provided, the read channel initial states from it - if restart_parameters.get("lite_channel_restart_file", None): - # FIXME: Change it for hyfeature! - q0, t0 = nhd_io.read_lite_restart( - restart_parameters['lite_channel_restart_file'] - ) - t0_str = None - - # when a restart file for hyfeature is provied, then read initial states from it. - elif restart_parameters.get("hyfeature_channel_restart_file", None): - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - channel_initial_states_file = restart_parameters["hyfeature_channel_restart_file"] - df = pd.read_csv(channel_initial_states_file) - t0_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") - t0 = datetime.strptime(t0_str,"%Y-%m-%d_%H:%M:%S") - - # build initial states from user-provided restart parameters - else: - # FIXME: Change it for hyfeature! - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - - # get initialization time from restart file - if restart_parameters.get("wrf_hydro_channel_restart_file", None): - channel_initial_states_file = restart_parameters[ - "wrf_hydro_channel_restart_file" - ] - t0_str = nhd_io.get_param_str( - channel_initial_states_file, - "Restart_Time" - ) - else: - t0_str = "2015-08-16_00:00:00" - - # convert timestamp from string to datetime - t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") - - # get initial time from user inputs - if restart_parameters.get("start_datetime", None): - t0_str = restart_parameters.get("start_datetime") - - def _try_parsing_date(text): - for fmt in ( - "%Y-%m-%d_%H:%M", - "%Y-%m-%d_%H:%M:%S", - "%Y-%m-%d %H:%M", - "%Y-%m-%d %H:%M:%S", - "%Y/%m/%d %H:%M", - "%Y/%m/%d %H:%M:%S" - ): - try: - return datetime.strptime(text, fmt) - except ValueError: - pass - LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') - quit() - - t0 = _try_parsing_date(t0_str) - else: - if t0_str == "2015-08-16_00:00:00": - LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') - else: - LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) - - LOG.debug( - "channel initial states complete in %s seconds."\ - % (time.time() - start_time) - ) - start_time = time.time() - - return ( - waterbodies_df, - q0, - t0, - ) - # TODO: This returns a full dataframe (waterbodies_df) with the - # merged initial states for waterbodies, but only the - # initial state values (q0; not merged with the channel properties) - # for the channels -- - # That is because that is how they are used downstream. Need to - # trace that back and decide if there is one of those two ways - # that is optimal and make both returns that way. - -def build_forcing_sets( - supernetwork_parameters, - forcing_parameters, - t0, - ): - - run_sets = forcing_parameters.get("qlat_forcing_sets", None) - qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) - nts = forcing_parameters.get("nts", None) - max_loop_size = forcing_parameters.get("max_loop_size", 12) - dt = forcing_parameters.get("dt", None) - - geo_file_type = supernetwork_parameters.get('geo_file_type') - - try: - qlat_input_folder = pathlib.Path(qlat_input_folder) - assert qlat_input_folder.is_dir() == True - except TypeError: - raise TypeError("Aborting simulation because no qlat_input_folder is specified in the forcing_parameters section of the .yaml control file.") from None - except AssertionError: - raise AssertionError("Aborting simulation because the qlat_input_folder:", qlat_input_folder,"does not exist. Please check the the nexus_input_folder variable is correctly entered in the .yaml control file") from None - - forcing_glob_filter = forcing_parameters.get("qlat_file_pattern_filter", "*.NEXOUT") - - if forcing_glob_filter=="nex-*": - print("Reformating qlat nexus files as hourly binary files...") - binary_folder = forcing_parameters.get('binary_nexus_file_folder', None) - qlat_files = qlat_input_folder.glob(forcing_glob_filter) - - #Check that directory/files specified will work - if not binary_folder: - raise(RuntimeError("No output binary qlat folder supplied in config")) - elif not os.path.exists(binary_folder): - raise(RuntimeError("Output binary qlat folder supplied in config does not exist")) - elif len(list(pathlib.Path(binary_folder).glob('*.parquet'))) != 0: - raise(RuntimeError("Output binary qlat folder supplied in config is not empty (already contains '.parquet' files)")) - - #Add tnx for backwards compatability - qlat_files_list = list(qlat_files) + list(qlat_input_folder.glob('tnx*.csv')) - #Convert files to binary hourly files, reset nexus input information - qlat_input_folder, forcing_glob_filter = nex_files_to_binary(qlat_files_list, binary_folder) - forcing_parameters["qlat_input_folder"] = qlat_input_folder - forcing_parameters["qlat_file_pattern_filter"] = forcing_glob_filter - - # TODO: Throw errors if insufficient input data are available - if run_sets: - #FIXME: Change it for hyfeature - ''' - # append final_timestamp variable to each set_list - qlat_input_folder = pathlib.Path(qlat_input_folder) - for (s, _) in enumerate(run_sets): - final_chrtout = qlat_input_folder.joinpath(run_sets[s]['qlat_files' - ][-1]) - final_timestamp_str = nhd_io.get_param_str(final_chrtout, - 'model_output_valid_time') - run_sets[s]['final_timestamp'] = \ - datetime.strptime(final_timestamp_str, '%Y-%m-%d_%H:%M:%S') - ''' - elif qlat_input_folder: - # Construct run_set dictionary from user-specified parameters - - # get the first and seconded files from an ordered list of all forcing files - qlat_input_folder = pathlib.Path(qlat_input_folder) - all_files = sorted(qlat_input_folder.glob(forcing_glob_filter)) - first_file = all_files[0] - second_file = all_files[1] - - # Deduce the timeinterval of the forcing data from the output timestamps of the first - # two ordered CHRTOUT files - if geo_file_type=='HYFeaturesNetwork': - df = read_file(first_file) - t1_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") - t1 = datetime.strptime(t1_str,"%Y-%m-%d_%H:%M:%S") - df = read_file(second_file) - t2_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") - t2 = datetime.strptime(t2_str,"%Y-%m-%d_%H:%M:%S") - elif geo_file_type=='NHDNetwork': - t1 = nhd_io.get_param_str(first_file, "model_output_valid_time") - t1 = datetime.strptime(t1, "%Y-%m-%d_%H:%M:%S") - t2 = nhd_io.get_param_str(second_file, "model_output_valid_time") - t2 = datetime.strptime(t2, "%Y-%m-%d_%H:%M:%S") - - dt_qlat_timedelta = t2 - t1 - dt_qlat = dt_qlat_timedelta.seconds - - # determine qts_subdivisions - qts_subdivisions = dt_qlat / dt - if dt_qlat % dt == 0: - qts_subdivisions = int(dt_qlat / dt) - # make sure that qts_subdivisions = dt_qlat / dt - forcing_parameters['qts_subdivisions']= qts_subdivisions - - # the number of files required for the simulation - nfiles = int(np.ceil(nts / qts_subdivisions)) - - # list of forcing file datetimes - #datetime_list = [t0 + dt_qlat_timedelta * (n + 1) for n in - # range(nfiles)] - # ** Correction ** Because qlat file at time t is constantly applied throughout [t, t+1], - # ** n + 1 should be replaced by n - datetime_list = [t0 + dt_qlat_timedelta * (n) for n in - range(nfiles)] - datetime_list_str = [datetime.strftime(d, '%Y%m%d%H%M') for d in - datetime_list] - - # list of forcing files - forcing_filename_list = [d_str + forcing_glob_filter[1:] for d_str in - datetime_list_str] - - # check that all forcing files exist - for f in forcing_filename_list: - try: - J = pathlib.Path(qlat_input_folder.joinpath(f)) - assert J.is_file() == True - except AssertionError: - raise AssertionError("Aborting simulation because forcing file", J, "cannot be not found.") from None - - # build run sets list - run_sets = [] - k = 0 - j = 0 - nts_accum = 0 - nts_last = 0 - while k < len(forcing_filename_list): - run_sets.append({}) - - if k + max_loop_size < len(forcing_filename_list): - run_sets[j]['qlat_files'] = forcing_filename_list[k:k - + max_loop_size] - else: - run_sets[j]['qlat_files'] = forcing_filename_list[k:] - - nts_accum += len(run_sets[j]['qlat_files']) * qts_subdivisions - if nts_accum <= nts: - run_sets[j]['nts'] = int(len(run_sets[j]['qlat_files']) - * qts_subdivisions) - else: - run_sets[j]['nts'] = int(nts - nts_last) - - final_qlat = qlat_input_folder.joinpath(run_sets[j]['qlat_files'][-1]) - if geo_file_type=='NHDNetwork': - final_timestamp_str = nhd_io.get_param_str(final_qlat,'model_output_valid_time') - elif geo_file_type=='HYFeaturesNetwork': - df = read_file(final_qlat) - final_timestamp_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") - - run_sets[j]['final_timestamp'] = \ - datetime.strptime(final_timestamp_str, '%Y-%m-%d_%H:%M:%S') - - nts_last = nts_accum - k += max_loop_size - j += 1 - - return run_sets - -def build_qlateral_array( - run, - cpu_pool, - nexus_to_upstream_flowpath_dict, - supernetwork_parameters, - segment_index=pd.Index([]), -): - - start_time = time.time() - LOG.info("Creating a DataFrame of lateral inflow forcings ...") - - # TODO: set default/optional arguments - qts_subdivisions = run.get("qts_subdivisions", 1) - nts = run.get("nts", 1) - qlat_input_folder = run.get("qlat_input_folder", None) - qlat_input_file = run.get("qlat_input_file", None) - - geo_file_type = supernetwork_parameters.get('geo_file_type') - - if qlat_input_folder: - qlat_input_folder = pathlib.Path(qlat_input_folder) - if "qlat_files" in run: - qlat_files = run.get("qlat_files") - qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] - elif "qlat_file_pattern_filter" in run: - qlat_file_pattern_filter = run.get( - "qlat_file_pattern_filter", "*CHRT_OUT*" - ) - qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) - - qlat_file_index_col = run.get( - "qlat_file_index_col", "feature_id" - ) - qlat_file_value_col = run.get("qlat_file_value_col", "q_lateral") - gw_bucket_col = run.get("qlat_file_gw_bucket_flux_col","qBucket") - terrain_ro_col = run.get("qlat_file_terrain_runoff_col","qSfcLatRunoff") - - if geo_file_type=='NHDNetwork': - # Parallel reading of qlateral data from CHRTOUT - with Parallel(n_jobs=cpu_pool) as parallel: - jobs = [] - for f in qlat_files: - jobs.append( - delayed(nhd_io.get_ql_from_chrtout) - #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) - #delayed(nhd_io.get_ql_from_csv) - (f) - ) - ql_list = parallel(jobs) - - # get feature_id from a single CHRTOUT file - with netCDF4.Dataset(qlat_files[0]) as ds: - idx = ds.variables[qlat_file_index_col][:].filled() - - # package data into a DataFrame - qlats_df = pd.DataFrame( - np.stack(ql_list).T, - index = idx, - columns = range(len(qlat_files)) - ) - - qlats_df = qlats_df[qlats_df.index.isin(segment_index)] - elif geo_file_type=='HYFeaturesNetwork': - dfs=[] - for f in qlat_files: - df = read_file(f).set_index(['feature_id']) - dfs.append(df) - - # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids - nexuses_lateralflows_df = pd.concat(dfs, axis=1) - - # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids - qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) - for k,v in nexus_to_upstream_flowpath_dict.items() ), axis=1 - ).T - qlats_df.columns=range(len(qlat_files)) - qlats_df = qlats_df[qlats_df.index.isin(segment_index)] - - # The segment_index has the full network set of segments/flowpaths. - # Whereas the set of flowpaths that are downstream of nexuses is a - # subset of the segment_index. Therefore, all of the segments/flowpaths - # that are not accounted for in the set of flowpaths downstream of - # nexuses need to be added to the qlateral dataframe and padded with - # zeros. - all_df = pd.DataFrame( np.zeros( (len(segment_index), len(qlats_df.columns)) ), index=segment_index, - columns=qlats_df.columns ) - all_df.loc[ qlats_df.index ] = qlats_df - qlats_df = all_df.sort_index() - - elif qlat_input_file: - qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) - else: - qlat_const = run.get("qlat_const", 0) - qlats_df = pd.DataFrame( - qlat_const, - index=segment_index, - columns=range(nts // qts_subdivisions), - dtype="float32", - ) - - # TODO: Make a more sophisticated date-based filter - max_col = 1 + nts // qts_subdivisions - if len(qlats_df.columns) > max_col: - qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) - - if not segment_index.empty: - qlats_df = qlats_df[qlats_df.index.isin(segment_index)] - - LOG.debug( - "lateral inflow DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - return qlats_df - -def nex_files_to_binary(nexus_files, binary_folder): - for f in nexus_files: - # read the csv file - df = pd.read_csv(f, usecols=[1,2], names=['Datetime','qlat']) - - # convert and reformat datetime column - df['Datetime']= pd.to_datetime(df['Datetime']).dt.strftime("%Y%m%d%H%M") - - # reformat the dataframe - df['feature_id'] = get_id_from_filename(f) - df = df.pivot(index="feature_id", columns="Datetime", values="qlat") - df.columns.name = None - - for col in df.columns: - table_new = pa.Table.from_pandas(df.loc[:, [col]]) - - if not os.path.exists(f'{binary_folder}/{col}NEXOUT.parquet'): - pq.write_table(table_new, f'{binary_folder}/{col}NEXOUT.parquet') - - else: - table_old = pq.read_table(f'{binary_folder}/{col}NEXOUT.parquet') - table = pa.concat_tables([table_old,table_new]) - pq.write_table(table, f'{binary_folder}/{col}NEXOUT.parquet') - - nexus_input_folder = binary_folder - forcing_glob_filter = '*NEXOUT.parquet' - - return nexus_input_folder, forcing_glob_filter - -def get_id_from_filename(file_name): - id = os.path.splitext(file_name)[0].split('-')[1].split('_')[0] - return int(id) - -def read_file(file_name): - extension = file_name.suffix - if extension=='.csv': - df = pd.read_csv(file_name) - elif extension=='.parquet': - df = pq.read_table(file_name).to_pandas().reset_index() - df.index.name = None - - return df \ No newline at end of file From 3927ceed5c9cec8af7935abad408ac1860baf604 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Thu, 8 Dec 2022 20:32:57 +0000 Subject: [PATCH 30/54] initial commit --- .../troute/HYFeaturesNetwork.py | 33 +- .../troute/abstractnetwork_preprocess.py | 752 ++++++++++++++++++ .../troute/hyfeature_preprocess.py | 109 ++- 3 files changed, 876 insertions(+), 18 deletions(-) create mode 100644 src/troute-network/troute/abstractnetwork_preprocess.py diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 0f43de5ab..c7f6ee793 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -16,19 +16,6 @@ __verbose__ = False __showtiming__ = False -def node_key_func_nexus(x): - return int(x[4:]) - -def node_key_func_wb(x): - return int(x[3:]) - -def numeric_id(flowpath): - id = flowpath['id'].split('-')[-1] - toid = flowpath['toid'].split('-')[-1] - flowpath['id'] = int(id) - flowpath['toid'] = int(toid) - return flowpath - def read_qlats(forcing_parameters, segment_index, nexus_to_downstream_flowpath_dict): # STEP 5: Read (or set) QLateral Inputs if __showtiming__: @@ -111,6 +98,7 @@ def read_qlats(forcing_parameters, segment_index, nexus_to_downstream_flowpath_d return qlat_df +<<<<<<< HEAD def read_nexus_file(nexus_file_path): #Currently reading data in format: @@ -158,6 +146,8 @@ def read_geopkg(file_path): flowpaths = pd.merge(flowpaths, attributes, on='id') return flowpaths +======= +>>>>>>> initial commit class HYFeaturesNetwork(AbstractNetwork): """ @@ -187,6 +177,23 @@ def __init__(self, print("creating supernetwork connections set") if __showtiming__: start_time = time.time() + + #------------------------------------------------ + # Load Geo File + #------------------------------------------------ + (self._dataframe, + self._flowpath_dict, + self._waterbody_df, + self._waterbody_types_df, + ) = hyfeature_prep.read_geo_file( + supernetwork_parameters, + waterbody_parameters, + ) + + + + + #------------------------------------------------ # Preprocess network attributes diff --git a/src/troute-network/troute/abstractnetwork_preprocess.py b/src/troute-network/troute/abstractnetwork_preprocess.py new file mode 100644 index 000000000..be1bc9734 --- /dev/null +++ b/src/troute-network/troute/abstractnetwork_preprocess.py @@ -0,0 +1,752 @@ +import json +import pathlib +from functools import partial, reduce +from itertools import chain +from datetime import datetime, timedelta +from collections import defaultdict, deque +import logging +import os +import time + +import pandas as pd +import numpy as np +import netCDF4 +from joblib import delayed, Parallel +import pyarrow as pa +import pyarrow.parquet as pq + +import troute.nhd_io as nhd_io +import troute.nhd_network as nhd_network +import troute.nhd_network_utilities_v02 as nnu + + +LOG = logging.getLogger('') + +def build_diffusive_domain( + compute_parameters, + connections, +): + + hybrid_params = compute_parameters.get("hybrid_parameters", False) + if hybrid_params: + # switch parameters + # if run_hybrid = False, run MC only + # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc + # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric + # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric + run_hybrid = hybrid_params.get('run_hybrid_routing', False) + use_topobathy = hybrid_params.get('use_natl_xsections', False) + run_refactored = hybrid_params.get('run_refactored_network', False) + + # file path parameters of non-refactored hydrofabric defined by RouteLink.nc + domain_file = hybrid_params.get("diffusive_domain", None) + topobathy_file = hybrid_params.get("topobathy_domain", None) + + # file path parameters of refactored hydrofabric for diffusive wave channel routing + refactored_domain_file = hybrid_params.get("refactored_domain", None) + refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) + #------------------------------------------------------------------------- + # for non-refactored hydofabric defined by RouteLink.nc + # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. + if run_hybrid and domain_file: + + LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') + + # read diffusive domain dictionary from yaml or json + diffusive_domain = nhd_io.read_diffusive_domain(domain_file) + + if use_topobathy and topobathy_file: + + LOG.debug('Natural cross section data on original hydrofabric are provided.') + + # read topobathy domain netcdf file, set index to 'comid' + # TODO: replace 'link' with a user-specified indexing variable name. + # ... if for whatever reason there is not a `link` variable in the + # ... dataframe returned from read_netcdf, then the code would break here. + topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) + + # TODO: Request GID make comID variable an integer in their product, so + # we do not need to change variable types, here. + topobathy_df.index = topobathy_df.index.astype(int) + + else: + topobathy_df = pd.DataFrame() + LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') + + # initialize a dictionary to hold network data for each of the diffusive domains + diffusive_network_data = {} + + else: + diffusive_domain = None + diffusive_network_data = None + topobathy_df = pd.DataFrame() + LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') + unrefactored_topobathy_df = pd.DataFrame() + #------------------------------------------------------------------------- + # for refactored hydofabric + if run_hybrid and run_refactored and refactored_domain_file: + + LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') + + # read diffusive domain dictionary from yaml or json + refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) + + if use_topobathy and refactored_topobathy_file: + + LOG.debug('Natural cross section data of refactored hydrofabric are provided.') + + # read topobathy domain netcdf file, set index to 'comid' + # TODO: replace 'link' with a user-specified indexing variable name. + # ... if for whatever reason there is not a `link` variable in the + # ... dataframe returned from read_netcdf, then the code would break here. + topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) + + # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy + # for crosswalking water elevations between non-refactored and refactored hydrofabrics. + unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) + unrefactored_topobathy_df.index = unrefactored_topobathy_df.index.astype(int) + + else: + topobathy_df = pd.DataFrame() + LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') + + # initialize a dictionary to hold network data for each of the diffusive domains + refactored_diffusive_network_data = {} + + else: + refactored_diffusive_domain = None + refactored_diffusive_network_data = None + refactored_reaches = {} + LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') + + else: + diffusive_domain = None + diffusive_network_data = None + topobathy_df = pd.DataFrame() + unrefactored_topobathy_df = pd.DataFrame() + refactored_diffusive_domain = None + refactored_diffusive_network_data = None + refactored_reaches = {} + LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') + + #============================================================================ + # build diffusive domain data and edit MC domain data for hybrid simulation + + # + if diffusive_domain: + rconn_diff0 = nhd_network.reverse_network(connections) + refactored_reaches = {} + + for tw in diffusive_domain: + mainstem_segs = diffusive_domain[tw]['links'] + # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is + # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. + upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] + if upstream_boundary_mainstem_link[0] in mainstem_segs: + mainstem_segs.remove(upstream_boundary_mainstem_link[0]) + + # ===== build diffusive network data objects ==== + diffusive_network_data[tw] = {} + + # add diffusive domain segments + diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs + + # diffusive domain tributary segments + trib_segs = [] + + for seg in mainstem_segs: + us_list = rconn_diff0[seg] + for u in us_list: + if u not in mainstem_segs: + trib_segs.append(u) + + diffusive_network_data[tw]['tributary_segments'] = trib_segs + # diffusive domain connections object + diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, reaches, rconn_diff = nnu.organize_independent_networks( + diffusive_network_data[tw]['connections'], + set(trib_segs), + set(), + ) + + diffusive_network_data[tw]['rconn'] = rconn_diff + diffusive_network_data[tw]['reaches'] = reaches[tw] + + # RouteLink parameters + diffusive_network_data[tw]['param_df'] = param_df.filter( + (mainstem_segs + trib_segs), + axis = 0, + ) + diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link + + if refactored_diffusive_domain: + diffusive_parameters = {'geo_file_path': refactored_topobathy_file} + refactored_connections = nnu.build_refac_connections(diffusive_parameters) + + # list of stream segments of a single refactored diffusive domain + refac_tw = refactored_diffusive_domain[tw]['refac_tw'] + rlinks_tw = refactored_diffusive_domain[tw]['rlinks'] + refactored_connections_tw = {} + + # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections + # for a single refactored diffusive domain defined by a current tw. + for k in rlinks_tw: + if k in refactored_connections.keys() and k != refac_tw: + refactored_connections_tw[k] = refactored_connections[k] + + refactored_diffusive_network_data[refac_tw] = {} + refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs + refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw + + for k in trib_segs: + refactored_diffusive_network_data[refac_tw]['connections'][k]= [refactored_diffusive_domain[tw]['incoming_tribs'][k]] + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, refactored_reaches_batch, refactored_conn_diff = nnu.organize_independent_networks( + refactored_diffusive_network_data[refac_tw]['connections'], + set(trib_segs), + set(), + ) + + refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] + refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = refactored_diffusive_domain[tw]['rlinks'] + refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = diffusive_network_data[tw]['upstream_boundary_link'] + else: + refactored_reaches={} + + # ==== remove diffusive domain segs from MC domain ==== + # drop indices from param_df + param_df = param_df.drop(mainstem_segs) + + # remove keys from connections dictionary + for s in mainstem_segs: + connections.pop(s) + + # update downstream connections of trib segs + for us in trib_segs: + connections[us] = [] + + return ( + param_df, + connections, + diffusive_network_data, + topobathy_df, + refactored_diffusive_domain, + refactored_reaches, + unrefactored_topobathy_df + ) + +def create_independent_networks( + waterbody_parameters, + connections, + wbody_conn, + gages, + ): + LOG.info("organizing connections into reaches ...") + start_time = time.time() + gage_break_segments = set() + wbody_break_segments = set() + + break_network_at_waterbodies = waterbody_parameters.get( + "break_network_at_waterbodies", False + ) + + # if streamflow DA, then break network at gages + break_network_at_gages = False + + if break_network_at_waterbodies: + wbody_break_segments = wbody_break_segments.union(wbody_conn.values()) + + if break_network_at_gages: + gage_break_segments = gage_break_segments.union(gages['gages'].keys()) + + independent_networks, reaches_bytw, rconn = nnu.organize_independent_networks( + connections, + wbody_break_segments, + gage_break_segments, + ) + + LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) + + return independent_networks, reaches_bytw, rconn + +def hyfeature_initial_warmstate_preprocess( + break_network_at_waterbodies, + restart_parameters, + segment_index, + waterbodies_df, + ): + + ''' + Assemble model initial condition data: + - waterbody inital states (outflow and pool elevation) + - channel initial states (flow and depth) + - initial time + + Arguments + --------- + - break_network_at_waterbodies (bool): If True, waterbody initial states will + be appended to the waterbody parameter + dataframe. If False, waterbodies will + not be simulated and the waterbody + parameter datataframe wil not be changed + - restart_parameters (dict): User-input simulation restart + parameters + - segment_index (Pandas Index): All segment IDs in the simulation + doamin + - waterbodies_df (Pandas DataFrame): Waterbody parameters + + Returns + ------- + - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial + states (outflow and pool elevation) + - q0 (Pandas DataFrame): Initial flow and depth states for each + segment in the model domain + - t0 (datetime): Datetime of the model initialization + + Notes + ----- + ''' + + #---------------------------------------------------------------------------- + # Assemble waterbody initial states (outflow and pool elevation + #---------------------------------------------------------------------------- + if break_network_at_waterbodies: + + start_time = time.time() + LOG.info("setting waterbody initial states ...") + + # if a lite restart file is provided, read initial states from it. + if restart_parameters.get("lite_waterbody_restart_file", None): + + waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( + restart_parameters['lite_waterbody_restart_file'] + ) + + # read waterbody initial states from WRF-Hydro type restart file + elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): + waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( + restart_parameters["wrf_hydro_waterbody_restart_file"], + restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], + restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", 'lake_id'), + restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], + restart_parameters.get( + "wrf_hydro_waterbody_crosswalk_filter_file_field_name", + 'NHDWaterbodyComID' + ), + ) + + # if no restart file is provided, default initial states + else: + # TODO: Consider adding option to read cold state from route-link file + waterbodies_initial_ds_flow_const = 0.0 + waterbodies_initial_depth_const = -1e9 + # Set initial states from cold-state + waterbodies_initial_states_df = pd.DataFrame( + 0, + index=waterbodies_df.index, + columns=[ + "qd0", + "h0", + ], + dtype="float32", + ) + # TODO: This assignment could probably by done in the above call + waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const + waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const + waterbodies_initial_states_df["index"] = range( + len(waterbodies_initial_states_df) + ) + + waterbodies_df = pd.merge( + waterbodies_df, waterbodies_initial_states_df, on="lake_id" + ) + + LOG.debug( + "waterbody initial states complete in %s seconds."\ + % (time.time() - start_time)) + start_time = time.time() + + #---------------------------------------------------------------------------- + # Assemble channel initial states (flow and depth) + # also establish simulation initialization timestamp + #---------------------------------------------------------------------------- + start_time = time.time() + LOG.info("setting channel initial states ...") + + # if lite restart file is provided, the read channel initial states from it + if restart_parameters.get("lite_channel_restart_file", None): + # FIXME: Change it for hyfeature! + q0, t0 = nhd_io.read_lite_restart( + restart_parameters['lite_channel_restart_file'] + ) + t0_str = None + + # when a restart file for hyfeature is provied, then read initial states from it. + elif restart_parameters.get("hyfeature_channel_restart_file", None): + q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) + channel_initial_states_file = restart_parameters["hyfeature_channel_restart_file"] + df = pd.read_csv(channel_initial_states_file) + t0_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + t0 = datetime.strptime(t0_str,"%Y-%m-%d_%H:%M:%S") + + # build initial states from user-provided restart parameters + else: + # FIXME: Change it for hyfeature! + q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) + + # get initialization time from restart file + if restart_parameters.get("wrf_hydro_channel_restart_file", None): + channel_initial_states_file = restart_parameters[ + "wrf_hydro_channel_restart_file" + ] + t0_str = nhd_io.get_param_str( + channel_initial_states_file, + "Restart_Time" + ) + else: + t0_str = "2015-08-16_00:00:00" + + # convert timestamp from string to datetime + t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") + + # get initial time from user inputs + if restart_parameters.get("start_datetime", None): + t0_str = restart_parameters.get("start_datetime") + + def _try_parsing_date(text): + for fmt in ( + "%Y-%m-%d_%H:%M", + "%Y-%m-%d_%H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%d %H:%M:%S", + "%Y/%m/%d %H:%M", + "%Y/%m/%d %H:%M:%S" + ): + try: + return datetime.strptime(text, fmt) + except ValueError: + pass + LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') + quit() + + t0 = _try_parsing_date(t0_str) + else: + if t0_str == "2015-08-16_00:00:00": + LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') + else: + LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) + + LOG.debug( + "channel initial states complete in %s seconds."\ + % (time.time() - start_time) + ) + start_time = time.time() + + return ( + waterbodies_df, + q0, + t0, + ) + # TODO: This returns a full dataframe (waterbodies_df) with the + # merged initial states for waterbodies, but only the + # initial state values (q0; not merged with the channel properties) + # for the channels -- + # That is because that is how they are used downstream. Need to + # trace that back and decide if there is one of those two ways + # that is optimal and make both returns that way. + +def build_forcing_sets( + supernetwork_parameters, + forcing_parameters, + t0, + ): + + run_sets = forcing_parameters.get("qlat_forcing_sets", None) + qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) + nts = forcing_parameters.get("nts", None) + max_loop_size = forcing_parameters.get("max_loop_size", 12) + dt = forcing_parameters.get("dt", None) + + geo_file_type = supernetwork_parameters.get('geo_file_type') + + try: + qlat_input_folder = pathlib.Path(qlat_input_folder) + assert qlat_input_folder.is_dir() == True + except TypeError: + raise TypeError("Aborting simulation because no qlat_input_folder is specified in the forcing_parameters section of the .yaml control file.") from None + except AssertionError: + raise AssertionError("Aborting simulation because the qlat_input_folder:", qlat_input_folder,"does not exist. Please check the the nexus_input_folder variable is correctly entered in the .yaml control file") from None + + forcing_glob_filter = forcing_parameters.get("qlat_file_pattern_filter", "*.NEXOUT") + + if forcing_glob_filter=="nex-*": + print("Reformating qlat nexus files as hourly binary files...") + binary_folder = forcing_parameters.get('binary_nexus_file_folder', None) + qlat_files = qlat_input_folder.glob(forcing_glob_filter) + + #Check that directory/files specified will work + if not binary_folder: + raise(RuntimeError("No output binary qlat folder supplied in config")) + elif not os.path.exists(binary_folder): + raise(RuntimeError("Output binary qlat folder supplied in config does not exist")) + elif len(list(pathlib.Path(binary_folder).glob('*.parquet'))) != 0: + raise(RuntimeError("Output binary qlat folder supplied in config is not empty (already contains '.parquet' files)")) + + #Add tnx for backwards compatability + qlat_files_list = list(qlat_files) + list(qlat_input_folder.glob('tnx*.csv')) + #Convert files to binary hourly files, reset nexus input information + qlat_input_folder, forcing_glob_filter = nex_files_to_binary(qlat_files_list, binary_folder) + forcing_parameters["qlat_input_folder"] = qlat_input_folder + forcing_parameters["qlat_file_pattern_filter"] = forcing_glob_filter + + # TODO: Throw errors if insufficient input data are available + if run_sets: + #FIXME: Change it for hyfeature + ''' + # append final_timestamp variable to each set_list + qlat_input_folder = pathlib.Path(qlat_input_folder) + for (s, _) in enumerate(run_sets): + final_chrtout = qlat_input_folder.joinpath(run_sets[s]['qlat_files' + ][-1]) + final_timestamp_str = nhd_io.get_param_str(final_chrtout, + 'model_output_valid_time') + run_sets[s]['final_timestamp'] = \ + datetime.strptime(final_timestamp_str, '%Y-%m-%d_%H:%M:%S') + ''' + elif qlat_input_folder: + # Construct run_set dictionary from user-specified parameters + + # get the first and seconded files from an ordered list of all forcing files + qlat_input_folder = pathlib.Path(qlat_input_folder) + all_files = sorted(qlat_input_folder.glob(forcing_glob_filter)) + first_file = all_files[0] + second_file = all_files[1] + + # Deduce the timeinterval of the forcing data from the output timestamps of the first + # two ordered CHRTOUT files + if geo_file_type=='HYFeaturesNetowrk': + df = read_file(first_file) + t1_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + t1 = datetime.strptime(t1_str,"%Y-%m-%d_%H:%M:%S") + df = read_file(second_file) + t2_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + t2 = datetime.strptime(t2_str,"%Y-%m-%d_%H:%M:%S") + elif geo_file_type=='NHDNetwork': + t1 = nhd_io.get_param_str(first_file, "model_output_valid_time") + t1 = datetime.strptime(t1, "%Y-%m-%d_%H:%M:%S") + t2 = nhd_io.get_param_str(second_file, "model_output_valid_time") + t2 = datetime.strptime(t2, "%Y-%m-%d_%H:%M:%S") + + dt_qlat_timedelta = t2 - t1 + dt_qlat = dt_qlat_timedelta.seconds + + # determine qts_subdivisions + qts_subdivisions = dt_qlat / dt + if dt_qlat % dt == 0: + qts_subdivisions = dt_qlat / dt + # make sure that qts_subdivisions = dt_qlat / dt + forcing_parameters['qts_subdivisions']= qts_subdivisions + + # the number of files required for the simulation + nfiles = int(np.ceil(nts / qts_subdivisions)) + + # list of forcing file datetimes + #datetime_list = [t0 + dt_qlat_timedelta * (n + 1) for n in + # range(nfiles)] + # ** Correction ** Because qlat file at time t is constantly applied throughout [t, t+1], + # ** n + 1 should be replaced by n + datetime_list = [t0 + dt_qlat_timedelta * (n) for n in + range(nfiles)] + datetime_list_str = [datetime.strftime(d, '%Y%m%d%H%M') for d in + datetime_list] + + # list of forcing files + forcing_filename_list = [d_str + forcing_glob_filter[1:] for d_str in + datetime_list_str] + + # check that all forcing files exist + for f in forcing_filename_list: + try: + J = pathlib.Path(qlat_input_folder.joinpath(f)) + assert J.is_file() == True + except AssertionError: + raise AssertionError("Aborting simulation because forcing file", J, "cannot be not found.") from None + + # build run sets list + run_sets = [] + k = 0 + j = 0 + nts_accum = 0 + nts_last = 0 + while k < len(forcing_filename_list): + run_sets.append({}) + + if k + max_loop_size < len(forcing_filename_list): + run_sets[j]['qlat_files'] = forcing_filename_list[k:k + + max_loop_size] + else: + run_sets[j]['qlat_files'] = forcing_filename_list[k:] + + nts_accum += len(run_sets[j]['qlat_files']) * qts_subdivisions + if nts_accum <= nts: + run_sets[j]['nts'] = int(len(run_sets[j]['qlat_files']) + * qts_subdivisions) + else: + run_sets[j]['nts'] = int(nts - nts_last) + + final_qlat = qlat_input_folder.joinpath(run_sets[j]['qlat_files'][-1]) + if geo_file_type=='NHDNetwork': + final_timestamp_str = nhd_io.get_param_str(final_qlat,'model_output_valid_time') + elif geo_file_type=='HYFeaturesNetowrk': + df = read_file(final_qlat) + final_timestamp_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + + run_sets[j]['final_timestamp'] = \ + datetime.strptime(final_timestamp_str, '%Y-%m-%d_%H:%M:%S') + + nts_last = nts_accum + k += max_loop_size + j += 1 + + return run_sets + +def build_qlateral_array( + run, + cpu_pool, + nexus_to_upstream_flowpath_dict, + supernetwork_parameters, + segment_index=pd.Index([]), +): + # TODO: set default/optional arguments + qts_subdivisions = run.get("qts_subdivisions", 1) + nts = run.get("nts", 1) + qlat_input_folder = run.get("qlat_input_folder", None) + qlat_input_file = run.get("qlat_input_file", None) + + geo_file_type = supernetwork_parameters.get('geo_file_type') + + if qlat_input_folder: + qlat_input_folder = pathlib.Path(qlat_input_folder) + if "qlat_files" in run: + qlat_files = run.get("qlat_files") + qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] + elif "qlat_file_pattern_filter" in run: + qlat_file_pattern_filter = run.get( + "qlat_file_pattern_filter", "*CHRT_OUT*" + ) + qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) + + qlat_file_index_col = run.get( + "qlat_file_index_col", "feature_id" + ) + qlat_file_value_col = run.get("qlat_file_value_col", "q_lateral") + gw_bucket_col = run.get("qlat_file_gw_bucket_flux_col","qBucket") + terrain_ro_col = run.get("qlat_file_terrain_runoff_col","qSfcLatRunoff") + + if geo_file_type=='NHDNetwork': + # Parallel reading of qlateral data from CHRTOUT + with Parallel(n_jobs=cpu_pool) as parallel: + jobs = [] + for f in qlat_files: + jobs.append( + #delayed(nhd_io.get_ql_from_chrtout) + #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) + delayed(nhd_io.get_ql_from_csv) + (f) + ) + ql_list = parallel(jobs) + + # get feature_id from a single CHRTOUT file + with netCDF4.Dataset(qlat_files[0]) as ds: + idx = ds.variables[qlat_file_index_col][:].filled() + + # package data into a DataFrame + qlats_df = pd.DataFrame( + np.stack(ql_list).T, + index = idx, + columns = range(len(qlat_files)) + ) + elif geo_file_type=='HYFeaturesNetowrk': + dfs=[] + for f in qlat_files: + df = read_file(f).set_index(['feature_id']) + dfs.append(df) + + # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids + nexuses_lateralflows_df = pd.concat(dfs, axis=1) + + # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids + qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) + for k,v in nexus_to_upstream_flowpath_dict.items() ), axis=1 + ).T + qlats_df.columns=range(len(qlat_files)) + + qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + elif qlat_input_file: + qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) + else: + qlat_const = run.get("qlat_const", 0) + qlats_df = pd.DataFrame( + qlat_const, + index=segment_index, + columns=range(nts // qts_subdivisions), + dtype="float32", + ) + + # TODO: Make a more sophisticated date-based filter + max_col = 1 + nts // qts_subdivisions + if len(qlats_df.columns) > max_col: + qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) + + if not segment_index.empty: + qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + + return qlats_df + +def nex_files_to_binary(nexus_files, binary_folder): + for f in nexus_files: + # read the csv file + df = pd.read_csv(f, usecols=[1,2], names=['Datetime','qlat']) + + # convert and reformat datetime column + df['Datetime']= pd.to_datetime(df['Datetime']).dt.strftime("%Y%m%d%H%M") + + # reformat the dataframe + df['feature_id'] = get_id_from_filename(f) + df = df.pivot(index="feature_id", columns="Datetime", values="qlat") + df.columns.name = None + + for col in df.columns: + table_new = pa.Table.from_pandas(df.loc[:, [col]]) + + if not os.path.exists(f'{binary_folder}/{col}NEXOUT.parquet'): + pq.write_table(table_new, f'{binary_folder}/{col}NEXOUT.parquet') + + else: + table_old = pq.read_table(f'{binary_folder}/{col}NEXOUT.parquet') + table = pa.concat_tables([table_old,table_new]) + pq.write_table(table, f'{binary_folder}/{col}NEXOUT.parquet') + + nexus_input_folder = binary_folder + forcing_glob_filter = '*NEXOUT.parquet' + + return nexus_input_folder, forcing_glob_filter + +def get_id_from_filename(file_name): + id = os.path.splitext(file_name)[0].split('-')[1].split('_')[0] + return int(id) + +def read_file(file_name): + extension = file_name.suffix + if extension=='.csv': + df = pd.read_csv(file_name) + elif extension=='.parquet': + df = pq.read_table(file_name).to_pandas().reset_index() + df.index.name = None + + return df \ No newline at end of file diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index 5d7605b7c..587bb677a 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -15,11 +15,77 @@ import troute.nhd_network as nhd_network import troute.nhd_io as nhd_io from troute.nhd_network import reverse_dict -import troute.HYFeaturesNetwork as hyf_network import troute.hyfeature_network_utilities as hnu LOG = logging.getLogger('') +def read_geo_file( + supernetwork_parameters, + waterbody_parameters, +): + + geo_file_path = supernetwork_parameters["geo_file_path"] + + file_type = Path(geo_file_path).suffix + if( file_type == '.gpkg' ): + dataframe = read_geopkg(geo_file_path) + elif( file_type == '.json') : + edge_list = supernetwork_parameters['flowpath_edge_list'] + dataframe = read_json(geo_file_path, edge_list) + else: + raise RuntimeError("Unsupported file type: {}".format(file_type)) + + # Don't need the string prefix anymore, drop it + mask = ~ dataframe['toid'].str.startswith("tnex") + dataframe = dataframe.apply(numeric_id, axis=1) + + # make the flowpath linkage, ignore the terminal nexus + flowpath_dict = dict(zip(dataframe.loc[mask].toid, dataframe.loc[mask].id)) + + # ********** need to be included in flowpath_attributes ************* + dataframe['alt'] = 1.0 #FIXME get the right value for this... + + #Load waterbody/reservoir info + if waterbody_parameters: + levelpool_params = waterbody_parameters.get('level_pool', None) + if not levelpool_params: + # FIXME should not be a hard requirement + raise(RuntimeError("No supplied levelpool parameters in routing config")) + + lake_id = levelpool_params.get("level_pool_waterbody_id", "wb-id") + waterbody_df = read_ngen_waterbody_df( + levelpool_params["level_pool_waterbody_parameter_file_path"], + lake_id, + ) + + # Remove duplicate lake_ids and rows + waterbody_df = ( + waterbody_df.reset_index() + .drop_duplicates(subset=lake_id) + .set_index(lake_id) + ) + + try: + waterbody_types_df = read_ngen_waterbody_type_df( + levelpool_params["reservoir_parameter_file"], + lake_id, + #self.waterbody_connections.values(), + ) + # Remove duplicate lake_ids and rows + waterbody_types_df =( + waterbody_types_df.reset_index() + .drop_duplicates(subset=lake_id) + .set_index(lake_id) + ) + + except ValueError: + #FIXME any reservoir operations requires some type + #So make this default to 1 (levelpool) + waterbody_types_df = pd.DataFrame(index=waterbody_df.index) + waterbody_types_df['reservoir_type'] = 1 + + return dataframe, flowpath_dict, waterbody_df, waterbody_types_df + def build_hyfeature_network(supernetwork_parameters, waterbody_parameters, ): @@ -779,7 +845,7 @@ def hyfeature_forcing( def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): """ - Reads lake.json file and prepares a dataframe, filtered + Reads .gpkg or lake.json file and prepares a dataframe, filtered to the relevant reservoirs, to provide the parameters for level-pool reservoir computation. """ @@ -792,8 +858,7 @@ def node_key_func(x): df.index = df.index.map(node_key_func) df.index.name = lake_index_field - #df = df.set_index(lake_index_field, append=True).reset_index(level=0) - #df.rename(columns={'level_0':'wb-id'}, inplace=True) + if lake_id_mask: df = df.loc[lake_id_mask] return df @@ -818,4 +883,38 @@ def node_key_func(x): if lake_id_mask: df = df.loc[lake_id_mask] - return df \ No newline at end of file + return df + +def read_geopkg(file_path): + flowpaths = gpd.read_file(file_path, layer="flowpaths") + attributes = gpd.read_file(file_path, layer="flowpath_attributes").drop('geometry', axis=1) + #merge all relevant data into a single dataframe + flowpaths = pd.merge(flowpaths, attributes, on='id') + + return flowpaths + +def read_json(file_path, edge_list): + dfs = [] + with open(edge_list) as edge_file: + edge_data = json.load(edge_file) + edge_map = {} + for id_dict in edge_data: + edge_map[ id_dict['id'] ] = id_dict['toid'] + with open(file_path) as data_file: + json_data = json.load(data_file) + for key_wb, value_params in json_data.items(): + df = pd.json_normalize(value_params) + df['id'] = key_wb + df['toid'] = edge_map[key_wb] + dfs.append(df) + df_main = pd.concat(dfs, ignore_index=True) + + return df_main + +def numeric_id(flowpath): + id = flowpath['id'].split('-')[-1] + toid = flowpath['toid'].split('-')[-1] + flowpath['id'] = int(id) + flowpath['toid'] = int(toid) + + return flowpath From 119b6c337e7af2aef6b9f0a96f4d8bc43b25e469 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 9 Dec 2022 18:45:05 +0000 Subject: [PATCH 31/54] updated Abstract and HyFeatures network objects to use new functions --- src/troute-network/troute/AbstractNetwork.py | 172 +++++++++++++++++- .../troute/HYFeaturesNetwork.py | 50 ----- .../troute/abstractnetwork_preprocess.py | 11 +- src/troute-nwm/src/nwm_routing/__main__.py | 50 ++--- 4 files changed, 201 insertions(+), 82 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index e0745f5fb..94594b6da 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -5,7 +5,8 @@ import time from troute.nhd_network import reverse_dict, extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition - +import troute.nhd_io as nhd_io +import troute.abstractnetwork_preprocess as abs_prep __verbose__ = False __showtiming__ = False @@ -19,7 +20,18 @@ class AbstractNetwork(ABC): "_reaches_by_tw", "_reverse_network", "_q0", "_t0", "_qlateral", "_break_segments", "_coastal_boundary_depth_df"] - def __init__(self, cols=None, terminal_code=None, break_points=None, verbose=False, showtiming=False): + def __init__( + self, + compute_parameters, + waterbody_parameters, + restart_parameters, + cols=None, + terminal_code=None, + break_points=None, + verbose=False, + showtiming=False + ): + global __verbose__, __showtiming__ __verbose__ = verbose __showtiming__ = showtiming @@ -44,7 +56,7 @@ def __init__(self, cols=None, terminal_code=None, break_points=None, verbose=Fal self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) self.set_index("key") self.sort_index() - self._waterbody_connections = None + self._waterbody_connections = {} self._gages = None self._connections = None self._independent_networks = None @@ -83,6 +95,160 @@ def __init__(self, cols=None, terminal_code=None, break_points=None, verbose=Fal self._break_segments = self._break_segments | set(self.waterbody_connections.values()) if break_points["break_network_at_gages"]: self._break_segments = self._break_segments | set(self.gages.values()) + + self._connections = extract_connections(self._dataframe, 'downstream', self._terminal_codes) + + ( + self._dataframe, + self._connections, + self.diffusive_network_data, + self.topobathy_df, + self.refactored_diffusive_domain, + self.refactored_reaches, + self.unrefactored_topobathy_df + ) = abs_prep.build_diffusive_domain( + compute_parameters, + self._dataframe, + self._connections, + ) + + ( + self._independent_networks, + self._reaches_by_tw, + self._reverse_network + ) = abs_prep.create_independent_networks( + waterbody_parameters, + self._connections, + self._waterbody_connections, + #gages, #TODO update how gages are provided when we figure out DA + ) + + ( + self._waterbody_df, + self._q0, + self._t0 + ) = abs_prep.initial_warmstate_preprocess( + break_points["break_network_at_waterbodies"], + restart_parameters, + self._dataframe.index, + self._waterbody_df, + ) + + def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): + """ + Assemble model forcings. Forcings include hydrological lateral inflows (qlats) + and coastal boundary depths for hybrid runs + + Aguments + -------- + - run (dict): List of forcing files pertaining to a + single run-set + - forcing_parameters (dict): User-input simulation forcing parameters + - hybrid_parameters (dict): User-input simulation hybrid parameters + - supernetwork_parameters (dict): User-input simulation supernetwork parameters + - segment_index (Int64): Reach segment ids + - cpu_pool (int): Number of CPUs in the process-parallel pool + + Returns + ------- + - qlats_df (Pandas DataFrame): Lateral inflow data, indexed by + segment ID + - coastal_bounary_depth_df (Pandas DataFrame): Coastal boundary water depths, + indexed by segment ID + + Notes + ----- + + """ + + # Unpack user-specified forcing parameters + dt = forcing_parameters.get("dt", None) + qts_subdivisions = forcing_parameters.get("qts_subdivisions", None) + qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) + qlat_file_index_col = forcing_parameters.get("qlat_file_index_col", "feature_id") + qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") + qlat_file_gw_bucket_flux_col = forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") + qlat_file_terrain_runoff_col = forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") + + + # TODO: find a better way to deal with these defaults and overrides. + run["t0"] = run.get("t0", self.t0) + run["nts"] = run.get("nts") + run["dt"] = run.get("dt", dt) + run["qts_subdivisions"] = run.get("qts_subdivisions", qts_subdivisions) + run["qlat_input_folder"] = run.get("qlat_input_folder", qlat_input_folder) + run["qlat_file_index_col"] = run.get("qlat_file_index_col", qlat_file_index_col) + run["qlat_file_value_col"] = run.get("qlat_file_value_col", qlat_file_value_col) + run["qlat_file_gw_bucket_flux_col"] = run.get("qlat_file_gw_bucket_flux_col", qlat_file_gw_bucket_flux_col) + run["qlat_file_terrain_runoff_col"] = run.get("qlat_file_terrain_runoff_col", qlat_file_terrain_runoff_col) + + #--------------------------------------------------------------------------- + # Assemble lateral inflow data + #--------------------------------------------------------------------------- + + # Place holder, if reading qlats from a file use this. + # TODO: add an option for reading qlat data from BMI/model engine + from_file = True + if from_file: + self._qlateral = abs_prep.build_qlateral_array( + run, + cpu_pool, + self._flowpath_dict, + supernetwork_parameters, + self._dataframe.index, + ) + + #--------------------------------------------------------------------- + # Assemble coastal coupling data [WIP] + #--------------------------------------------------------------------- + # Run if coastal_boundary_depth_df has not already been created: + if self._coastal_boundary_depth_df.empty: + coastal_boundary_elev_files = forcing_parameters.get('coastal_boundary_input_file', None) + coastal_boundary_domain_files = hybrid_parameters.get('coastal_boundary_domain', None) + + if coastal_boundary_elev_files: + #start_time = time.time() + #LOG.info("creating coastal dataframe ...") + + coastal_boundary_domain = nhd_io.read_coastal_boundary_domain(coastal_boundary_domain_files) + self._coastal_boundary_depth_df = nhd_io.build_coastal_ncdf_dataframe( + coastal_boundary_elev_files, + coastal_boundary_domain, + ) + + #LOG.debug( + # "coastal boundary elevation observation DataFrame creation complete in %s seconds." \ + # % (time.time() - start_time) + #) + + def new_q0(self, run_results): + """ + Prepare a new q0 dataframe with initial flow and depth to act as + a warmstate for the next simulation chunk. + """ + self._q0 = pd.concat( + [ + pd.DataFrame( + r[1][:, [-3, -3, -1]], index=r[0], columns=["qu0", "qd0", "h0"] + ) + for r in run_results + ], + copy=False, + ) + return self._q0 + + def update_waterbody_water_elevation(self): + """ + Update the starting water_elevation of each lake/reservoir + with flow and depth values from q0 + """ + self._waterbody_df.update(self._q0) + + def new_t0(self, dt, nts): + """ + Update t0 value for next loop iteration + """ + self._t0 += timedelta(seconds = dt * nts) @property def network_break_segments(self): diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index c7f6ee793..d69c0bf3f 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -98,56 +98,6 @@ def read_qlats(forcing_parameters, segment_index, nexus_to_downstream_flowpath_d return qlat_df -<<<<<<< HEAD -def read_nexus_file(nexus_file_path): - - #Currently reading data in format: - #[ - #{ - #"ID": "wb-44", - #"toID": "nex-45" - #}, - with open(nexus_file_path) as data_file: - json_data_list = json.load(data_file) - - nexus_to_downstream_flowpath_dict_str = {} - - for id_dict in json_data_list: - if "nex" in id_dict['ID']: - nexus_to_downstream_flowpath_dict_str[id_dict['ID']] = id_dict['toID'] - - # Extract the ID integer values - nexus_to_downstream_flowpath_dict = {node_key_func_nexus(k): node_key_func_wb(v) for k, v in nexus_to_downstream_flowpath_dict_str.items()} - - return nexus_to_downstream_flowpath_dict - -def read_json(file_path, edge_list): - dfs = [] - with open(edge_list) as edge_file: - edge_data = json.load(edge_file) - edge_map = {} - for id_dict in edge_data: - edge_map[ id_dict['id'] ] = id_dict['toid'] - with open(file_path) as data_file: - json_data = json.load(data_file) - for key_wb, value_params in json_data.items(): - df = pd.json_normalize(value_params) - df['id'] = key_wb - df['toid'] = edge_map[key_wb] - dfs.append(df) - df_main = pd.concat(dfs, ignore_index=True) - - return df_main - -def read_geopkg(file_path): - flowpaths = gpd.read_file(file_path, layer="flowpaths") - attributes = gpd.read_file(file_path, layer="flowpath_attributes").drop('geometry', axis=1) - #merge all relevant data into a single dataframe - flowpaths = pd.merge(flowpaths, attributes, on='id') - return flowpaths - -======= ->>>>>>> initial commit class HYFeaturesNetwork(AbstractNetwork): """ diff --git a/src/troute-network/troute/abstractnetwork_preprocess.py b/src/troute-network/troute/abstractnetwork_preprocess.py index be1bc9734..ab3c1942d 100644 --- a/src/troute-network/troute/abstractnetwork_preprocess.py +++ b/src/troute-network/troute/abstractnetwork_preprocess.py @@ -24,6 +24,7 @@ def build_diffusive_domain( compute_parameters, + param_df, connections, ): @@ -244,8 +245,9 @@ def create_independent_networks( waterbody_parameters, connections, wbody_conn, - gages, + gages = pd.DataFrame() #FIXME update default value when we update 'break_network_at_gages', ): + LOG.info("organizing connections into reaches ...") start_time = time.time() gage_break_segments = set() @@ -256,6 +258,7 @@ def create_independent_networks( ) # if streamflow DA, then break network at gages + #TODO update to work with HYFeatures, need to determine how we'll do DA... break_network_at_gages = False if break_network_at_waterbodies: @@ -274,7 +277,7 @@ def create_independent_networks( return independent_networks, reaches_bytw, rconn -def hyfeature_initial_warmstate_preprocess( +def initial_warmstate_preprocess( break_network_at_waterbodies, restart_parameters, segment_index, @@ -529,7 +532,7 @@ def build_forcing_sets( # Deduce the timeinterval of the forcing data from the output timestamps of the first # two ordered CHRTOUT files - if geo_file_type=='HYFeaturesNetowrk': + if geo_file_type=='HYFeaturesNetwork': df = read_file(first_file) t1_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") t1 = datetime.strptime(t1_str,"%Y-%m-%d_%H:%M:%S") @@ -602,7 +605,7 @@ def build_forcing_sets( final_qlat = qlat_input_folder.joinpath(run_sets[j]['qlat_files'][-1]) if geo_file_type=='NHDNetwork': final_timestamp_str = nhd_io.get_param_str(final_qlat,'model_output_valid_time') - elif geo_file_type=='HYFeaturesNetowrk': + elif geo_file_type=='HYFeaturesNetwork': df = read_file(final_qlat) final_timestamp_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index d3c8c6f33..dfd6d7b7d 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -30,6 +30,7 @@ import troute.nhd_network_utilities_v02 as nnu import troute.routing.diffusive_utils as diff_utils import troute.hyfeature_network_utilities as hnu +import troute.abstractnetwork_preprocess as abs_prep LOG = logging.getLogger('') @@ -84,14 +85,6 @@ def main_v04(argv): restart_parameters, forcing_parameters, verbose=True, showtiming=showtiming) - - network.create_routing_network(network.connections, - network.dataframe, - network.waterbody_connections, - network.gages, - preprocessing_parameters, - compute_parameters, - waterbody_parameters,) elif supernetwork_parameters["geo_file_type"] == 'NHDNetwork': network = NHDNetwork(supernetwork_parameters, @@ -110,10 +103,17 @@ def main_v04(argv): task_times['network_creation_time'] = network_end_time - network_start_time # Create run_sets: sets of forcing files for each loop + run_sets = abs_prep.build_forcing_sets( + supernetwork_parameters, + forcing_parameters, + network.t0 + ) + ''' if supernetwork_parameters["geo_file_type"] == 'NHDNetwork': run_sets = nnu.build_forcing_sets(forcing_parameters, network.t0) elif supernetwork_parameters["geo_file_type"] == 'HYFeaturesNetwork': run_sets = hnu.build_forcing_sets(forcing_parameters, network.t0) + ''' # Create da_sets: sets of TimeSlice files for each loop if "data_assimilation_parameters" in compute_parameters: @@ -126,22 +126,22 @@ def main_v04(argv): parity_sets = [] # Create forcing data within network object for first loop iteration - network.assemble_forcings(run_sets[0], forcing_parameters, hybrid_parameters, cpu_pool) + network.assemble_forcings(run_sets[0], forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool) # Create data assimilation object from da_sets for first loop iteration # TODO: Add data_assimilation for hyfeature network - if 1==2: - data_assimilation = AllDA(data_assimilation_parameters, - run_parameters, - waterbody_parameters, - network, - da_sets[0]) + data_assimilation = AllDA( + data_assimilation_parameters, + run_parameters, + waterbody_parameters, + network, + da_sets[0] + ) if showtiming: forcing_end_time = time.time() task_times['forcing_time'] += forcing_end_time - network_end_time - parallel_compute_method = compute_parameters.get("parallel_compute_method", None) subnetwork_target_size = compute_parameters.get("subnetwork_target_size", 1) qts_subdivisions = forcing_parameters.get("qts_subdivisions", 1) @@ -183,18 +183,18 @@ def main_v04(argv): network.dataframe, network.q0, network._qlateral, - pd.DataFrame(), #data_assimilation.usgs_df, - pd.DataFrame(), #data_assimilation.lastobs_df, - pd.DataFrame(), #data_assimilation.reservoir_usgs_df, - pd.DataFrame(), #data_assimilation.reservoir_usgs_param_df, - pd.DataFrame(), #data_assimilation.reservoir_usace_df, - pd.DataFrame(), #data_assimilation.reservoir_usace_param_df, - {}, #data_assimilation.assimilation_parameters, + data_assimilation.usgs_df, + data_assimilation.lastobs_df, + data_assimilation.reservoir_usgs_df, + data_assimilation.reservoir_usgs_param_df, + data_assimilation.reservoir_usace_df, + data_assimilation.reservoir_usace_param_df, + data_assimilation.assimilation_parameters, assume_short_ts, return_courant, - network._waterbody_df, ## check: network._waterbody_df ?? def name is different from return self._ .. + network.waterbody_dataframe, waterbody_parameters, - network._waterbody_types_df, ## check: network._waterbody_types_df ?? def name is different from return self._ .. + network.waterbody_types_dataframe, network.waterbody_type_specified, network.diffusive_network_data, network.topobathy_df, From 4ffeeb8127df375514f2e41cc1a61ab00dbea72e Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Mon, 12 Dec 2022 17:35:11 +0000 Subject: [PATCH 32/54] bug fixes to get hyfeatures to run properly --- src/troute-network/troute/AbstractNetwork.py | 21 +++++++++---- .../troute/HYFeaturesNetwork.py | 17 ++++++++--- .../troute/abstractnetwork_preprocess.py | 30 ++++++++++++++++--- src/troute-nwm/src/nwm_routing/__main__.py | 1 + .../unittest_hyfeature.yaml | 2 +- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 94594b6da..189a48700 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -16,8 +16,9 @@ class AbstractNetwork(ABC): """ __slots__ = ["_dataframe", "_waterbody_connections", "_gages", "_terminal_codes", "_connections", "_waterbody_df", - "_waterbody_types_df", "_independent_networks", - "_reaches_by_tw", "_reverse_network", "_q0", "_t0", + "_waterbody_types_df", "_waterbody_type_specified", + "_independent_networks", "_reaches_by_tw", + "_reverse_network", "_q0", "_t0", "_qlateral", "_break_segments", "_coastal_boundary_depth_df"] def __init__( @@ -31,7 +32,7 @@ def __init__( verbose=False, showtiming=False ): - + global __verbose__, __showtiming__ __verbose__ = verbose __showtiming__ = showtiming @@ -65,6 +66,7 @@ def __init__( self._q0 = None self._t0 = None self._qlateral = None + self._waterbody_type_specified = None #qlat_const = forcing_parameters.get("qlat_const", 0) #FIXME qlat_const """ Figure out a good way to default initialize to qlat_const/c @@ -86,9 +88,10 @@ def __init__( ~self._dataframe["downstream"].isin(self._dataframe.index) ]["downstream"].values ) + # There can be an externally determined terminal code -- that's this value self._terminal_codes.add(terminal_code) - + self._break_segments = set() if break_points: if break_points["break_network_at_waterbodies"]: @@ -97,7 +100,7 @@ def __init__( self._break_segments = self._break_segments | set(self.gages.values()) self._connections = extract_connections(self._dataframe, 'downstream', self._terminal_codes) - + ( self._dataframe, self._connections, @@ -111,7 +114,7 @@ def __init__( self._dataframe, self._connections, ) - + ( self._independent_networks, self._reaches_by_tw, @@ -310,6 +313,12 @@ def waterbody_dataframe(self): @property def waterbody_types_dataframe(self): return self._waterbody_types_df + + @property + def waterbody_type_specified(self): + if self._waterbody_type_specified is None: + self._waterbody_type_specified = False + return self._waterbody_type_specified @property def connections(self): diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index d69c0bf3f..dab10aa00 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -103,8 +103,7 @@ class HYFeaturesNetwork(AbstractNetwork): """ __slots__ = ["_flowpath_dict", - "segment_index", - "waterbody_type_specified", + "segment_index", "diffusive_network_data", "topobathy_df", "refactored_diffusive_domain", @@ -112,7 +111,8 @@ class HYFeaturesNetwork(AbstractNetwork): "unrefactored_topobathy_df"] def __init__(self, supernetwork_parameters, - waterbody_parameters=None, + waterbody_parameters, + data_assimilation_parameters, restart_parameters=None, forcing_parameters=None, verbose=False, @@ -139,7 +139,16 @@ def __init__(self, supernetwork_parameters, waterbody_parameters, ) - + + cols = supernetwork_parameters.get('columns',None) + terminal_code = supernetwork_parameters.get('terminal_code',0) + break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) + streamflow_da = data_assimilation_parameters.get('streamflow_da', False) + break_network_at_gages = False + if streamflow_da: + break_network_at_gages = streamflow_da.get('streamflow_nudging', False) + break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, + "break_network_at_gages": break_network_at_gages} diff --git a/src/troute-network/troute/abstractnetwork_preprocess.py b/src/troute-network/troute/abstractnetwork_preprocess.py index ab3c1942d..2b295be9f 100644 --- a/src/troute-network/troute/abstractnetwork_preprocess.py +++ b/src/troute-network/troute/abstractnetwork_preprocess.py @@ -366,7 +366,7 @@ def initial_warmstate_preprocess( ) waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on="lake_id" + waterbodies_df, waterbodies_initial_states_df, on="wb-id" ) LOG.debug( @@ -625,6 +625,10 @@ def build_qlateral_array( supernetwork_parameters, segment_index=pd.Index([]), ): + + start_time = time.time() + LOG.info("Creating a DataFrame of lateral inflow forcings ...") + # TODO: set default/optional arguments qts_subdivisions = run.get("qts_subdivisions", 1) nts = run.get("nts", 1) @@ -674,7 +678,9 @@ def build_qlateral_array( index = idx, columns = range(len(qlat_files)) ) - elif geo_file_type=='HYFeaturesNetowrk': + + qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + elif geo_file_type=='HYFeaturesNetwork': dfs=[] for f in qlat_files: df = read_file(f).set_index(['feature_id']) @@ -683,13 +689,24 @@ def build_qlateral_array( # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids nexuses_lateralflows_df = pd.concat(dfs, axis=1) - # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids + # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) for k,v in nexus_to_upstream_flowpath_dict.items() ), axis=1 ).T qlats_df.columns=range(len(qlat_files)) + qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + + # The segment_index has the full network set of segments/flowpaths. + # Whereas the set of flowpaths that are downstream of nexuses is a + # subset of the segment_index. Therefore, all of the segments/flowpaths + # that are not accounted for in the set of flowpaths downstream of + # nexuses need to be added to the qlateral dataframe and padded with + # zeros. + all_df = pd.DataFrame( np.zeros( (len(segment_index), len(qlats_df.columns)) ), index=segment_index, + columns=qlats_df.columns ) + all_df.loc[ qlats_df.index ] = qlats_df + qlats_df = all_df.sort_index() - qlats_df = qlats_df[qlats_df.index.isin(segment_index)] elif qlat_input_file: qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) else: @@ -708,6 +725,11 @@ def build_qlateral_array( if not segment_index.empty: qlats_df = qlats_df[qlats_df.index.isin(segment_index)] + + LOG.debug( + "lateral inflow DataFrame creation complete in %s seconds." \ + % (time.time() - start_time) + ) return qlats_df diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index dfd6d7b7d..dd6ee9283 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -82,6 +82,7 @@ def main_v04(argv): if supernetwork_parameters["geo_file_type"] == 'HYFeaturesNetwork': network = HYFeaturesNetwork(supernetwork_parameters, waterbody_parameters, + data_assimilation_parameters, restart_parameters, forcing_parameters, verbose=True, showtiming=showtiming) diff --git a/test/unit_test_hyfeature/unittest_hyfeature.yaml b/test/unit_test_hyfeature/unittest_hyfeature.yaml index a04032d8c..5ecaeb3ca 100644 --- a/test/unit_test_hyfeature/unittest_hyfeature.yaml +++ b/test/unit_test_hyfeature/unittest_hyfeature.yaml @@ -60,7 +60,7 @@ compute_parameters: qts_subdivisions : 12 dt : 300 # [sec] qlat_input_folder : channel_forcing/ - qlat_file_pattern_filter : "*.CHRTOUT_DOMAIN1" + qlat_file_pattern_filter : "*NEXOUT.csv" nexus_input_folder : channel_forcing/ nexus_file_pattern_filter : "*NEXOUT.csv" #OR "*NEXOUT.parquet" OR "nex-*" binary_nexus_file_folder : binary_files # this is required if nexus_file_pattern_filter="nex-*" From d2cfe51f1fce89ac76ce192f9b5392b8aed5ae0f Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Thu, 15 Dec 2022 22:00:55 +0000 Subject: [PATCH 33/54] updates to NHDNetwork and functions in AbstractNetwork --- src/troute-network/troute/AbstractNetwork.py | 44 ++- .../troute/HYFeaturesNetwork.py | 94 ++----- src/troute-network/troute/NHDNetwork.py | 258 ++++-------------- .../troute/abstractnetwork_preprocess.py | 15 +- .../troute/hyfeature_preprocess.py | 22 +- src/troute-network/troute/nhd_io.py | 2 +- src/troute-network/troute/nhd_preprocess.py | 232 ++++++++++++++++ src/troute-nwm/src/nwm_routing/__main__.py | 19 +- test/LowerColorado_TX/test_AnA.yaml | 30 +- test/ngen/test_AnA.yaml | 106 +++++++ 10 files changed, 479 insertions(+), 343 deletions(-) create mode 100644 test/ngen/test_AnA.yaml diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 189a48700..0a19d9156 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -1,14 +1,12 @@ from abc import ABC, abstractmethod from functools import partial import pandas as pd -from datetime import datetime +from datetime import datetime, timedelta import time from troute.nhd_network import reverse_dict, extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition import troute.nhd_io as nhd_io import troute.abstractnetwork_preprocess as abs_prep -__verbose__ = False -__showtiming__ = False class AbstractNetwork(ABC): """ @@ -19,7 +17,9 @@ class AbstractNetwork(ABC): "_waterbody_types_df", "_waterbody_type_specified", "_independent_networks", "_reaches_by_tw", "_reverse_network", "_q0", "_t0", - "_qlateral", "_break_segments", "_coastal_boundary_depth_df"] + "_qlateral", "_break_segments", "_coastal_boundary_depth_df", + "diffusive_network_data", "topobathy_df", "refactored_diffusive_domain", + "refactored_reaches", "unrefactored_topobathy_df", "segment_index"] def __init__( self, @@ -27,15 +27,15 @@ def __init__( waterbody_parameters, restart_parameters, cols=None, - terminal_code=None, break_points=None, verbose=False, showtiming=False ): - + global __verbose__, __showtiming__ __verbose__ = verbose __showtiming__ = showtiming + if cols: self._dataframe = self._dataframe[list(cols.values())] # Rename parameter columns to standard names: from route-link names @@ -57,9 +57,8 @@ def __init__( self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) self.set_index("key") self.sort_index() - self._waterbody_connections = {} - self._gages = None - self._connections = None + self._waterbody_connections = {} #TODO set in individual network objects?... + self._gages = None #TODO set in individual network objects?... self._independent_networks = None self._reverse_network = None self._reaches_by_tw = None @@ -78,20 +77,6 @@ def __init__( dtype="float32", ) """ - # there may be off-domain nodes that are not explicitly identified - # but which are terminal (i.e., off-domain) as a result of a mask or some other - # an interior domain truncation that results in a - # otherwise valid node value being pointed to, but which is masked out or - # being intentionally separated into another domain. - self._terminal_codes = set( - self._dataframe[ - ~self._dataframe["downstream"].isin(self._dataframe.index) - ]["downstream"].values - ) - - # There can be an externally determined terminal code -- that's this value - self._terminal_codes.add(terminal_code) - self._break_segments = set() if break_points: if break_points["break_network_at_waterbodies"]: @@ -99,8 +84,6 @@ def __init__( if break_points["break_network_at_gages"]: self._break_segments = self._break_segments | set(self.gages.values()) - self._connections = extract_connections(self._dataframe, 'downstream', self._terminal_codes) - ( self._dataframe, self._connections, @@ -126,6 +109,11 @@ def __init__( #gages, #TODO update how gages are provided when we figure out DA ) + if __verbose__: + print("setting waterbody and channel initial states ...") + if __showtiming__: + start_time = time.time() + ( self._waterbody_df, self._q0, @@ -137,6 +125,12 @@ def __init__( self._waterbody_df, ) + if __verbose__: + print("waterbody and channel initial states complete") + if __showtiming__: + print("... in %s seconds." % (time.time() - start_time)) + start_time = time.time() + def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): """ Assemble model forcings. Forcings include hydrological lateral inflows (qlats) diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index dab10aa00..56c25a524 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -1,17 +1,9 @@ from .AbstractNetwork import AbstractNetwork -import pathlib -import json import pandas as pd import numpy as np import time -import re import troute.nhd_io as nhd_io #FIXME -from itertools import chain -import geopandas as gpd -from pathlib import Path -import math import troute.hyfeature_preprocess as hyfeature_prep -from datetime import datetime, timedelta __verbose__ = False __showtiming__ = False @@ -104,11 +96,7 @@ class HYFeaturesNetwork(AbstractNetwork): """ __slots__ = ["_flowpath_dict", "segment_index", - "diffusive_network_data", - "topobathy_df", - "refactored_diffusive_domain", - "refactored_reaches", - "unrefactored_topobathy_df"] + ] def __init__(self, supernetwork_parameters, waterbody_parameters, @@ -133,15 +121,21 @@ def __init__(self, #------------------------------------------------ (self._dataframe, self._flowpath_dict, + self._connections, self._waterbody_df, self._waterbody_types_df, + self._terminal_codes, ) = hyfeature_prep.read_geo_file( supernetwork_parameters, waterbody_parameters, ) + if __verbose__: + print("supernetwork connections set complete") + if __showtiming__: + print("... in %s seconds." % (time.time() - start_time)) + cols = supernetwork_parameters.get('columns',None) - terminal_code = supernetwork_parameters.get('terminal_code',0) break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) streamflow_da = data_assimilation_parameters.get('streamflow_da', False) break_network_at_gages = False @@ -150,69 +144,15 @@ def __init__(self, break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} - - - - - #------------------------------------------------ - # Preprocess network attributes - #------------------------------------------------ - (self._dataframe, - self._flowpath_dict, - self._waterbody_types_df, - self._waterbody_df, - self.waterbody_type_specified, - cols, - terminal_code, - break_points, - ) = hyfeature_prep.build_hyfeature_network( - supernetwork_parameters, - waterbody_parameters - ) - - # called to mainly initialize _waterbody_connections, _connections, _independent_networks, - # _reverse_network, _reaches_by_tw - super().__init__(cols, terminal_code, break_points) - - if __verbose__: - print("supernetwork connections set complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) - - - - # list of all segments in the domain (MC + diffusive) - self.segment_index = self._dataframe.index - #if self.diffusive_network_data: - # for tw in self.diffusive_network_data: - # self.segment_index = self.segment_index.append( - # pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) - # ) - - #------------------------------------------------ - # Handle Channel Initial States - #------------------------------------------------ - if __verbose__: - print("setting waterbody and channel initial states ...") - if __showtiming__: - start_time = time.time() - - (#self._waterbody_df, - self._q0, - self._t0,) = hyfeature_prep.hyfeature_initial_warmstate_preprocess( - #break_network_at_waterbodies, - restart_parameters, - #data_assimilation_parameters, - self.segment_index, - #self._waterbody_df, - #self.link_lake_crosswalk, - ) - - if __verbose__: - print("waterbody and channel initial states complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) - start_time = time.time() + super().__init__( + compute_parameters, + waterbody_parameters, + restart_parameters, + cols, + break_points, + verbose=__verbose__, + showtiming=__showtiming__, + ) # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index b72996a71..ff2c3ab0d 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -11,74 +11,15 @@ __showtiming__ = True #FIXME pass flag __verbose__ = True #FIXME pass verbosity -def read_qlats(forcing_parameters, segment_index): - # STEP 5: Read (or set) QLateral Inputs - if __showtiming__: - start_time = time.time() - if __verbose__: - print("creating qlateral array ...") - qts_subdivisions = forcing_parameters.get("qts_subdivisions", 1) - nts = forcing_parameters.get("nts", 1) - qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) - qlat_input_file = forcing_parameters.get("qlat_input_file", None) - if qlat_input_folder: - qlat_input_folder = pathlib.Path(qlat_input_folder) - if "qlat_files" in forcing_parameters: - qlat_files = forcing_parameters.get("qlat_files") - qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] - elif "qlat_file_pattern_filter" in forcing_parameters: - qlat_file_pattern_filter = forcing_parameters.get( - "qlat_file_pattern_filter", "*CHRT_OUT*" - ) - qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) - - qlat_file_index_col = forcing_parameters.get( - "qlat_file_index_col", "feature_id" - ) - qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") - - qlat_df = nhd_io.get_ql_from_wrf_hydro_mf( - qlat_files=qlat_files, - #ts_iterator=ts_iterator, - #file_run_size=file_run_size, - index_col=qlat_file_index_col, - value_col=qlat_file_value_col, - ) - qlat_df = qlat_df[qlat_df.index.isin(segment_index)] - elif qlat_input_file: - qlat_df = nhd_io.get_ql_from_csv(qlat_input_file) - else: - qlat_const = forcing_parameters.get("qlat_const", 0) - qlat_df = pd.DataFrame( - qlat_const, - index=segment_index, - columns=range(nts // qts_subdivisions), - dtype="float32", - ) - - max_col = 1 + nts // qts_subdivisions - - if len(qlat_df.columns) > max_col: - qlat_df.drop(qlat_df.columns[max_col:], axis=1, inplace=True) - - if not segment_index.empty: - qlat_df = qlat_df[qlat_df.index.isin(segment_index)] - - if __verbose__: - print("qlateral array complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) - - return qlat_df class NHDNetwork(AbstractNetwork): """ """ - __slots__ = ["waterbody_type_specified", "link_lake_crosswalk", "link_gage_df", - "usgs_lake_gage_crosswalk", "usace_lake_gage_crosswalk", - "diffusive_network_data", "topobathy_df", "refactored_diffusive_domain", - "refactored_reaches", "unrefactored_topobathy_df", "segment_index"] + __slots__ = [ + "_link_lake_crosswalk", "_usgs_lake_gage_crosswalk", + "_usace_lake_gage_crosswalk", "_flowpath_dict" + ] def __init__( self, @@ -90,9 +31,8 @@ def __init__( data_assimilation_parameters=None, preprocessing_parameters=None, verbose=False, - showtiming=False, - layer_string=None, - driver_string=None,): + showtiming=False, + ): """ """ @@ -104,167 +44,65 @@ def __init__( if __showtiming__: start_time = time.time() - #------------------------------------------------ - # Preprocess network attributes + # Load Geo Data #------------------------------------------------ - - (self._connections, - self._dataframe, - self._waterbody_connections, - self._waterbody_df, - self._waterbody_types_df, - break_network_at_waterbodies, - self.waterbody_type_specified, - self.link_lake_crosswalk, - self._independent_networks, - self._reaches_by_tw, - self._reverse_network, - self.link_gage_df, - self.usgs_lake_gage_crosswalk, - self.usace_lake_gage_crosswalk, - self.diffusive_network_data, - self.topobathy_df, - self.refactored_diffusive_domain, - self.refactored_reaches, - self.unrefactored_topobathy_df - ) = nhd_prep.build_nhd_network( + + ( + self._dataframe, + self._connections, + self._terminal_codes, + self._waterbody_df, + self._waterbody_types_df, + self._waterbody_type_specified, + self._waterbody_connections, + self._link_lake_crosswalk, + self._gages, + self._usgs_lake_gage_crosswalk, + self._usace_lake_gage_crosswalk, + ) = nhd_prep.read_geo_file( supernetwork_parameters, waterbody_parameters, - preprocessing_parameters, - compute_parameters, - data_assimilation_parameters - ) - - # list of all segments in the domain (MC + diffusive) - self.segment_index = self._dataframe.index - if self.diffusive_network_data: - for tw in self.diffusive_network_data: - self.segment_index = self.segment_index.append( - pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) - ) - - ''' - #FIXME the base class constructor is finiky - #as it requires the _dataframe, then sets some - #initial default properties...which, at the moment - #are used by the subclass constructor. - #So it needs to be called at just the right spot... - cols = supernetwork_parameters.get( - 'columns', - { - 'key' : 'link', - 'downstream': 'to', - 'dx' : 'Length', - 'n' : 'n', - 'ncc' : 'nCC', - 's0' : 'So', - 'bw' : 'BtmWdth', - 'waterbody' : 'NHDWaterbodyComID', - 'gages' : 'gages', - 'tw' : 'TopWdth', - 'twcc' : 'TopWdthCC', - 'alt' : 'alt', - 'musk' : 'MusK', - 'musx' : 'MusX', - 'cs' : 'ChSlp', - } - ) - terminal_code = supernetwork_parameters.get("terminal_code", 0) - break_network_at_waterbodies = supernetwork_parameters.get( - "break_network_at_waterbodies", False - ) - break_network_at_gages = supernetwork_parameters.get( - "break_network_at_gages", False + data_assimilation_parameters, ) - break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, - "break_network_at_gages": break_network_at_gages} - super().__init__(cols, terminal_code, break_points) - ''' - if __verbose__: print("supernetwork connections set complete") if __showtiming__: print("... in %s seconds." % (time.time() - start_time)) - - #----------------------------------------------------- - # Set initial waterbody and channel states - #----------------------------------------------------- + + cols = supernetwork_parameters.get('columns',None) + break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) + streamflow_da = data_assimilation_parameters.get('streamflow_da', False) + break_network_at_gages = False + if streamflow_da: + break_network_at_gages = streamflow_da.get('streamflow_nudging', False) + break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, + "break_network_at_gages": break_network_at_gages} - if __verbose__: - print("setting waterbody and channel initial states ...") - if __showtiming__: - start_time = time.time() + self._flowpath_dict = {} - (self._waterbody_df, - self._q0, - self._t0,) = nhd_prep.nhd_initial_warmstate_preprocess( - break_network_at_waterbodies, - restart_parameters, - data_assimilation_parameters, - self.segment_index, - self._waterbody_df, - self.link_lake_crosswalk, - ) + super().__init__( + compute_parameters, + waterbody_parameters, + restart_parameters, + cols, + break_points, + verbose=__verbose__, + showtiming=__showtiming__, + ) - if __verbose__: - print("waterbody and channel initial states complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) - start_time = time.time() + # list of all segments in the domain (MC + diffusive) + self.segment_index = self._dataframe.index + if self.diffusive_network_data: + for tw in self.diffusive_network_data: + self.segment_index = self.segment_index.append( + pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) + ) # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't self._coastal_boundary_depth_df = pd.DataFrame() - - - def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, cpu_pool): - """ - Assembles model forcings for hydrological lateral inflows and coastal boundary - depths (hybrid simulations). Run this function after network initialization - and after any iteration loop in main. - """ - (self._qlateral, - self._coastal_boundary_depth_df - ) = nhd_prep.nhd_forcing( - run, - forcing_parameters, - hybrid_parameters, - self.segment_index, - cpu_pool, - self._t0, - self._coastal_boundary_depth_df, - ) - - def new_q0(self, run_results): - """ - Prepare a new q0 dataframe with initial flow and depth to act as - a warmstate for the next simulation chunk. - """ - self._q0 = pd.concat( - [ - pd.DataFrame( - r[1][:, [-3, -3, -1]], index=r[0], columns=["qu0", "qd0", "h0"] - ) - for r in run_results - ], - copy=False, - ) - - #def update_waterbody_water_elevation(self): - def update_waterbody_water_elevation(self): - """ - Update the starting water_elevation of each lake/reservoir - with flow and depth values from q0 - """ - self._waterbody_df.update(self._q0) - - def new_t0(self, dt, nts): - """ - Update t0 value for next loop iteration - """ - self._t0 += timedelta(seconds = dt * nts) def extract_waterbody_connections(rows, target_col, waterbody_null=-9999): """Extract waterbody mapping from dataframe. diff --git a/src/troute-network/troute/abstractnetwork_preprocess.py b/src/troute-network/troute/abstractnetwork_preprocess.py index 2b295be9f..25d76ffde 100644 --- a/src/troute-network/troute/abstractnetwork_preprocess.py +++ b/src/troute-network/troute/abstractnetwork_preprocess.py @@ -154,7 +154,7 @@ def build_diffusive_domain( # diffusive domain tributary segments trib_segs = [] - + for seg in mainstem_segs: us_list = rconn_diff0[seg] for u in us_list: @@ -315,6 +315,9 @@ def initial_warmstate_preprocess( ----- ''' + # generalize waterbody ID's to be used with any network + index_id = waterbodies_df.index.names[0] + #---------------------------------------------------------------------------- # Assemble waterbody initial states (outflow and pool elevation #---------------------------------------------------------------------------- @@ -335,7 +338,7 @@ def initial_warmstate_preprocess( waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( restart_parameters["wrf_hydro_waterbody_restart_file"], restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], - restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", 'lake_id'), + restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", index_id), restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], restart_parameters.get( "wrf_hydro_waterbody_crosswalk_filter_file_field_name", @@ -366,7 +369,7 @@ def initial_warmstate_preprocess( ) waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on="wb-id" + waterbodies_df, waterbodies_initial_states_df, on=index_id ) LOG.debug( @@ -551,7 +554,7 @@ def build_forcing_sets( # determine qts_subdivisions qts_subdivisions = dt_qlat / dt if dt_qlat % dt == 0: - qts_subdivisions = dt_qlat / dt + qts_subdivisions = int(dt_qlat / dt) # make sure that qts_subdivisions = dt_qlat / dt forcing_parameters['qts_subdivisions']= qts_subdivisions @@ -661,9 +664,9 @@ def build_qlateral_array( jobs = [] for f in qlat_files: jobs.append( - #delayed(nhd_io.get_ql_from_chrtout) + delayed(nhd_io.get_ql_from_chrtout) #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) - delayed(nhd_io.get_ql_from_csv) + #delayed(nhd_io.get_ql_from_csv) (f) ) ql_list = parallel(jobs) diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index 587bb677a..1c2aabc75 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -45,6 +45,26 @@ def read_geo_file( # ********** need to be included in flowpath_attributes ************* dataframe['alt'] = 1.0 #FIXME get the right value for this... + # numeric code used to indicate network terminal segments + terminal_code = supernetwork_parameters.get("terminal_code", 0) + + # There can be an externally determined terminal code -- that's this first value + terminal_codes = set() + terminal_codes.add(terminal_code) + # ... but there may also be off-domain nodes that are not explicitly identified + # but which are terminal (i.e., off-domain) as a result of a mask or some other + # an interior domain truncation that results in a + # otherwise valid node value being pointed to, but which is masked out or + # being intentionally separated into another domain. + terminal_codes = terminal_codes | set( + dataframe[~dataframe["downstream"].isin(dataframe.index)]["downstream"].values + ) + + # build connections dictionary + connections = nhd_network.extract_connections( + dataframe, "downstream", terminal_codes=terminal_codes + ) + #Load waterbody/reservoir info if waterbody_parameters: levelpool_params = waterbody_parameters.get('level_pool', None) @@ -84,7 +104,7 @@ def read_geo_file( waterbody_types_df = pd.DataFrame(index=waterbody_df.index) waterbody_types_df['reservoir_type'] = 1 - return dataframe, flowpath_dict, waterbody_df, waterbody_types_df + return dataframe, flowpath_dict, connections, waterbody_df, waterbody_types_df, terminal_codes def build_hyfeature_network(supernetwork_parameters, waterbody_parameters, diff --git a/src/troute-network/troute/nhd_io.py b/src/troute-network/troute/nhd_io.py index 3515cd2d6..c278d5a9a 100644 --- a/src/troute-network/troute/nhd_io.py +++ b/src/troute-network/troute/nhd_io.py @@ -746,7 +746,7 @@ def write_chrtout( LOG.debug("%d CHRTOUT files will be written." % (nfiles_to_write)) LOG.debug("Extracting flow DataFrame on qts_subdivisions from FVD DataFrame") start = time.time() - + flow = flowveldepth.loc[:, ::3].iloc[:, qts_subdivisions-1::qts_subdivisions] LOG.debug("Extracting flow DataFrame took %s seconds." % (time.time() - start)) diff --git a/src/troute-network/troute/nhd_preprocess.py b/src/troute-network/troute/nhd_preprocess.py index dc303815b..2422bac84 100644 --- a/src/troute-network/troute/nhd_preprocess.py +++ b/src/troute-network/troute/nhd_preprocess.py @@ -14,6 +14,238 @@ LOG = logging.getLogger('') +def read_geo_file(supernetwork_parameters, waterbody_parameters, data_assimilation_parameters): + ''' + Construct network connections network, parameter dataframe, waterbody mapping, + and gage mapping. This is an intermediate-level function that calls several + lower level functions to read data, conduct network operations, and extract mappings. + + Arguments + --------- + supernetwork_parameters (dict): User input network parameters + + Returns: + -------- + connections (dict int: [int]): Network connections + param_df (DataFrame): Geometry and hydraulic parameters + wbodies (dict, int: int): segment-waterbody mapping + gages (dict, int: int): segment-gage mapping + + ''' + + # crosswalking dictionary between variables names in input dataset and + # variable names recognized by troute.routing module. + cols = supernetwork_parameters.get( + 'columns', + { + 'key' : 'link', + 'downstream': 'to', + 'dx' : 'Length', + 'n' : 'n', + 'ncc' : 'nCC', + 's0' : 'So', + 'bw' : 'BtmWdth', + 'waterbody' : 'NHDWaterbodyComID', + 'gages' : 'gages', + 'tw' : 'TopWdth', + 'twcc' : 'TopWdthCC', + 'alt' : 'alt', + 'musk' : 'MusK', + 'musx' : 'MusX', + 'cs' : 'ChSlp', + } + ) + + # numeric code used to indicate network terminal segments + terminal_code = supernetwork_parameters.get("terminal_code", 0) + + # read parameter dataframe + param_df = nhd_io.read(pathlib.Path(supernetwork_parameters["geo_file_path"])) + + # select the column names specified in the values in the cols dict variable + param_df = param_df[list(cols.values())] + + # rename dataframe columns to keys in the cols dict variable + param_df = param_df.rename(columns=nhd_network.reverse_dict(cols)) + + # handle synthetic waterbody segments + synthetic_wb_segments = supernetwork_parameters.get("synthetic_wb_segments", None) + synthetic_wb_id_offset = supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) + if synthetic_wb_segments: + # rename the current key column to key32 + key32_d = {"key":"key32"} + param_df = param_df.rename(columns=key32_d) + # create a key index that is int64 + # copy the links into the new column + param_df["key"] = param_df.key32.astype("int64") + # update the values of the synthetic reservoir segments + fix_idx = param_df.key.isin(set(synthetic_wb_segments)) + param_df.loc[fix_idx,"key"] = (param_df[fix_idx].key + synthetic_wb_id_offset).astype("int64") + + # set parameter dataframe index as segment id number, sort + param_df = param_df.set_index("key").sort_index() + + # get and apply domain mask + if "mask_file_path" in supernetwork_parameters: + data_mask = nhd_io.read_mask( + pathlib.Path(supernetwork_parameters["mask_file_path"]), + layer_string=supernetwork_parameters.get("mask_layer_string", None), + ) + data_mask = data_mask.set_index(data_mask.columns[0]) + param_df = param_df.filter(data_mask.index, axis=0) + + # map segment ids to waterbody ids + wbodies = {} + if "waterbody" in cols: + wbodies = nhd_network.extract_waterbody_connections( + param_df[["waterbody"]] + ) + param_df = param_df.drop("waterbody", axis=1) + + # map segment ids to gage ids + gages = {} + if "gages" in cols: + gages = nhd_network.gage_mapping(param_df[["gages"]]) + param_df = param_df.drop("gages", axis=1) + + # There can be an externally determined terminal code -- that's this first value + terminal_codes = set() + terminal_codes.add(terminal_code) + # ... but there may also be off-domain nodes that are not explicitly identified + # but which are terminal (i.e., off-domain) as a result of a mask or some other + # an interior domain truncation that results in a + # otherwise valid node value being pointed to, but which is masked out or + # being intentionally separated into another domain. + terminal_codes = terminal_codes | set( + param_df[~param_df["downstream"].isin(param_df.index)]["downstream"].values + ) + + # build connections dictionary + connections = nhd_network.extract_connections( + param_df, "downstream", terminal_codes=terminal_codes + ) + param_df = param_df.drop("downstream", axis=1) + + param_df = param_df.astype("float32") + + break_network_at_waterbodies = waterbody_parameters.get( + "break_network_at_waterbodies", False + ) + + # if waterbodies are being simulated, adjust the connections graph so that + # waterbodies are collapsed to single nodes. Also, build a mapping between + # waterbody outlet segments and lake ids + if break_network_at_waterbodies: + connections, link_lake_crosswalk = nhd_network.replace_waterbodies_connections( + connections, wbodies + ) + else: + link_lake_crosswalk = None + + #============================================================================ + # Retrieve and organize waterbody parameters + + waterbody_type_specified = False + if break_network_at_waterbodies: + + # Read waterbody parameters from LAKEPARM file + level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) + waterbodies_df = nhd_io.read_lakeparm( + level_pool_params['level_pool_waterbody_parameter_file_path'], + level_pool_params.get("level_pool_waterbody_id", 'lake_id'), + wbodies.values() + ) + + # Remove duplicate lake_ids and rows + waterbodies_df = ( + waterbodies_df.reset_index() + .drop_duplicates(subset="lake_id") + .set_index("lake_id") + ) + + # Declare empty dataframe + waterbody_types_df = pd.DataFrame() + + # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True + reservoir_da = data_assimilation_parameters.get( + 'reservoir_da', + {} + ) + + if reservoir_da: + usgs_hybrid = reservoir_da.get( + 'reservoir_persistence_usgs', + False + ) + usace_hybrid = reservoir_da.get( + 'reservoir_persistence_usace', + False + ) + param_file = reservoir_da.get( + 'gage_lakeID_crosswalk_file', + None + ) + else: + param_file = None + usace_hybrid = False + usgs_hybrid = False + + # check if RFC-type reservoirs are set to true + rfc_params = waterbody_parameters.get('rfc') + if rfc_params: + rfc_forecast = rfc_params.get( + 'reservoir_rfc_forecasts', + False + ) + param_file = rfc_params.get('reservoir_parameter_file',None) + else: + rfc_forecast = False + + if (param_file and reservoir_da) or (param_file and rfc_forecast): + waterbody_type_specified = True + ( + waterbody_types_df, + usgs_lake_gage_crosswalk, + usace_lake_gage_crosswalk + ) = nhd_io.read_reservoir_parameter_file( + param_file, + usgs_hybrid, + usace_hybrid, + rfc_forecast, + level_pool_params.get("level_pool_waterbody_id", 'lake_id'), + reservoir_da.get('crosswalk_usgs_gage_field', 'usgs_gage_id'), + reservoir_da.get('crosswalk_usgs_lakeID_field', 'usgs_lake_id'), + reservoir_da.get('crosswalk_usace_gage_field', 'usace_gage_id'), + reservoir_da.get('crosswalk_usace_lakeID_field', 'usace_lake_id'), + wbodies.values(), + ) + else: + waterbody_type_specified = True + waterbody_types_df = pd.DataFrame(data = 1, index = waterbodies_df.index, columns = ['reservoir_type']) + usgs_lake_gage_crosswalk = None + usace_lake_gage_crosswalk = None + + else: + # Declare empty dataframes + waterbody_types_df = pd.DataFrame() + waterbodies_df = pd.DataFrame() + usgs_lake_gage_crosswalk = None + usace_lake_gage_crosswalk = None + + return ( + param_df, + connections, + terminal_codes, + waterbodies_df, + waterbody_types_df, + waterbody_type_specified, + wbodies, + link_lake_crosswalk, + gages, + usgs_lake_gage_crosswalk, + usace_lake_gage_crosswalk) + + def build_nhd_network(supernetwork_parameters,waterbody_parameters, preprocessing_parameters,compute_parameters, data_assimilation_parameters): diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index dd6ee9283..a0b5ce40b 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -239,13 +239,11 @@ def main_v04(argv): cpu_pool) # get reservoir DA initial parameters for next loop iteration - # TODO: Add data_assimilation for hyfeature network - if 1==2: - data_assimilation.update(run_results, - data_assimilation_parameters, - run_parameters, - network, - da_sets[run_set_iterator + 1]) + data_assimilation.update(run_results, + data_assimilation_parameters, + run_parameters, + network, + da_sets[run_set_iterator + 1]) if showtiming: forcing_end_time = time.time() @@ -253,7 +251,9 @@ def main_v04(argv): if showtiming: output_start_time = time.time() - + + ''' + #TODO Update this to work with either network type... nwm_output_generator( run, run_results, @@ -268,10 +268,11 @@ def main_v04(argv): network._waterbody_df, ## check: network._waterbody_df ?? def name is different from return self._ .. network._waterbody_types_df, ## check: network._waterbody_types_df ?? def name is different from return self._ .. data_assimilation_parameters, - pd.DataFrame(), #data_assimilation.lastobs_df, + data_assimilation.lastobs_df, pd.DataFrame(), #network.link_gage_df, None, #network.link_lake_crosswalk, ) + ''' if showtiming: output_end_time = time.time() diff --git a/test/LowerColorado_TX/test_AnA.yaml b/test/LowerColorado_TX/test_AnA.yaml index 3f73a4cb4..799222edf 100644 --- a/test/LowerColorado_TX/test_AnA.yaml +++ b/test/LowerColorado_TX/test_AnA.yaml @@ -9,14 +9,15 @@ network_topology_parameters: #---------- supernetwork_parameters: #---------- - geo_file_path: domain/RouteLink_NWMv2.1.nc + geo_file_path: domain/RouteLink.nc mask_file_path: domain/coastal_subset.txt + geo_file_type: 'NHDNetwork' waterbody_parameters: #---------- break_network_at_waterbodies: True level_pool: #---------- - level_pool_waterbody_parameter_file_path: domain/LAKEPARM_NWMv2.1.nc + level_pool_waterbody_parameter_file_path: domain/LAKEPARM.nc rfc: #---------- reservoir_parameter_file : domain/reservoir_index_AnA.nc @@ -33,19 +34,20 @@ compute_parameters: cpu_pool : 36 restart_parameters: #---------- - wrf_hydro_channel_restart_file : restart/HYDRO_RST.2020-08-26_00:00_DOMAIN1 + start_datetime: 2021-08-23_13:00 + wrf_hydro_channel_restart_file : restart/HYDRO_RST.2021-08-23_12:00_DOMAIN1 #lite_channel_restart_file : restart/RESTART.2020082600_DOMAIN1 - wrf_hydro_channel_ID_crosswalk_file : domain/RouteLink_NWMv2.1.nc - wrf_hydro_waterbody_restart_file : restart/HYDRO_RST.2020-08-26_00:00_DOMAIN1 + wrf_hydro_channel_ID_crosswalk_file : domain/RouteLink.nc + wrf_hydro_waterbody_restart_file : restart/HYDRO_RST.2021-08-23_12:00_DOMAIN1 #lite_waterbody_restart_file : restart/waterbody_restart_202006011200 - wrf_hydro_waterbody_ID_crosswalk_file : domain/LAKEPARM_NWMv2.1.nc - wrf_hydro_waterbody_crosswalk_filter_file: domain/RouteLink_NWMv2.1.nc + wrf_hydro_waterbody_ID_crosswalk_file : domain/LAKEPARM.nc + wrf_hydro_waterbody_crosswalk_filter_file: domain/RouteLink.nc hybrid_parameters: - run_hybrid_routing: True + run_hybrid_routing: False diffusive_domain : domain/coastal_domain_subset.yaml - use_natl_xsections: True + use_natl_xsections: False topobathy_domain : domain/final_diffusive_natural_xs.nc - run_refactored_network: True + run_refactored_network: False refactored_domain: domain/refactored_coastal_domain_subset.yaml refactored_topobathy_domain: domain/refac_final_diffusive_natural_xs.nc coastal_boundary_domain: domain/coastal_boundary_domain.yaml @@ -55,8 +57,8 @@ compute_parameters: dt : 300 # [sec] qlat_input_folder : channel_forcing qlat_file_pattern_filter : "*.CHRTOUT_DOMAIN1" - coastal_boundary_input_file : boundary_forcing - nts : 2592 # 288 for 1day; 2592 for 9 days + coastal_boundary_input_file : #boundary_forcing + nts : 288 # 288 for 1day; 2592 for 9 days max_loop_size : 24 # [hr] data_assimilation_parameters: #---------- @@ -68,10 +70,10 @@ compute_parameters: #---------- streamflow_nudging : False diffusive_streamflow_nudging : False - gage_segID_crosswalk_file : domain/RouteLink_NWMv2.1.nc + gage_segID_crosswalk_file : domain/RouteLink.nc crosswalk_gage_field : 'gages' crosswalk_segID_field : 'link' - wrf_hydro_lastobs_file : lastobs/nudgingLastObs.2020-06-01_12:00:00.nc + wrf_hydro_lastobs_file : lastobs/nudgingLastObs.2021-08-23_13:00:00.nc lastobs_output_folder : lastobs/ reservoir_da: #---------- diff --git a/test/ngen/test_AnA.yaml b/test/ngen/test_AnA.yaml new file mode 100644 index 000000000..03e35cc35 --- /dev/null +++ b/test/ngen/test_AnA.yaml @@ -0,0 +1,106 @@ +# $ python -m nwm_routing -f -V3 test_AnA.yaml +#-------------------------------------------------------------------------------- +log_parameters: + #---------- + showtiming: True + log_level : DEBUG +#-------------------------------------------------------------------------------- +network_topology_parameters: + #---------- + supernetwork_parameters: + #---------- + geo_file_type: HYFeaturesNetwork + ngen_nexus_file: /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + geo_file_path: /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + columns: + key: 'id' + downstream: 'toid' + dx : 'lengthkm' + n : 'n' + ncc : 'nCC' + s0 : 'So' + bw : 'BtmWdth' + waterbody : 'rl_NHDWaterbodyComID' + gages : 'rl_gages' + tw : 'TopWdth' + twcc : 'TopWdthCC' + musk : 'MusK' + musx : 'MusX' + cs : 'ChSlp' + waterbody_parameters: + #---------- + break_network_at_waterbodies: True + level_pool: + #---------- + level_pool_waterbody_parameter_file_path: /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + reservoir_parameter_file: /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + +#-------------------------------------------------------------------------------- +compute_parameters: + #---------- + parallel_compute_method: serial + compute_kernel : V02-structured + assume_short_ts : True + restart_parameters: + #---------- + wrf_hydro_channel_restart_file : + #lite_channel_restart_file : restart/RESTART.2020082600_DOMAIN1 + wrf_hydro_channel_ID_crosswalk_file : /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + wrf_hydro_waterbody_restart_file : #restart/HYDRO_RST.2021-08-23_12:00_DOMAIN1 + #lite_waterbody_restart_file : restart/waterbody_restart_202006011200 + wrf_hydro_waterbody_ID_crosswalk_file : /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + wrf_hydro_waterbody_crosswalk_filter_file: /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + hybrid_parameters: + run_hybrid_routing: False + diffusive_domain : domain/coastal_domain_subset.yaml + use_natl_xsections: False + topobathy_domain : domain/final_diffusive_natural_xs.nc + run_refactored_network: False + refactored_domain: domain/refactored_coastal_domain_subset.yaml + refactored_topobathy_domain: domain/refac_final_diffusive_natural_xs.nc + coastal_boundary_domain: domain/coastal_boundary_domain.yaml + forcing_parameters: + #---------- + qts_subdivisions : 12 + dt : 300 # [sec] + qlat_input_folder : /home/sean.horvath/projects/data/large_network/sample_qlat_files + qlat_file_pattern_filter : "*.CHRTOUT_DOMAIN1.csv" +# coastal_boundary_input_file : boundary_forcing + nts : 288 # 288 for 1day; 2592 for 9 days + max_loop_size : 24 # [hr] + data_assimilation_parameters: + #---------- + usgs_timeslices_folder : usgs_TimeSlice/ + usace_timeslices_folder : usace_TimeSlice/ + timeslice_lookback_hours : 48 + qc_threshold : 1 + streamflow_da: + #---------- + streamflow_nudging : False + diffusive_streamflow_nudging : False + gage_segID_crosswalk_file : /home/sean.horvath/projects/data/large_network/gauge_01013500.gpkg + crosswalk_gage_field : 'gages' + crosswalk_segID_field : 'link' + wrf_hydro_lastobs_file : #lastobs/nudgingLastObs.2021-08-23_12:00:00.nc + lastobs_output_folder : #lastobs/ + reservoir_da: + #---------- + reservoir_persistence_usgs : False + reservoir_persistence_usace : False + gage_lakeID_crosswalk_file : domain/reservoir_index_AnA.nc +#-------------------------------------------------------------------------------- +output_parameters: + #---------- +# test_output: output/lcr_flowveldepth.pkl + lite_restart: + #---------- + lite_restart_output_directory: + chrtout_output: + #---------- +# wrf_hydro_channel_output_source_folder: channel_forcing/ + chanobs_output: + #---------- +# chanobs_output_directory: output/ +# chanobs_filepath : lcr_chanobs.nc +# lakeout_output: lakeout/ + \ No newline at end of file From 32c2fbdf40defc54cba835ac81da02031e53c533 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 16 Dec 2022 17:42:44 +0000 Subject: [PATCH 34/54] moved renaming param_df column into individual networks, removed cols from input argument for AbstractNetwork --- src/troute-network/troute/AbstractNetwork.py | 27 +---------------- .../troute/HYFeaturesNetwork.py | 30 +++++++++++++++++-- src/troute-network/troute/NHDNetwork.py | 3 +- .../troute/hyfeature_preprocess.py | 4 +-- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 0a19d9156..f79954761 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta import time -from troute.nhd_network import reverse_dict, extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition +from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition import troute.nhd_io as nhd_io import troute.abstractnetwork_preprocess as abs_prep @@ -26,7 +26,6 @@ def __init__( compute_parameters, waterbody_parameters, restart_parameters, - cols=None, break_points=None, verbose=False, showtiming=False @@ -36,36 +35,12 @@ def __init__( __verbose__ = verbose __showtiming__ = showtiming - if cols: - self._dataframe = self._dataframe[list(cols.values())] - # Rename parameter columns to standard names: from route-link names - # key: "link" - # downstream: "to" - # dx: "Length" - # n: "n" # TODO: rename to `manningn` - # ncc: "nCC" # TODO: rename to `mannningncc` - # s0: "So" # TODO: rename to `bedslope` - # bw: "BtmWdth" # TODO: rename to `bottomwidth` - # waterbody: "NHDWaterbodyComID" - # gages: "gages" - # tw: "TopWdth" # TODO: rename to `topwidth` - # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` - # alt: "alt" - # musk: "MusK" - # musx: "MusX" - # cs: "ChSlp" # TODO: rename to `sideslope` - self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) - self.set_index("key") - self.sort_index() - self._waterbody_connections = {} #TODO set in individual network objects?... - self._gages = None #TODO set in individual network objects?... self._independent_networks = None self._reverse_network = None self._reaches_by_tw = None self._q0 = None self._t0 = None self._qlateral = None - self._waterbody_type_specified = None #qlat_const = forcing_parameters.get("qlat_const", 0) #FIXME qlat_const """ Figure out a good way to default initialize to qlat_const/c diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 56c25a524..84375d1ee 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -4,6 +4,7 @@ import time import troute.nhd_io as nhd_io #FIXME import troute.hyfeature_preprocess as hyfeature_prep +from troute.nhd_network import reverse_dict __verbose__ = False __showtiming__ = False @@ -130,6 +131,10 @@ def __init__(self, waterbody_parameters, ) + self._waterbody_connections = {} + self._waterbody_type_specified = None + self._gages = None + if __verbose__: print("supernetwork connections set complete") if __showtiming__: @@ -143,12 +148,33 @@ def __init__(self, break_network_at_gages = streamflow_da.get('streamflow_nudging', False) break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} + + if cols: + self._dataframe = self._dataframe[list(cols.values())] + # Rename parameter columns to standard names: from route-link names + # key: "link" + # downstream: "to" + # dx: "Length" + # n: "n" # TODO: rename to `manningn` + # ncc: "nCC" # TODO: rename to `mannningncc` + # s0: "So" # TODO: rename to `bedslope` + # bw: "BtmWdth" # TODO: rename to `bottomwidth` + # waterbody: "NHDWaterbodyComID" + # gages: "gages" + # tw: "TopWdth" # TODO: rename to `topwidth` + # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` + # alt: "alt" + # musk: "MusK" + # musx: "MusX" + # cs: "ChSlp" # TODO: rename to `sideslope` + self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) + self.set_index("key") + self.sort_index() super().__init__( compute_parameters, waterbody_parameters, - restart_parameters, - cols, + restart_parameters, break_points, verbose=__verbose__, showtiming=__showtiming__, diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index ff2c3ab0d..9edd4c73b 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -85,8 +85,7 @@ def __init__( super().__init__( compute_parameters, waterbody_parameters, - restart_parameters, - cols, + restart_parameters, break_points, verbose=__verbose__, showtiming=__showtiming__, diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index 1c2aabc75..d2b0078e8 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -57,12 +57,12 @@ def read_geo_file( # otherwise valid node value being pointed to, but which is masked out or # being intentionally separated into another domain. terminal_codes = terminal_codes | set( - dataframe[~dataframe["downstream"].isin(dataframe.index)]["downstream"].values + dataframe[~dataframe["toid"].isin(dataframe.index)]["toid"].values ) # build connections dictionary connections = nhd_network.extract_connections( - dataframe, "downstream", terminal_codes=terminal_codes + dataframe, "toid", terminal_codes=terminal_codes ) #Load waterbody/reservoir info From 00adf8014dedb2b6a8763c5b144cbc6faa1478bb Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 16 Dec 2022 20:14:40 +0000 Subject: [PATCH 35/54] move renaming dataframe columns and terminal codes/connections creation into read_geo_file function for HyFeatures --- .../troute/HYFeaturesNetwork.py | 23 -------------- .../troute/hyfeature_preprocess.py | 30 +++++++++++++++++-- src/troute-nwm/src/nwm_routing/__main__.py | 1 + 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 84375d1ee..c1f997c6e 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -140,7 +140,6 @@ def __init__(self, if __showtiming__: print("... in %s seconds." % (time.time() - start_time)) - cols = supernetwork_parameters.get('columns',None) break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) streamflow_da = data_assimilation_parameters.get('streamflow_da', False) break_network_at_gages = False @@ -148,28 +147,6 @@ def __init__(self, break_network_at_gages = streamflow_da.get('streamflow_nudging', False) break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} - - if cols: - self._dataframe = self._dataframe[list(cols.values())] - # Rename parameter columns to standard names: from route-link names - # key: "link" - # downstream: "to" - # dx: "Length" - # n: "n" # TODO: rename to `manningn` - # ncc: "nCC" # TODO: rename to `mannningncc` - # s0: "So" # TODO: rename to `bedslope` - # bw: "BtmWdth" # TODO: rename to `bottomwidth` - # waterbody: "NHDWaterbodyComID" - # gages: "gages" - # tw: "TopWdth" # TODO: rename to `topwidth` - # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` - # alt: "alt" - # musk: "MusK" - # musx: "MusX" - # cs: "ChSlp" # TODO: rename to `sideslope` - self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) - self.set_index("key") - self.sort_index() super().__init__( compute_parameters, diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index d2b0078e8..141ad07f8 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -43,7 +43,31 @@ def read_geo_file( flowpath_dict = dict(zip(dataframe.loc[mask].toid, dataframe.loc[mask].id)) # ********** need to be included in flowpath_attributes ************* - dataframe['alt'] = 1.0 #FIXME get the right value for this... + dataframe['alt'] = 1.0 #FIXME get the right value for this... + + cols = supernetwork_parameters.get('columns',None) + + if cols: + dataframe = dataframe[list(cols.values())] + # Rename parameter columns to standard names: from route-link names + # key: "link" + # downstream: "to" + # dx: "Length" + # n: "n" # TODO: rename to `manningn` + # ncc: "nCC" # TODO: rename to `mannningncc` + # s0: "So" # TODO: rename to `bedslope` + # bw: "BtmWdth" # TODO: rename to `bottomwidth` + # waterbody: "NHDWaterbodyComID" + # gages: "gages" + # tw: "TopWdth" # TODO: rename to `topwidth` + # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` + # alt: "alt" + # musk: "MusK" + # musx: "MusX" + # cs: "ChSlp" # TODO: rename to `sideslope` + dataframe = dataframe.rename(columns=reverse_dict(cols)) + dataframe.set_index("key", inplace=True) + dataframe.sort_index() # numeric code used to indicate network terminal segments terminal_code = supernetwork_parameters.get("terminal_code", 0) @@ -57,12 +81,12 @@ def read_geo_file( # otherwise valid node value being pointed to, but which is masked out or # being intentionally separated into another domain. terminal_codes = terminal_codes | set( - dataframe[~dataframe["toid"].isin(dataframe.index)]["toid"].values + dataframe[~dataframe["downstream"].isin(dataframe.index)]["downstream"].values ) # build connections dictionary connections = nhd_network.extract_connections( - dataframe, "toid", terminal_codes=terminal_codes + dataframe, "downstream", terminal_codes=terminal_codes ) #Load waterbody/reservoir info diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index a0b5ce40b..ac89e44dd 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -236,6 +236,7 @@ def main_v04(argv): network.assemble_forcings(run_sets[run_set_iterator + 1], forcing_parameters, hybrid_parameters, + supernetwork_parameters, cpu_pool) # get reservoir DA initial parameters for next loop iteration From ed9db178999d7802f1c22c80e7bf8ca3be0ce530 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Wed, 21 Dec 2022 15:47:24 +0000 Subject: [PATCH 36/54] move functions from ..._preprocess.py files into the network objects as inherent functions --- src/troute-network/troute/AbstractNetwork.py | 654 ++++++++++++++++-- .../troute/HYFeaturesNetwork.py | 273 +++++--- src/troute-network/troute/NHDNetwork.py | 263 ++++++- .../troute/hyfeature_preprocess.py | 2 +- src/troute-nwm/src/nwm_routing/__main__.py | 11 +- 5 files changed, 1039 insertions(+), 164 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index f79954761..dc5e2f72d 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -1,12 +1,20 @@ from abc import ABC, abstractmethod from functools import partial import pandas as pd +import numpy as np +import pyarrow.parquet as pq from datetime import datetime, timedelta +from joblib import delayed, Parallel +import netCDF4 import time +import logging +import pathlib from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition -import troute.nhd_io as nhd_io -import troute.abstractnetwork_preprocess as abs_prep +from troute.nhd_network_utilities_v02 import organize_independent_networks, build_channel_initial_state, build_refac_connections +import troute.nhd_io as nhd_io + +LOG = logging.getLogger('') class AbstractNetwork(ABC): """ @@ -15,11 +23,11 @@ class AbstractNetwork(ABC): __slots__ = ["_dataframe", "_waterbody_connections", "_gages", "_terminal_codes", "_connections", "_waterbody_df", "_waterbody_types_df", "_waterbody_type_specified", - "_independent_networks", "_reaches_by_tw", - "_reverse_network", "_q0", "_t0", + "_independent_networks", "_reaches_by_tw", "_flowpath_dict", + "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", "_qlateral", "_break_segments", "_coastal_boundary_depth_df", - "diffusive_network_data", "topobathy_df", "refactored_diffusive_domain", - "refactored_reaches", "unrefactored_topobathy_df", "segment_index"] + "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", + "_refactored_reaches", "_unrefactored_topobathy_df", "_segment_index"] def __init__( self, @@ -53,58 +61,19 @@ def __init__( ) """ self._break_segments = set() + if break_points: if break_points["break_network_at_waterbodies"]: self._break_segments = self._break_segments | set(self.waterbody_connections.values()) if break_points["break_network_at_gages"]: - self._break_segments = self._break_segments | set(self.gages.values()) + self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) - ( - self._dataframe, - self._connections, - self.diffusive_network_data, - self.topobathy_df, - self.refactored_diffusive_domain, - self.refactored_reaches, - self.unrefactored_topobathy_df - ) = abs_prep.build_diffusive_domain( - compute_parameters, - self._dataframe, - self._connections, - ) + self.build_diffusive_domain(compute_parameters) - ( - self._independent_networks, - self._reaches_by_tw, - self._reverse_network - ) = abs_prep.create_independent_networks( - waterbody_parameters, - self._connections, - self._waterbody_connections, - #gages, #TODO update how gages are provided when we figure out DA - ) - - if __verbose__: - print("setting waterbody and channel initial states ...") - if __showtiming__: - start_time = time.time() + self.create_independent_networks(waterbody_parameters) + + self.initial_warmstate_preprocess(break_points["break_network_at_waterbodies"],restart_parameters) - ( - self._waterbody_df, - self._q0, - self._t0 - ) = abs_prep.initial_warmstate_preprocess( - break_points["break_network_at_waterbodies"], - restart_parameters, - self._dataframe.index, - self._waterbody_df, - ) - - if __verbose__: - print("waterbody and channel initial states complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) - start_time = time.time() def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): """ @@ -162,12 +131,10 @@ def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernet # TODO: add an option for reading qlat data from BMI/model engine from_file = True if from_file: - self._qlateral = abs_prep.build_qlateral_array( + self.build_qlateral_array( run, cpu_pool, - self._flowpath_dict, - supernetwork_parameters, - self._dataframe.index, + supernetwork_parameters, ) #--------------------------------------------------------------------- @@ -277,17 +244,21 @@ def reaches_by_tailwater(self): @property def waterbody_dataframe(self): - return self._waterbody_df + return self._waterbody_df.sort_index() @property def waterbody_types_dataframe(self): - return self._waterbody_types_df + return self._waterbody_types_df.sort_index() @property def waterbody_type_specified(self): if self._waterbody_type_specified is None: self._waterbody_type_specified = False return self._waterbody_type_specified + + @property + def link_lake_crosswalk(self): + return self._link_lake_crosswalk @property def connections(self): @@ -335,6 +306,47 @@ def t0(self, value): self._t0 = value else: self._t0 = datetime.strptime(value, "%Y-%m-%d_%H:%M:%S") + + @property + def segment_index(self): + """ + Segment IDs of all reaches in parameter dataframe + and diffusive domain. + """ + # list of all segments in the domain (MC + diffusive) + self._segment_index = self.dataframe.index + if self.diffusive_network_data: + for tw in self.diffusive_network_data: + self._segment_index = self._segment_index.append( + pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) + ) + return self._segment_index + + @property + def link_gage_df(self): + link_gage_df = pd.DataFrame.from_dict(self._gages) + link_gage_df.index.name = 'link' + return link_gage_df + + @property + def diffusive_network_data(self): + return self._diffusive_network_data + + @property + def topobathy_df(self): + return self._topobathy_df + + @property + def refactored_diffusive_domain(self): + return self._refactored_diffusive_domain + + @property + def refactored_reaches(self): + return self._refactored_reaches + + @property + def unrefactored_topobathy_df(self): + return self._unrefactored_topobathy_df @property @abstractmethod @@ -425,3 +437,531 @@ def astype(self, type, columns=None): else: self._dataframe = self._dataframe.astype(type) + + def build_diffusive_domain(self, compute_parameters): + """ + + """ + hybrid_params = compute_parameters.get("hybrid_parameters", False) + if hybrid_params: + # switch parameters + # if run_hybrid = False, run MC only + # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc + # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric + # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric + run_hybrid = hybrid_params.get('run_hybrid_routing', False) + use_topobathy = hybrid_params.get('use_natl_xsections', False) + run_refactored = hybrid_params.get('run_refactored_network', False) + + # file path parameters of non-refactored hydrofabric defined by RouteLink.nc + domain_file = hybrid_params.get("diffusive_domain", None) + topobathy_file = hybrid_params.get("topobathy_domain", None) + + # file path parameters of refactored hydrofabric for diffusive wave channel routing + refactored_domain_file = hybrid_params.get("refactored_domain", None) + refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) + #------------------------------------------------------------------------- + # for non-refactored hydofabric defined by RouteLink.nc + # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. + if run_hybrid and domain_file: + + LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') + + # read diffusive domain dictionary from yaml or json + diffusive_domain = nhd_io.read_diffusive_domain(domain_file) + + if use_topobathy and topobathy_file: + + LOG.debug('Natural cross section data on original hydrofabric are provided.') + + # read topobathy domain netcdf file, set index to 'comid' + # TODO: replace 'link' with a user-specified indexing variable name. + # ... if for whatever reason there is not a `link` variable in the + # ... dataframe returned from read_netcdf, then the code would break here. + self._topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) + + # TODO: Request GID make comID variable an integer in their product, so + # we do not need to change variable types, here. + self._topobathy_df.index = self._topobathy_df.index.astype(int) + + else: + self._topobathy_df = pd.DataFrame() + LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') + + # initialize a dictionary to hold network data for each of the diffusive domains + self._diffusive_network_data = {} + + else: + diffusive_domain = None + self._diffusive_network_data = None + self._topobathy_df = pd.DataFrame() + LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') + self._unrefactored_topobathy_df = pd.DataFrame() + #------------------------------------------------------------------------- + # for refactored hydofabric + if run_hybrid and run_refactored and refactored_domain_file: + + LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') + + # read diffusive domain dictionary from yaml or json + self._refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) + + if use_topobathy and refactored_topobathy_file: + + LOG.debug('Natural cross section data of refactored hydrofabric are provided.') + + # read topobathy domain netcdf file, set index to 'comid' + # TODO: replace 'link' with a user-specified indexing variable name. + # ... if for whatever reason there is not a `link` variable in the + # ... dataframe returned from read_netcdf, then the code would break here. + self._topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) + + # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy + # for crosswalking water elevations between non-refactored and refactored hydrofabrics. + self._unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) + self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) + + else: + self._topobathy_df = pd.DataFrame() + LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') + + # initialize a dictionary to hold network data for each of the diffusive domains + refactored_diffusive_network_data = {} + + else: + self._refactored_diffusive_domain = None + refactored_diffusive_network_data = None + self._refactored_reaches = {} + LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') + + else: + diffusive_domain = None + self._diffusive_network_data = None + self._topobathy_df = pd.DataFrame() + self._unrefactored_topobathy_df = pd.DataFrame() + self._refactored_diffusive_domain = None + refactored_diffusive_network_data = None + self._refactored_reaches = {} + LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') + + #============================================================================ + # build diffusive domain data and edit MC domain data for hybrid simulation + + # + if diffusive_domain: + rconn_diff0 = reverse_network(self._connections) + self._refactored_reaches = {} + + for tw in diffusive_domain: + mainstem_segs = diffusive_domain[tw]['links'] + # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is + # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. + upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] + if upstream_boundary_mainstem_link[0] in mainstem_segs: + mainstem_segs.remove(upstream_boundary_mainstem_link[0]) + + # ===== build diffusive network data objects ==== + self._diffusive_network_data[tw] = {} + + # add diffusive domain segments + self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs + + # diffusive domain tributary segments + trib_segs = [] + + for seg in mainstem_segs: + us_list = rconn_diff0[seg] + for u in us_list: + if u not in mainstem_segs: + trib_segs.append(u) + + self._diffusive_network_data[tw]['tributary_segments'] = trib_segs + # diffusive domain connections object + self._diffusive_network_data[tw]['connections'] = {k: self._connections[k] for k in (mainstem_segs + trib_segs)} + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, reaches, rconn_diff = organize_independent_networks( + self._diffusive_network_data[tw]['connections'], + set(trib_segs), + set(), + ) + + self._diffusive_network_data[tw]['rconn'] = rconn_diff + self._diffusive_network_data[tw]['reaches'] = reaches[tw] + + # RouteLink parameters + self._diffusive_network_data[tw]['param_df'] = self._dataframe.filter( + (mainstem_segs + trib_segs), + axis = 0, + ) + self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link + + if self._refactored_diffusive_domain: + diffusive_parameters = {'geo_file_path': refactored_topobathy_file} + refactored_connections = build_refac_connections(diffusive_parameters) + + # list of stream segments of a single refactored diffusive domain + refac_tw = self._refactored_diffusive_domain[tw]['refac_tw'] + rlinks_tw = self._refactored_diffusive_domain[tw]['rlinks'] + refactored_connections_tw = {} + + # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections + # for a single refactored diffusive domain defined by a current tw. + for k in rlinks_tw: + if k in refactored_connections.keys() and k != refac_tw: + refactored_connections_tw[k] = refactored_connections[k] + + refactored_diffusive_network_data[refac_tw] = {} + refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs + refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw + + for k in trib_segs: + refactored_diffusive_network_data[refac_tw]['connections'][k]= [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( + refactored_diffusive_network_data[refac_tw]['connections'], + set(trib_segs), + set(), + ) + + self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] + refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] + refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] + else: + self._refactored_reaches={} + + # ==== remove diffusive domain segs from MC domain ==== + # drop indices from param_df + self._dataframe = self._dataframe.drop(mainstem_segs) + + # remove keys from connections dictionary + for s in mainstem_segs: + self._connections.pop(s) + + # update downstream connections of trib segs + for us in trib_segs: + self._connections[us] = [] + + def create_independent_networks(self, waterbody_parameters): + + LOG.info("organizing connections into reaches ...") + start_time = time.time() + gage_break_segments = set() + wbody_break_segments = set() + + break_network_at_waterbodies = waterbody_parameters.get( + "break_network_at_waterbodies", False + ) + + # if streamflow DA, then break network at gages + #TODO update to work with HYFeatures, need to determine how we'll do DA... + break_network_at_gages = False + + if break_network_at_waterbodies: + wbody_break_segments = wbody_break_segments.union(self._waterbody_connections.values()) + + if break_network_at_gages: + gage_break_segments = gage_break_segments.union(self.gages['gages'].keys()) + + ( + self._independent_networks, + self._reaches_by_tw, + self._reverse_network + ) = organize_independent_networks( + self.connections, + wbody_break_segments, + gage_break_segments, + ) + + LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) + + def initial_warmstate_preprocess(self, break_network_at_waterbodies, restart_parameters,): + + ''' + Assemble model initial condition data: + - waterbody inital states (outflow and pool elevation) + - channel initial states (flow and depth) + - initial time + + Arguments + --------- + - break_network_at_waterbodies (bool): If True, waterbody initial states will + be appended to the waterbody parameter + dataframe. If False, waterbodies will + not be simulated and the waterbody + parameter datataframe wil not be changed + - restart_parameters (dict): User-input simulation restart + parameters + - segment_index (Pandas Index): All segment IDs in the simulation + doamin + - waterbodies_df (Pandas DataFrame): Waterbody parameters + + Returns + ------- + - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial + states (outflow and pool elevation) + - q0 (Pandas DataFrame): Initial flow and depth states for each + segment in the model domain + - t0 (datetime): Datetime of the model initialization + + Notes + ----- + ''' + + # generalize waterbody ID's to be used with any network + index_id = self.waterbody_dataframe.index.names[0] + + #---------------------------------------------------------------------------- + # Assemble waterbody initial states (outflow and pool elevation + #---------------------------------------------------------------------------- + if break_network_at_waterbodies: + + start_time = time.time() + LOG.info("setting waterbody initial states ...") + + # if a lite restart file is provided, read initial states from it. + if restart_parameters.get("lite_waterbody_restart_file", None): + + waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( + restart_parameters['lite_waterbody_restart_file'] + ) + + # read waterbody initial states from WRF-Hydro type restart file + elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): + waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( + restart_parameters["wrf_hydro_waterbody_restart_file"], + restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], + restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", index_id), + restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], + restart_parameters.get( + "wrf_hydro_waterbody_crosswalk_filter_file_field_name", + 'NHDWaterbodyComID' + ), + ) + + # if no restart file is provided, default initial states + else: + # TODO: Consider adding option to read cold state from route-link file + waterbodies_initial_ds_flow_const = 0.0 + waterbodies_initial_depth_const = -1e9 + # Set initial states from cold-state + waterbodies_initial_states_df = pd.DataFrame( + 0, + index=self.waterbody_dataframe.index, + columns=[ + "qd0", + "h0", + ], + dtype="float32", + ) + # TODO: This assignment could probably by done in the above call + waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const + waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const + waterbodies_initial_states_df["index"] = range( + len(waterbodies_initial_states_df) + ) + + self._waterbody_df = pd.merge( + self.waterbody_dataframe, waterbodies_initial_states_df, on=index_id + ) + + LOG.debug( + "waterbody initial states complete in %s seconds."\ + % (time.time() - start_time)) + start_time = time.time() + + #---------------------------------------------------------------------------- + # Assemble channel initial states (flow and depth) + # also establish simulation initialization timestamp + #---------------------------------------------------------------------------- + start_time = time.time() + LOG.info("setting channel initial states ...") + + # if lite restart file is provided, the read channel initial states from it + if restart_parameters.get("lite_channel_restart_file", None): + # FIXME: Change it for hyfeature! + self._q0, self._t0 = nhd_io.read_lite_restart( + restart_parameters['lite_channel_restart_file'] + ) + t0_str = None + + # when a restart file for hyfeature is provied, then read initial states from it. + elif restart_parameters.get("hyfeature_channel_restart_file", None): + self._q0 = build_channel_initial_state(restart_parameters, self.segment_index) + channel_initial_states_file = restart_parameters["hyfeature_channel_restart_file"] + df = pd.read_csv(channel_initial_states_file) + t0_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + self._t0 = datetime.strptime(t0_str,"%Y-%m-%d_%H:%M:%S") + + # build initial states from user-provided restart parameters + else: + # FIXME: Change it for hyfeature! + self._q0 = build_channel_initial_state(restart_parameters, self.segment_index) + + # get initialization time from restart file + if restart_parameters.get("wrf_hydro_channel_restart_file", None): + channel_initial_states_file = restart_parameters[ + "wrf_hydro_channel_restart_file" + ] + t0_str = nhd_io.get_param_str( + channel_initial_states_file, + "Restart_Time" + ) + else: + t0_str = "2015-08-16_00:00:00" + + # convert timestamp from string to datetime + self._t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") + + # get initial time from user inputs + if restart_parameters.get("start_datetime", None): + t0_str = restart_parameters.get("start_datetime") + + def _try_parsing_date(text): + for fmt in ( + "%Y-%m-%d_%H:%M", + "%Y-%m-%d_%H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%d %H:%M:%S", + "%Y/%m/%d %H:%M", + "%Y/%m/%d %H:%M:%S" + ): + try: + return datetime.strptime(text, fmt) + except ValueError: + pass + LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') + quit() + + self._t0 = _try_parsing_date(t0_str) + else: + if t0_str == "2015-08-16_00:00:00": + LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') + else: + LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) + + LOG.debug( + "channel initial states complete in %s seconds."\ + % (time.time() - start_time) + ) + + def build_qlateral_array(self, run, cpu_pool, supernetwork_parameters,): + + start_time = time.time() + LOG.info("Creating a DataFrame of lateral inflow forcings ...") + + # TODO: set default/optional arguments + qts_subdivisions = run.get("qts_subdivisions", 1) + nts = run.get("nts", 1) + qlat_input_folder = run.get("qlat_input_folder", None) + qlat_input_file = run.get("qlat_input_file", None) + + geo_file_type = supernetwork_parameters.get('geo_file_type') + + if qlat_input_folder: + qlat_input_folder = pathlib.Path(qlat_input_folder) + if "qlat_files" in run: + qlat_files = run.get("qlat_files") + qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] + elif "qlat_file_pattern_filter" in run: + qlat_file_pattern_filter = run.get( + "qlat_file_pattern_filter", "*CHRT_OUT*" + ) + qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) + + qlat_file_index_col = run.get( + "qlat_file_index_col", "feature_id" + ) + + if geo_file_type=='NHDNetwork': + # Parallel reading of qlateral data from CHRTOUT + with Parallel(n_jobs=cpu_pool) as parallel: + jobs = [] + for f in qlat_files: + jobs.append( + delayed(nhd_io.get_ql_from_chrtout) + #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) + #delayed(nhd_io.get_ql_from_csv) + (f) + ) + ql_list = parallel(jobs) + + # get feature_id from a single CHRTOUT file + with netCDF4.Dataset(qlat_files[0]) as ds: + idx = ds.variables[qlat_file_index_col][:].filled() + + # package data into a DataFrame + qlats_df = pd.DataFrame( + np.stack(ql_list).T, + index = idx, + columns = range(len(qlat_files)) + ) + + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + elif geo_file_type=='HYFeaturesNetwork': + dfs=[] + for f in qlat_files: + df = read_file(f).set_index(['feature_id']) + dfs.append(df) + + # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids + nexuses_lateralflows_df = pd.concat(dfs, axis=1) + + # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids + qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) + for k,v in self.downstream_flowpath_dict.items() ), axis=1 + ).T + qlats_df.columns=range(len(qlat_files)) + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + # The segment_index has the full network set of segments/flowpaths. + # Whereas the set of flowpaths that are downstream of nexuses is a + # subset of the segment_index. Therefore, all of the segments/flowpaths + # that are not accounted for in the set of flowpaths downstream of + # nexuses need to be added to the qlateral dataframe and padded with + # zeros. + all_df = pd.DataFrame( np.zeros( (len(self.segment_index), len(qlats_df.columns)) ), index=self.segment_index, + columns=qlats_df.columns ) + all_df.loc[ qlats_df.index ] = qlats_df + qlats_df = all_df.sort_index() + + elif qlat_input_file: + qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) + else: + qlat_const = run.get("qlat_const", 0) + qlats_df = pd.DataFrame( + qlat_const, + index=self.segment_index, + columns=range(nts // qts_subdivisions), + dtype="float32", + ) + + # TODO: Make a more sophisticated date-based filter + max_col = 1 + nts // qts_subdivisions + if len(qlats_df.columns) > max_col: + qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) + + if not self.segment_index.empty: + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + LOG.debug( + "lateral inflow DataFrame creation complete in %s seconds." \ + % (time.time() - start_time) + ) + + self._qlateral = qlats_df + + + +def read_file(file_name): + extension = file_name.suffix + if extension=='.csv': + df = pd.read_csv(file_name) + elif extension=='.parquet': + df = pq.read_table(file_name).to_pandas().reset_index() + df.index.name = None + + return df \ No newline at end of file diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index c1f997c6e..96be4e634 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -1,103 +1,99 @@ from .AbstractNetwork import AbstractNetwork import pandas as pd import numpy as np +import geopandas as gpd import time +import os +import json +from pathlib import Path + import troute.nhd_io as nhd_io #FIXME import troute.hyfeature_preprocess as hyfeature_prep -from troute.nhd_network import reverse_dict +from troute.nhd_network import reverse_dict, extract_connections __verbose__ = False __showtiming__ = False -def read_qlats(forcing_parameters, segment_index, nexus_to_downstream_flowpath_dict): - # STEP 5: Read (or set) QLateral Inputs - if __showtiming__: - start_time = time.time() - if __verbose__: - print("creating qlateral array ...") - qts_subdivisions = forcing_parameters.get("qts_subdivisions", 1) - nts = forcing_parameters.get("nts", 1) - nexus_input_folder = forcing_parameters.get("nexus_input_folder", None) - nexus_input_folder = pathlib.Path(nexus_input_folder) - #rt0 = time.time() #FIXME showtiming flag - if not "nexus_file_pattern_filter" in forcing_parameters: - raise( RuntimeError("No value for nexus file pattern in config" ) ) - - nexus_file_pattern_filter = forcing_parameters.get( - "nexus_file_pattern_filter", "nex-*" - ) - nexus_files = nexus_input_folder.glob(nexus_file_pattern_filter) - #TODO Find a way to test that we found some files - #without consuming the generator...Otherwise, if nexus_files - #is empty, the following concat raises a ValueError which - #It may be sufficient to catch this exception and warn that - #there may not be any pattern matching files in the dir - - nexus_files_list = list(nexus_files) - - if len(nexus_files_list) == 0: - raise ValueError('No nexus input files found. Recommend checking \ - nexus_input_folder path in YAML configuration.') - - #pd.concat((pd.read_csv(f, index_col=0, usecols=[1,2], header=None, engine='c').rename(columns={2:id_regex.match(f).group(1)}) for f in all_files[0:2]), axis=1).T - id_regex = re.compile(r".*nex-(\d+)_.*.csv") - nexuses_flows_df = pd.concat( - #Read the nexus csv file - (pd.read_csv(f, index_col=0, usecols=[1,2], header=None, engine='c', skipinitialspace=True, parse_dates=True).rename( - #Rename the flow column to the id of the nexus - columns={2:int(id_regex.match(f.name).group(1))}) - for f in nexus_files_list #Build the generator for each required file - ), axis=1).T #Have now concatenated a single df (along axis 1). Transpose it. - missing = nexuses_flows_df[ nexuses_flows_df.isna().any(axis=1) ] - if not missing.empty: - raise ValueError("The following nexus inputs are incomplete: "+str(missing.index)) - rt1 = time.time() - #print("Time to build nexus_flows_df: {} seconds".format(rt1-rt0)) - - qlat_df = pd.concat( (nexuses_flows_df.loc[int(k)].rename(index={int(k):v}) - for k,v in nexus_to_downstream_flowpath_dict.items() ), axis=1 - ).T - #qlat_df = pd.concat( (nexuses_flows_df.loc[int(k)].rename(v) - # for k,v in nexus_to_downstream_flowpath_dict.items() ), axis=1 - # ).T - - # The segment_index has the full network set of segments/flowpaths. - # Whereas the set of flowpaths that are downstream of nexuses is a - # subset of the segment_index. Therefore, all of the segments/flowpaths - # that are not accounted for in the set of flowpaths downstream of - # nexuses need to be added to the qlateral dataframe and padded with - # zeros. - all_df = pd.DataFrame( np.zeros( (len(segment_index), len(qlat_df.columns)) ), index=segment_index, - columns=qlat_df.columns ) - all_df.loc[ qlat_df.index ] = qlat_df - qlat_df = all_df.sort_index() - - # Set new nts based upon total nexus inputs - nts = (qlat_df.shape[1]) * qts_subdivisions - max_col = 1 + nts // qts_subdivisions - - #dt = 300 # [sec] - #dt_qlat = 3600 # [sec] - #nts = 24 # steps - #max_col = math.ceil(nts*dt/dt_qlat) +def read_geopkg(file_path): + flowpaths = gpd.read_file(file_path, layer="flowpaths") + attributes = gpd.read_file(file_path, layer="flowpath_attributes").drop('geometry', axis=1) + #merge all relevant data into a single dataframe + flowpaths = pd.merge(flowpaths, attributes, on='id') + + return flowpaths + +def read_json(file_path, edge_list): + dfs = [] + with open(edge_list) as edge_file: + edge_data = json.load(edge_file) + edge_map = {} + for id_dict in edge_data: + edge_map[ id_dict['id'] ] = id_dict['toid'] + with open(file_path) as data_file: + json_data = json.load(data_file) + for key_wb, value_params in json_data.items(): + df = pd.json_normalize(value_params) + df['id'] = key_wb + df['toid'] = edge_map[key_wb] + dfs.append(df) + df_main = pd.concat(dfs, ignore_index=True) + + return df_main + +def numeric_id(flowpath): + id = flowpath['id'].split('-')[-1] + toid = flowpath['toid'].split('-')[-1] + flowpath['id'] = int(id) + flowpath['toid'] = int(toid) + +def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): + """ + Reads .gpkg or lake.json file and prepares a dataframe, filtered + to the relevant reservoirs, to provide the parameters + for level-pool reservoir computation. + """ + def node_key_func(x): + return int(x[3:]) + if os.path.splitext(parm_file)[1]=='.gpkg': + df = gpd.read_file(parm_file, layer="lake_attributes").set_index('id') + elif os.path.splitext(parm_file)[1]=='.json': + df = pd.read_json(parm_file, orient="index") - if len(qlat_df.columns) > max_col: - qlat_df.drop(qlat_df.columns[max_col:], axis=1, inplace=True) + df.index = df.index.map(node_key_func) + df.index.name = lake_index_field - if __verbose__: - print("qlateral array complete") - if __showtiming__: - print("... in %s seconds." % (time.time() - start_time)) + if lake_id_mask: + df = df.loc[lake_id_mask] + return df + +def read_ngen_waterbody_type_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): + """ + """ + #FIXME: this function is likely not correct. Unclear how we will get + # reservoir type from the gpkg files. Information should be in 'crosswalk' + # layer, but as of now (Nov 22, 2022) there doesn't seem to be a differentiation + # between USGS reservoirs, USACE reservoirs, or RFC reservoirs... + def node_key_func(x): + return int(x[3:]) + + if os.path.splitext(parm_file)[1]=='.gpkg': + df = gpd.read_file(parm_file, layer="crosswalk").set_index('id') + elif os.path.splitext(parm_file)[1]=='.json': + df = pd.read_json(parm_file, orient="index") + + df.index = df.index.map(node_key_func) + df.index.name = lake_index_field + if lake_id_mask: + df = df.loc[lake_id_mask] + + return df - return qlat_df class HYFeaturesNetwork(AbstractNetwork): """ """ - __slots__ = ["_flowpath_dict", - "segment_index", - ] + __slots__ = [] def __init__(self, supernetwork_parameters, waterbody_parameters, @@ -131,9 +127,12 @@ def __init__(self, waterbody_parameters, ) + #TODO Update for waterbodies and DA specific to HYFeatures... self._waterbody_connections = {} self._waterbody_type_specified = None self._gages = None + self._link_lake_crosswalk = None + if __verbose__: print("supernetwork connections set complete") @@ -324,4 +323,114 @@ def gages(self): @property def waterbody_null(self): return np.nan #pd.NA + + def read_geo_file( + self, + supernetwork_parameters, + waterbody_parameters, + ): + + geo_file_path = supernetwork_parameters["geo_file_path"] + + file_type = Path(geo_file_path).suffix + if( file_type == '.gpkg' ): + self._dataframe = read_geopkg(geo_file_path) + elif( file_type == '.json') : + edge_list = supernetwork_parameters['flowpath_edge_list'] + self._dataframe = read_json(geo_file_path, edge_list) + else: + raise RuntimeError("Unsupported file type: {}".format(file_type)) + + # Don't need the string prefix anymore, drop it + mask = ~ self.dataframe['toid'].str.startswith("tnex") + self._dataframe = self.dataframe.apply(numeric_id, axis=1) + + # make the flowpath linkage, ignore the terminal nexus + self._flowpath_dict = dict(zip(self.dataframe.loc[mask].toid, self.dataframe.loc[mask].id)) + + # ********** need to be included in flowpath_attributes ************* + self._dataframe['alt'] = 1.0 #FIXME get the right value for this... + + cols = supernetwork_parameters.get('columns',None) + + if cols: + self._dataframe = self.dataframe[list(cols.values())] + # Rename parameter columns to standard names: from route-link names + # key: "link" + # downstream: "to" + # dx: "Length" + # n: "n" # TODO: rename to `manningn` + # ncc: "nCC" # TODO: rename to `mannningncc` + # s0: "So" # TODO: rename to `bedslope` + # bw: "BtmWdth" # TODO: rename to `bottomwidth` + # waterbody: "NHDWaterbodyComID" + # gages: "gages" + # tw: "TopWdth" # TODO: rename to `topwidth` + # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` + # alt: "alt" + # musk: "MusK" + # musx: "MusX" + # cs: "ChSlp" # TODO: rename to `sideslope` + self._dataframe = self.dataframe.rename(columns=reverse_dict(cols)) + self._dataframe.set_index("key", inplace=True) + self._dataframe = self.dataframe.sort_index() + + # numeric code used to indicate network terminal segments + terminal_code = supernetwork_parameters.get("terminal_code", 0) + + # There can be an externally determined terminal code -- that's this first value + self._terminal_codes = set() + self._terminal_codes.add(terminal_code) + # ... but there may also be off-domain nodes that are not explicitly identified + # but which are terminal (i.e., off-domain) as a result of a mask or some other + # an interior domain truncation that results in a + # otherwise valid node value being pointed to, but which is masked out or + # being intentionally separated into another domain. + self._terminal_codes = self.terminal_codes | set( + self.dataframe[~self.dataframe["downstream"].isin(self.dataframe.index)]["downstream"].values + ) + + # build connections dictionary + self._connections = extract_connections( + self.dataframe, "downstream", terminal_codes=self.terminal_codes + ) + + #Load waterbody/reservoir info + if waterbody_parameters: + levelpool_params = waterbody_parameters.get('level_pool', None) + if not levelpool_params: + # FIXME should not be a hard requirement + raise(RuntimeError("No supplied levelpool parameters in routing config")) + + lake_id = levelpool_params.get("level_pool_waterbody_id", "wb-id") + self._waterbody_df = read_ngen_waterbody_df( + levelpool_params["level_pool_waterbody_parameter_file_path"], + lake_id, + ) + + # Remove duplicate lake_ids and rows + self._waterbody_df = ( + self.waterbody_dataframe.reset_index() + .drop_duplicates(subset=lake_id) + .set_index(lake_id) + ) + + try: + self._waterbody_types_df = read_ngen_waterbody_type_df( + levelpool_params["reservoir_parameter_file"], + lake_id, + #self.waterbody_connections.values(), + ) + # Remove duplicate lake_ids and rows + self._waterbody_types_df =( + self.waterbody_types_dataframe.reset_index() + .drop_duplicates(subset=lake_id) + .set_index(lake_id) + ) + + except ValueError: + #FIXME any reservoir operations requires some type + #So make this default to 1 (levelpool) + self._waterbody_types_df = pd.DataFrame(index=self.waterbody_dataframe.index) + self._waterbody_types_df['reservoir_type'] = 1 diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index 9edd4c73b..06b614405 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -1,12 +1,12 @@ from .AbstractNetwork import AbstractNetwork -import xarray as xr -import pathlib -from collections import defaultdict import troute.nhd_io as nhd_io import troute.nhd_preprocess as nhd_prep import pandas as pd import time -from datetime import datetime, timedelta +import pathlib +from collections import defaultdict + +from troute.nhd_network import reverse_dict, extract_waterbody_connections, gage_mapping, extract_connections, replace_waterbodies_connections __showtiming__ = True #FIXME pass flag __verbose__ = True #FIXME pass verbosity @@ -17,8 +17,8 @@ class NHDNetwork(AbstractNetwork): """ __slots__ = [ - "_link_lake_crosswalk", "_usgs_lake_gage_crosswalk", - "_usace_lake_gage_crosswalk", "_flowpath_dict" + "_usgs_lake_gage_crosswalk", + "_usace_lake_gage_crosswalk", ] def __init__( @@ -48,6 +48,12 @@ def __init__( # Load Geo Data #------------------------------------------------ + self.read_geo_file( + supernetwork_parameters, + waterbody_parameters, + data_assimilation_parameters, + ) + ''' ( self._dataframe, self._connections, @@ -65,13 +71,12 @@ def __init__( waterbody_parameters, data_assimilation_parameters, ) - + ''' if __verbose__: print("supernetwork connections set complete") if __showtiming__: print("... in %s seconds." % (time.time() - start_time)) - cols = supernetwork_parameters.get('columns',None) break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) streamflow_da = data_assimilation_parameters.get('streamflow_da', False) break_network_at_gages = False @@ -90,14 +95,6 @@ def __init__( verbose=__verbose__, showtiming=__showtiming__, ) - - # list of all segments in the domain (MC + diffusive) - self.segment_index = self._dataframe.index - if self.diffusive_network_data: - for tw in self.diffusive_network_data: - self.segment_index = self.segment_index.append( - pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) - ) # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't @@ -125,14 +122,244 @@ def gages(self): """ if self._gages is None and "gages" in self._dataframe.columns: self._gages = nhd_io.build_filtered_gage_df(self._dataframe[["gages"]]) - else: - self._gages = {} + return self._gages @property def waterbody_null(self): return -9999 + + @property + def usgs_lake_gage_crosswalk(self): + return self._usgs_lake_gage_crosswalk + + @property + def usace_lake_gage_crosswalk(self): + return self._usace_lake_gage_crosswalk #@property #def wbody_conn(self): # return self._waterbody_connections + + def read_geo_file( + self, + supernetwork_parameters, + waterbody_parameters, + data_assimilation_parameters + ): + ''' + Construct network connections network, parameter dataframe, waterbody mapping, + and gage mapping. This is an intermediate-level function that calls several + lower level functions to read data, conduct network operations, and extract mappings. + + Arguments + --------- + supernetwork_parameters (dict): User input network parameters + + Returns: + -------- + connections (dict int: [int]): Network connections + param_df (DataFrame): Geometry and hydraulic parameters + wbodies (dict, int: int): segment-waterbody mapping + gages (dict, int: int): segment-gage mapping + + ''' + + # crosswalking dictionary between variables names in input dataset and + # variable names recognized by troute.routing module. + cols = supernetwork_parameters.get( + 'columns', + { + 'key' : 'link', + 'downstream': 'to', + 'dx' : 'Length', + 'n' : 'n', + 'ncc' : 'nCC', + 's0' : 'So', + 'bw' : 'BtmWdth', + 'waterbody' : 'NHDWaterbodyComID', + 'gages' : 'gages', + 'tw' : 'TopWdth', + 'twcc' : 'TopWdthCC', + 'alt' : 'alt', + 'musk' : 'MusK', + 'musx' : 'MusX', + 'cs' : 'ChSlp', + } + ) + + # numeric code used to indicate network terminal segments + terminal_code = supernetwork_parameters.get("terminal_code", 0) + + # read parameter dataframe + self._dataframe = nhd_io.read(pathlib.Path(supernetwork_parameters["geo_file_path"])) + + # select the column names specified in the values in the cols dict variable + self._dataframe = self.dataframe[list(cols.values())] + + # rename dataframe columns to keys in the cols dict variable + self._dataframe = self.dataframe.rename(columns=reverse_dict(cols)) + + # handle synthetic waterbody segments + synthetic_wb_segments = supernetwork_parameters.get("synthetic_wb_segments", None) + synthetic_wb_id_offset = supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) + if synthetic_wb_segments: + # rename the current key column to key32 + key32_d = {"key":"key32"} + self._dataframe = self.dataframe.rename(columns=key32_d) + # create a key index that is int64 + # copy the links into the new column + self._dataframe["key"] = self.dataframe.key32.astype("int64") + # update the values of the synthetic reservoir segments + fix_idx = self.dataframe.key.isin(set(synthetic_wb_segments)) + self._dataframe.loc[fix_idx,"key"] = (self.dataframe[fix_idx].key + synthetic_wb_id_offset).astype("int64") + + # set parameter dataframe index as segment id number, sort + self._dataframe = self.dataframe.set_index("key").sort_index() + + # get and apply domain mask + if "mask_file_path" in supernetwork_parameters: + data_mask = nhd_io.read_mask( + pathlib.Path(supernetwork_parameters["mask_file_path"]), + layer_string=supernetwork_parameters.get("mask_layer_string", None), + ) + data_mask = data_mask.set_index(data_mask.columns[0]) + self._dataframe = self.dataframe.filter(data_mask.index, axis=0) + + # map segment ids to waterbody ids + self._waterbody_connections = {} + if "waterbody" in cols: + self._waterbody_connections = extract_waterbody_connections( + self.dataframe[["waterbody"]] + ) + self._dataframe = self.dataframe.drop("waterbody", axis=1) + + # map segment ids to gage ids + self._gages = {} + if "gages" in cols: + self._gages = gage_mapping(self.dataframe[["gages"]]) + self._dataframe = self.dataframe.drop("gages", axis=1) + + # There can be an externally determined terminal code -- that's this first value + self._terminal_codes = set() + self._terminal_codes.add(terminal_code) + # ... but there may also be off-domain nodes that are not explicitly identified + # but which are terminal (i.e., off-domain) as a result of a mask or some other + # an interior domain truncation that results in a + # otherwise valid node value being pointed to, but which is masked out or + # being intentionally separated into another domain. + self._terminal_codes = self.terminal_codes | set( + self.dataframe[~self.dataframe["downstream"].isin(self.dataframe.index)]["downstream"].values + ) + + # build connections dictionary + self._connections = extract_connections( + self.dataframe, "downstream", terminal_codes=self.terminal_codes + ) + self._dataframe = self.dataframe.drop("downstream", axis=1) + + self._dataframe = self.dataframe.astype("float32") + + break_network_at_waterbodies = waterbody_parameters.get( + "break_network_at_waterbodies", False + ) + + # if waterbodies are being simulated, adjust the connections graph so that + # waterbodies are collapsed to single nodes. Also, build a mapping between + # waterbody outlet segments and lake ids + if break_network_at_waterbodies: + self._connections, self._link_lake_crosswalk = replace_waterbodies_connections( + self.connections, self.waterbody_connections + ) + else: + self._link_lake_crosswalk = None + + #============================================================================ + # Retrieve and organize waterbody parameters + + self._waterbody_type_specified = False + if break_network_at_waterbodies: + + # Read waterbody parameters from LAKEPARM file + level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) + self._waterbody_df = nhd_io.read_lakeparm( + level_pool_params['level_pool_waterbody_parameter_file_path'], + level_pool_params.get("level_pool_waterbody_id", 'lake_id'), + self.waterbody_connections.values() + ) + + # Remove duplicate lake_ids and rows + self._waterbody_df = ( + self.waterbody_dataframe.reset_index() + .drop_duplicates(subset="lake_id") + .set_index("lake_id") + ) + + # Declare empty dataframe + self._waterbody_types_df = pd.DataFrame() + + # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True + reservoir_da = data_assimilation_parameters.get( + 'reservoir_da', + {} + ) + + if reservoir_da: + usgs_hybrid = reservoir_da.get( + 'reservoir_persistence_usgs', + False + ) + usace_hybrid = reservoir_da.get( + 'reservoir_persistence_usace', + False + ) + param_file = reservoir_da.get( + 'gage_lakeID_crosswalk_file', + None + ) + else: + param_file = None + usace_hybrid = False + usgs_hybrid = False + + # check if RFC-type reservoirs are set to true + rfc_params = waterbody_parameters.get('rfc') + if rfc_params: + rfc_forecast = rfc_params.get( + 'reservoir_rfc_forecasts', + False + ) + param_file = rfc_params.get('reservoir_parameter_file',None) + else: + rfc_forecast = False + + if (param_file and reservoir_da) or (param_file and rfc_forecast): + self._waterbody_type_specified = True + ( + self._waterbody_types_df, + self._usgs_lake_gage_crosswalk, + self._usace_lake_gage_crosswalk + ) = nhd_io.read_reservoir_parameter_file( + param_file, + usgs_hybrid, + usace_hybrid, + rfc_forecast, + level_pool_params.get("level_pool_waterbody_id", 'lake_id'), + reservoir_da.get('crosswalk_usgs_gage_field', 'usgs_gage_id'), + reservoir_da.get('crosswalk_usgs_lakeID_field', 'usgs_lake_id'), + reservoir_da.get('crosswalk_usace_gage_field', 'usace_gage_id'), + reservoir_da.get('crosswalk_usace_lakeID_field', 'usace_lake_id'), + self.waterbody_connections.values(), + ) + else: + self._waterbody_type_specified = True + self._waterbody_types_df = pd.DataFrame(data = 1, index = self.waterbody_dataframe.index, columns = ['reservoir_type']) + self._usgs_lake_gage_crosswalk = None + self._usace_lake_gage_crosswalk = None + + else: + # Declare empty dataframes + self._waterbody_types_df = pd.DataFrame() + self._waterbody_df = pd.DataFrame() + self._usgs_lake_gage_crosswalk = None + self._usace_lake_gage_crosswalk = None diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py index 141ad07f8..7d0187f5c 100644 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ b/src/troute-network/troute/hyfeature_preprocess.py @@ -67,7 +67,7 @@ def read_geo_file( # cs: "ChSlp" # TODO: rename to `sideslope` dataframe = dataframe.rename(columns=reverse_dict(cols)) dataframe.set_index("key", inplace=True) - dataframe.sort_index() + dataframe = dataframe.sort_index() # numeric code used to indicate network terminal segments terminal_code = supernetwork_parameters.get("terminal_code", 0) diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index ac89e44dd..23f056209 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -253,7 +253,6 @@ def main_v04(argv): if showtiming: output_start_time = time.time() - ''' #TODO Update this to work with either network type... nwm_output_generator( run, @@ -266,14 +265,14 @@ def main_v04(argv): qts_subdivisions, compute_parameters.get("return_courant", False), cpu_pool, - network._waterbody_df, ## check: network._waterbody_df ?? def name is different from return self._ .. - network._waterbody_types_df, ## check: network._waterbody_types_df ?? def name is different from return self._ .. + network.waterbody_dataframe, + network.waterbody_types_dataframe, data_assimilation_parameters, data_assimilation.lastobs_df, - pd.DataFrame(), #network.link_gage_df, - None, #network.link_lake_crosswalk, + network.link_gage_df, + network.link_lake_crosswalk, ) - ''' + if showtiming: output_end_time = time.time() From e68eaa6bf6d3fb6140514fb67e32e83c323b2d1a Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Wed, 21 Dec 2022 18:07:44 +0000 Subject: [PATCH 37/54] fixed errors in data_assimilation.update() function --- src/troute-network/troute/DataAssimilation.py | 48 ++++++++++++------- src/troute-nwm/src/nwm_routing/__main__.py | 17 +++++-- test/LowerColorado_TX/test_AnA.yaml | 8 ++-- test/ngen/test_AnA.yaml | 6 +-- 4 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/troute-network/troute/DataAssimilation.py b/src/troute-network/troute/DataAssimilation.py index 68d5c8b1e..195f0d75e 100644 --- a/src/troute-network/troute/DataAssimilation.py +++ b/src/troute-network/troute/DataAssimilation.py @@ -6,6 +6,7 @@ import time import xarray as xr from collections import defaultdict +from datetime import datetime #FIXME parameterize into construciton showtiming = True @@ -798,9 +799,27 @@ def __init__(self, data_assimilation_parameters, run_parameters, waterbody_param # an error. Need to think through this more. if not self._usgs_df.empty: self._usgs_df = self._usgs_df.loc[:,network.t0:] + + def update_after_compute(self, run_results, data_assimilation_parameters, run_parameters,): + ''' + + ''' + # get reservoir DA initial parameters for next loop itteration + self._reservoir_usgs_param_df, self._reservoir_usace_param_df = _set_reservoir_da_params(run_results) + streamflow_da_parameters = data_assimilation_parameters.get('streamflow_da', None) + + if streamflow_da_parameters: + if streamflow_da_parameters.get('streamflow_nudging', False): + self._last_obs_df = new_lastobs(run_results, run_parameters.get("dt") * run_parameters.get("nts")) - def update(self, run_results, data_assimilation_parameters, run_parameters, network, da_run): + def update_for_next_loop( + self, + data_assimilation_parameters, + run_parameters, + network, + da_run + ): ''' Function to update data assimilation object for the next loop iteration. @@ -834,9 +853,6 @@ def update(self, run_results, data_assimilation_parameters, run_parameters, netw - reservoir_usace_df (DataFrame): USACE reservoir observations - reservoir_usace_param_df (DataFrame): USACE reservoir DA parameters ''' - # get reservoir DA initial parameters for next loop itteration - self._reservoir_usgs_param_df, self._reservoir_usace_param_df = _set_reservoir_da_params(run_results) - # update usgs_df if it is not empty streamflow_da_parameters = data_assimilation_parameters.get('streamflow_da', None) reservoir_da_parameters = data_assimilation_parameters.get('reservoir_da', None) @@ -917,21 +933,21 @@ def update(self, run_results, data_assimilation_parameters, run_parameters, netw # but there are DA parameters from the previous loop, then create a # dummy observations df. This allows the reservoir persistence to continue across loops. # USGS Reservoirs - if not network._waterbody_types_df.empty: - if 2 in network._waterbody_types_df['reservoir_type'].unique(): - if self._reservoir_usgs_df.empty and len(self._reservoir_usgs_param_df.index) > 0: + if not network.waterbody_types_dataframe.empty: + if 2 in network.waterbody_types_dataframe['reservoir_type'].unique(): + if self.reservoir_usgs_df.empty and len(self.reservoir_usgs_param_df.index) > 0: self._reservoir_usgs_df = pd.DataFrame( data = np.nan, - index = self._reservoir_usgs_param_df.index, + index = self.reservoir_usgs_param_df.index, columns = [network.t0] ) # USACE Reservoirs - if 3 in network._waterbody_types_df['reservoir_type'].unique(): - if self._reservoir_usace_df.empty and len(self._reservoir_usace_param_df.index) > 0: + if 3 in network._waterbody_types_dataframe['reservoir_type'].unique(): + if self.reservoir_usace_df.empty and len(self.reservoir_usace_param_df.index) > 0: self._reservoir_usace_df = pd.DataFrame( data = np.nan, - index = self._reservoir_usace_param_df.index, + index = self.reservoir_usace_param_df.index, columns = [network.t0] ) @@ -940,17 +956,13 @@ def update(self, run_results, data_assimilation_parameters, run_parameters, netw if 4 in network._waterbody_types_df['reservoir_type'].unique(): waterbody_parameters = update_lookback_hours(run_parameters.get("dt"), run_parameters.get("nts"), waterbody_parameters) ''' - - if streamflow_da_parameters: - if streamflow_da_parameters.get('streamflow_nudging', False): - self._last_obs_df = new_lastobs(run_results, run_parameters.get("dt") * run_parameters.get("nts")) - + # Trim the time-extent of the streamflow_da usgs_df # what happens if there are timeslice files missing on the front-end? # if the first column is some timestamp greater than t0, then this will throw # an error. Need to think through this more. - if not self._usgs_df.empty: - self._usgs_df = self._usgs_df.loc[:,network.t0:] + if not self.usgs_df.empty: + self._usgs_df = self.usgs_df.loc[:,network.t0:] @property def assimilation_parameters(self): diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index 23f056209..225de164a 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -218,6 +218,13 @@ def main_v04(argv): network.new_q0(run_results) network.update_waterbody_water_elevation() + # update reservoir parameters and lastobs_df + data_assimilation.update_after_compute( + run_results, + data_assimilation_parameters, + run_parameters, + ) + # TODO move the conditional call to write_lite_restart to nwm_output_generator. if "lite_restart" in output_parameters: nhd_io.write_lite_restart( @@ -240,11 +247,11 @@ def main_v04(argv): cpu_pool) # get reservoir DA initial parameters for next loop iteration - data_assimilation.update(run_results, - data_assimilation_parameters, - run_parameters, - network, - da_sets[run_set_iterator + 1]) + data_assimilation.update_for_next_loop( + data_assimilation_parameters, + run_parameters, + network, + da_sets[run_set_iterator + 1]) if showtiming: forcing_end_time = time.time() diff --git a/test/LowerColorado_TX/test_AnA.yaml b/test/LowerColorado_TX/test_AnA.yaml index 799222edf..813e90a8d 100644 --- a/test/LowerColorado_TX/test_AnA.yaml +++ b/test/LowerColorado_TX/test_AnA.yaml @@ -68,17 +68,17 @@ compute_parameters: qc_threshold : 1 streamflow_da: #---------- - streamflow_nudging : False + streamflow_nudging : True diffusive_streamflow_nudging : False gage_segID_crosswalk_file : domain/RouteLink.nc crosswalk_gage_field : 'gages' crosswalk_segID_field : 'link' - wrf_hydro_lastobs_file : lastobs/nudgingLastObs.2021-08-23_13:00:00.nc + wrf_hydro_lastobs_file : lastobs/nudgingLastObs.2021-08-23_12:00:00.nc lastobs_output_folder : lastobs/ reservoir_da: #---------- - reservoir_persistence_usgs : False - reservoir_persistence_usace : False + reservoir_persistence_usgs : True + reservoir_persistence_usace : True gage_lakeID_crosswalk_file : domain/reservoir_index_AnA.nc #-------------------------------------------------------------------------------- output_parameters: diff --git a/test/ngen/test_AnA.yaml b/test/ngen/test_AnA.yaml index 03e35cc35..1794094fb 100644 --- a/test/ngen/test_AnA.yaml +++ b/test/ngen/test_AnA.yaml @@ -91,7 +91,7 @@ compute_parameters: #-------------------------------------------------------------------------------- output_parameters: #---------- -# test_output: output/lcr_flowveldepth.pkl + test_output: output/lcr_flowveldepth.pkl lite_restart: #---------- lite_restart_output_directory: @@ -100,7 +100,7 @@ output_parameters: # wrf_hydro_channel_output_source_folder: channel_forcing/ chanobs_output: #---------- -# chanobs_output_directory: output/ -# chanobs_filepath : lcr_chanobs.nc + chanobs_output_directory: output/ + chanobs_filepath : lcr_chanobs.nc # lakeout_output: lakeout/ \ No newline at end of file From 07a7d0a101a4e6ea9af34bc60bd06df9632f658b Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Tue, 27 Dec 2022 20:04:53 +0000 Subject: [PATCH 38/54] add 'RoutingScheme' subclass of AbstractNetwork to set MC/diffusive related variables --- src/troute-network/troute/AbstractNetwork.py | 68 ++--- .../troute/HYFeaturesNetwork.py | 67 ++--- src/troute-network/troute/RoutingScheme.py | 240 ++++++++++++++++++ 3 files changed, 286 insertions(+), 89 deletions(-) create mode 100644 src/troute-network/troute/RoutingScheme.py diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index dc5e2f72d..530eeb267 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -27,21 +27,11 @@ class AbstractNetwork(ABC): "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", "_qlateral", "_break_segments", "_coastal_boundary_depth_df", "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", - "_refactored_reaches", "_unrefactored_topobathy_df", "_segment_index"] + "_refactored_reaches", "_unrefactored_topobathy_df", "_segment_index", + "supernetwork_parameters", "waterbody_parameters","data_assimilation_parameters", + "restart_parameters", "compute_parameters", "verbose", "showtiming", "break_points"] - def __init__( - self, - compute_parameters, - waterbody_parameters, - restart_parameters, - break_points=None, - verbose=False, - showtiming=False - ): - - global __verbose__, __showtiming__ - __verbose__ = verbose - __showtiming__ = showtiming + def __init__(self,): self._independent_networks = None self._reverse_network = None @@ -62,17 +52,17 @@ def __init__( """ self._break_segments = set() - if break_points: - if break_points["break_network_at_waterbodies"]: + if self.break_points: + if self.break_points["break_network_at_waterbodies"]: self._break_segments = self._break_segments | set(self.waterbody_connections.values()) - if break_points["break_network_at_gages"]: + if self.break_points["break_network_at_gages"]: self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) - self.build_diffusive_domain(compute_parameters) + #self.build_diffusive_domain(compute_parameters) - self.create_independent_networks(waterbody_parameters) + self.create_independent_networks() - self.initial_warmstate_preprocess(break_points["break_network_at_waterbodies"],restart_parameters) + self.initial_warmstate_preprocess()) def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): @@ -211,16 +201,16 @@ def independent_networks(self): """ if self._independent_networks is None: # STEP 2: Identify Independent Networks and Reaches by Network - if __showtiming__: + if self.showtiming: start_time = time.time() - if __verbose__: + if self.verbose: print("organizing connections into reaches ...") self._independent_networks = reachable_network(self.reverse_network) - if __verbose__: + if self.verbose: print("reach organization complete") - if __showtiming__: + if self.showtiming: print("... in %s seconds." % (time.time() - start_time)) return self._independent_networks @@ -328,26 +318,6 @@ def link_gage_df(self): link_gage_df.index.name = 'link' return link_gage_df - @property - def diffusive_network_data(self): - return self._diffusive_network_data - - @property - def topobathy_df(self): - return self._topobathy_df - - @property - def refactored_diffusive_domain(self): - return self._refactored_diffusive_domain - - @property - def refactored_reaches(self): - return self._refactored_reaches - - @property - def unrefactored_topobathy_df(self): - return self._unrefactored_topobathy_df - @property @abstractmethod def waterbody_connections(self): @@ -645,14 +615,14 @@ def build_diffusive_domain(self, compute_parameters): for us in trib_segs: self._connections[us] = [] - def create_independent_networks(self, waterbody_parameters): + def create_independent_networks(self,): LOG.info("organizing connections into reaches ...") start_time = time.time() gage_break_segments = set() wbody_break_segments = set() - break_network_at_waterbodies = waterbody_parameters.get( + break_network_at_waterbodies = self.waterbody_parameters.get( "break_network_at_waterbodies", False ) @@ -678,7 +648,7 @@ def create_independent_networks(self, waterbody_parameters): LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) - def initial_warmstate_preprocess(self, break_network_at_waterbodies, restart_parameters,): + def initial_warmstate_preprocess(self,): ''' Assemble model initial condition data: @@ -711,13 +681,15 @@ def initial_warmstate_preprocess(self, break_network_at_waterbodies, restart_par ----- ''' + restart_parameters = self.restart_parameters + # generalize waterbody ID's to be used with any network index_id = self.waterbody_dataframe.index.names[0] #---------------------------------------------------------------------------- # Assemble waterbody initial states (outflow and pool elevation #---------------------------------------------------------------------------- - if break_network_at_waterbodies: + if self.break_points['break_network_at_waterbodies']: start_time = time.time() LOG.info("setting waterbody initial states ...") diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 96be4e634..80a58cecc 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -1,4 +1,4 @@ -from .AbstractNetwork import AbstractNetwork +from .RoutingScheme import RoutingScheme import pandas as pd import numpy as np import geopandas as gpd @@ -89,7 +89,7 @@ def node_key_func(x): return df -class HYFeaturesNetwork(AbstractNetwork): +class HYFeaturesNetwork(RoutingScheme): """ """ @@ -105,27 +105,23 @@ def __init__(self, """ """ - global __verbose__, __showtiming__ - __verbose__ = verbose - __showtiming__ = showtiming - if __verbose__: + self.supernetwork_parameters = supernetwork_parameters + self.waterbody_parameters = waterbody_parameters + self.data_assimilation_parameters = data_assimilation_parameters + self.restart_parameters = restart_parameters + self.compute_parameters = compute_parameters + self.verbose = verbose + self.showtiming = showtiming + + if self.verbose: print("creating supernetwork connections set") - if __showtiming__: + if self.showtiming: start_time = time.time() #------------------------------------------------ # Load Geo File #------------------------------------------------ - (self._dataframe, - self._flowpath_dict, - self._connections, - self._waterbody_df, - self._waterbody_types_df, - self._terminal_codes, - ) = hyfeature_prep.read_geo_file( - supernetwork_parameters, - waterbody_parameters, - ) + self.read_geo_file() #TODO Update for waterbodies and DA specific to HYFeatures... self._waterbody_connections = {} @@ -134,27 +130,20 @@ def __init__(self, self._link_lake_crosswalk = None - if __verbose__: + if self.verbose: print("supernetwork connections set complete") - if __showtiming__: + if self.showtiming: print("... in %s seconds." % (time.time() - start_time)) - break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) - streamflow_da = data_assimilation_parameters.get('streamflow_da', False) + break_network_at_waterbodies = self.waterbody_parameters.get("break_network_at_waterbodies", False) + streamflow_da = self.data_assimilation_parameters.get('streamflow_da', False) break_network_at_gages = False if streamflow_da: break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, + self.break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} - super().__init__( - compute_parameters, - waterbody_parameters, - restart_parameters, - break_points, - verbose=__verbose__, - showtiming=__showtiming__, - ) + super().__init__() # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't @@ -324,19 +313,15 @@ def gages(self): def waterbody_null(self): return np.nan #pd.NA - def read_geo_file( - self, - supernetwork_parameters, - waterbody_parameters, - ): + def read_geo_file(self,): - geo_file_path = supernetwork_parameters["geo_file_path"] + geo_file_path = self.supernetwork_parameters["geo_file_path"] file_type = Path(geo_file_path).suffix if( file_type == '.gpkg' ): self._dataframe = read_geopkg(geo_file_path) elif( file_type == '.json') : - edge_list = supernetwork_parameters['flowpath_edge_list'] + edge_list = self.supernetwork_parameters['flowpath_edge_list'] self._dataframe = read_json(geo_file_path, edge_list) else: raise RuntimeError("Unsupported file type: {}".format(file_type)) @@ -351,7 +336,7 @@ def read_geo_file( # ********** need to be included in flowpath_attributes ************* self._dataframe['alt'] = 1.0 #FIXME get the right value for this... - cols = supernetwork_parameters.get('columns',None) + cols = self.supernetwork_parameters.get('columns',None) if cols: self._dataframe = self.dataframe[list(cols.values())] @@ -376,7 +361,7 @@ def read_geo_file( self._dataframe = self.dataframe.sort_index() # numeric code used to indicate network terminal segments - terminal_code = supernetwork_parameters.get("terminal_code", 0) + terminal_code = self.supernetwork_parameters.get("terminal_code", 0) # There can be an externally determined terminal code -- that's this first value self._terminal_codes = set() @@ -396,8 +381,8 @@ def read_geo_file( ) #Load waterbody/reservoir info - if waterbody_parameters: - levelpool_params = waterbody_parameters.get('level_pool', None) + if self.waterbody_parameters: + levelpool_params = self.waterbody_parameters.get('level_pool', None) if not levelpool_params: # FIXME should not be a hard requirement raise(RuntimeError("No supplied levelpool parameters in routing config")) diff --git a/src/troute-network/troute/RoutingScheme.py b/src/troute-network/troute/RoutingScheme.py new file mode 100644 index 000000000..195c16dcd --- /dev/null +++ b/src/troute-network/troute/RoutingScheme.py @@ -0,0 +1,240 @@ +from .AbstractNetwork import AbstractNetwork +import logging +import yaml +import json +import xarray as xr +import pandas as pd + +from troute.nhd_network import reverse_network +from troute.nhd_network_utilities_v02 import organize_independent_networks, build_refac_connections + +LOG = logging.getLogger('') + +def read_diffusive_domain(domain_file): + ''' + Read diffusive domain data from .ymal or .json file. + + Arguments + --------- + domain_file (str or pathlib.Path): Path of diffusive domain file + + Returns + ------- + data (dict int: [int]): domain tailwater segments: list of segments in domain + (includeing tailwater segment) + + ''' + if domain_file[-4:] == "yaml": + with open(domain_file) as domain: + data = yaml.load(domain, Loader=yaml.SafeLoader) + else: + with open(domain_file) as domain: + data = json.load(domain) + + return data + +def read_netcdf(geo_file_path): + ''' + Open a netcdf file with xarray and convert to dataframe + + Arguments + --------- + geo_file_path (str or pathlib.Path): netCDF filepath + + Returns + ------- + ds.to_dataframe() (DataFrame): netCDF contents + + Notes + ----- + - When handling large volumes of netCDF files, xarray is not the most efficient. + + ''' + with xr.open_dataset(geo_file_path) as ds: + return ds.to_dataframe() + + +class RoutingScheme(AbstractNetwork): + """ + + """ + __slots__ = ["hybrid_params"] + + def __init__(self,): + """ + + """ + self.hybrid_params = self.compute_parameters.get("hybrid_parameters", False) + + self._diffusive_domain = None + self._diffusive_network_data = None + self._topobathy_df = pd.DataFrame() + self._unrefactored_topobathy_df = pd.DataFrame() + self._refactored_diffusive_domain = None + self._refactored_diffusive_network_data = None + self._refactored_reaches = {} + + # Determine whether to run hybrid routing from user input + run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) + domain_file = self.hybrid_params.get("diffusive_domain", None) + + if run_hybrid and domain_file: + #========================================================================== + # build diffusive domain data and edit MC domain data for hybrid simulation + self._diffusive_domain = read_diffusive_domain(domain_file) + self._diffusive_network_data = {} + + rconn_diff0 = reverse_network(self._connections) + + for tw in self._diffusive_domain: + mainstem_segs = self._diffusive_domain[tw]['links'] + # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is + # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. + upstream_boundary_mainstem_link = self._diffusive_domain[tw]['upstream_boundary_link_mainstem'] + if upstream_boundary_mainstem_link[0] in mainstem_segs: + mainstem_segs.remove(upstream_boundary_mainstem_link[0]) + + # ===== build diffusive network data objects ==== + self._diffusive_network_data[tw] = {} + + # add diffusive domain segments + self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs + + # diffusive domain tributary segments + trib_segs = [] + + for seg in mainstem_segs: + us_list = rconn_diff0[seg] + for u in us_list: + if u not in mainstem_segs: + trib_segs.append(u) + + self._diffusive_network_data[tw]['tributary_segments'] = trib_segs + # diffusive domain connections object + self._diffusive_network_data[tw]['connections'] = {k: self._connections[k] for k in (mainstem_segs + trib_segs)} + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, reaches, rconn_diff = organize_independent_networks( + self._diffusive_network_data[tw]['connections'], + set(trib_segs), + set(), + ) + + self._diffusive_network_data[tw]['rconn'] = rconn_diff + self._diffusive_network_data[tw]['reaches'] = reaches[tw] + + # RouteLink parameters + self._diffusive_network_data[tw]['param_df'] = self._dataframe.filter( + (mainstem_segs + trib_segs), + axis = 0, + ) + self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link + + # ==== remove diffusive domain segs from MC domain ==== + # drop indices from param_df + self._dataframe = self._dataframe.drop(mainstem_segs) + + # remove keys from connections dictionary + for s in mainstem_segs: + self._connections.pop(s) + + # update downstream connections of trib segs + for us in trib_segs: + self._connections[us] = [] + + super().__init__() + + + def diffusive_network_data(self,): + return self._diffusive_network_data + + def topobathy_df(self,): + if self._topobathy_df.empty(): + run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) + use_topobathy = self.hybrid_params.get('use_natl_xsections', False) + + if run_hybrid and use_topobathy: + run_refactored = self.hybrid_params.get('run_refactored_network', False) + + if run_refactored: + refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + self._topobathy_df = read_netcdf(refactored_topobathy_file).set_index('link') + else: + topobathy_file = self.hybrid_params.get("topobathy_domain", None) + self._topobathy_df = read_netcdf(topobathy_file).set_index('link') + self._topobathy_df.index = self._topobathy_df.index.astype(int) + + return self._topobathy_df + + def refactored_diffusive_domain(self,): + if not self._refactored_diffusive_domain: + run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) + run_refactored = self.hybrid_params.get('run_refactored_network', False) + + if run_hybrid and run_refactored: + refactored_domain_file = self.hybrid_params.get("refactored_domain", None) + + self._refactored_diffusive_domain = read_diffusive_domain(refactored_domain_file) + + return self._refactored_diffusive_domain + + def refactored_reaches(self,): + if not self._refactored_reaches: + run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) + run_refactored = self.hybrid_params.get('run_refactored_network', False) + + if run_hybrid and run_refactored: + refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + diffusive_parameters = {'geo_file_path': refactored_topobathy_file} + refactored_connections = build_refac_connections(diffusive_parameters) + + for tw in self._diffusive_domain: + + # list of stream segments of a single refactored diffusive domain + refac_tw = self.refactored_diffusive_domain[tw]['refac_tw'] + rlinks_tw = self.refactored_diffusive_domain[tw]['rlinks'] + refactored_connections_tw = {} + + # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections + # for a single refactored diffusive domain defined by a current tw. + for k in rlinks_tw: + if k in refactored_connections.keys() and k != refac_tw: + refactored_connections_tw[k] = refactored_connections[k] + + trib_segs = self.diffusive_network_data[tw]['tributary_segments'] + refactored_diffusive_network_data = {} + refactored_diffusive_network_data[refac_tw] = {} + refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs + refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw + + for k in trib_segs: + refactored_diffusive_network_data[refac_tw]['connections'][k] = [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( + refactored_diffusive_network_data[refac_tw]['connections'], + set(trib_segs), + set(), + ) + + self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] + refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] + refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] + + return self._refactored_reaches + + def unrefactored_topobathy_df(self,): + if self._unrefactored_topobathy_df.empty(): + run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) + use_topobathy = self.hybrid_params.get('use_natl_xsections', False) + run_refactored = self.hybrid_params.get('run_refactored_network', False) + + if run_hybrid and use_topobathy and run_refactored: + topobathy_file = self.hybrid_params.get("topobathy_domain", None) + self._unrefactored_topobathy_df = read_netcdf(topobathy_file).set_index('link') + self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) + + return self._unrefactored_topobathy_df + From 8213c77ee4a746607f7f4a53a64f61a4a3b71024 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Wed, 28 Dec 2022 15:52:23 +0000 Subject: [PATCH 39/54] bug fixes to get test HYfeatures with MC only run working --- src/troute-network/troute/AbstractNetwork.py | 8 +++----- src/troute-network/troute/HYFeaturesNetwork.py | 7 ++++--- src/troute-network/troute/RoutingScheme.py | 17 ++++++++++++----- src/troute-nwm/src/nwm_routing/__main__.py | 4 ++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 530eeb267..fa7e123fe 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -25,9 +25,7 @@ class AbstractNetwork(ABC): "_waterbody_types_df", "_waterbody_type_specified", "_independent_networks", "_reaches_by_tw", "_flowpath_dict", "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", - "_qlateral", "_break_segments", "_coastal_boundary_depth_df", - "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", - "_refactored_reaches", "_unrefactored_topobathy_df", "_segment_index", + "_qlateral", "_break_segments", "_segment_index", "supernetwork_parameters", "waterbody_parameters","data_assimilation_parameters", "restart_parameters", "compute_parameters", "verbose", "showtiming", "break_points"] @@ -62,7 +60,7 @@ def __init__(self,): self.create_independent_networks() - self.initial_warmstate_preprocess()) + self.initial_warmstate_preprocess() def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): @@ -305,7 +303,7 @@ def segment_index(self): """ # list of all segments in the domain (MC + diffusive) self._segment_index = self.dataframe.index - if self.diffusive_network_data: + if self._diffusive_network_data: for tw in self.diffusive_network_data: self._segment_index = self._segment_index.append( pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index 80a58cecc..ff4c4456e 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -45,6 +45,7 @@ def numeric_id(flowpath): toid = flowpath['toid'].split('-')[-1] flowpath['id'] = int(id) flowpath['toid'] = int(toid) + return flowpath def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): """ @@ -316,7 +317,7 @@ def waterbody_null(self): def read_geo_file(self,): geo_file_path = self.supernetwork_parameters["geo_file_path"] - + file_type = Path(geo_file_path).suffix if( file_type == '.gpkg' ): self._dataframe = read_geopkg(geo_file_path) @@ -325,11 +326,11 @@ def read_geo_file(self,): self._dataframe = read_json(geo_file_path, edge_list) else: raise RuntimeError("Unsupported file type: {}".format(file_type)) - + # Don't need the string prefix anymore, drop it mask = ~ self.dataframe['toid'].str.startswith("tnex") self._dataframe = self.dataframe.apply(numeric_id, axis=1) - + # make the flowpath linkage, ignore the terminal nexus self._flowpath_dict = dict(zip(self.dataframe.loc[mask].toid, self.dataframe.loc[mask].id)) diff --git a/src/troute-network/troute/RoutingScheme.py b/src/troute-network/troute/RoutingScheme.py index 195c16dcd..11d27a2cf 100644 --- a/src/troute-network/troute/RoutingScheme.py +++ b/src/troute-network/troute/RoutingScheme.py @@ -58,7 +58,10 @@ class RoutingScheme(AbstractNetwork): """ """ - __slots__ = ["hybrid_params"] + __slots__ = ["hybrid_params", "_diffusive_domain", "_coastal_boundary_depth_df", + "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", + "_refactored_diffusive_network_data", "_refactored_reaches", + "_unrefactored_topobathy_df",] def __init__(self,): """ @@ -145,12 +148,13 @@ def __init__(self,): super().__init__() - + @property def diffusive_network_data(self,): return self._diffusive_network_data + @property def topobathy_df(self,): - if self._topobathy_df.empty(): + if self._topobathy_df.empty: run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) use_topobathy = self.hybrid_params.get('use_natl_xsections', False) @@ -166,7 +170,8 @@ def topobathy_df(self,): self._topobathy_df.index = self._topobathy_df.index.astype(int) return self._topobathy_df - + + @property def refactored_diffusive_domain(self,): if not self._refactored_diffusive_domain: run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) @@ -179,6 +184,7 @@ def refactored_diffusive_domain(self,): return self._refactored_diffusive_domain + @property def refactored_reaches(self,): if not self._refactored_reaches: run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) @@ -225,8 +231,9 @@ def refactored_reaches(self,): return self._refactored_reaches + @property def unrefactored_topobathy_df(self,): - if self._unrefactored_topobathy_df.empty(): + if self._unrefactored_topobathy_df.empty: run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) use_topobathy = self.hybrid_params.get('use_natl_xsections', False) run_refactored = self.hybrid_params.get('run_refactored_network', False) diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index 225de164a..13527c705 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -77,14 +77,14 @@ def main_v04(argv): # perform initial warmstate preprocess. if showtiming: network_start_time = time.time() - + #if "ngen_nexus_file" in supernetwork_parameters: if supernetwork_parameters["geo_file_type"] == 'HYFeaturesNetwork': network = HYFeaturesNetwork(supernetwork_parameters, waterbody_parameters, data_assimilation_parameters, restart_parameters, - forcing_parameters, + compute_parameters, verbose=True, showtiming=showtiming) elif supernetwork_parameters["geo_file_type"] == 'NHDNetwork': From c0c0c917b89e62c03676830f85cd8779da5d60de Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 30 Dec 2022 15:12:14 +0000 Subject: [PATCH 40/54] move build_qlateral_array function from AbstractNetwork to individual networks --- src/troute-network/troute/AbstractNetwork.py | 366 +----------------- src/troute-network/troute/DataAssimilation.py | 2 +- .../troute/HYFeaturesNetwork.py | 123 +++++- src/troute-network/troute/NHDNetwork.py | 79 ++++ src/troute-network/troute/RoutingScheme.py | 36 +- src/troute-nwm/src/nwm_routing/__main__.py | 10 +- 6 files changed, 241 insertions(+), 375 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index fa7e123fe..72afb0c44 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -1,14 +1,10 @@ from abc import ABC, abstractmethod from functools import partial import pandas as pd -import numpy as np -import pyarrow.parquet as pq from datetime import datetime, timedelta -from joblib import delayed, Parallel -import netCDF4 + import time import logging -import pathlib from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition from troute.nhd_network_utilities_v02 import organize_independent_networks, build_channel_initial_state, build_refac_connections @@ -27,7 +23,8 @@ class AbstractNetwork(ABC): "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", "_qlateral", "_break_segments", "_segment_index", "supernetwork_parameters", "waterbody_parameters","data_assimilation_parameters", - "restart_parameters", "compute_parameters", "verbose", "showtiming", "break_points"] + "restart_parameters", "compute_parameters", "forcing_parameters", + "hybrid_parameters", "verbose", "showtiming", "break_points"] def __init__(self,): @@ -55,15 +52,13 @@ def __init__(self,): self._break_segments = self._break_segments | set(self.waterbody_connections.values()) if self.break_points["break_network_at_gages"]: self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) - - #self.build_diffusive_domain(compute_parameters) self.create_independent_networks() self.initial_warmstate_preprocess() - def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool): + def assemble_forcings(self, run,): """ Assemble model forcings. Forcings include hydrological lateral inflows (qlats) and coastal boundary depths for hybrid runs @@ -91,13 +86,13 @@ def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernet """ # Unpack user-specified forcing parameters - dt = forcing_parameters.get("dt", None) - qts_subdivisions = forcing_parameters.get("qts_subdivisions", None) - qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) - qlat_file_index_col = forcing_parameters.get("qlat_file_index_col", "feature_id") - qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") - qlat_file_gw_bucket_flux_col = forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") - qlat_file_terrain_runoff_col = forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") + dt = self.forcing_parameters.get("dt", None) + qts_subdivisions = self.forcing_parameters.get("qts_subdivisions", None) + qlat_input_folder = self.forcing_parameters.get("qlat_input_folder", None) + qlat_file_index_col = self.forcing_parameters.get("qlat_file_index_col", "feature_id") + qlat_file_value_col = self.forcing_parameters.get("qlat_file_value_col", "q_lateral") + qlat_file_gw_bucket_flux_col = self.forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") + qlat_file_terrain_runoff_col = self.forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") # TODO: find a better way to deal with these defaults and overrides. @@ -117,21 +112,27 @@ def assemble_forcings(self, run, forcing_parameters, hybrid_parameters, supernet # Place holder, if reading qlats from a file use this. # TODO: add an option for reading qlat data from BMI/model engine + start_time = time.time() + LOG.info("Creating a DataFrame of lateral inflow forcings ...") + from_file = True if from_file: self.build_qlateral_array( run, - cpu_pool, - supernetwork_parameters, ) + + LOG.debug( + "lateral inflow DataFrame creation complete in %s seconds." \ + % (time.time() - start_time) + ) #--------------------------------------------------------------------- # Assemble coastal coupling data [WIP] #--------------------------------------------------------------------- # Run if coastal_boundary_depth_df has not already been created: if self._coastal_boundary_depth_df.empty: - coastal_boundary_elev_files = forcing_parameters.get('coastal_boundary_input_file', None) - coastal_boundary_domain_files = hybrid_parameters.get('coastal_boundary_domain', None) + coastal_boundary_elev_files = self.forcing_parameters.get('coastal_boundary_input_file', None) + coastal_boundary_domain_files = self.hybrid_parameters.get('coastal_boundary_domain', None) if coastal_boundary_elev_files: #start_time = time.time() @@ -405,213 +406,6 @@ def astype(self, type, columns=None): else: self._dataframe = self._dataframe.astype(type) - - def build_diffusive_domain(self, compute_parameters): - """ - - """ - hybrid_params = compute_parameters.get("hybrid_parameters", False) - if hybrid_params: - # switch parameters - # if run_hybrid = False, run MC only - # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc - # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric - # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric - run_hybrid = hybrid_params.get('run_hybrid_routing', False) - use_topobathy = hybrid_params.get('use_natl_xsections', False) - run_refactored = hybrid_params.get('run_refactored_network', False) - - # file path parameters of non-refactored hydrofabric defined by RouteLink.nc - domain_file = hybrid_params.get("diffusive_domain", None) - topobathy_file = hybrid_params.get("topobathy_domain", None) - - # file path parameters of refactored hydrofabric for diffusive wave channel routing - refactored_domain_file = hybrid_params.get("refactored_domain", None) - refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) - #------------------------------------------------------------------------- - # for non-refactored hydofabric defined by RouteLink.nc - # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. - if run_hybrid and domain_file: - - LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - diffusive_domain = nhd_io.read_diffusive_domain(domain_file) - - if use_topobathy and topobathy_file: - - LOG.debug('Natural cross section data on original hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - self._topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - - # TODO: Request GID make comID variable an integer in their product, so - # we do not need to change variable types, here. - self._topobathy_df.index = self._topobathy_df.index.astype(int) - - else: - self._topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - self._diffusive_network_data = {} - - else: - diffusive_domain = None - self._diffusive_network_data = None - self._topobathy_df = pd.DataFrame() - LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') - self._unrefactored_topobathy_df = pd.DataFrame() - #------------------------------------------------------------------------- - # for refactored hydofabric - if run_hybrid and run_refactored and refactored_domain_file: - - LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - self._refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) - - if use_topobathy and refactored_topobathy_file: - - LOG.debug('Natural cross section data of refactored hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - self._topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) - - # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy - # for crosswalking water elevations between non-refactored and refactored hydrofabrics. - self._unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) - - else: - self._topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - refactored_diffusive_network_data = {} - - else: - self._refactored_diffusive_domain = None - refactored_diffusive_network_data = None - self._refactored_reaches = {} - LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') - - else: - diffusive_domain = None - self._diffusive_network_data = None - self._topobathy_df = pd.DataFrame() - self._unrefactored_topobathy_df = pd.DataFrame() - self._refactored_diffusive_domain = None - refactored_diffusive_network_data = None - self._refactored_reaches = {} - LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') - - #============================================================================ - # build diffusive domain data and edit MC domain data for hybrid simulation - - # - if diffusive_domain: - rconn_diff0 = reverse_network(self._connections) - self._refactored_reaches = {} - - for tw in diffusive_domain: - mainstem_segs = diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - self._diffusive_network_data[tw] = {} - - # add diffusive domain segments - self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - self._diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - self._diffusive_network_data[tw]['connections'] = {k: self._connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = organize_independent_networks( - self._diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - self._diffusive_network_data[tw]['rconn'] = rconn_diff - self._diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - self._diffusive_network_data[tw]['param_df'] = self._dataframe.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - if self._refactored_diffusive_domain: - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = build_refac_connections(diffusive_parameters) - - # list of stream segments of a single refactored diffusive domain - refac_tw = self._refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = self._refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k]= [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] - else: - self._refactored_reaches={} - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - self._dataframe = self._dataframe.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - self._connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - self._connections[us] = [] def create_independent_networks(self,): @@ -817,121 +611,3 @@ def _try_parsing_date(text): "channel initial states complete in %s seconds."\ % (time.time() - start_time) ) - - def build_qlateral_array(self, run, cpu_pool, supernetwork_parameters,): - - start_time = time.time() - LOG.info("Creating a DataFrame of lateral inflow forcings ...") - - # TODO: set default/optional arguments - qts_subdivisions = run.get("qts_subdivisions", 1) - nts = run.get("nts", 1) - qlat_input_folder = run.get("qlat_input_folder", None) - qlat_input_file = run.get("qlat_input_file", None) - - geo_file_type = supernetwork_parameters.get('geo_file_type') - - if qlat_input_folder: - qlat_input_folder = pathlib.Path(qlat_input_folder) - if "qlat_files" in run: - qlat_files = run.get("qlat_files") - qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] - elif "qlat_file_pattern_filter" in run: - qlat_file_pattern_filter = run.get( - "qlat_file_pattern_filter", "*CHRT_OUT*" - ) - qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) - - qlat_file_index_col = run.get( - "qlat_file_index_col", "feature_id" - ) - - if geo_file_type=='NHDNetwork': - # Parallel reading of qlateral data from CHRTOUT - with Parallel(n_jobs=cpu_pool) as parallel: - jobs = [] - for f in qlat_files: - jobs.append( - delayed(nhd_io.get_ql_from_chrtout) - #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) - #delayed(nhd_io.get_ql_from_csv) - (f) - ) - ql_list = parallel(jobs) - - # get feature_id from a single CHRTOUT file - with netCDF4.Dataset(qlat_files[0]) as ds: - idx = ds.variables[qlat_file_index_col][:].filled() - - # package data into a DataFrame - qlats_df = pd.DataFrame( - np.stack(ql_list).T, - index = idx, - columns = range(len(qlat_files)) - ) - - qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] - elif geo_file_type=='HYFeaturesNetwork': - dfs=[] - for f in qlat_files: - df = read_file(f).set_index(['feature_id']) - dfs.append(df) - - # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids - nexuses_lateralflows_df = pd.concat(dfs, axis=1) - - # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids - qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) - for k,v in self.downstream_flowpath_dict.items() ), axis=1 - ).T - qlats_df.columns=range(len(qlat_files)) - qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] - - # The segment_index has the full network set of segments/flowpaths. - # Whereas the set of flowpaths that are downstream of nexuses is a - # subset of the segment_index. Therefore, all of the segments/flowpaths - # that are not accounted for in the set of flowpaths downstream of - # nexuses need to be added to the qlateral dataframe and padded with - # zeros. - all_df = pd.DataFrame( np.zeros( (len(self.segment_index), len(qlats_df.columns)) ), index=self.segment_index, - columns=qlats_df.columns ) - all_df.loc[ qlats_df.index ] = qlats_df - qlats_df = all_df.sort_index() - - elif qlat_input_file: - qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) - else: - qlat_const = run.get("qlat_const", 0) - qlats_df = pd.DataFrame( - qlat_const, - index=self.segment_index, - columns=range(nts // qts_subdivisions), - dtype="float32", - ) - - # TODO: Make a more sophisticated date-based filter - max_col = 1 + nts // qts_subdivisions - if len(qlats_df.columns) > max_col: - qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) - - if not self.segment_index.empty: - qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] - - LOG.debug( - "lateral inflow DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - self._qlateral = qlats_df - - - -def read_file(file_name): - extension = file_name.suffix - if extension=='.csv': - df = pd.read_csv(file_name) - elif extension=='.parquet': - df = pq.read_table(file_name).to_pandas().reset_index() - df.index.name = None - - return df \ No newline at end of file diff --git a/src/troute-network/troute/DataAssimilation.py b/src/troute-network/troute/DataAssimilation.py index 195f0d75e..b5bb0a2c6 100644 --- a/src/troute-network/troute/DataAssimilation.py +++ b/src/troute-network/troute/DataAssimilation.py @@ -943,7 +943,7 @@ def update_for_next_loop( ) # USACE Reservoirs - if 3 in network._waterbody_types_dataframe['reservoir_type'].unique(): + if 3 in network.waterbody_types_dataframe['reservoir_type'].unique(): if self.reservoir_usace_df.empty and len(self.reservoir_usace_param_df.index) > 0: self._reservoir_usace_df = pd.DataFrame( data = np.nan, diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index ff4c4456e..ea6a8a07c 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -6,9 +6,9 @@ import os import json from pathlib import Path +import pyarrow.parquet as pq import troute.nhd_io as nhd_io #FIXME -import troute.hyfeature_preprocess as hyfeature_prep from troute.nhd_network import reverse_dict, extract_connections __verbose__ = False @@ -94,13 +94,16 @@ class HYFeaturesNetwork(RoutingScheme): """ """ - __slots__ = [] + __slots__ = ["_upstream_terminal"] + def __init__(self, supernetwork_parameters, waterbody_parameters, data_assimilation_parameters, - restart_parameters=None, - forcing_parameters=None, + restart_parameters, + compute_parameters, + forcing_parameters, + hybrid_parameters, verbose=False, showtiming=False): """ @@ -111,6 +114,8 @@ def __init__(self, self.data_assimilation_parameters = data_assimilation_parameters self.restart_parameters = restart_parameters self.compute_parameters = compute_parameters + self.forcing_parameters = forcing_parameters + self.hybrid_parameters = hybrid_parameters self.verbose = verbose self.showtiming = showtiming @@ -376,6 +381,15 @@ def read_geo_file(self,): self.dataframe[~self.dataframe["downstream"].isin(self.dataframe.index)]["downstream"].values ) + #This is NEARLY redundant to the self.terminal_codes property, but in this case + #we actually need the mapping of what is upstream of that terminal node as well. + #we also only want terminals that actually exist based on definition, not user input + terminal_mask = ~self._dataframe["downstream"].isin(self._dataframe.index) + terminal = self._dataframe.loc[ terminal_mask ]["downstream"] + self._upstream_terminal = dict() + for key, value in terminal.items(): + self._upstream_terminal.setdefault(value, set()).add(key) + # build connections dictionary self._connections = extract_connections( self.dataframe, "downstream", terminal_codes=self.terminal_codes @@ -419,4 +433,105 @@ def read_geo_file(self,): #So make this default to 1 (levelpool) self._waterbody_types_df = pd.DataFrame(index=self.waterbody_dataframe.index) self._waterbody_types_df['reservoir_type'] = 1 + + def build_qlateral_array(self, run,): + + # TODO: set default/optional arguments + qts_subdivisions = run.get("qts_subdivisions", 1) + nts = run.get("nts", 1) + qlat_input_folder = run.get("qlat_input_folder", None) + qlat_input_file = run.get("qlat_input_file", None) + + if qlat_input_folder: + qlat_input_folder = Path(qlat_input_folder) + if "qlat_files" in run: + qlat_files = run.get("qlat_files") + qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] + elif "qlat_file_pattern_filter" in run: + qlat_file_pattern_filter = run.get( + "qlat_file_pattern_filter", "*CHRT_OUT*" + ) + qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) + dfs=[] + for f in qlat_files: + df = read_file(f).set_index(['feature_id']) + dfs.append(df) + + # lateral flows [m^3/s] are stored at NEXUS points with NEXUS ids + nexuses_lateralflows_df = pd.concat(dfs, axis=1) + + # Take flowpath ids entering NEXUS and replace NEXUS ids by the upstream flowpath ids + qlats_df = pd.concat( (nexuses_lateralflows_df.loc[int(k)].rename(v) + for k,v in self.downstream_flowpath_dict.items() ), axis=1 + ).T + qlats_df.columns=range(len(qlat_files)) + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + ''' + #For a terminal nexus, we want to include the lateral flow from the catchment contributing to that nexus + #one way to do that is to cheat and put that lateral flow at the upstream...this is probably the simplest way + #right now. The other is to create a virtual channel segment downstream to "route" i.e accumulate into + #but it isn't clear right now how to do that with flow/velocity/depth requirements + #find the terminal nodes + for tnx, test_up in self._upstream_terminal.items(): + #first need to ensure there is an upstream location to dump to + pdb.set_trace() + for nex in test_up: + try: + #FIXME if multiple upstreams exist in this case then a choice is to be made as to which it goes into + #some cases the choice is easy cause the upstream doesn't exist, but in others, it may not be so simple + #in such cases where multiple valid upstream nexuses exist, perhaps the mainstem should be used? + pdb.set_trace() + qlats_df.loc[up] += nexuses_lateralflows_df.loc[tnx] + break #flow added, don't add it again! + except KeyError: + #this upstream doesn't actually exist on the network (maybe it is a headwater?) + #or perhaps the output file doesnt exist? If this is the case, this isn't a good trap + #but for now, add the flow to a known good nexus upstream of the terminal + continue + #TODO what happens if can't put the qlat anywhere? Right now this silently ignores the issue... + qlats_df.drop(tnx, inplace=True) + ''' + + # The segment_index has the full network set of segments/flowpaths. + # Whereas the set of flowpaths that are downstream of nexuses is a + # subset of the segment_index. Therefore, all of the segments/flowpaths + # that are not accounted for in the set of flowpaths downstream of + # nexuses need to be added to the qlateral dataframe and padded with + # zeros. + all_df = pd.DataFrame( np.zeros( (len(self.segment_index), len(qlats_df.columns)) ), index=self.segment_index, + columns=qlats_df.columns ) + all_df.loc[ qlats_df.index ] = qlats_df + qlats_df = all_df.sort_index() + + elif qlat_input_file: + qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) + else: + qlat_const = run.get("qlat_const", 0) + qlats_df = pd.DataFrame( + qlat_const, + index=self.segment_index, + columns=range(nts // qts_subdivisions), + dtype="float32", + ) + + # TODO: Make a more sophisticated date-based filter + max_col = 1 + nts // qts_subdivisions + if len(qlats_df.columns) > max_col: + qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) + + if not self.segment_index.empty: + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + self._qlateral = qlats_df + +def read_file(file_name): + extension = file_name.suffix + if extension=='.csv': + df = pd.read_csv(file_name) + elif extension=='.parquet': + df = pq.read_table(file_name).to_pandas().reset_index() + df.index.name = None + + return df diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index 06b614405..eb47e81d7 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -363,3 +363,82 @@ def read_geo_file( self._waterbody_df = pd.DataFrame() self._usgs_lake_gage_crosswalk = None self._usace_lake_gage_crosswalk = None + + def build_qlateral_array(self, run, cpu_pool): + + # TODO: set default/optional arguments + qts_subdivisions = run.get("qts_subdivisions", 1) + nts = run.get("nts", 1) + qlat_input_folder = run.get("qlat_input_folder", None) + qlat_input_file = run.get("qlat_input_file", None) + + if qlat_input_folder: + qlat_input_folder = pathlib.Path(qlat_input_folder) + if "qlat_files" in run: + qlat_files = run.get("qlat_files") + qlat_files = [qlat_input_folder.joinpath(f) for f in qlat_files] + elif "qlat_file_pattern_filter" in run: + qlat_file_pattern_filter = run.get( + "qlat_file_pattern_filter", "*CHRT_OUT*" + ) + qlat_files = sorted(qlat_input_folder.glob(qlat_file_pattern_filter)) + + qlat_file_index_col = run.get( + "qlat_file_index_col", "feature_id" + ) + + # Parallel reading of qlateral data from CHRTOUT + with Parallel(n_jobs=cpu_pool) as parallel: + jobs = [] + for f in qlat_files: + jobs.append( + delayed(nhd_io.get_ql_from_chrtout) + #(f, qlat_file_value_col, gw_bucket_col, terrain_ro_col) + #delayed(nhd_io.get_ql_from_csv) + (f) + ) + ql_list = parallel(jobs) + + # get feature_id from a single CHRTOUT file + with netCDF4.Dataset(qlat_files[0]) as ds: + idx = ds.variables[qlat_file_index_col][:].filled() + + # package data into a DataFrame + qlats_df = pd.DataFrame( + np.stack(ql_list).T, + index = idx, + columns = range(len(qlat_files)) + ) + + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + elif qlat_input_file: + qlats_df = nhd_io.get_ql_from_csv(qlat_input_file) + else: + qlat_const = run.get("qlat_const", 0) + qlats_df = pd.DataFrame( + qlat_const, + index=self.segment_index, + columns=range(nts // qts_subdivisions), + dtype="float32", + ) + + # TODO: Make a more sophisticated date-based filter + max_col = 1 + nts // qts_subdivisions + if len(qlats_df.columns) > max_col: + qlats_df.drop(qlats_df.columns[max_col:], axis=1, inplace=True) + + if not self.segment_index.empty: + qlats_df = qlats_df[qlats_df.index.isin(self.segment_index)] + + self._qlateral = qlats_df + +def read_file(file_name): + extension = file_name.suffix + if extension=='.csv': + df = pd.read_csv(file_name) + elif extension=='.parquet': + df = pq.read_table(file_name).to_pandas().reset_index() + df.index.name = None + + return df \ No newline at end of file diff --git a/src/troute-network/troute/RoutingScheme.py b/src/troute-network/troute/RoutingScheme.py index 11d27a2cf..049b0b84f 100644 --- a/src/troute-network/troute/RoutingScheme.py +++ b/src/troute-network/troute/RoutingScheme.py @@ -67,8 +67,6 @@ def __init__(self,): """ """ - self.hybrid_params = self.compute_parameters.get("hybrid_parameters", False) - self._diffusive_domain = None self._diffusive_network_data = None self._topobathy_df = pd.DataFrame() @@ -78,8 +76,8 @@ def __init__(self,): self._refactored_reaches = {} # Determine whether to run hybrid routing from user input - run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) - domain_file = self.hybrid_params.get("diffusive_domain", None) + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + domain_file = self.hybrid_parameters.get("diffusive_domain", None) if run_hybrid and domain_file: #========================================================================== @@ -155,17 +153,17 @@ def diffusive_network_data(self,): @property def topobathy_df(self,): if self._topobathy_df.empty: - run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) - use_topobathy = self.hybrid_params.get('use_natl_xsections', False) + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) if run_hybrid and use_topobathy: - run_refactored = self.hybrid_params.get('run_refactored_network', False) + run_refactored = self.hybrid_parameters.get('run_refactored_network', False) if run_refactored: - refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + refactored_topobathy_file = self.hybrid_parameters.get("refactored_topobathy_domain", None) self._topobathy_df = read_netcdf(refactored_topobathy_file).set_index('link') else: - topobathy_file = self.hybrid_params.get("topobathy_domain", None) + topobathy_file = self.hybrid_parameters.get("topobathy_domain", None) self._topobathy_df = read_netcdf(topobathy_file).set_index('link') self._topobathy_df.index = self._topobathy_df.index.astype(int) @@ -174,11 +172,11 @@ def topobathy_df(self,): @property def refactored_diffusive_domain(self,): if not self._refactored_diffusive_domain: - run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) - run_refactored = self.hybrid_params.get('run_refactored_network', False) + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + run_refactored = self.hybrid_parameters.get('run_refactored_network', False) if run_hybrid and run_refactored: - refactored_domain_file = self.hybrid_params.get("refactored_domain", None) + refactored_domain_file = self.hybrid_parameters.get("refactored_domain", None) self._refactored_diffusive_domain = read_diffusive_domain(refactored_domain_file) @@ -187,11 +185,11 @@ def refactored_diffusive_domain(self,): @property def refactored_reaches(self,): if not self._refactored_reaches: - run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) - run_refactored = self.hybrid_params.get('run_refactored_network', False) + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + run_refactored = self.hybrid_parameters.get('run_refactored_network', False) if run_hybrid and run_refactored: - refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + refactored_topobathy_file = self.hybrid_parameters.get("refactored_topobathy_domain", None) diffusive_parameters = {'geo_file_path': refactored_topobathy_file} refactored_connections = build_refac_connections(diffusive_parameters) @@ -234,12 +232,12 @@ def refactored_reaches(self,): @property def unrefactored_topobathy_df(self,): if self._unrefactored_topobathy_df.empty: - run_hybrid = self.hybrid_params.get("run_hybrid_routing", False) - use_topobathy = self.hybrid_params.get('use_natl_xsections', False) - run_refactored = self.hybrid_params.get('run_refactored_network', False) + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) + run_refactored = self.hybrid_parameters.get('run_refactored_network', False) if run_hybrid and use_topobathy and run_refactored: - topobathy_file = self.hybrid_params.get("topobathy_domain", None) + topobathy_file = self.hybrid_parameters.get("topobathy_domain", None) self._unrefactored_topobathy_df = read_netcdf(topobathy_file).set_index('link') self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index 13527c705..235f7cf3b 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -85,6 +85,8 @@ def main_v04(argv): data_assimilation_parameters, restart_parameters, compute_parameters, + forcing_parameters, + hybrid_parameters, verbose=True, showtiming=showtiming) elif supernetwork_parameters["geo_file_type"] == 'NHDNetwork': @@ -127,7 +129,7 @@ def main_v04(argv): parity_sets = [] # Create forcing data within network object for first loop iteration - network.assemble_forcings(run_sets[0], forcing_parameters, hybrid_parameters, supernetwork_parameters, cpu_pool) + network.assemble_forcings(run_sets[0],) # Create data assimilation object from da_sets for first loop iteration # TODO: Add data_assimilation for hyfeature network @@ -240,11 +242,7 @@ def main_v04(argv): network.new_t0(dt,nts) # update forcing data - network.assemble_forcings(run_sets[run_set_iterator + 1], - forcing_parameters, - hybrid_parameters, - supernetwork_parameters, - cpu_pool) + network.assemble_forcings(run_sets[run_set_iterator + 1],) # get reservoir DA initial parameters for next loop iteration data_assimilation.update_for_next_loop( From 6da91c1b00c9d69e1e900f227ec9bc4007e8241f Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 30 Dec 2022 18:35:16 +0000 Subject: [PATCH 41/54] bug fixes to get NHDNetwork working with new configuration --- src/troute-network/troute/NHDNetwork.py | 116 +++++++++------------ src/troute-nwm/src/nwm_routing/__main__.py | 2 +- 2 files changed, 48 insertions(+), 70 deletions(-) diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index eb47e81d7..e93d47f15 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -1,10 +1,14 @@ -from .AbstractNetwork import AbstractNetwork +from .RoutingScheme import RoutingScheme import troute.nhd_io as nhd_io import troute.nhd_preprocess as nhd_prep import pandas as pd +import numpy as np import time import pathlib from collections import defaultdict +import netCDF4 +from joblib import delayed, Parallel +import pyarrow.parquet as pq from troute.nhd_network import reverse_dict, extract_waterbody_connections, gage_mapping, extract_connections, replace_waterbodies_connections @@ -12,7 +16,7 @@ __verbose__ = True #FIXME pass verbosity -class NHDNetwork(AbstractNetwork): +class NHDNetwork(RoutingScheme): """ """ @@ -24,77 +28,55 @@ class NHDNetwork(AbstractNetwork): def __init__( self, supernetwork_parameters, - waterbody_parameters=None, - restart_parameters=None, - forcing_parameters=None, - compute_parameters=None, - data_assimilation_parameters=None, - preprocessing_parameters=None, + waterbody_parameters, + restart_parameters, + forcing_parameters, + compute_parameters, + data_assimilation_parameters, + hybrid_parameters, verbose=False, showtiming=False, ): """ """ - global __verbose__, __showtiming__ - __verbose__ = verbose - __showtiming__ = showtiming - if __verbose__: + self.supernetwork_parameters = supernetwork_parameters + self.waterbody_parameters = waterbody_parameters + self.data_assimilation_parameters = data_assimilation_parameters + self.restart_parameters = restart_parameters + self.compute_parameters = compute_parameters + self.forcing_parameters = forcing_parameters + self.hybrid_parameters = hybrid_parameters + self.verbose = verbose + self.showtiming = showtiming + + if self.verbose: print("creating supernetwork connections set") - if __showtiming__: + if self.showtiming: start_time = time.time() #------------------------------------------------ # Load Geo Data #------------------------------------------------ - self.read_geo_file( - supernetwork_parameters, - waterbody_parameters, - data_assimilation_parameters, - ) - ''' - ( - self._dataframe, - self._connections, - self._terminal_codes, - self._waterbody_df, - self._waterbody_types_df, - self._waterbody_type_specified, - self._waterbody_connections, - self._link_lake_crosswalk, - self._gages, - self._usgs_lake_gage_crosswalk, - self._usace_lake_gage_crosswalk, - ) = nhd_prep.read_geo_file( - supernetwork_parameters, - waterbody_parameters, - data_assimilation_parameters, - ) - ''' - if __verbose__: + self.read_geo_file() + + if self.verbose: print("supernetwork connections set complete") - if __showtiming__: + if self.showtiming: print("... in %s seconds." % (time.time() - start_time)) - break_network_at_waterbodies = waterbody_parameters.get("break_network_at_waterbodies", False) - streamflow_da = data_assimilation_parameters.get('streamflow_da', False) + break_network_at_waterbodies = self.waterbody_parameters.get("break_network_at_waterbodies", False) + streamflow_da = self.data_assimilation_parameters.get('streamflow_da', False) break_network_at_gages = False if streamflow_da: break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, + self.break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, "break_network_at_gages": break_network_at_gages} self._flowpath_dict = {} - super().__init__( - compute_parameters, - waterbody_parameters, - restart_parameters, - break_points, - verbose=__verbose__, - showtiming=__showtiming__, - ) + super().__init__() # Create empty dataframe for coastal_boundary_depth_df. This way we can check if # it exists, and only read in SCHISM data during 'assemble_forcings' if it doesn't @@ -141,12 +123,7 @@ def usace_lake_gage_crosswalk(self): #def wbody_conn(self): # return self._waterbody_connections - def read_geo_file( - self, - supernetwork_parameters, - waterbody_parameters, - data_assimilation_parameters - ): + def read_geo_file(self,): ''' Construct network connections network, parameter dataframe, waterbody mapping, and gage mapping. This is an intermediate-level function that calls several @@ -167,7 +144,7 @@ def read_geo_file( # crosswalking dictionary between variables names in input dataset and # variable names recognized by troute.routing module. - cols = supernetwork_parameters.get( + cols = self.supernetwork_parameters.get( 'columns', { 'key' : 'link', @@ -189,10 +166,10 @@ def read_geo_file( ) # numeric code used to indicate network terminal segments - terminal_code = supernetwork_parameters.get("terminal_code", 0) + terminal_code = self.supernetwork_parameters.get("terminal_code", 0) # read parameter dataframe - self._dataframe = nhd_io.read(pathlib.Path(supernetwork_parameters["geo_file_path"])) + self._dataframe = nhd_io.read(pathlib.Path(self.supernetwork_parameters["geo_file_path"])) # select the column names specified in the values in the cols dict variable self._dataframe = self.dataframe[list(cols.values())] @@ -201,8 +178,8 @@ def read_geo_file( self._dataframe = self.dataframe.rename(columns=reverse_dict(cols)) # handle synthetic waterbody segments - synthetic_wb_segments = supernetwork_parameters.get("synthetic_wb_segments", None) - synthetic_wb_id_offset = supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) + synthetic_wb_segments = self.supernetwork_parameters.get("synthetic_wb_segments", None) + synthetic_wb_id_offset = self.supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) if synthetic_wb_segments: # rename the current key column to key32 key32_d = {"key":"key32"} @@ -218,10 +195,10 @@ def read_geo_file( self._dataframe = self.dataframe.set_index("key").sort_index() # get and apply domain mask - if "mask_file_path" in supernetwork_parameters: + if "mask_file_path" in self.supernetwork_parameters: data_mask = nhd_io.read_mask( - pathlib.Path(supernetwork_parameters["mask_file_path"]), - layer_string=supernetwork_parameters.get("mask_layer_string", None), + pathlib.Path(self.supernetwork_parameters["mask_file_path"]), + layer_string=self.supernetwork_parameters.get("mask_layer_string", None), ) data_mask = data_mask.set_index(data_mask.columns[0]) self._dataframe = self.dataframe.filter(data_mask.index, axis=0) @@ -260,7 +237,7 @@ def read_geo_file( self._dataframe = self.dataframe.astype("float32") - break_network_at_waterbodies = waterbody_parameters.get( + break_network_at_waterbodies = self.waterbody_parameters.get( "break_network_at_waterbodies", False ) @@ -281,7 +258,7 @@ def read_geo_file( if break_network_at_waterbodies: # Read waterbody parameters from LAKEPARM file - level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) + level_pool_params = self.waterbody_parameters.get('level_pool', defaultdict(list)) self._waterbody_df = nhd_io.read_lakeparm( level_pool_params['level_pool_waterbody_parameter_file_path'], level_pool_params.get("level_pool_waterbody_id", 'lake_id'), @@ -299,7 +276,7 @@ def read_geo_file( self._waterbody_types_df = pd.DataFrame() # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True - reservoir_da = data_assimilation_parameters.get( + reservoir_da = self.data_assimilation_parameters.get( 'reservoir_da', {} ) @@ -323,7 +300,7 @@ def read_geo_file( usgs_hybrid = False # check if RFC-type reservoirs are set to true - rfc_params = waterbody_parameters.get('rfc') + rfc_params = self.waterbody_parameters.get('rfc') if rfc_params: rfc_forecast = rfc_params.get( 'reservoir_rfc_forecasts', @@ -364,13 +341,14 @@ def read_geo_file( self._usgs_lake_gage_crosswalk = None self._usace_lake_gage_crosswalk = None - def build_qlateral_array(self, run, cpu_pool): + def build_qlateral_array(self, run,): # TODO: set default/optional arguments qts_subdivisions = run.get("qts_subdivisions", 1) nts = run.get("nts", 1) qlat_input_folder = run.get("qlat_input_folder", None) qlat_input_file = run.get("qlat_input_file", None) + cpu_pool = self.compute_parameters.get('cpu_pool', 1) if qlat_input_folder: qlat_input_folder = pathlib.Path(qlat_input_folder) diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index 235f7cf3b..cf6bc29cb 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -96,7 +96,7 @@ def main_v04(argv): forcing_parameters, compute_parameters, data_assimilation_parameters, - preprocessing_parameters, + hybrid_parameters, verbose=True, showtiming=showtiming, ) From e4015c8c966459ba5c18cd4b90b3b427948f4203 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Thu, 5 Jan 2023 22:22:24 +0000 Subject: [PATCH 42/54] modified routing classes to be separate from network classes --- src/troute-network/troute/AbstractNetwork.py | 69 +++- src/troute-network/troute/AbstractRouting.py | 352 ++++++++++++++++++ .../troute/HYFeaturesNetwork.py | 4 +- src/troute-network/troute/NHDNetwork.py | 4 +- 4 files changed, 420 insertions(+), 9 deletions(-) create mode 100644 src/troute-network/troute/AbstractRouting.py diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 72afb0c44..e48671cc5 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -2,6 +2,7 @@ from functools import partial import pandas as pd from datetime import datetime, timedelta +from collections import defaultdict import time import logging @@ -9,6 +10,7 @@ from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition from troute.nhd_network_utilities_v02 import organize_independent_networks, build_channel_initial_state, build_refac_connections import troute.nhd_io as nhd_io +from .AbstractRouting import * LOG = logging.getLogger('') @@ -21,10 +23,10 @@ class AbstractNetwork(ABC): "_waterbody_types_df", "_waterbody_type_specified", "_independent_networks", "_reaches_by_tw", "_flowpath_dict", "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", - "_qlateral", "_break_segments", "_segment_index", + "_qlateral", "_break_segments", "_segment_index", "_coastal_boundary_depth_df", "supernetwork_parameters", "waterbody_parameters","data_assimilation_parameters", "restart_parameters", "compute_parameters", "forcing_parameters", - "hybrid_parameters", "verbose", "showtiming", "break_points"] + "hybrid_parameters", "verbose", "showtiming", "break_points", "_routing"] def __init__(self,): @@ -52,6 +54,8 @@ def __init__(self,): self._break_segments = self._break_segments | set(self.waterbody_connections.values()) if self.break_points["break_network_at_gages"]: self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) + + self.initialize_routing_scheme() self.create_independent_networks() @@ -304,10 +308,10 @@ def segment_index(self): """ # list of all segments in the domain (MC + diffusive) self._segment_index = self.dataframe.index - if self._diffusive_network_data: - for tw in self.diffusive_network_data: + if self._routing.diffusive_network_data: + for tw in self._routing.diffusive_network_data: self._segment_index = self._segment_index.append( - pd.Index(self.diffusive_network_data[tw]['mainstem_segs']) + pd.Index(self._routing.diffusive_network_data[tw]['mainstem_segs']) ) return self._segment_index @@ -347,6 +351,27 @@ def coastal_boundary_depth_df(self): """ return self._coastal_boundary_depth_df + @property + def diffusive_network_data(self): + return self._routing.diffusive_network_data + + @property + def topobathy_df(self): + return self._routing.topobathy_df + + @property + def refactored_diffusive_domain(self): + return self._routing.refactored_diffusive_domain + + @property + def refactored_reaches(self): + return self._routing.refactored_reaches + + @property + def unrefactored_topobathy_df(self): + return self._routing.unrefactored_topobathy_df + + def set_synthetic_wb_segments(self, synthetic_wb_segments, synthetic_wb_id_offset): """ @@ -406,7 +431,41 @@ def astype(self, type, columns=None): else: self._dataframe = self._dataframe.astype(type) + def initialize_routing_scheme(self,): + ''' + + ''' + # Get user inputs from configuration file + run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) + use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) + run_refactored = self.hybrid_parameters.get('run_refactored_network', False) + + routing_type = [run_hybrid, use_topobathy, run_refactored] + + _routing_scheme_map = { + MCOnly: [False, False, False], + SimpleHybridDiffusive: [True, False, False], + HybridNatlXSectionNonRefactored: [True, True, False], + HybridNatlXSectionRefactored: [True, True, True], + } + + # Default to MCOnly routing + routing_scheme = MCOnly + # Check user input to determine the routing scheme + for key, value in _routing_scheme_map.items(): + if value==routing_type: + routing_scheme = key + + routing = routing_scheme(self.hybrid_parameters) + + ( + self._dataframe, + self._connections + ) = routing.update_routing_domain(self.dataframe, self.connections) + + self._routing = routing + def create_independent_networks(self,): LOG.info("organizing connections into reaches ...") diff --git a/src/troute-network/troute/AbstractRouting.py b/src/troute-network/troute/AbstractRouting.py new file mode 100644 index 000000000..2190acc76 --- /dev/null +++ b/src/troute-network/troute/AbstractRouting.py @@ -0,0 +1,352 @@ +from abc import ABC, abstractmethod +import logging +import yaml +import json +import xarray as xr +import pandas as pd + +from troute.nhd_network import reverse_network +from troute.nhd_network_utilities_v02 import organize_independent_networks, build_refac_connections + +LOG = logging.getLogger('') + +def read_diffusive_domain(domain_file): + ''' + Read diffusive domain data from .ymal or .json file. + + Arguments + --------- + domain_file (str or pathlib.Path): Path of diffusive domain file + + Returns + ------- + data (dict int: [int]): domain tailwater segments: list of segments in domain + (includeing tailwater segment) + + ''' + if domain_file[-4:] == "yaml": + with open(domain_file) as domain: + data = yaml.load(domain, Loader=yaml.SafeLoader) + else: + with open(domain_file) as domain: + data = json.load(domain) + + return data + +def read_netcdf(geo_file_path): + ''' + Open a netcdf file with xarray and convert to dataframe + + Arguments + --------- + geo_file_path (str or pathlib.Path): netCDF filepath + + Returns + ------- + ds.to_dataframe() (DataFrame): netCDF contents + + Notes + ----- + - When handling large volumes of netCDF files, xarray is not the most efficient. + + ''' + with xr.open_dataset(geo_file_path) as ds: + return ds.to_dataframe() + + +class AbstractRouting(ABC): + """ + + """ + __slots__ = ["hybrid_params", "_diffusive_domain", "_coastal_boundary_depth_df", + "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", + "_refactored_diffusive_network_data", "_refactored_reaches", + "_unrefactored_topobathy_df",] + + def __init__(self): + """ + + """ + self._diffusive_domain = None + self._diffusive_network_data = None + self._topobathy_df = pd.DataFrame() + self._unrefactored_topobathy_df = pd.DataFrame() + self._refactored_diffusive_domain = None + self._refactored_diffusive_network_data = None + self._refactored_reaches = {} + + @abstractmethod + def update_routing_domain(self, dataframe, connections): + pass + + @property + @abstractmethod + def diffusive_network_data(self): + pass + + @property + @abstractmethod + def topobathy_df(self): + pass + + @property + @abstractmethod + def refactored_diffusive_domain(self): + pass + + @property + @abstractmethod + def refactored_reaches(self): + pass + + @property + @abstractmethod + def unrefactored_topobathy_df(self): + pass + + +class MCOnly(AbstractRouting): + + def __init__(self, hybrid_params): + self.hybrid_params = hybrid_params + + super().__init__() + + def update_routing_domain(self, dataframe, connections): + return dataframe, connections, self._diffusive_domain + + @property + def diffusive_network_data(self): + self._diffusive_network_data = None + return self._diffusive_network_data + + @property + def topobathy_df(self): + self._topobathy_df = pd.DataFrame() + return self._topobathy_df + + @property + def refactored_diffusive_domain(self): + self._refactored_diffusive_domain = None + return self._refactored_diffusive_domain + + @property + def refactored_reaches(self): + self._refactored_reaches = {} + return self._refactored_reaches + + @property + def unrefactored_topobathy_df(self): + self._unrefactored_topobathy_df = pd.DataFrame() + return self._unrefactored_topobathy_df + + +class SimpleHybridDiffusive(AbstractRouting): + + def __init__(self, hybrid_params): + self.hybrid_params = hybrid_params + + super().__init__() + + def update_routing_domain(self, dataframe, connections): + #========================================================================== + # build diffusive domain data and edit MC domain data for hybrid simulation + domain_file = self.hybrid_params.get("diffusive_domain", None) + self._diffusive_domain = read_diffusive_domain(domain_file) + self._diffusive_network_data = {} + + rconn_diff0 = reverse_network(connections) + + for tw in self._diffusive_domain: + mainstem_segs = self._diffusive_domain[tw]['links'] + # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is + # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. + upstream_boundary_mainstem_link = self._diffusive_domain[tw]['upstream_boundary_link_mainstem'] + if upstream_boundary_mainstem_link[0] in mainstem_segs: + mainstem_segs.remove(upstream_boundary_mainstem_link[0]) + + # ===== build diffusive network data objects ==== + self._diffusive_network_data[tw] = {} + + # add diffusive domain segments + self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs + + # diffusive domain tributary segments + trib_segs = [] + + for seg in mainstem_segs: + us_list = rconn_diff0[seg] + for u in us_list: + if u not in mainstem_segs: + trib_segs.append(u) + + self._diffusive_network_data[tw]['tributary_segments'] = trib_segs + # diffusive domain connections object + self._diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, reaches, rconn_diff = organize_independent_networks( + self._diffusive_network_data[tw]['connections'], + set(trib_segs), + set(), + ) + + self._diffusive_network_data[tw]['rconn'] = rconn_diff + self._diffusive_network_data[tw]['reaches'] = reaches[tw] + + # RouteLink parameters + self._diffusive_network_data[tw]['param_df'] = dataframe.filter( + (mainstem_segs + trib_segs), + axis = 0, + ) + self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link + + # ==== remove diffusive domain segs from MC domain ==== + # drop indices from param_df + dataframe = dataframe.drop(mainstem_segs) + + # remove keys from connections dictionary + for s in mainstem_segs: + connections.pop(s) + + # update downstream connections of trib segs + for us in trib_segs: + connections[us] = [] + + return dataframe, connections + + @property + def diffusive_network_data(self): + return self._diffusive_network_data + + @property + def topobathy_df(self): + self._topobathy_df = pd.DataFrame() + return self._topobathy_df + + @property + def refactored_diffusive_domain(self): + self._refactored_diffusive_domain = None + return self._refactored_diffusive_domain + + @property + def refactored_reaches(self): + self._refactored_reaches = {} + return self._refactored_reaches + + @property + def unrefactored_topobathy_df(self): + self._unrefactored_topobathy_df = pd.DataFrame() + return self._unrefactored_topobathy_df + + +class HybridNatlXSectionNonRefactored(SimpleHybridDiffusive): + + def __init__(self, hybrid_params): + + super().__init__(hybrid_params = hybrid_params) + + @property + def diffusive_network_data(self): + return self._diffusive_network_data + + @property + def topobathy_df(self): + if self._topobathy_df.empty: + topobathy_file = self.hybrid_params.get("topobathy_domain", None) + self._topobathy_df = read_netcdf(topobathy_file).set_index('link') + self._topobathy_df.index = self._topobathy_df.index.astype(int) + return self._topobathy_df + + @property + def refactored_diffusive_domain(self): + self._refactored_diffusive_domain = None + return self._refactored_diffusive_domain + + @property + def refactored_reaches(self): + self._refactored_reaches = {} + return self._refactored_reaches + + @property + def unrefactored_topobathy_df(self): + self._unrefactored_topobathy_df = pd.DataFrame() + return self._unrefactored_topobathy_df + + +class HybridNatlXSectionRefactored(SimpleHybridDiffusive): + + def __init__(self, hybrid_params): + + super().__init__(hybrid_params = hybrid_params) + + @property + def diffusive_network_data(self): + return self._diffusive_network_data + + @property + def topobathy_df(self): + if self._topobathy_df.empty: + refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + self._topobathy_df = read_netcdf(refactored_topobathy_file).set_index('link') + return self._topobathy_df + + @property + def refactored_diffusive_domain(self): + if not self._refactored_diffusive_domain: + refactored_domain_file = self.hybrid_params.get("refactored_domain", None) + self._refactored_diffusive_domain = read_diffusive_domain(refactored_domain_file) + return self._refactored_diffusive_domain + + @property + def refactored_reaches(self): + if not self._refactored_reaches: + refactored_topobathy_file = self.hybrid_params.get("refactored_topobathy_domain", None) + diffusive_parameters = {'geo_file_path': refactored_topobathy_file} + refactored_connections = build_refac_connections(diffusive_parameters) + + for tw in self._diffusive_domain: + + # list of stream segments of a single refactored diffusive domain + refac_tw = self.refactored_diffusive_domain[tw]['refac_tw'] + rlinks_tw = self.refactored_diffusive_domain[tw]['rlinks'] + refactored_connections_tw = {} + + # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections + # for a single refactored diffusive domain defined by a current tw. + for k in rlinks_tw: + if k in refactored_connections.keys() and k != refac_tw: + refactored_connections_tw[k] = refactored_connections[k] + + trib_segs = self.diffusive_network_data[tw]['tributary_segments'] + refactored_diffusive_network_data = {} + refactored_diffusive_network_data[refac_tw] = {} + refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs + refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw + + for k in trib_segs: + refactored_diffusive_network_data[refac_tw]['connections'][k] = [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] + + # diffusive domain reaches and upstream connections. + # break network at tributary segments + _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( + refactored_diffusive_network_data[refac_tw]['connections'], + set(trib_segs), + set(), + ) + + self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] + refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] + refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] + return self._refactored_reaches + + @property + def unrefactored_topobathy_df(self): + if self._unrefactored_topobathy_df.empty: + topobathy_file = self.hybrid_params.get("topobathy_domain", None) + self._unrefactored_topobathy_df = read_netcdf(topobathy_file).set_index('link') + self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) + return self._unrefactored_topobathy_df + + diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index ea6a8a07c..c7cffa397 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -1,4 +1,4 @@ -from .RoutingScheme import RoutingScheme +from .AbstractNetwork import AbstractNetwork import pandas as pd import numpy as np import geopandas as gpd @@ -90,7 +90,7 @@ def node_key_func(x): return df -class HYFeaturesNetwork(RoutingScheme): +class HYFeaturesNetwork(AbstractNetwork): """ """ diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index e93d47f15..77caf44c6 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -1,4 +1,4 @@ -from .RoutingScheme import RoutingScheme +from .AbstractNetwork import AbstractNetwork import troute.nhd_io as nhd_io import troute.nhd_preprocess as nhd_prep import pandas as pd @@ -16,7 +16,7 @@ __verbose__ = True #FIXME pass verbosity -class NHDNetwork(RoutingScheme): +class NHDNetwork(AbstractNetwork): """ """ From d68d840ec141a7fee39b1a3e585ef2170717e2f0 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Tue, 10 Jan 2023 20:14:31 +0000 Subject: [PATCH 43/54] edited names of routing objects, consolidated inputs to nwm_route into fewer inputs --- src/troute-network/troute/AbstractNetwork.py | 6 +- src/troute-network/troute/AbstractRouting.py | 36 +------- src/troute-nwm/src/nwm_routing/__main__.py | 95 ++------------------ src/troute-routing/troute/routing/compute.py | 72 ++++++++------- 4 files changed, 55 insertions(+), 154 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index e48671cc5..eb497531a 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -444,9 +444,9 @@ def initialize_routing_scheme(self,): _routing_scheme_map = { MCOnly: [False, False, False], - SimpleHybridDiffusive: [True, False, False], - HybridNatlXSectionNonRefactored: [True, True, False], - HybridNatlXSectionRefactored: [True, True, True], + MCwithDiffusive: [True, False, False], + MCwithDiffusiveNatlXSectionNonRefactored: [True, True, False], + MCwithDiffusiveNatlXSectionRefactored: [True, True, True], } # Default to MCOnly routing diff --git a/src/troute-network/troute/AbstractRouting.py b/src/troute-network/troute/AbstractRouting.py index 2190acc76..be10b4eb8 100644 --- a/src/troute-network/troute/AbstractRouting.py +++ b/src/troute-network/troute/AbstractRouting.py @@ -113,35 +113,30 @@ def __init__(self, hybrid_params): super().__init__() def update_routing_domain(self, dataframe, connections): - return dataframe, connections, self._diffusive_domain + return dataframe, connections @property def diffusive_network_data(self): - self._diffusive_network_data = None return self._diffusive_network_data @property def topobathy_df(self): - self._topobathy_df = pd.DataFrame() return self._topobathy_df @property def refactored_diffusive_domain(self): - self._refactored_diffusive_domain = None return self._refactored_diffusive_domain @property def refactored_reaches(self): - self._refactored_reaches = {} return self._refactored_reaches @property def unrefactored_topobathy_df(self): - self._unrefactored_topobathy_df = pd.DataFrame() return self._unrefactored_topobathy_df -class SimpleHybridDiffusive(AbstractRouting): +class MCwithDiffusive(AbstractRouting): def __init__(self, hybrid_params): self.hybrid_params = hybrid_params @@ -241,16 +236,12 @@ def unrefactored_topobathy_df(self): return self._unrefactored_topobathy_df -class HybridNatlXSectionNonRefactored(SimpleHybridDiffusive): +class MCwithDiffusiveNatlXSectionNonRefactored(MCwithDiffusive): def __init__(self, hybrid_params): super().__init__(hybrid_params = hybrid_params) - @property - def diffusive_network_data(self): - return self._diffusive_network_data - @property def topobathy_df(self): if self._topobathy_df.empty: @@ -258,33 +249,14 @@ def topobathy_df(self): self._topobathy_df = read_netcdf(topobathy_file).set_index('link') self._topobathy_df.index = self._topobathy_df.index.astype(int) return self._topobathy_df - - @property - def refactored_diffusive_domain(self): - self._refactored_diffusive_domain = None - return self._refactored_diffusive_domain - - @property - def refactored_reaches(self): - self._refactored_reaches = {} - return self._refactored_reaches - - @property - def unrefactored_topobathy_df(self): - self._unrefactored_topobathy_df = pd.DataFrame() - return self._unrefactored_topobathy_df -class HybridNatlXSectionRefactored(SimpleHybridDiffusive): +class MCwithDiffusiveNatlXSectionRefactored(MCwithDiffusive): def __init__(self, hybrid_params): super().__init__(hybrid_params = hybrid_params) - @property - def diffusive_network_data(self): - return self._diffusive_network_data - @property def topobathy_df(self): if self._topobathy_df.empty: diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index cf6bc29cb..15a357c98 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -170,42 +170,18 @@ def main_v04(argv): route_start_time = time.time() run_results = nwm_route( - network.connections, - network.reverse_network, - network.waterbody_connections, - network._reaches_by_tw, ## check: def name is different from return self._ .. + network, + data_assimilation, parallel_compute_method, compute_kernel, subnetwork_target_size, cpu_pool, - network.t0, ## check if t0 is being updated dt, nts, qts_subdivisions, - network.independent_networks, - network.dataframe, - network.q0, - network._qlateral, - data_assimilation.usgs_df, - data_assimilation.lastobs_df, - data_assimilation.reservoir_usgs_df, - data_assimilation.reservoir_usgs_param_df, - data_assimilation.reservoir_usace_df, - data_assimilation.reservoir_usace_param_df, - data_assimilation.assimilation_parameters, assume_short_ts, return_courant, - network.waterbody_dataframe, - waterbody_parameters, - network.waterbody_types_dataframe, - network.waterbody_type_specified, - network.diffusive_network_data, - network.topobathy_df, - network.refactored_diffusive_domain, - network.refactored_reaches, subnetwork_list, - network.coastal_boundary_depth_df, - network.unrefactored_topobathy_df, ) # returns list, first item is run result, second item is subnetwork items @@ -1031,42 +1007,18 @@ def _handle_args_v03(argv): return parser.parse_args(argv) def nwm_route( - downstream_connections, - upstream_connections, - waterbodies_in_connections, - reaches_bytw, + network, + data_assimilation, parallel_compute_method, compute_kernel, subnetwork_target_size, cpu_pool, - t0, dt, nts, qts_subdivisions, - independent_networks, - param_df, - q0, - qlats, - usgs_df, - lastobs_df, - reservoir_usgs_df, - reservoir_usgs_param_df, - reservoir_usace_df, - reservoir_usace_param_df, - da_parameter_dict, assume_short_ts, return_courant, - waterbodies_df, - waterbody_parameters, - waterbody_types_df, - waterbody_type_specified, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, subnetwork_list, - coastal_boundary_depth_df, - unrefactored_topobathy_df, ): ################### Main Execution Loop across ordered networks @@ -1083,35 +1035,17 @@ def nwm_route( start_time_mc = time.time() results = compute_nhd_routing_v02( - downstream_connections, - upstream_connections, - waterbodies_in_connections, - reaches_bytw, + network, + data_assimilation, compute_kernel, parallel_compute_method, subnetwork_target_size, # The default here might be the whole network or some percentage... cpu_pool, - t0, dt, nts, qts_subdivisions, - independent_networks, - param_df, - q0, - qlats, - usgs_df, - lastobs_df, - reservoir_usgs_df, - reservoir_usgs_param_df, - reservoir_usace_df, - reservoir_usace_param_df, - da_parameter_dict, assume_short_ts, return_courant, - waterbodies_df, - waterbody_parameters, - waterbody_types_df, - waterbody_type_specified, subnetwork_list, ) LOG.debug("MC computation complete in %s seconds." % (time.time() - start_time_mc)) @@ -1120,7 +1054,7 @@ def nwm_route( results = results[0] # run diffusive side of a hybrid simulation - if diffusive_network_data: + if network.diffusive_network_data: start_time_diff = time.time() ''' # retrieve MC-computed streamflow value at upstream boundary of diffusive mainstem @@ -1144,23 +1078,12 @@ def nwm_route( results.extend( compute_diffusive_routing( results, - diffusive_network_data, + network, + data_assimilation, cpu_pool, - t0, dt, nts, - q0, - qlats, qts_subdivisions, - usgs_df, - lastobs_df, - da_parameter_dict, - waterbodies_df, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - coastal_boundary_depth_df, - unrefactored_topobathy_df, ) ) LOG.debug("Diffusive computation complete in %s seconds." % (time.time() - start_time_diff)) diff --git a/src/troute-routing/troute/routing/compute.py b/src/troute-routing/troute/routing/compute.py index a5673817c..f1130362f 100644 --- a/src/troute-routing/troute/routing/compute.py +++ b/src/troute-routing/troute/routing/compute.py @@ -218,38 +218,41 @@ def _prep_reservoir_da_dataframes(reservoir_usgs_df, reservoir_usgs_param_df, re return reservoir_usgs_df_sub, reservoir_usgs_df_time, reservoir_usgs_update_time, reservoir_usgs_prev_persisted_flow, reservoir_usgs_persistence_update_time, reservoir_usgs_persistence_index, reservoir_usace_df_sub, reservoir_usace_df_time, reservoir_usace_update_time, reservoir_usace_prev_persisted_flow, reservoir_usace_persistence_update_time, reservoir_usace_persistence_index, waterbody_types_df_sub def compute_nhd_routing_v02( - connections, - rconn, - wbody_conn, - reaches_bytw, + network, + data_assimilation, compute_func_name, parallel_compute_method, subnetwork_target_size, cpu_pool, - t0, dt, nts, qts_subdivisions, - independent_networks, - param_df, - q0, - qlats, - usgs_df, - lastobs_df, - reservoir_usgs_df, - reservoir_usgs_param_df, - reservoir_usace_df, - reservoir_usace_param_df, - da_parameter_dict, assume_short_ts, return_courant, - waterbodies_df, - waterbody_parameters, - waterbody_types_df, - waterbody_type_specified, subnetwork_list, ): + connections = network.connections + rconn = network.reverse_network + wbody_conn = network.waterbody_connections + reaches_bytw = network.reaches_by_tailwater + t0 = network.t0 + independent_networks = network.independent_networks + param_df = network.dataframe + q0 = network.q0 + qlats = network.qlateral + usgs_df = data_assimilation.usgs_df + lastobs_df = data_assimilation.lastobs_df + reservoir_usgs_df = data_assimilation.reservoir_usgs_df + reservoir_usgs_param_df = data_assimilation.reservoir_usgs_param_df + reservoir_usace_df = data_assimilation.reservoir_usace_df + reservoir_usace_param_df = data_assimilation.reservoir_usace_param_df + da_parameter_dict = data_assimilation.assimilation_parameters + waterbodies_df = network.waterbody_dataframe + waterbody_parameters = network.waterbody_parameters + waterbody_types_df = network.waterbody_types_dataframe + waterbody_type_specified = network.waterbody_type_specified + da_decay_coefficient = da_parameter_dict.get("da_decay_coefficient", 0) param_df["dt"] = dt param_df = param_df.astype("float32") @@ -1124,25 +1127,28 @@ def compute_nhd_routing_v02( def compute_diffusive_routing( results, - diffusive_network_data, + network, + data_assimilation, cpu_pool, - t0, dt, nts, - q0, - qlats, qts_subdivisions, - usgs_df, - lastobs_df, - da_parameter_dict, - waterbodies_df, - topobathy, - refactored_diffusive_domain, - refactored_reaches, - coastal_boundary_depth_df, - unrefactored_topobathy, ): + diffusive_network_data = network.diffusive_network_data + t0 = network.t0 + q0 = network.q0 + qlats = network.qlateral + usgs_df = data_assimilation.usgs_df + lastobs_df = data_assimilation.lastobs_df + da_parameter_dict = data_assimilation.assimilation_parameters + waterbodies_df = network.waterbody_dataframe + topobathy = network.topobathy_df + refactored_diffusive_domain = network.refactored_diffusive_domain + refactored_reaches = network.refactored_reaches + coastal_boundary_depth_df = network.coastal_boundary_depth_df + unrefactored_topobathy = network.unrefactored_topobathy_df + results_diffusive = [] for tw in diffusive_network_data: # <------- TODO - by-network parallel loop, here. trib_segs = None From 127e37ce79ae0aa83ba448bd05d18f9ca56246b0 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Mon, 23 Jan 2023 17:44:51 +0000 Subject: [PATCH 44/54] edits to diffusive routing --- src/troute-network/troute/AbstractNetwork.py | 4 +++- src/troute-network/troute/AbstractRouting.py | 3 +++ .../troute/routing/diffusive_utils.py | 21 +++++++++++-------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index eb497531a..bbe75fe5f 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -151,7 +151,9 @@ def assemble_forcings(self, run,): #LOG.debug( # "coastal boundary elevation observation DataFrame creation complete in %s seconds." \ # % (time.time() - start_time) - #) + #) + else: + self._coastal_boundary_depth_df = pd.DataFrame() def new_q0(self, run_results): """ diff --git a/src/troute-network/troute/AbstractRouting.py b/src/troute-network/troute/AbstractRouting.py index be10b4eb8..7ff5a46f9 100644 --- a/src/troute-network/troute/AbstractRouting.py +++ b/src/troute-network/troute/AbstractRouting.py @@ -179,6 +179,9 @@ def update_routing_domain(self, dataframe, connections): # diffusive domain connections object self._diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} + # make sure that no downstream link below tw + self._diffusive_network_data[tw]['connections'][tw] = [] + # diffusive domain reaches and upstream connections. # break network at tributary segments _, reaches, rconn_diff = organize_independent_networks( diff --git a/src/troute-routing/troute/routing/diffusive_utils.py b/src/troute-routing/troute/routing/diffusive_utils.py index 0365e740e..1d37acf47 100644 --- a/src/troute-routing/troute/routing/diffusive_utils.py +++ b/src/troute-routing/troute/routing/diffusive_utils.py @@ -993,15 +993,15 @@ def fp_coastal_boundary_input_map( dsbd_option -- (int) 1 or 2 for coastal boundary depth data or normal depth data, respectively nts_db_g -- (int) number of coastal boundary input data timesteps dbcd_g -- (float) coastal boundary input data time series [m] - """ - - date_time_obj1 = datetime.strptime(coastal_boundary_depth_df.columns[1], '%Y-%m-%d %H:%M:%S') - date_time_obj0 = datetime.strptime(coastal_boundary_depth_df.columns[0], '%Y-%m-%d %H:%M:%S') - dt_db_g = (date_time_obj1 - date_time_obj0).total_seconds() - nts_db_g = int((tfin_g - t0_g) * 3600.0 / dt_db_g) + 1 # include initial time 0 to the final time - dbcd_g = np.ones(nts_db_g) + """ - if not coastal_boundary_depth_df.empty: + if not coastal_boundary_depth_df.empty: + date_time_obj1 = datetime.strptime(coastal_boundary_depth_df.columns[1], '%Y-%m-%d %H:%M:%S') + date_time_obj0 = datetime.strptime(coastal_boundary_depth_df.columns[0], '%Y-%m-%d %H:%M:%S') + dt_db_g = (date_time_obj1 - date_time_obj0).total_seconds() + nts_db_g = int((tfin_g - t0_g) * 3600.0 / dt_db_g) + 1 # include initial time 0 to the final time + dbcd_g = np.ones(nts_db_g) + dt_timeslice = timedelta(minutes=dt_db_g/60.0) tfin = t0 + dt_timeslice*(nts_db_g-1) timestamps = pd.date_range(t0, tfin, freq=dt_timeslice) @@ -1043,8 +1043,11 @@ def fp_coastal_boundary_input_map( dbcd_g[:] = dbcd_df_interpolated.loc[tw].values else: + dt_db_g = 3600.0 # by default in sec + nts_db_g = int((tfin_g - t0_g) * 3600.0 / dt_db_g) + 1 # include initial time 0 to the final time + dbcd_g = np.ones(nts_db_g) dsbd_option = 2 # instead, use normal depth as the downstream boundary condition - dbcd_g[:] = 0.0 + dbcd_g[:] = 0.0 return dt_db_g, dsbd_option, nts_db_g, dbcd_g From 7df99c096463d635d6656b02cd464bb3d15e7419 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Mon, 23 Jan 2023 18:11:13 +0000 Subject: [PATCH 45/54] temporary fix for writing CHRTOUT files in serial rather than parallel --- src/troute-network/troute/nhd_io.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/troute-network/troute/nhd_io.py b/src/troute-network/troute/nhd_io.py index c278d5a9a..94427efc3 100644 --- a/src/troute-network/troute/nhd_io.py +++ b/src/troute-network/troute/nhd_io.py @@ -773,19 +773,28 @@ def write_chrtout( LOG.debug("Writing t-route data to %d CHRTOUT files" % (nfiles_to_write)) start = time.time() - with Parallel(n_jobs=cpu_pool) as parallel: - - jobs = [] + try: + with Parallel(n_jobs=cpu_pool) as parallel: + + jobs = [] + for i, f in enumerate(chrtout_files[:nfiles_to_write]): + + s = time.time() + variables = { + varname: (qtrt[:,i], dim, attrs) + } + jobs.append(delayed(write_to_netcdf)(f, variables)) + #LOG.debug("Writing %s." % (f)) + + parallel(jobs) + except: for i, f in enumerate(chrtout_files[:nfiles_to_write]): - s = time.time() variables = { - varname: (qtrt[:,i], dim, attrs) + varname: (qtrt[:i], dim, attrs) } - jobs.append(delayed(write_to_netcdf)(f, variables)) + write_to_netcdf(f, variables) LOG.debug("Writing %s." % (f)) - - parallel(jobs) LOG.debug("Writing t-route data to %d CHRTOUT files took %s seconds." % (nfiles_to_write, (time.time() - start))) From 2a02439328f8518134ca1b84cb43a1fcf5ab13a4 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Wed, 25 Jan 2023 21:41:48 +0000 Subject: [PATCH 46/54] minor updates to previous push --- src/troute-network/troute/AbstractNetwork.py | 16 +++++++++------- src/troute-network/troute/AbstractRouting.py | 2 +- src/troute-network/troute/HYFeaturesNetwork.py | 15 ++++++++------- src/troute-network/troute/NHDNetwork.py | 1 + src/troute-network/troute/nhd_io.py | 1 + 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index bbe75fe5f..a77053342 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -10,7 +10,7 @@ from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition from troute.nhd_network_utilities_v02 import organize_independent_networks, build_channel_initial_state, build_refac_connections import troute.nhd_io as nhd_io -from .AbstractRouting import * +from .AbstractRouting import MCOnly, MCwithDiffusive, MCwithDiffusiveNatlXSectionNonRefactored, MCwithDiffusiveNatlXSectionRefactored LOG = logging.getLogger('') @@ -20,7 +20,7 @@ class AbstractNetwork(ABC): """ __slots__ = ["_dataframe", "_waterbody_connections", "_gages", "_terminal_codes", "_connections", "_waterbody_df", - "_waterbody_types_df", "_waterbody_type_specified", + "_waterbody_types_df", "_waterbody_type_specified", "_link_gage_df", "_independent_networks", "_reaches_by_tw", "_flowpath_dict", "_reverse_network", "_q0", "_t0", "_link_lake_crosswalk", "_qlateral", "_break_segments", "_segment_index", "_coastal_boundary_depth_df", @@ -36,6 +36,7 @@ def __init__(self,): self._q0 = None self._t0 = None self._qlateral = None + self._link_gage_df = None #qlat_const = forcing_parameters.get("qlat_const", 0) #FIXME qlat_const """ Figure out a good way to default initialize to qlat_const/c @@ -239,11 +240,11 @@ def reaches_by_tailwater(self): @property def waterbody_dataframe(self): - return self._waterbody_df.sort_index() + return self._waterbody_df @property def waterbody_types_dataframe(self): - return self._waterbody_types_df.sort_index() + return self._waterbody_types_df @property def waterbody_type_specified(self): @@ -319,9 +320,10 @@ def segment_index(self): @property def link_gage_df(self): - link_gage_df = pd.DataFrame.from_dict(self._gages) - link_gage_df.index.name = 'link' - return link_gage_df + if self._link_gage_df is None: + self._link_gage_df = pd.DataFrame.from_dict(self._gages) + self._link_gage_df.index.name = 'link' + return self._link_gage_df @property @abstractmethod diff --git a/src/troute-network/troute/AbstractRouting.py b/src/troute-network/troute/AbstractRouting.py index 7ff5a46f9..ddeab6162 100644 --- a/src/troute-network/troute/AbstractRouting.py +++ b/src/troute-network/troute/AbstractRouting.py @@ -108,7 +108,7 @@ def unrefactored_topobathy_df(self): class MCOnly(AbstractRouting): def __init__(self, hybrid_params): - self.hybrid_params = hybrid_params + self.hybrid_params = None super().__init__() diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index c7cffa397..f801ef1c9 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -3,7 +3,6 @@ import numpy as np import geopandas as gpd import time -import os import json from pathlib import Path import pyarrow.parquet as pq @@ -54,10 +53,10 @@ def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=Non for level-pool reservoir computation. """ def node_key_func(x): - return int(x[3:]) - if os.path.splitext(parm_file)[1]=='.gpkg': + return int( x.split('-')[-1] ) + if Path(parm_file).suffix=='.gpkg': df = gpd.read_file(parm_file, layer="lake_attributes").set_index('id') - elif os.path.splitext(parm_file)[1]=='.json': + elif Path(parm_file).suffix=='.json': df = pd.read_json(parm_file, orient="index") df.index = df.index.map(node_key_func) @@ -75,11 +74,11 @@ def read_ngen_waterbody_type_df(parm_file, lake_index_field="wb-id", lake_id_mas # layer, but as of now (Nov 22, 2022) there doesn't seem to be a differentiation # between USGS reservoirs, USACE reservoirs, or RFC reservoirs... def node_key_func(x): - return int(x[3:]) + return int( x.split('-')[-1] ) - if os.path.splitext(parm_file)[1]=='.gpkg': + if Path(parm_file).suffix=='.gpkg': df = gpd.read_file(parm_file, layer="crosswalk").set_index('id') - elif os.path.splitext(parm_file)[1]=='.json': + elif Path(parm_file).suffix=='.json': df = pd.read_json(parm_file, orient="index") df.index = df.index.map(node_key_func) @@ -413,6 +412,7 @@ def read_geo_file(self,): self.waterbody_dataframe.reset_index() .drop_duplicates(subset=lake_id) .set_index(lake_id) + .sort_index() ) try: @@ -426,6 +426,7 @@ def read_geo_file(self,): self.waterbody_types_dataframe.reset_index() .drop_duplicates(subset=lake_id) .set_index(lake_id) + .sort_index() ) except ValueError: diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index 77caf44c6..f5f47289e 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -270,6 +270,7 @@ def read_geo_file(self,): self.waterbody_dataframe.reset_index() .drop_duplicates(subset="lake_id") .set_index("lake_id") + .sort_index() ) # Declare empty dataframe diff --git a/src/troute-network/troute/nhd_io.py b/src/troute-network/troute/nhd_io.py index 94427efc3..710d5b87a 100644 --- a/src/troute-network/troute/nhd_io.py +++ b/src/troute-network/troute/nhd_io.py @@ -356,6 +356,7 @@ def read_reservoir_parameter_file( df1 = (df1.reset_index() .drop_duplicates(subset="lake_id") .set_index("lake_id") + .sort_index() ) # recode to levelpool (1) for reservoir DA types set to false From 96bf87c2b60bd8dff24bf83e2b2d46d3128f8ea3 Mon Sep 17 00:00:00 2001 From: shorvath-noaa <103054653+shorvath-noaa@users.noreply.github.com> Date: Wed, 25 Jan 2023 15:22:52 -0700 Subject: [PATCH 47/54] Delete RoutingScheme.py --- src/troute-network/troute/RoutingScheme.py | 245 --------------------- 1 file changed, 245 deletions(-) delete mode 100644 src/troute-network/troute/RoutingScheme.py diff --git a/src/troute-network/troute/RoutingScheme.py b/src/troute-network/troute/RoutingScheme.py deleted file mode 100644 index 049b0b84f..000000000 --- a/src/troute-network/troute/RoutingScheme.py +++ /dev/null @@ -1,245 +0,0 @@ -from .AbstractNetwork import AbstractNetwork -import logging -import yaml -import json -import xarray as xr -import pandas as pd - -from troute.nhd_network import reverse_network -from troute.nhd_network_utilities_v02 import organize_independent_networks, build_refac_connections - -LOG = logging.getLogger('') - -def read_diffusive_domain(domain_file): - ''' - Read diffusive domain data from .ymal or .json file. - - Arguments - --------- - domain_file (str or pathlib.Path): Path of diffusive domain file - - Returns - ------- - data (dict int: [int]): domain tailwater segments: list of segments in domain - (includeing tailwater segment) - - ''' - if domain_file[-4:] == "yaml": - with open(domain_file) as domain: - data = yaml.load(domain, Loader=yaml.SafeLoader) - else: - with open(domain_file) as domain: - data = json.load(domain) - - return data - -def read_netcdf(geo_file_path): - ''' - Open a netcdf file with xarray and convert to dataframe - - Arguments - --------- - geo_file_path (str or pathlib.Path): netCDF filepath - - Returns - ------- - ds.to_dataframe() (DataFrame): netCDF contents - - Notes - ----- - - When handling large volumes of netCDF files, xarray is not the most efficient. - - ''' - with xr.open_dataset(geo_file_path) as ds: - return ds.to_dataframe() - - -class RoutingScheme(AbstractNetwork): - """ - - """ - __slots__ = ["hybrid_params", "_diffusive_domain", "_coastal_boundary_depth_df", - "_diffusive_network_data", "_topobathy_df", "_refactored_diffusive_domain", - "_refactored_diffusive_network_data", "_refactored_reaches", - "_unrefactored_topobathy_df",] - - def __init__(self,): - """ - - """ - self._diffusive_domain = None - self._diffusive_network_data = None - self._topobathy_df = pd.DataFrame() - self._unrefactored_topobathy_df = pd.DataFrame() - self._refactored_diffusive_domain = None - self._refactored_diffusive_network_data = None - self._refactored_reaches = {} - - # Determine whether to run hybrid routing from user input - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - domain_file = self.hybrid_parameters.get("diffusive_domain", None) - - if run_hybrid and domain_file: - #========================================================================== - # build diffusive domain data and edit MC domain data for hybrid simulation - self._diffusive_domain = read_diffusive_domain(domain_file) - self._diffusive_network_data = {} - - rconn_diff0 = reverse_network(self._connections) - - for tw in self._diffusive_domain: - mainstem_segs = self._diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = self._diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - self._diffusive_network_data[tw] = {} - - # add diffusive domain segments - self._diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - self._diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - self._diffusive_network_data[tw]['connections'] = {k: self._connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = organize_independent_networks( - self._diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - self._diffusive_network_data[tw]['rconn'] = rconn_diff - self._diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - self._diffusive_network_data[tw]['param_df'] = self._dataframe.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - self._diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - self._dataframe = self._dataframe.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - self._connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - self._connections[us] = [] - - super().__init__() - - @property - def diffusive_network_data(self,): - return self._diffusive_network_data - - @property - def topobathy_df(self,): - if self._topobathy_df.empty: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) - - if run_hybrid and use_topobathy: - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_refactored: - refactored_topobathy_file = self.hybrid_parameters.get("refactored_topobathy_domain", None) - self._topobathy_df = read_netcdf(refactored_topobathy_file).set_index('link') - else: - topobathy_file = self.hybrid_parameters.get("topobathy_domain", None) - self._topobathy_df = read_netcdf(topobathy_file).set_index('link') - self._topobathy_df.index = self._topobathy_df.index.astype(int) - - return self._topobathy_df - - @property - def refactored_diffusive_domain(self,): - if not self._refactored_diffusive_domain: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_hybrid and run_refactored: - refactored_domain_file = self.hybrid_parameters.get("refactored_domain", None) - - self._refactored_diffusive_domain = read_diffusive_domain(refactored_domain_file) - - return self._refactored_diffusive_domain - - @property - def refactored_reaches(self,): - if not self._refactored_reaches: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_hybrid and run_refactored: - refactored_topobathy_file = self.hybrid_parameters.get("refactored_topobathy_domain", None) - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = build_refac_connections(diffusive_parameters) - - for tw in self._diffusive_domain: - - # list of stream segments of a single refactored diffusive domain - refac_tw = self.refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = self.refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - trib_segs = self.diffusive_network_data[tw]['tributary_segments'] - refactored_diffusive_network_data = {} - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k] = [self._refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - self._refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = self._refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = self._diffusive_network_data[tw]['upstream_boundary_link'] - - return self._refactored_reaches - - @property - def unrefactored_topobathy_df(self,): - if self._unrefactored_topobathy_df.empty: - run_hybrid = self.hybrid_parameters.get("run_hybrid_routing", False) - use_topobathy = self.hybrid_parameters.get('use_natl_xsections', False) - run_refactored = self.hybrid_parameters.get('run_refactored_network', False) - - if run_hybrid and use_topobathy and run_refactored: - topobathy_file = self.hybrid_parameters.get("topobathy_domain", None) - self._unrefactored_topobathy_df = read_netcdf(topobathy_file).set_index('link') - self._unrefactored_topobathy_df.index = self._unrefactored_topobathy_df.index.astype(int) - - return self._unrefactored_topobathy_df - From e8f19d556e6804c13ef56d969fde5482fcfcfafe Mon Sep 17 00:00:00 2001 From: shorvath-noaa <103054653+shorvath-noaa@users.noreply.github.com> Date: Fri, 27 Jan 2023 11:59:54 -0700 Subject: [PATCH 48/54] Delete nhd_preprocess.py --- src/troute-network/troute/nhd_preprocess.py | 1148 ------------------- 1 file changed, 1148 deletions(-) delete mode 100644 src/troute-network/troute/nhd_preprocess.py diff --git a/src/troute-network/troute/nhd_preprocess.py b/src/troute-network/troute/nhd_preprocess.py deleted file mode 100644 index 2422bac84..000000000 --- a/src/troute-network/troute/nhd_preprocess.py +++ /dev/null @@ -1,1148 +0,0 @@ -import time -import pathlib -import logging -from datetime import datetime -from collections import defaultdict - -import pandas as pd -import numpy as np -import xarray as xr - -import troute.nhd_network_utilities_v02 as nnu -import troute.nhd_network as nhd_network -import troute.nhd_io as nhd_io - -LOG = logging.getLogger('') - -def read_geo_file(supernetwork_parameters, waterbody_parameters, data_assimilation_parameters): - ''' - Construct network connections network, parameter dataframe, waterbody mapping, - and gage mapping. This is an intermediate-level function that calls several - lower level functions to read data, conduct network operations, and extract mappings. - - Arguments - --------- - supernetwork_parameters (dict): User input network parameters - - Returns: - -------- - connections (dict int: [int]): Network connections - param_df (DataFrame): Geometry and hydraulic parameters - wbodies (dict, int: int): segment-waterbody mapping - gages (dict, int: int): segment-gage mapping - - ''' - - # crosswalking dictionary between variables names in input dataset and - # variable names recognized by troute.routing module. - cols = supernetwork_parameters.get( - 'columns', - { - 'key' : 'link', - 'downstream': 'to', - 'dx' : 'Length', - 'n' : 'n', - 'ncc' : 'nCC', - 's0' : 'So', - 'bw' : 'BtmWdth', - 'waterbody' : 'NHDWaterbodyComID', - 'gages' : 'gages', - 'tw' : 'TopWdth', - 'twcc' : 'TopWdthCC', - 'alt' : 'alt', - 'musk' : 'MusK', - 'musx' : 'MusX', - 'cs' : 'ChSlp', - } - ) - - # numeric code used to indicate network terminal segments - terminal_code = supernetwork_parameters.get("terminal_code", 0) - - # read parameter dataframe - param_df = nhd_io.read(pathlib.Path(supernetwork_parameters["geo_file_path"])) - - # select the column names specified in the values in the cols dict variable - param_df = param_df[list(cols.values())] - - # rename dataframe columns to keys in the cols dict variable - param_df = param_df.rename(columns=nhd_network.reverse_dict(cols)) - - # handle synthetic waterbody segments - synthetic_wb_segments = supernetwork_parameters.get("synthetic_wb_segments", None) - synthetic_wb_id_offset = supernetwork_parameters.get("synthetic_wb_id_offset", 9.99e11) - if synthetic_wb_segments: - # rename the current key column to key32 - key32_d = {"key":"key32"} - param_df = param_df.rename(columns=key32_d) - # create a key index that is int64 - # copy the links into the new column - param_df["key"] = param_df.key32.astype("int64") - # update the values of the synthetic reservoir segments - fix_idx = param_df.key.isin(set(synthetic_wb_segments)) - param_df.loc[fix_idx,"key"] = (param_df[fix_idx].key + synthetic_wb_id_offset).astype("int64") - - # set parameter dataframe index as segment id number, sort - param_df = param_df.set_index("key").sort_index() - - # get and apply domain mask - if "mask_file_path" in supernetwork_parameters: - data_mask = nhd_io.read_mask( - pathlib.Path(supernetwork_parameters["mask_file_path"]), - layer_string=supernetwork_parameters.get("mask_layer_string", None), - ) - data_mask = data_mask.set_index(data_mask.columns[0]) - param_df = param_df.filter(data_mask.index, axis=0) - - # map segment ids to waterbody ids - wbodies = {} - if "waterbody" in cols: - wbodies = nhd_network.extract_waterbody_connections( - param_df[["waterbody"]] - ) - param_df = param_df.drop("waterbody", axis=1) - - # map segment ids to gage ids - gages = {} - if "gages" in cols: - gages = nhd_network.gage_mapping(param_df[["gages"]]) - param_df = param_df.drop("gages", axis=1) - - # There can be an externally determined terminal code -- that's this first value - terminal_codes = set() - terminal_codes.add(terminal_code) - # ... but there may also be off-domain nodes that are not explicitly identified - # but which are terminal (i.e., off-domain) as a result of a mask or some other - # an interior domain truncation that results in a - # otherwise valid node value being pointed to, but which is masked out or - # being intentionally separated into another domain. - terminal_codes = terminal_codes | set( - param_df[~param_df["downstream"].isin(param_df.index)]["downstream"].values - ) - - # build connections dictionary - connections = nhd_network.extract_connections( - param_df, "downstream", terminal_codes=terminal_codes - ) - param_df = param_df.drop("downstream", axis=1) - - param_df = param_df.astype("float32") - - break_network_at_waterbodies = waterbody_parameters.get( - "break_network_at_waterbodies", False - ) - - # if waterbodies are being simulated, adjust the connections graph so that - # waterbodies are collapsed to single nodes. Also, build a mapping between - # waterbody outlet segments and lake ids - if break_network_at_waterbodies: - connections, link_lake_crosswalk = nhd_network.replace_waterbodies_connections( - connections, wbodies - ) - else: - link_lake_crosswalk = None - - #============================================================================ - # Retrieve and organize waterbody parameters - - waterbody_type_specified = False - if break_network_at_waterbodies: - - # Read waterbody parameters from LAKEPARM file - level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) - waterbodies_df = nhd_io.read_lakeparm( - level_pool_params['level_pool_waterbody_parameter_file_path'], - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - wbodies.values() - ) - - # Remove duplicate lake_ids and rows - waterbodies_df = ( - waterbodies_df.reset_index() - .drop_duplicates(subset="lake_id") - .set_index("lake_id") - ) - - # Declare empty dataframe - waterbody_types_df = pd.DataFrame() - - # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True - reservoir_da = data_assimilation_parameters.get( - 'reservoir_da', - {} - ) - - if reservoir_da: - usgs_hybrid = reservoir_da.get( - 'reservoir_persistence_usgs', - False - ) - usace_hybrid = reservoir_da.get( - 'reservoir_persistence_usace', - False - ) - param_file = reservoir_da.get( - 'gage_lakeID_crosswalk_file', - None - ) - else: - param_file = None - usace_hybrid = False - usgs_hybrid = False - - # check if RFC-type reservoirs are set to true - rfc_params = waterbody_parameters.get('rfc') - if rfc_params: - rfc_forecast = rfc_params.get( - 'reservoir_rfc_forecasts', - False - ) - param_file = rfc_params.get('reservoir_parameter_file',None) - else: - rfc_forecast = False - - if (param_file and reservoir_da) or (param_file and rfc_forecast): - waterbody_type_specified = True - ( - waterbody_types_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk - ) = nhd_io.read_reservoir_parameter_file( - param_file, - usgs_hybrid, - usace_hybrid, - rfc_forecast, - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - reservoir_da.get('crosswalk_usgs_gage_field', 'usgs_gage_id'), - reservoir_da.get('crosswalk_usgs_lakeID_field', 'usgs_lake_id'), - reservoir_da.get('crosswalk_usace_gage_field', 'usace_gage_id'), - reservoir_da.get('crosswalk_usace_lakeID_field', 'usace_lake_id'), - wbodies.values(), - ) - else: - waterbody_type_specified = True - waterbody_types_df = pd.DataFrame(data = 1, index = waterbodies_df.index, columns = ['reservoir_type']) - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - else: - # Declare empty dataframes - waterbody_types_df = pd.DataFrame() - waterbodies_df = pd.DataFrame() - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - return ( - param_df, - connections, - terminal_codes, - waterbodies_df, - waterbody_types_df, - waterbody_type_specified, - wbodies, - link_lake_crosswalk, - gages, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk) - - -def build_nhd_network(supernetwork_parameters,waterbody_parameters, - preprocessing_parameters,compute_parameters, - data_assimilation_parameters): - - # Build routing network data objects. Network data objects specify river - # network connectivity, channel geometry, and waterbody parameters. - if preprocessing_parameters.get('use_preprocessed_data', False): - - # get data from pre-processed file - ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) = unpack_nhd_preprocess_data( - preprocessing_parameters - ) - else: - - # build data objects from scratch - ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) = nhd_network_preprocess( - supernetwork_parameters, - waterbody_parameters, - preprocessing_parameters, - compute_parameters, - data_assimilation_parameters, - ) - - return (connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df - ) - - -def nhd_network_preprocess( - supernetwork_parameters, - waterbody_parameters, - preprocessing_parameters, - compute_parameters, - data_assimilation_parameters, -): - ''' - Creation of routing network data objects. Logical ordering of lower-level - function calls that build individual network data objects. - - Arguments - --------- - supernetwork_parameters (dict): user input data re network extent - waterbody_parameters (dict): user input data re waterbodies - preprocessing_parameters (dict): user input data re preprocessing - compute_parameters (dict): user input data re compute configuration - data_assimilation_parameters (dict): user input data re data assimilation - - Returns - ------- - connections (dict of int: [int]): {segment id: [downsteram adjacent segment ids]} - param_df (DataFrame): Hydraulic geometry and roughness parameters, by segment - wbody_conn (dict of int: int): {segment id: associated lake id} - waterbodies_df (DataFrame): Waterbody (reservoir) parameters - waterbody_types_df (DataFrame): Waterbody type codes (1 - levelpool, 2 - USGS, 3 - USACE, 4 - RFC) - break_network_at_waterbodies (bool): If True, waterbodies occpy reaches of their own - waterbody_type_specified (bool): If True, more than just levelpool waterbodies exist - link_lake_crosswalk (dict of int: int): {lake id: outlet segment id} - independent_networks (dict of int: {int: [int]}): {tailwater id: {segment id: [upstream adjacent segment ids]}} - reaches_bytw (dict of int: [[int]]): {tailwater id: list or reach lists} - rconn (dict of int: [int]): {segment id: [upstream adjacent segment ids]} - pd.DataFrame.from_dict(gages) (DataFrame): Gage ids and corresponding segment ids at which they are located - diffusive_network_data (dict or None): Network data objects for diffusive domain - topobathy_df (DataFrame): Natural cross section data for diffusive domain - - Notes - ----- - - waterbody_type_specified is likely an excessive return and can be removed and inferred from the - contents of waterbody_types_df - - The values of the link_lake_crosswalk dictionary are the downstream-most segments within - the waterbody extent to which waterbody data are written. They are NOT the first segments - downsteram of the waterbody - ''' - - #============================================================================ - # Establish diffusive domain for MC/diffusive hybrid simulations - - hybrid_params = compute_parameters.get("hybrid_parameters", False) - if hybrid_params: - # switch parameters - # if run_hybrid = False, run MC only - # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc - # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric - # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric - run_hybrid = hybrid_params.get('run_hybrid_routing', False) - use_topobathy = hybrid_params.get('use_natl_xsections', False) - run_refactored = hybrid_params.get('run_refactored_network', False) - - # file path parameters of non-refactored hydrofabric defined by RouteLink.nc - domain_file = hybrid_params.get("diffusive_domain", None) - topobathy_file = hybrid_params.get("topobathy_domain", None) - - # file path parameters of refactored hydrofabric for diffusive wave channel routing - refactored_domain_file = hybrid_params.get("refactored_domain", None) - refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) - #------------------------------------------------------------------------- - # for non-refactored hydofabric defined by RouteLink.nc - # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. - if run_hybrid and domain_file: - - LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - diffusive_domain = nhd_io.read_diffusive_domain(domain_file) - - if use_topobathy and topobathy_file: - - LOG.debug('Natural cross section data on original hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - - # TODO: Request GID make comID variable an integer in their product, so - # we do not need to change variable types, here. - topobathy_df.index = topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - diffusive_network_data = {} - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') - unrefactored_topobathy_df = pd.DataFrame() - #------------------------------------------------------------------------- - # for refactored hydofabric - if run_hybrid and run_refactored and refactored_domain_file: - - LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) - - if use_topobathy and refactored_topobathy_file: - - LOG.debug('Natural cross section data of refactored hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) - - # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy - # for crosswalking water elevations between non-refactored and refactored hydrofabrics. - unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - unrefactored_topobathy_df.index = unrefactored_topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - refactored_diffusive_network_data = {} - - else: - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - unrefactored_topobathy_df = pd.DataFrame() - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') - #============================================================================ - # Build network connections graph, assemble parameter dataframe, - # establish segment-waterbody, and segment-gage mappings - LOG.info("creating network connections graph") - start_time = time.time() - - connections, param_df, wbody_conn, gages = nnu.build_connections( - supernetwork_parameters, - ) - - link_gage_df = pd.DataFrame.from_dict(gages) - link_gage_df.index.name = 'link' - break_network_at_waterbodies = waterbody_parameters.get( - "break_network_at_waterbodies", False - ) - - # if streamflow DA, then break network at gages - break_network_at_gages = False - streamflow_da = data_assimilation_parameters.get('streamflow_da', False) - if streamflow_da: - break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - - if not wbody_conn: - # Turn off any further reservoir processing if the network contains no - # waterbodies - break_network_at_waterbodies = False - - # if waterbodies are being simulated, adjust the connections graph so that - # waterbodies are collapsed to single nodes. Also, build a mapping between - # waterbody outlet segments and lake ids - if break_network_at_waterbodies: - connections, link_lake_crosswalk = nhd_network.replace_waterbodies_connections( - connections, wbody_conn - ) - else: - link_lake_crosswalk = None - - LOG.debug("network connections graph created in %s seconds." % (time.time() - start_time)) - - #============================================================================ - # Retrieve and organize waterbody parameters - - waterbody_type_specified = False - if break_network_at_waterbodies: - - # Read waterbody parameters from LAKEPARM file - level_pool_params = waterbody_parameters.get('level_pool', defaultdict(list)) - waterbodies_df = nhd_io.read_lakeparm( - level_pool_params['level_pool_waterbody_parameter_file_path'], - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - wbody_conn.values() - ) - - # Remove duplicate lake_ids and rows - waterbodies_df = ( - waterbodies_df.reset_index() - .drop_duplicates(subset="lake_id") - .set_index("lake_id") - ) - - # Declare empty dataframe - waterbody_types_df = pd.DataFrame() - - # Check if hybrid-usgs or hybrid-usace reservoir DA is set to True - reservoir_da = data_assimilation_parameters.get( - 'reservoir_da', - {} - ) - - if reservoir_da: - usgs_hybrid = reservoir_da.get( - 'reservoir_persistence_usgs', - False - ) - usace_hybrid = reservoir_da.get( - 'reservoir_persistence_usace', - False - ) - param_file = reservoir_da.get( - 'gage_lakeID_crosswalk_file', - None - ) - else: - param_file = None - usace_hybrid = False - usgs_hybrid = False - - # check if RFC-type reservoirs are set to true - rfc_params = waterbody_parameters.get('rfc') - if rfc_params: - rfc_forecast = rfc_params.get( - 'reservoir_rfc_forecasts', - False - ) - param_file = rfc_params.get('reservoir_parameter_file',None) - else: - rfc_forecast = False - - if (param_file and reservoir_da) or (param_file and rfc_forecast): - waterbody_type_specified = True - ( - waterbody_types_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk - ) = nhd_io.read_reservoir_parameter_file( - param_file, - usgs_hybrid, - usace_hybrid, - rfc_forecast, - level_pool_params.get("level_pool_waterbody_id", 'lake_id'), - reservoir_da.get('crosswalk_usgs_gage_field', 'usgs_gage_id'), - reservoir_da.get('crosswalk_usgs_lakeID_field', 'usgs_lake_id'), - reservoir_da.get('crosswalk_usace_gage_field', 'usace_gage_id'), - reservoir_da.get('crosswalk_usace_lakeID_field', 'usace_lake_id'), - wbody_conn.values(), - ) - else: - waterbody_type_specified = True - waterbody_types_df = pd.DataFrame(data = 1, index = waterbodies_df.index, columns = ['reservoir_type']) - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - else: - # Declare empty dataframes - waterbody_types_df = pd.DataFrame() - waterbodies_df = pd.DataFrame() - usgs_lake_gage_crosswalk = None - usace_lake_gage_crosswalk = None - - #============================================================================ - # build diffusive domain data and edit MC domain data for hybrid simulation - - # - if diffusive_domain: - rconn_diff0 = nhd_network.reverse_network(connections) - refactored_reaches = {} - - for tw in diffusive_domain: - mainstem_segs = diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - diffusive_network_data[tw] = {} - - # add diffusive domain segments - diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = nnu.organize_independent_networks( - diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - diffusive_network_data[tw]['rconn'] = rconn_diff - diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - diffusive_network_data[tw]['param_df'] = param_df.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - if refactored_diffusive_domain: - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = nnu.build_refac_connections(diffusive_parameters) - - # list of stream segments of a single refactored diffusive domain - refac_tw = refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k]= [refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = nnu.organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = diffusive_network_data[tw]['upstream_boundary_link'] - else: - refactored_reaches={} - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - param_df = param_df.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - connections[us] = [] - - #============================================================================ - # Identify Independent Networks and Reaches by Network - LOG.info("organizing connections into reaches ...") - start_time = time.time() - gage_break_segments = set() - wbody_break_segments = set() - if break_network_at_waterbodies: - wbody_break_segments = wbody_break_segments.union(wbody_conn.values()) - - if break_network_at_gages: - gage_break_segments = gage_break_segments.union(gages['gages'].keys()) - - independent_networks, reaches_bytw, rconn = nnu.organize_independent_networks( - connections, - wbody_break_segments, - gage_break_segments, - ) - - LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) - - if preprocessing_parameters.get('preprocess_only', False): - - LOG.debug("saving preprocessed network data to disk for future use") - # todo: consider a better default than None - destination_folder = preprocessing_parameters.get('preprocess_output_folder', None) - if destination_folder: - - output_filename = preprocessing_parameters.get( - 'preprocess_output_filename', - 'preprocess_output' - ) - - outputs = {} - outputs.update( - {'connections': connections, - 'param_df': param_df, - 'wbody_conn': wbody_conn, - 'waterbodies_df': waterbodies_df, - 'waterbody_types_df': waterbody_types_df, - 'break_network_at_waterbodies': break_network_at_waterbodies, - 'waterbody_type_specified': waterbody_type_specified, - 'link_lake_crosswalk': link_lake_crosswalk, - 'independent_networks': independent_networks, - 'reaches_bytw': reaches_bytw, - 'rconn': rconn, - 'link_gage_df': link_gage_df, - 'usgs_lake_gage_crosswalk': usgs_lake_gage_crosswalk, - 'usace_lake_gage_crosswalk': usace_lake_gage_crosswalk, - 'diffusive_network_data': diffusive_network_data, - 'topobathy_data': topobathy_df, - } - ) - try: - np.save( - pathlib.Path(destination_folder).joinpath(output_filename), - outputs - ) - except: - LOG.critical('Canonot find %s. Aborting preprocessing routine' % pathlib.Path(destination_folder)) - quit() - - LOG.debug( - "writing preprocessed network data to %s"\ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - LOG.critical( - "Preprocessed network data written to %s aborting preprocessing sequence" \ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - quit() - - else: - LOG.critical( - "No destination folder specified for preprocessing. Please specify preprocess_output_folder in configuration file. Aborting preprocessing routine" - ) - quit() - - return ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - link_gage_df, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) - -def unpack_nhd_preprocess_data(preprocessing_parameters): - - preprocess_filepath = preprocessing_parameters.get('preprocess_source_file',None) - if preprocess_filepath: - try: - inputs = np.load(pathlib.Path(preprocess_filepath),allow_pickle='TRUE').item() - except: - LOG.critical('Canonot find %s' % pathlib.Path(preprocess_filepath)) - quit() - - connections = inputs.get('connections',None) - param_df = inputs.get('param_df',None) - wbody_conn = inputs.get('wbody_conn',None) - waterbodies_df = inputs.get('waterbodies_df',None) - waterbody_types_df = inputs.get('waterbody_types_df',None) - break_network_at_waterbodies = inputs.get('break_network_at_waterbodies',None) - waterbody_type_specified = inputs.get('waterbody_type_specified',None) - link_lake_crosswalk = inputs.get('link_lake_crosswalk', None) - independent_networks = inputs.get('independent_networks',None) - reaches_bytw = inputs.get('reaches_bytw',None) - rconn = inputs.get('rconn',None) - gages = inputs.get('link_gage_df',None) - usgs_lake_gage_crosswalk = inputs.get('usgs_lake_gage_crosswalk',None) - usace_lake_gage_crosswalk = inputs.get('usace_lake_gage_crosswalk',None) - diffusive_network_data = inputs.get('diffusive_network_data',None) - topobathy_df = inputs.get('topobathy_data',None) - refactored_diffusive_domain = inputs.get('refactored_diffusive_domain',None) - refactored_reaches = inputs.get('refactored_reaches',None) - unrefactored_topobathy_df = inputs.get('unrefactored_topobathy',None) - - else: - LOG.critical("use_preprocessed_data = True, but no preprocess_source_file is specified. Aborting the simulation.") - quit() - - return ( - connections, - param_df, - wbody_conn, - waterbodies_df, - waterbody_types_df, - break_network_at_waterbodies, - waterbody_type_specified, - link_lake_crosswalk, - independent_networks, - reaches_bytw, - rconn, - gages, - usgs_lake_gage_crosswalk, - usace_lake_gage_crosswalk, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) - - -def nhd_initial_warmstate_preprocess( - break_network_at_waterbodies, - restart_parameters, - data_assimilation_parameters, - segment_index, - waterbodies_df, - link_lake_crosswalk, -): - - ''' - Assemble model initial condition data: - - waterbody inital states (outflow and pool elevation) - - channel initial states (flow and depth) - - initial time - - Arguments - --------- - - break_network_at_waterbodies (bool): If True, waterbody initial states will - be appended to the waterbody parameter - dataframe. If False, waterbodies will - not be simulated and the waterbody - parameter datataframe wil not be changed - - restart_parameters (dict): User-input simulation restart - parameters - - data_assimilation_parameters (dict): User-input data assimilation - parameters - - segment_index (Pandas Index): All segment IDs in the simulation - doamin - - waterbodies_df (Pandas DataFrame): Waterbody parameters - - link_lake_crosswalk (dict): Crosswalking between lake ids and the link - id of the lake outlet segment - - Returns - ------- - - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial - states (outflow and pool elevation) - - q0 (Pandas DataFrame): Initial flow and depth states for each - segment in the model domain - - t0 (datetime): Datetime of the model initialization - - Notes - ----- - ''' - - #---------------------------------------------------------------------------- - # Assemble waterbody initial states (outflow and pool elevation - #---------------------------------------------------------------------------- - - if break_network_at_waterbodies: - - start_time = time.time() - LOG.info("setting waterbody initial states ...") - - # if a lite restart file is provided, read initial states from it. - if restart_parameters.get("lite_waterbody_restart_file", None): - - waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( - restart_parameters['lite_waterbody_restart_file'] - ) - - # read waterbody initial states from WRF-Hydro type restart file - elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): - waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( - restart_parameters["wrf_hydro_waterbody_restart_file"], - restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], - restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", 'lake_id'), - restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], - restart_parameters.get( - "wrf_hydro_waterbody_crosswalk_filter_file_field_name", - 'NHDWaterbodyComID' - ), - ) - - # if no restart file is provided, default initial states - else: - # TODO: Consider adding option to read cold state from route-link file - waterbodies_initial_ds_flow_const = 0.0 - waterbodies_initial_depth_const = -1e9 - # Set initial states from cold-state - waterbodies_initial_states_df = pd.DataFrame( - 0, - index=waterbodies_df.index, - columns=[ - "qd0", - "h0", - ], - dtype="float32", - ) - # TODO: This assignment could probably by done in the above call - waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const - waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const - waterbodies_initial_states_df["index"] = range( - len(waterbodies_initial_states_df) - ) - - waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on="lake_id" - ) - - LOG.debug( - "waterbody initial states complete in %s seconds."\ - % (time.time() - start_time)) - start_time = time.time() - - #---------------------------------------------------------------------------- - # Assemble channel initial states (flow and depth) - # also establish simulation initialization timestamp - #---------------------------------------------------------------------------- - start_time = time.time() - LOG.info("setting channel initial states ...") - - # if lite restart file is provided, the read channel initial states from it - if restart_parameters.get("lite_channel_restart_file", None): - - q0, t0 = nhd_io.read_lite_restart( - restart_parameters['lite_channel_restart_file'] - ) - t0_str = None - - # build initial states from user-provided restart parameters - else: - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - - # get initialization time from restart file - if restart_parameters.get("wrf_hydro_channel_restart_file", None): - channel_initial_states_file = restart_parameters[ - "wrf_hydro_channel_restart_file" - ] - t0_str = nhd_io.get_param_str( - channel_initial_states_file, - "Restart_Time" - ) - else: - t0_str = "2015-08-16_00:00:00" - - # convert timestamp from string to datetime - t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") - - # get initial time from user inputs - if restart_parameters.get("start_datetime", None): - t0_str = restart_parameters.get("start_datetime") - - def _try_parsing_date(text): - for fmt in ( - "%Y-%m-%d_%H:%M", - "%Y-%m-%d_%H:%M:%S", - "%Y-%m-%d %H:%M", - "%Y-%m-%d %H:%M:%S", - "%Y/%m/%d %H:%M", - "%Y/%m/%d %H:%M:%S" - ): - try: - return datetime.strptime(text, fmt) - except ValueError: - pass - LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') - quit() - - t0 = _try_parsing_date(t0_str) - else: - if t0_str == "2015-08-16_00:00:00": - LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') - else: - LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) - - LOG.debug( - "channel initial states complete in %s seconds."\ - % (time.time() - start_time) - ) - start_time = time.time() - - return waterbodies_df, q0, t0 - # TODO: This returns a full dataframe (waterbodies_df) with the - # merged initial states for waterbodies, but only the - # initial state values (q0; not merged with the channel properties) - # for the channels -- - # That is because that is how they are used downstream. Need to - # trace that back and decide if there is one of those two ways - # that is optimal and make both returns that way. - - -def nhd_forcing( - run, - forcing_parameters, - hybrid_parameters, - segment_index, - cpu_pool, - t0, - coastal_boundary_depth_df, -): - """ - Assemble model forcings. Forcings include hydrological lateral inflows (qlats) - and coastal boundary depths for hybrid runs - - Aguments - -------- - - run (dict): List of forcing files pertaining to a - single run-set - - forcing_parameters (dict): User-input simulation forcing parameters - - hybrid_parameters (dict): User-input simulation hybrid parameters - - segment_index (Int64): Reach segment ids - - cpu_pool (int): Number of CPUs in the process-parallel pool - - Returns - ------- - - qlats_df (Pandas DataFrame): Lateral inflow data, indexed by - segment ID - - coastal_bounary_depth_df (Pandas DataFrame): Coastal boundary water depths, - indexed by segment ID - - Notes - ----- - - """ - - # Unpack user-specified forcing parameters - dt = forcing_parameters.get("dt", None) - qts_subdivisions = forcing_parameters.get("qts_subdivisions", None) - qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) - qlat_file_index_col = forcing_parameters.get("qlat_file_index_col", "feature_id") - qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") - qlat_file_gw_bucket_flux_col = forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") - qlat_file_terrain_runoff_col = forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") - - - # TODO: find a better way to deal with these defaults and overrides. - run["t0"] = run.get("t0", t0) - run["nts"] = run.get("nts") - run["dt"] = run.get("dt", dt) - run["qts_subdivisions"] = run.get("qts_subdivisions", qts_subdivisions) - run["qlat_input_folder"] = run.get("qlat_input_folder", qlat_input_folder) - run["qlat_file_index_col"] = run.get("qlat_file_index_col", qlat_file_index_col) - run["qlat_file_value_col"] = run.get("qlat_file_value_col", qlat_file_value_col) - run["qlat_file_gw_bucket_flux_col"] = run.get("qlat_file_gw_bucket_flux_col", qlat_file_gw_bucket_flux_col) - run["qlat_file_terrain_runoff_col"] = run.get("qlat_file_terrain_runoff_col", qlat_file_terrain_runoff_col) - - #--------------------------------------------------------------------------- - # Assemble lateral inflow data - #--------------------------------------------------------------------------- - - start_time = time.time() - LOG.info("Creating a DataFrame of lateral inflow forcings ...") - - # Place holder, if reading qlats from a file use this. - # TODO: add an option for reading qlat data from BMI/model engine - from_file = True - if from_file: - qlats_df = nnu.build_qlateral_array( - run, - cpu_pool, - segment_index, - ) - - LOG.debug( - "lateral inflow DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - #--------------------------------------------------------------------- - # Assemble coastal coupling data [WIP] - #--------------------------------------------------------------------- - # Run if coastal_boundary_depth_df has not already been created: - if coastal_boundary_depth_df.empty: - coastal_boundary_elev_files = forcing_parameters.get('coastal_boundary_input_file', None) - coastal_boundary_domain_files = hybrid_parameters.get('coastal_boundary_domain', None) - - if coastal_boundary_elev_files: - start_time = time.time() - LOG.info("creating coastal dataframe ...") - - coastal_boundary_domain = nhd_io.read_coastal_boundary_domain(coastal_boundary_domain_files) - coastal_boundary_depth_df = nhd_io.build_coastal_ncdf_dataframe( - coastal_boundary_elev_files, - coastal_boundary_domain, - ) - - LOG.debug( - "coastal boundary elevation observation DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - return qlats_df, coastal_boundary_depth_df From 4f12e4eef441d15068b7baec21e75a3feafd7bf5 Mon Sep 17 00:00:00 2001 From: shorvath-noaa <103054653+shorvath-noaa@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:00:11 -0700 Subject: [PATCH 49/54] Delete hyfeature_preprocess.py --- .../troute/hyfeature_preprocess.py | 964 ------------------ 1 file changed, 964 deletions(-) delete mode 100644 src/troute-network/troute/hyfeature_preprocess.py diff --git a/src/troute-network/troute/hyfeature_preprocess.py b/src/troute-network/troute/hyfeature_preprocess.py deleted file mode 100644 index 7d0187f5c..000000000 --- a/src/troute-network/troute/hyfeature_preprocess.py +++ /dev/null @@ -1,964 +0,0 @@ -import time -import pathlib -import logging -from datetime import datetime -from collections import defaultdict -from pathlib import Path -import os - -import pandas as pd -import numpy as np -import xarray as xr -import geopandas as gpd - -import troute.nhd_network_utilities_v02 as nnu -import troute.nhd_network as nhd_network -import troute.nhd_io as nhd_io -from troute.nhd_network import reverse_dict -import troute.hyfeature_network_utilities as hnu - -LOG = logging.getLogger('') - -def read_geo_file( - supernetwork_parameters, - waterbody_parameters, -): - - geo_file_path = supernetwork_parameters["geo_file_path"] - - file_type = Path(geo_file_path).suffix - if( file_type == '.gpkg' ): - dataframe = read_geopkg(geo_file_path) - elif( file_type == '.json') : - edge_list = supernetwork_parameters['flowpath_edge_list'] - dataframe = read_json(geo_file_path, edge_list) - else: - raise RuntimeError("Unsupported file type: {}".format(file_type)) - - # Don't need the string prefix anymore, drop it - mask = ~ dataframe['toid'].str.startswith("tnex") - dataframe = dataframe.apply(numeric_id, axis=1) - - # make the flowpath linkage, ignore the terminal nexus - flowpath_dict = dict(zip(dataframe.loc[mask].toid, dataframe.loc[mask].id)) - - # ********** need to be included in flowpath_attributes ************* - dataframe['alt'] = 1.0 #FIXME get the right value for this... - - cols = supernetwork_parameters.get('columns',None) - - if cols: - dataframe = dataframe[list(cols.values())] - # Rename parameter columns to standard names: from route-link names - # key: "link" - # downstream: "to" - # dx: "Length" - # n: "n" # TODO: rename to `manningn` - # ncc: "nCC" # TODO: rename to `mannningncc` - # s0: "So" # TODO: rename to `bedslope` - # bw: "BtmWdth" # TODO: rename to `bottomwidth` - # waterbody: "NHDWaterbodyComID" - # gages: "gages" - # tw: "TopWdth" # TODO: rename to `topwidth` - # twcc: "TopWdthCC" # TODO: rename to `topwidthcc` - # alt: "alt" - # musk: "MusK" - # musx: "MusX" - # cs: "ChSlp" # TODO: rename to `sideslope` - dataframe = dataframe.rename(columns=reverse_dict(cols)) - dataframe.set_index("key", inplace=True) - dataframe = dataframe.sort_index() - - # numeric code used to indicate network terminal segments - terminal_code = supernetwork_parameters.get("terminal_code", 0) - - # There can be an externally determined terminal code -- that's this first value - terminal_codes = set() - terminal_codes.add(terminal_code) - # ... but there may also be off-domain nodes that are not explicitly identified - # but which are terminal (i.e., off-domain) as a result of a mask or some other - # an interior domain truncation that results in a - # otherwise valid node value being pointed to, but which is masked out or - # being intentionally separated into another domain. - terminal_codes = terminal_codes | set( - dataframe[~dataframe["downstream"].isin(dataframe.index)]["downstream"].values - ) - - # build connections dictionary - connections = nhd_network.extract_connections( - dataframe, "downstream", terminal_codes=terminal_codes - ) - - #Load waterbody/reservoir info - if waterbody_parameters: - levelpool_params = waterbody_parameters.get('level_pool', None) - if not levelpool_params: - # FIXME should not be a hard requirement - raise(RuntimeError("No supplied levelpool parameters in routing config")) - - lake_id = levelpool_params.get("level_pool_waterbody_id", "wb-id") - waterbody_df = read_ngen_waterbody_df( - levelpool_params["level_pool_waterbody_parameter_file_path"], - lake_id, - ) - - # Remove duplicate lake_ids and rows - waterbody_df = ( - waterbody_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - - try: - waterbody_types_df = read_ngen_waterbody_type_df( - levelpool_params["reservoir_parameter_file"], - lake_id, - #self.waterbody_connections.values(), - ) - # Remove duplicate lake_ids and rows - waterbody_types_df =( - waterbody_types_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - - except ValueError: - #FIXME any reservoir operations requires some type - #So make this default to 1 (levelpool) - waterbody_types_df = pd.DataFrame(index=waterbody_df.index) - waterbody_types_df['reservoir_type'] = 1 - - return dataframe, flowpath_dict, connections, waterbody_df, waterbody_types_df, terminal_codes - -def build_hyfeature_network(supernetwork_parameters, - waterbody_parameters, -): - - geo_file_path = supernetwork_parameters["geo_file_path"] - cols = supernetwork_parameters["columns"] - terminal_code = supernetwork_parameters.get("terminal_code", 0) - - break_network_at_waterbodies = supernetwork_parameters.get("break_network_at_waterbodies", False) - break_network_at_gages = supernetwork_parameters.get("break_network_at_gages", False) - break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, - "break_network_at_gages": break_network_at_gages} - - file_type = Path(geo_file_path).suffix - if( file_type == '.gpkg' ): - dataframe = hyf_network.read_geopkg(geo_file_path) - elif( file_type == '.json') : - edge_list = supernetwork_parameters['flowpath_edge_list'] - dataframe = hyf_network.read_json(geo_file_path, edge_list) - else: - raise RuntimeError("Unsupported file type: {}".format(file_type)) - - # Don't need the string prefix anymore, drop it - mask = ~ dataframe['toid'].str.startswith("tnex") - dataframe = dataframe.apply(hyf_network.numeric_id, axis=1) - - # make the flowpath linkage, ignore the terminal nexus - flowpath_dict = dict(zip(dataframe.loc[mask].toid, dataframe.loc[mask].id)) - waterbody_types_df = pd.DataFrame() - waterbody_df = pd.DataFrame() - waterbody_type_specified = False - - # ********** need to be included in flowpath_attributes ************* - # FIXME once again, order here can hurt....to hack `alt` in, either need to - # put it as a column in the config, or do this AFTER the super constructor - # otherwise the alt column gets sliced out... - dataframe['alt'] = 1.0 #FIXME get the right value for this... - - #Load waterbody/reservoir info - #For ngen HYFeatures, the reservoirs to be simulated - #are determined by the lake.json file - #we limit waterbody_connections to only the flowpaths - #that coincide with a lake listed in this file - #see `waterbody_connections` - if waterbody_parameters: - # FIXME later, DO ALL LAKE PARAMS BETTER - levelpool_params = waterbody_parameters.get('level_pool', None) - if not levelpool_params: - # FIXME should not be a hard requirement - raise(RuntimeError("No supplied levelpool parameters in routing config")) - - lake_id = levelpool_params.get("level_pool_waterbody_id", "wb-id") - waterbody_df = read_ngen_waterbody_df( - levelpool_params["level_pool_waterbody_parameter_file_path"], - lake_id, - #self.waterbody_connections.values() - ) - - # Remove duplicate lake_ids and rows - waterbody_df = ( - waterbody_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - waterbody_df["qd0"] = 0.0 - waterbody_df["h0"] = -1e9 - - try: - waterbody_types_df = read_ngen_waterbody_type_df( - levelpool_params["reservoir_parameter_file"], - lake_id, - #self.waterbody_connections.values(), - ) - # Remove duplicate lake_ids and rows - waterbody_types_df =( - waterbody_types_df.reset_index() - .drop_duplicates(subset=lake_id) - .set_index(lake_id) - ) - - except ValueError: - #FIXME any reservoir operations requires some type - #So make this default to 1 (levelpool) - waterbody_types_df = pd.DataFrame(index=waterbody_df.index) - waterbody_types_df['reservoir_type'] = 1 - - return (dataframe, - flowpath_dict, - waterbody_types_df, - waterbody_df, - waterbody_type_specified, - cols, - terminal_code, - break_points, - ) - -def hyfeature_hybrid_routing_preprocess( - connections, - param_df, - wbody_conn, - gages, - preprocessing_parameters, - compute_parameters, - waterbody_parameters, -): - ''' - Creation of routing network data objects. Logical ordering of lower-level - function calls that build individual network data objects. - - Arguments - --------- - supernetwork_parameters (dict): user input data re network extent - waterbody_parameters (dict): user input data re waterbodies - preprocessing_parameters (dict): user input data re preprocessing - compute_parameters (dict): user input data re compute configuration - data_assimilation_parameters (dict): user input data re data assimilation - - Returns - ------- - connections (dict of int: [int]): {segment id: [downsteram adjacent segment ids]} - param_df (DataFrame): Hydraulic geometry and roughness parameters, by segment - wbody_conn (dict of int: int): {segment id: associated lake id} - waterbodies_df (DataFrame): Waterbody (reservoir) parameters - waterbody_types_df (DataFrame): Waterbody type codes (1 - levelpool, 2 - USGS, 3 - USACE, 4 - RFC) - break_network_at_waterbodies (bool): If True, waterbodies occpy reaches of their own - waterbody_type_specified (bool): If True, more than just levelpool waterbodies exist - link_lake_crosswalk (dict of int: int): {lake id: outlet segment id} - independent_networks (dict of int: {int: [int]}): {tailwater id: {segment id: [upstream adjacent segment ids]}} - reaches_bytw (dict of int: [[int]]): {tailwater id: list or reach lists} - rconn (dict of int: [int]): {segment id: [upstream adjacent segment ids]} - pd.DataFrame.from_dict(gages) (DataFrame): Gage ids and corresponding segment ids at which they are located - diffusive_network_data (dict or None): Network data objects for diffusive domain - topobathy_df (DataFrame): Natural cross section data for diffusive domain - - Notes - ----- - - waterbody_type_specified is likely an excessive return and can be removed and inferred from the - contents of waterbody_types_df - - The values of the link_lake_crosswalk dictionary are the downstream-most segments within - the waterbody extent to which waterbody data are written. They are NOT the first segments - downsteram of the waterbody - ''' - - #============================================================================ - # Establish diffusive domain for MC/diffusive hybrid simulations - - hybrid_params = compute_parameters.get("hybrid_parameters", False) - if hybrid_params: - # switch parameters - # if run_hybrid = False, run MC only - # if run_hybrid = True, if use_topobathy = False, run MC+diffusive on RouteLink.nc - # " " " , if use_topobathy = True, if run_refactored_network = False, run MC+diffusive on original hydrofabric - # " " " , if use_topobathy = True, if run_refactored_network = True, run MC+diffusive on refactored hydrofabric - run_hybrid = hybrid_params.get('run_hybrid_routing', False) - use_topobathy = hybrid_params.get('use_natl_xsections', False) - run_refactored = hybrid_params.get('run_refactored_network', False) - - # file path parameters of non-refactored hydrofabric defined by RouteLink.nc - domain_file = hybrid_params.get("diffusive_domain", None) - topobathy_file = hybrid_params.get("topobathy_domain", None) - - # file path parameters of refactored hydrofabric for diffusive wave channel routing - refactored_domain_file = hybrid_params.get("refactored_domain", None) - refactored_topobathy_file = hybrid_params.get("refactored_topobathy_domain", None) - #------------------------------------------------------------------------- - # for non-refactored hydofabric defined by RouteLink.nc - # TODO: By default, make diffusive available for both non-refactored and refactored hydrofabric for now. Place a switch in the future. - if run_hybrid and domain_file: - - LOG.info('reading diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - diffusive_domain = nhd_io.read_diffusive_domain(domain_file) - - if use_topobathy and topobathy_file: - - LOG.debug('Natural cross section data on original hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - - # TODO: Request GID make comID variable an integer in their product, so - # we do not need to change variable types, here. - topobathy_df.index = topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - diffusive_network_data = {} - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - LOG.info('No diffusive domain file specified in configuration file. This is an MC-only simulation') - unrefactored_topobathy_df = pd.DataFrame() - #------------------------------------------------------------------------- - # for refactored hydofabric - if run_hybrid and run_refactored and refactored_domain_file: - - LOG.info('reading refactored diffusive domain extent for MC/Diffusive hybrid simulation') - - # read diffusive domain dictionary from yaml or json - refactored_diffusive_domain = nhd_io.read_diffusive_domain(refactored_domain_file) - - if use_topobathy and refactored_topobathy_file: - - LOG.debug('Natural cross section data of refactored hydrofabric are provided.') - - # read topobathy domain netcdf file, set index to 'comid' - # TODO: replace 'link' with a user-specified indexing variable name. - # ... if for whatever reason there is not a `link` variable in the - # ... dataframe returned from read_netcdf, then the code would break here. - topobathy_df = (nhd_io.read_netcdf(refactored_topobathy_file).set_index('link')) - - # unrefactored_topobaty_data is passed to diffusive kernel to provide thalweg elevation of unrefactored topobathy - # for crosswalking water elevations between non-refactored and refactored hydrofabrics. - unrefactored_topobathy_df = (nhd_io.read_netcdf(topobathy_file).set_index('link')) - unrefactored_topobathy_df.index = unrefactored_topobathy_df.index.astype(int) - - else: - topobathy_df = pd.DataFrame() - LOG.debug('No natural cross section topobathy data of refactored hydrofabric provided. Hybrid simualtion will run on compound trapezoidal geometry.') - - # initialize a dictionary to hold network data for each of the diffusive domains - refactored_diffusive_network_data = {} - - else: - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No refactored diffusive domain file specified in configuration file. This is an MC-only simulation') - - else: - diffusive_domain = None - diffusive_network_data = None - topobathy_df = pd.DataFrame() - unrefactored_topobathy_df = pd.DataFrame() - refactored_diffusive_domain = None - refactored_diffusive_network_data = None - refactored_reaches = {} - LOG.info('No hybrid parameters specified in configuration file. This is an MC-only simulation') - - #============================================================================ - # build diffusive domain data and edit MC domain data for hybrid simulation - - # - if diffusive_domain: - rconn_diff0 = nhd_network.reverse_network(connections) - refactored_reaches = {} - - for tw in diffusive_domain: - mainstem_segs = diffusive_domain[tw]['links'] - # we want mainstem_segs start at a mainstem link right after the upstream boundary mainstem link, which is - # in turn not under any waterbody. This boundary mainstem link should be turned into a tributary segment. - upstream_boundary_mainstem_link = diffusive_domain[tw]['upstream_boundary_link_mainstem'] - if upstream_boundary_mainstem_link[0] in mainstem_segs: - mainstem_segs.remove(upstream_boundary_mainstem_link[0]) - - # ===== build diffusive network data objects ==== - diffusive_network_data[tw] = {} - - # add diffusive domain segments - diffusive_network_data[tw]['mainstem_segs'] = mainstem_segs - - # diffusive domain tributary segments - trib_segs = [] - - for seg in mainstem_segs: - us_list = rconn_diff0[seg] - for u in us_list: - if u not in mainstem_segs: - trib_segs.append(u) - - diffusive_network_data[tw]['tributary_segments'] = trib_segs - # diffusive domain connections object - diffusive_network_data[tw]['connections'] = {k: connections[k] for k in (mainstem_segs + trib_segs)} - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, reaches, rconn_diff = nnu.organize_independent_networks( - diffusive_network_data[tw]['connections'], - set(trib_segs), - set(), - ) - - diffusive_network_data[tw]['rconn'] = rconn_diff - diffusive_network_data[tw]['reaches'] = reaches[tw] - - # RouteLink parameters - diffusive_network_data[tw]['param_df'] = param_df.filter( - (mainstem_segs + trib_segs), - axis = 0, - ) - diffusive_network_data[tw]['upstream_boundary_link'] = upstream_boundary_mainstem_link - - if refactored_diffusive_domain: - diffusive_parameters = {'geo_file_path': refactored_topobathy_file} - refactored_connections = nnu.build_refac_connections(diffusive_parameters) - - # list of stream segments of a single refactored diffusive domain - refac_tw = refactored_diffusive_domain[tw]['refac_tw'] - rlinks_tw = refactored_diffusive_domain[tw]['rlinks'] - refactored_connections_tw = {} - - # Subset a connection dictionary (upstream segment as key : downstream segments as values) from refactored_connections - # for a single refactored diffusive domain defined by a current tw. - for k in rlinks_tw: - if k in refactored_connections.keys() and k != refac_tw: - refactored_connections_tw[k] = refactored_connections[k] - - refactored_diffusive_network_data[refac_tw] = {} - refactored_diffusive_network_data[refac_tw]['tributary_segments'] = trib_segs - refactored_diffusive_network_data[refac_tw]['connections'] = refactored_connections_tw - - for k in trib_segs: - refactored_diffusive_network_data[refac_tw]['connections'][k]= [refactored_diffusive_domain[tw]['incoming_tribs'][k]] - - # diffusive domain reaches and upstream connections. - # break network at tributary segments - _, refactored_reaches_batch, refactored_conn_diff = nnu.organize_independent_networks( - refactored_diffusive_network_data[refac_tw]['connections'], - set(trib_segs), - set(), - ) - - refactored_reaches[refac_tw] = refactored_reaches_batch[refac_tw] - refactored_diffusive_network_data[refac_tw]['mainstem_segs'] = refactored_diffusive_domain[tw]['rlinks'] - refactored_diffusive_network_data[refac_tw]['upstream_boundary_link'] = diffusive_network_data[tw]['upstream_boundary_link'] - else: - refactored_reaches={} - - # ==== remove diffusive domain segs from MC domain ==== - # drop indices from param_df - param_df = param_df.drop(mainstem_segs) - - # remove keys from connections dictionary - for s in mainstem_segs: - connections.pop(s) - - # update downstream connections of trib segs - for us in trib_segs: - connections[us] = [] - - #============================================================================ - # Identify Independent Networks and Reaches by Network - LOG.info("organizing connections into reaches ...") - start_time = time.time() - gage_break_segments = set() - wbody_break_segments = set() - - break_network_at_waterbodies = waterbody_parameters.get( - "break_network_at_waterbodies", False - ) - - # if streamflow DA, then break network at gages - break_network_at_gages = False - - # temporary code to build diffusive_domain using given IDs of head segment and tailwater segment of mainstem. - ''' - headlink_mainstem = 242 - twlink_mainstem = 160 - uslink_mainstem = headlink_mainstem - dslink_mainstem = 1 # initial value - mainstem_list =[headlink_mainstem] - while dslink_mainstem != twlink_mainstem: - dslink_mainstem = connections[uslink_mainstem][0] - mainstem_list.append(dslink_mainstem) - uslink_mainstem = dslink_mainstem - diffusive_domain={} - diffusive_domain[twlink_mainstem] = mainstem_list - ''' - - if break_network_at_waterbodies: - wbody_break_segments = wbody_break_segments.union(wbody_conn.values()) - - if break_network_at_gages: - gage_break_segments = gage_break_segments.union(gages['gages'].keys()) - - independent_networks, reaches_bytw, rconn = nnu.organize_independent_networks( - connections, - wbody_break_segments, - gage_break_segments, - ) - - LOG.debug("reach organization complete in %s seconds." % (time.time() - start_time)) - # FIXME: Make this commented out alive - ''' - if preprocessing_parameters.get('preprocess_only', False): - - LOG.debug("saving preprocessed network data to disk for future use") - # todo: consider a better default than None - destination_folder = preprocessing_parameters.get('preprocess_output_folder', None) - if destination_folder: - - output_filename = preprocessing_parameters.get( - 'preprocess_output_filename', - 'preprocess_output' - ) - - outputs = {} - outputs.update( - {'connections': connections, - 'param_df': param_df, - 'wbody_conn': wbody_conn, - 'waterbodies_df': waterbodies_df, - 'waterbody_types_df': waterbody_types_df, - 'break_network_at_waterbodies': break_network_at_waterbodies, - 'waterbody_type_specified': waterbody_type_specified, - 'link_lake_crosswalk': link_lake_crosswalk, - 'independent_networks': independent_networks, - 'reaches_bytw': reaches_bytw, - 'rconn': rconn, - 'link_gage_df': link_gage_df, - 'usgs_lake_gage_crosswalk': usgs_lake_gage_crosswalk, - 'usace_lake_gage_crosswalk': usace_lake_gage_crosswalk, - 'diffusive_network_data': diffusive_network_data, - 'topobathy_data': topobathy_df, - } - ) - try: - np.save( - pathlib.Path(destination_folder).joinpath(output_filename), - outputs - ) - except: - LOG.critical('Canonot find %s. Aborting preprocessing routine' % pathlib.Path(destination_folder)) - quit() - - LOG.debug( - "writing preprocessed network data to %s"\ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - LOG.critical( - "Preprocessed network data written to %s aborting preprocessing sequence" \ - % pathlib.Path(destination_folder).joinpath(output_filename + '.npy')) - quit() - - else: - LOG.critical( - "No destination folder specified for preprocessing. Please specify preprocess_output_folder in configuration file. Aborting preprocessing routine" - ) - quit() - ''' - return(independent_networks, - reaches_bytw, - rconn, - diffusive_network_data, - topobathy_df, - refactored_diffusive_domain, - refactored_reaches, - unrefactored_topobathy_df, - ) - -def hyfeature_initial_warmstate_preprocess( - # break_network_at_waterbodies, - restart_parameters, - # data_assimilation_parameters, - segment_index, - # waterbodies_df, - # link_lake_crosswalk, -): - - ''' - Assemble model initial condition data: - - waterbody inital states (outflow and pool elevation) - - channel initial states (flow and depth) - - initial time - - Arguments - --------- - - break_network_at_waterbodies (bool): If True, waterbody initial states will - be appended to the waterbody parameter - dataframe. If False, waterbodies will - not be simulated and the waterbody - parameter datataframe wil not be changed - - restart_parameters (dict): User-input simulation restart - parameters - - data_assimilation_parameters (dict): User-input data assimilation - parameters - - segment_index (Pandas Index): All segment IDs in the simulation - doamin - - waterbodies_df (Pandas DataFrame): Waterbody parameters - - link_lake_crosswalk (dict): Crosswalking between lake ids and the link - id of the lake outlet segment - - Returns - ------- - - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial - states (outflow and pool elevation) - - q0 (Pandas DataFrame): Initial flow and depth states for each - segment in the model domain - - t0 (datetime): Datetime of the model initialization - - Notes - ----- - ''' - - #---------------------------------------------------------------------------- - # Assemble waterbody initial states (outflow and pool elevation - #---------------------------------------------------------------------------- - ''' - if break_network_at_waterbodies: - - start_time = time.time() - LOG.info("setting waterbody initial states ...") - - # if a lite restart file is provided, read initial states from it. - if restart_parameters.get("lite_waterbody_restart_file", None): - - waterbodies_initial_states_df, _ = nhd_io.read_lite_restart( - restart_parameters['lite_waterbody_restart_file'] - ) - - # read waterbody initial states from WRF-Hydro type restart file - elif restart_parameters.get("wrf_hydro_waterbody_restart_file", None): - waterbodies_initial_states_df = nhd_io.get_reservoir_restart_from_wrf_hydro( - restart_parameters["wrf_hydro_waterbody_restart_file"], - restart_parameters["wrf_hydro_waterbody_ID_crosswalk_file"], - restart_parameters.get("wrf_hydro_waterbody_ID_crosswalk_file_field_name", 'lake_id'), - restart_parameters["wrf_hydro_waterbody_crosswalk_filter_file"], - restart_parameters.get( - "wrf_hydro_waterbody_crosswalk_filter_file_field_name", - 'NHDWaterbodyComID' - ), - ) - - # if no restart file is provided, default initial states - else: - # TODO: Consider adding option to read cold state from route-link file - waterbodies_initial_ds_flow_const = 0.0 - waterbodies_initial_depth_const = -1e9 - # Set initial states from cold-state - waterbodies_initial_states_df = pd.DataFrame( - 0, - index=waterbodies_df.index, - columns=[ - "qd0", - "h0", - ], - dtype="float32", - ) - # TODO: This assignment could probably by done in the above call - waterbodies_initial_states_df["qd0"] = waterbodies_initial_ds_flow_const - waterbodies_initial_states_df["h0"] = waterbodies_initial_depth_const - waterbodies_initial_states_df["index"] = range( - len(waterbodies_initial_states_df) - ) - - waterbodies_df = pd.merge( - waterbodies_df, waterbodies_initial_states_df, on="lake_id" - ) - - LOG.debug( - "waterbody initial states complete in %s seconds."\ - % (time.time() - start_time)) - start_time = time.time() - ''' - - #---------------------------------------------------------------------------- - # Assemble channel initial states (flow and depth) - # also establish simulation initialization timestamp - #---------------------------------------------------------------------------- - start_time = time.time() - LOG.info("setting channel initial states ...") - - # if lite restart file is provided, the read channel initial states from it - if restart_parameters.get("lite_channel_restart_file", None): - # FIXME: Change it for hyfeature! - ''' - q0, t0 = nhd_io.read_lite_restart( - restart_parameters['lite_channel_restart_file'] - ) - t0_str = None - ''' - # when a restart file for hyfeature is provied, then read initial states from it. - elif restart_parameters.get("hyfeature_channel_restart_file", None): - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - channel_initial_states_file = restart_parameters["hyfeature_channel_restart_file"] - df = pd.read_csv(channel_initial_states_file) - t0_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") - t0 = datetime.strptime(t0_str,"%Y-%m-%d_%H:%M:%S") - - # build initial states from user-provided restart parameters - else: - # FIXME: Change it for hyfeature! - ''' - q0 = nnu.build_channel_initial_state(restart_parameters, segment_index) - - # get initialization time from restart file - if restart_parameters.get("wrf_hydro_channel_restart_file", None): - channel_initial_states_file = restart_parameters[ - "wrf_hydro_channel_restart_file" - ] - t0_str = nhd_io.get_param_str( - channel_initial_states_file, - "Restart_Time" - ) - else: - t0_str = "2015-08-16_00:00:00" - - # convert timestamp from string to datetime - t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") - ''' - # get initial time from user inputs - if restart_parameters.get("start_datetime", None): - t0_str = restart_parameters.get("start_datetime") - - def _try_parsing_date(text): - for fmt in ( - "%Y-%m-%d_%H:%M", - "%Y-%m-%d_%H:%M:%S", - "%Y-%m-%d %H:%M", - "%Y-%m-%d %H:%M:%S", - "%Y/%m/%d %H:%M", - "%Y/%m/%d %H:%M:%S" - ): - try: - return datetime.strptime(text, fmt) - except ValueError: - pass - LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') - quit() - - t0 = _try_parsing_date(t0_str) - else: - if t0_str == "2015-08-16_00:00:00": - LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') - else: - LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) - - LOG.debug( - "channel initial states complete in %s seconds."\ - % (time.time() - start_time) - ) - start_time = time.time() - - return ( - #waterbodies_df, - q0, - t0, - ) - # TODO: This returns a full dataframe (waterbodies_df) with the - # merged initial states for waterbodies, but only the - # initial state values (q0; not merged with the channel properties) - # for the channels -- - # That is because that is how they are used downstream. Need to - # trace that back and decide if there is one of those two ways - # that is optimal and make both returns that way. - -def hyfeature_forcing( - run, - forcing_parameters, - hybrid_parameters, - nexus_to_upstream_flowpath_dict, - segment_index, - cpu_pool, - t0, - coastal_boundary_depth_df, -): - """ - Assemble model forcings. Forcings include hydrological lateral inflows (qlats) - and coastal boundary depths for hybrid runs - - Aguments - -------- - - run (dict): List of forcing files pertaining to a - single run-set - - forcing_parameters (dict): User-input simulation forcing parameters - - hybrid_parameters (dict): User-input simulation hybrid parameters - - segment_index (Int64): Reach segment ids - - cpu_pool (int): Number of CPUs in the process-parallel pool - Returns - ------- - - qlats_df (Pandas DataFrame): Lateral inflow data, indexed by - segment ID - - coastal_bounary_depth_df (Pandas DataFrame): Coastal boundary water depths, - indexed by segment ID - - Notes - ----- - - """ - - # Unpack user-specified forcing parameters - dt = forcing_parameters.get("dt", None) - qts_subdivisions = forcing_parameters.get("qts_subdivisions", None) - nexus_input_folder = forcing_parameters.get("nexus_input_folder", None) - qlat_file_index_col = forcing_parameters.get("qlat_file_index_col", "feature_id") - qlat_file_value_col = forcing_parameters.get("qlat_file_value_col", "q_lateral") - qlat_file_gw_bucket_flux_col = forcing_parameters.get("qlat_file_gw_bucket_flux_col", "qBucket") - qlat_file_terrain_runoff_col = forcing_parameters.get("qlat_file_terrain_runoff_col", "qSfcLatRunoff") - - - # TODO: find a better way to deal with these defaults and overrides. - run["t0"] = run.get("t0", t0) - run["nts"] = run.get("nts") - run["dt"] = run.get("dt", dt) - run["qts_subdivisions"] = run.get("qts_subdivisions", qts_subdivisions) - run["nexus_input_folder"] = run.get("nexus_input_folder", nexus_input_folder) - run["qlat_file_index_col"] = run.get("qlat_file_index_col", qlat_file_index_col) - run["qlat_file_value_col"] = run.get("qlat_file_value_col", qlat_file_value_col) - run["qlat_file_gw_bucket_flux_col"] = run.get("qlat_file_gw_bucket_flux_col", qlat_file_gw_bucket_flux_col) - run["qlat_file_terrain_runoff_col"] = run.get("qlat_file_terrain_runoff_col", qlat_file_terrain_runoff_col) - - #--------------------------------------------------------------------------- - # Assemble lateral inflow data - #--------------------------------------------------------------------------- - - start_time = time.time() - LOG.info("Creating a DataFrame of lateral inflow forcings ...") - - # Place holder, if reading qlats from a file use this. - # TODO: add an option for reading qlat data from BMI/model engine - from_file = True - if from_file: - qlats_df = hnu.build_qlateral_array( - run, - cpu_pool, - nexus_to_upstream_flowpath_dict, - segment_index, - ) - - LOG.debug( - "lateral inflow DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - #--------------------------------------------------------------------- - # Assemble coastal coupling data [WIP] - #--------------------------------------------------------------------- - # Run if coastal_boundary_depth_df has not already been created: - if coastal_boundary_depth_df.empty: - coastal_boundary_elev_files = forcing_parameters.get('coastal_boundary_input_file', None) - coastal_boundary_domain_files = hybrid_parameters.get('coastal_boundary_domain', None) - - if coastal_boundary_elev_files: - start_time = time.time() - LOG.info("creating coastal dataframe ...") - - coastal_boundary_domain = nhd_io.read_coastal_boundary_domain(coastal_boundary_domain_files) - coastal_boundary_depth_df = nhd_io.build_coastal_ncdf_dataframe( - coastal_boundary_elev_files, - coastal_boundary_domain, - ) - - LOG.debug( - "coastal boundary elevation observation DataFrame creation complete in %s seconds." \ - % (time.time() - start_time) - ) - - return qlats_df, coastal_boundary_depth_df - -def read_ngen_waterbody_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): - """ - Reads .gpkg or lake.json file and prepares a dataframe, filtered - to the relevant reservoirs, to provide the parameters - for level-pool reservoir computation. - """ - def node_key_func(x): - return int(x[3:]) - if os.path.splitext(parm_file)[1]=='.gpkg': - df = gpd.read_file(parm_file, layer="lake_attributes").set_index('id') - elif os.path.splitext(parm_file)[1]=='.json': - df = pd.read_json(parm_file, orient="index") - - df.index = df.index.map(node_key_func) - df.index.name = lake_index_field - - if lake_id_mask: - df = df.loc[lake_id_mask] - return df - -def read_ngen_waterbody_type_df(parm_file, lake_index_field="wb-id", lake_id_mask=None): - """ - """ - #FIXME: this function is likely not correct. Unclear how we will get - # reservoir type from the gpkg files. Information should be in 'crosswalk' - # layer, but as of now (Nov 22, 2022) there doesn't seem to be a differentiation - # between USGS reservoirs, USACE reservoirs, or RFC reservoirs... - def node_key_func(x): - return int(x[3:]) - - if os.path.splitext(parm_file)[1]=='.gpkg': - df = gpd.read_file(parm_file, layer="crosswalk").set_index('id') - elif os.path.splitext(parm_file)[1]=='.json': - df = pd.read_json(parm_file, orient="index") - - df.index = df.index.map(node_key_func) - df.index.name = lake_index_field - if lake_id_mask: - df = df.loc[lake_id_mask] - - return df - -def read_geopkg(file_path): - flowpaths = gpd.read_file(file_path, layer="flowpaths") - attributes = gpd.read_file(file_path, layer="flowpath_attributes").drop('geometry', axis=1) - #merge all relevant data into a single dataframe - flowpaths = pd.merge(flowpaths, attributes, on='id') - - return flowpaths - -def read_json(file_path, edge_list): - dfs = [] - with open(edge_list) as edge_file: - edge_data = json.load(edge_file) - edge_map = {} - for id_dict in edge_data: - edge_map[ id_dict['id'] ] = id_dict['toid'] - with open(file_path) as data_file: - json_data = json.load(data_file) - for key_wb, value_params in json_data.items(): - df = pd.json_normalize(value_params) - df['id'] = key_wb - df['toid'] = edge_map[key_wb] - dfs.append(df) - df_main = pd.concat(dfs, ignore_index=True) - - return df_main - -def numeric_id(flowpath): - id = flowpath['id'].split('-')[-1] - toid = flowpath['toid'].split('-')[-1] - flowpath['id'] = int(id) - flowpath['toid'] = int(toid) - - return flowpath From ffa0f26f189abb5e14c4231bdc38be3140b093ce Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 27 Jan 2023 19:03:51 +0000 Subject: [PATCH 50/54] more updates to address comments above --- src/troute-network/troute/AbstractNetwork.py | 144 ++++++++---------- src/troute-network/troute/AbstractRouting.py | 6 +- .../troute/HYFeaturesNetwork.py | 7 - src/troute-network/troute/NHDNetwork.py | 8 - 4 files changed, 63 insertions(+), 102 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index a77053342..9941f39ef 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -48,13 +48,20 @@ def __init__(self,): dtype="float32", ) """ + break_network_at_waterbodies = self.waterbody_parameters.get("break_network_at_waterbodies", False) + streamflow_da = self.data_assimilation_parameters.get('streamflow_da', False) + break_network_at_gages = False + if streamflow_da: + break_network_at_gages = streamflow_da.get('streamflow_nudging', False) + self.break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, + "break_network_at_gages": break_network_at_gages} + self._break_segments = set() - - if self.break_points: - if self.break_points["break_network_at_waterbodies"]: - self._break_segments = self._break_segments | set(self.waterbody_connections.values()) - if self.break_points["break_network_at_gages"]: - self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) + + if self.break_points["break_network_at_waterbodies"]: + self._break_segments = self._break_segments | set(self.waterbody_connections.values()) + if self.break_points["break_network_at_gages"]: + self._break_segments = self._break_segments | set(self.gages.get('gages').keys()) self.initialize_routing_scheme() @@ -70,20 +77,11 @@ def assemble_forcings(self, run,): Aguments -------- - - run (dict): List of forcing files pertaining to a - single run-set - - forcing_parameters (dict): User-input simulation forcing parameters - - hybrid_parameters (dict): User-input simulation hybrid parameters - - supernetwork_parameters (dict): User-input simulation supernetwork parameters - - segment_index (Int64): Reach segment ids - - cpu_pool (int): Number of CPUs in the process-parallel pool + - run (dict): List of forcing files pertaining to a + single run-set Returns ------- - - qlats_df (Pandas DataFrame): Lateral inflow data, indexed by - segment ID - - coastal_bounary_depth_df (Pandas DataFrame): Coastal boundary water depths, - indexed by segment ID Notes ----- @@ -102,7 +100,6 @@ def assemble_forcings(self, run,): # TODO: find a better way to deal with these defaults and overrides. run["t0"] = run.get("t0", self.t0) - run["nts"] = run.get("nts") run["dt"] = run.get("dt", dt) run["qts_subdivisions"] = run.get("qts_subdivisions", qts_subdivisions) run["qlat_input_folder"] = run.get("qlat_input_folder", qlat_input_folder) @@ -513,24 +510,9 @@ def initial_warmstate_preprocess(self,): Arguments --------- - - break_network_at_waterbodies (bool): If True, waterbody initial states will - be appended to the waterbody parameter - dataframe. If False, waterbodies will - not be simulated and the waterbody - parameter datataframe wil not be changed - - restart_parameters (dict): User-input simulation restart - parameters - - segment_index (Pandas Index): All segment IDs in the simulation - doamin - - waterbodies_df (Pandas DataFrame): Waterbody parameters Returns ------- - - waterbodies_df (Pandas DataFrame): Waterbody parameters with initial - states (outflow and pool elevation) - - q0 (Pandas DataFrame): Initial flow and depth states for each - segment in the model domain - - t0 (datetime): Datetime of the model initialization Notes ----- @@ -603,72 +585,70 @@ def initial_warmstate_preprocess(self,): #---------------------------------------------------------------------------- # Assemble channel initial states (flow and depth) # also establish simulation initialization timestamp + # 3 Restart Options: + # 1. From t-route generated lite restart file (network agnostic) + # 2. From wrf_hydro_restart file (valid for NHDNetwork only) + # 3. Cold start, requires user specified start datetime #---------------------------------------------------------------------------- start_time = time.time() LOG.info("setting channel initial states ...") # if lite restart file is provided, the read channel initial states from it if restart_parameters.get("lite_channel_restart_file", None): - # FIXME: Change it for hyfeature! self._q0, self._t0 = nhd_io.read_lite_restart( restart_parameters['lite_channel_restart_file'] ) - t0_str = None - - # when a restart file for hyfeature is provied, then read initial states from it. - elif restart_parameters.get("hyfeature_channel_restart_file", None): - self._q0 = build_channel_initial_state(restart_parameters, self.segment_index) - channel_initial_states_file = restart_parameters["hyfeature_channel_restart_file"] - df = pd.read_csv(channel_initial_states_file) - t0_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") - self._t0 = datetime.strptime(t0_str,"%Y-%m-%d_%H:%M:%S") + + elif restart_parameters.get("wrf_hydro_channel_restart_file", None): + self._q0 = nhd_io.get_channel_restart_from_wrf_hydro( + restart_parameters["wrf_hydro_channel_restart_file"], + restart_parameters["wrf_hydro_channel_ID_crosswalk_file"], + restart_parameters.get("wrf_hydro_channel_ID_crosswalk_file_field_name", 'link'), + restart_parameters.get("wrf_hydro_channel_restart_upstream_flow_field_name", 'qlink1'), + restart_parameters.get("wrf_hydro_channel_restart_downstream_flow_field_name", 'qlink2'), + restart_parameters.get("wrf_hydro_channel_restart_depth_flow_field_name", 'hlink'), + ) - # build initial states from user-provided restart parameters - else: - # FIXME: Change it for hyfeature! - self._q0 = build_channel_initial_state(restart_parameters, self.segment_index) - - # get initialization time from restart file - if restart_parameters.get("wrf_hydro_channel_restart_file", None): - channel_initial_states_file = restart_parameters[ - "wrf_hydro_channel_restart_file" - ] - t0_str = nhd_io.get_param_str( - channel_initial_states_file, + t0_str = nhd_io.get_param_str( + restart_parameters["wrf_hydro_channel_restart_file"], "Restart_Time" ) - else: - t0_str = "2015-08-16_00:00:00" - + # convert timestamp from string to datetime self._t0 = datetime.strptime(t0_str, "%Y-%m-%d_%H:%M:%S") - - # get initial time from user inputs - if restart_parameters.get("start_datetime", None): - t0_str = restart_parameters.get("start_datetime") + + else: + # Set cold initial state + # assume to be zero + # 0, index=connections.keys(), columns=["qu0", "qd0", "h0",], dtype="float32" + self._q0 = pd.DataFrame( + 0, index=self.segment_index, columns=["qu0", "qd0", "h0"], dtype="float32", + ) - def _try_parsing_date(text): - for fmt in ( - "%Y-%m-%d_%H:%M", - "%Y-%m-%d_%H:%M:%S", - "%Y-%m-%d %H:%M", - "%Y-%m-%d %H:%M:%S", - "%Y/%m/%d %H:%M", - "%Y/%m/%d %H:%M:%S" - ): - try: - return datetime.strptime(text, fmt) - except ValueError: - pass - LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') - quit() + # get initial time from user inputs + if restart_parameters.get("start_datetime", None): + t0_str = restart_parameters.get("start_datetime") - self._t0 = _try_parsing_date(t0_str) - else: - if t0_str == "2015-08-16_00:00:00": - LOG.info('No user-input start_datetime and no restart file, start time arbitrarily 2015-08-16_00:00:00') + def _try_parsing_date(text): + for fmt in ( + "%Y-%m-%d_%H:%M", + "%Y-%m-%d_%H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%d %H:%M:%S", + "%Y/%m/%d %H:%M", + "%Y/%m/%d %H:%M:%S" + ): + try: + return datetime.strptime(text, fmt) + except ValueError: + pass + LOG.error('No valid date format found for start_datetime input. Please use format YYYY-MM-DD_HH:MM') + quit() + + self._t0 = _try_parsing_date(t0_str) + else: - LOG.info('No user-specified start_datetime, continuing with start time from restart file: %s', t0_str) + raise(RuntimeError("No start_datetime provided in config file for cold start.")) LOG.debug( "channel initial states complete in %s seconds."\ diff --git a/src/troute-network/troute/AbstractRouting.py b/src/troute-network/troute/AbstractRouting.py index ddeab6162..ab8310c45 100644 --- a/src/troute-network/troute/AbstractRouting.py +++ b/src/troute-network/troute/AbstractRouting.py @@ -107,7 +107,7 @@ def unrefactored_topobathy_df(self): class MCOnly(AbstractRouting): - def __init__(self, hybrid_params): + def __init__(self, _): self.hybrid_params = None super().__init__() @@ -220,22 +220,18 @@ def diffusive_network_data(self): @property def topobathy_df(self): - self._topobathy_df = pd.DataFrame() return self._topobathy_df @property def refactored_diffusive_domain(self): - self._refactored_diffusive_domain = None return self._refactored_diffusive_domain @property def refactored_reaches(self): - self._refactored_reaches = {} return self._refactored_reaches @property def unrefactored_topobathy_df(self): - self._unrefactored_topobathy_df = pd.DataFrame() return self._unrefactored_topobathy_df diff --git a/src/troute-network/troute/HYFeaturesNetwork.py b/src/troute-network/troute/HYFeaturesNetwork.py index f801ef1c9..2298d7c18 100644 --- a/src/troute-network/troute/HYFeaturesNetwork.py +++ b/src/troute-network/troute/HYFeaturesNetwork.py @@ -140,13 +140,6 @@ def __init__(self, if self.showtiming: print("... in %s seconds." % (time.time() - start_time)) - break_network_at_waterbodies = self.waterbody_parameters.get("break_network_at_waterbodies", False) - streamflow_da = self.data_assimilation_parameters.get('streamflow_da', False) - break_network_at_gages = False - if streamflow_da: - break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - self.break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, - "break_network_at_gages": break_network_at_gages} super().__init__() diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index f5f47289e..ceb1ec116 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -66,14 +66,6 @@ def __init__( if self.showtiming: print("... in %s seconds." % (time.time() - start_time)) - break_network_at_waterbodies = self.waterbody_parameters.get("break_network_at_waterbodies", False) - streamflow_da = self.data_assimilation_parameters.get('streamflow_da', False) - break_network_at_gages = False - if streamflow_da: - break_network_at_gages = streamflow_da.get('streamflow_nudging', False) - self.break_points = {"break_network_at_waterbodies": break_network_at_waterbodies, - "break_network_at_gages": break_network_at_gages} - self._flowpath_dict = {} super().__init__() From 25f8cffdfaa740e8ebabd9e2832965659189aef3 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 10 Feb 2023 15:26:05 +0000 Subject: [PATCH 51/54] removed loading nhd_preprocess.py as this file was removed --- src/troute-network/troute/NHDNetwork.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/troute-network/troute/NHDNetwork.py b/src/troute-network/troute/NHDNetwork.py index ceb1ec116..71864b1d0 100644 --- a/src/troute-network/troute/NHDNetwork.py +++ b/src/troute-network/troute/NHDNetwork.py @@ -1,6 +1,5 @@ from .AbstractNetwork import AbstractNetwork import troute.nhd_io as nhd_io -import troute.nhd_preprocess as nhd_prep import pandas as pd import numpy as np import time From 857bfd1f2a276c530576c37785f93c067f86b5fb Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 10 Feb 2023 16:32:48 +0000 Subject: [PATCH 52/54] reverse changes to compute_nhd_routing_v02 input arguments --- src/troute-nwm/src/nwm_routing/__main__.py | 95 ++++++++++++++++++-- src/troute-routing/troute/routing/compute.py | 72 +++++++-------- 2 files changed, 119 insertions(+), 48 deletions(-) diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index 15a357c98..b4f0d906b 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -170,18 +170,42 @@ def main_v04(argv): route_start_time = time.time() run_results = nwm_route( - network, - data_assimilation, + network.connections, + network.reverse_network, + network.waterbody_connections, + network.reaches_by_tailwater, parallel_compute_method, compute_kernel, subnetwork_target_size, cpu_pool, + network.t0, dt, nts, qts_subdivisions, + network.independent_networks, + network.dataframe, + network.q0, + network._qlateral, + data_assimilation.usgs_df, + data_assimilation.lastobs_df, + data_assimilation.reservoir_usgs_df, + data_assimilation.reservoir_usgs_param_df, + data_assimilation.reservoir_usace_df, + data_assimilation.reservoir_usace_param_df, + data_assimilation.assimilation_parameters, assume_short_ts, return_courant, + network.waterbody_dataframe, + waterbody_parameters, + network.waterbody_types_dataframe, + network.waterbody_type_specified, + network.diffusive_network_data, + network.topobathy_df, + network.refactored_diffusive_domain, + network.refactored_reaches, subnetwork_list, + network.coastal_boundary_depth_df, + network.unrefactored_topobathy_df, ) # returns list, first item is run result, second item is subnetwork items @@ -1007,18 +1031,42 @@ def _handle_args_v03(argv): return parser.parse_args(argv) def nwm_route( - network, - data_assimilation, + downstream_connections, + upstream_connections, + waterbodies_in_connections, + reaches_bytw, parallel_compute_method, compute_kernel, subnetwork_target_size, cpu_pool, + t0, dt, nts, qts_subdivisions, + independent_networks, + param_df, + q0, + qlats, + usgs_df, + lastobs_df, + reservoir_usgs_df, + reservoir_usgs_param_df, + reservoir_usace_df, + reservoir_usace_param_df, + da_parameter_dict, assume_short_ts, return_courant, + waterbodies_df, + waterbody_parameters, + waterbody_types_df, + waterbody_type_specified, + diffusive_network_data, + topobathy_df, + refactored_diffusive_domain, + refactored_reaches, subnetwork_list, + coastal_boundary_depth_df, + unrefactored_topobathy_df, ): ################### Main Execution Loop across ordered networks @@ -1035,17 +1083,35 @@ def nwm_route( start_time_mc = time.time() results = compute_nhd_routing_v02( - network, - data_assimilation, + downstream_connections, + upstream_connections, + waterbodies_in_connections, + reaches_bytw, compute_kernel, parallel_compute_method, subnetwork_target_size, # The default here might be the whole network or some percentage... cpu_pool, + t0, dt, nts, qts_subdivisions, + independent_networks, + param_df, + q0, + qlats, + usgs_df, + lastobs_df, + reservoir_usgs_df, + reservoir_usgs_param_df, + reservoir_usace_df, + reservoir_usace_param_df, + da_parameter_dict, assume_short_ts, return_courant, + waterbodies_df, + waterbody_parameters, + waterbody_types_df, + waterbody_type_specified, subnetwork_list, ) LOG.debug("MC computation complete in %s seconds." % (time.time() - start_time_mc)) @@ -1054,7 +1120,7 @@ def nwm_route( results = results[0] # run diffusive side of a hybrid simulation - if network.diffusive_network_data: + if diffusive_network_data: start_time_diff = time.time() ''' # retrieve MC-computed streamflow value at upstream boundary of diffusive mainstem @@ -1078,12 +1144,23 @@ def nwm_route( results.extend( compute_diffusive_routing( results, - network, - data_assimilation, + diffusive_network_data, cpu_pool, + t0, dt, nts, + q0, + qlats, qts_subdivisions, + usgs_df, + lastobs_df, + da_parameter_dict, + waterbodies_df, + topobathy_df, + refactored_diffusive_domain, + refactored_reaches, + coastal_boundary_depth_df, + unrefactored_topobathy_df, ) ) LOG.debug("Diffusive computation complete in %s seconds." % (time.time() - start_time_diff)) diff --git a/src/troute-routing/troute/routing/compute.py b/src/troute-routing/troute/routing/compute.py index f1130362f..a5673817c 100644 --- a/src/troute-routing/troute/routing/compute.py +++ b/src/troute-routing/troute/routing/compute.py @@ -218,41 +218,38 @@ def _prep_reservoir_da_dataframes(reservoir_usgs_df, reservoir_usgs_param_df, re return reservoir_usgs_df_sub, reservoir_usgs_df_time, reservoir_usgs_update_time, reservoir_usgs_prev_persisted_flow, reservoir_usgs_persistence_update_time, reservoir_usgs_persistence_index, reservoir_usace_df_sub, reservoir_usace_df_time, reservoir_usace_update_time, reservoir_usace_prev_persisted_flow, reservoir_usace_persistence_update_time, reservoir_usace_persistence_index, waterbody_types_df_sub def compute_nhd_routing_v02( - network, - data_assimilation, + connections, + rconn, + wbody_conn, + reaches_bytw, compute_func_name, parallel_compute_method, subnetwork_target_size, cpu_pool, + t0, dt, nts, qts_subdivisions, + independent_networks, + param_df, + q0, + qlats, + usgs_df, + lastobs_df, + reservoir_usgs_df, + reservoir_usgs_param_df, + reservoir_usace_df, + reservoir_usace_param_df, + da_parameter_dict, assume_short_ts, return_courant, + waterbodies_df, + waterbody_parameters, + waterbody_types_df, + waterbody_type_specified, subnetwork_list, ): - connections = network.connections - rconn = network.reverse_network - wbody_conn = network.waterbody_connections - reaches_bytw = network.reaches_by_tailwater - t0 = network.t0 - independent_networks = network.independent_networks - param_df = network.dataframe - q0 = network.q0 - qlats = network.qlateral - usgs_df = data_assimilation.usgs_df - lastobs_df = data_assimilation.lastobs_df - reservoir_usgs_df = data_assimilation.reservoir_usgs_df - reservoir_usgs_param_df = data_assimilation.reservoir_usgs_param_df - reservoir_usace_df = data_assimilation.reservoir_usace_df - reservoir_usace_param_df = data_assimilation.reservoir_usace_param_df - da_parameter_dict = data_assimilation.assimilation_parameters - waterbodies_df = network.waterbody_dataframe - waterbody_parameters = network.waterbody_parameters - waterbody_types_df = network.waterbody_types_dataframe - waterbody_type_specified = network.waterbody_type_specified - da_decay_coefficient = da_parameter_dict.get("da_decay_coefficient", 0) param_df["dt"] = dt param_df = param_df.astype("float32") @@ -1127,28 +1124,25 @@ def compute_nhd_routing_v02( def compute_diffusive_routing( results, - network, - data_assimilation, + diffusive_network_data, cpu_pool, + t0, dt, nts, + q0, + qlats, qts_subdivisions, + usgs_df, + lastobs_df, + da_parameter_dict, + waterbodies_df, + topobathy, + refactored_diffusive_domain, + refactored_reaches, + coastal_boundary_depth_df, + unrefactored_topobathy, ): - diffusive_network_data = network.diffusive_network_data - t0 = network.t0 - q0 = network.q0 - qlats = network.qlateral - usgs_df = data_assimilation.usgs_df - lastobs_df = data_assimilation.lastobs_df - da_parameter_dict = data_assimilation.assimilation_parameters - waterbodies_df = network.waterbody_dataframe - topobathy = network.topobathy_df - refactored_diffusive_domain = network.refactored_diffusive_domain - refactored_reaches = network.refactored_reaches - coastal_boundary_depth_df = network.coastal_boundary_depth_df - unrefactored_topobathy = network.unrefactored_topobathy_df - results_diffusive = [] for tw in diffusive_network_data: # <------- TODO - by-network parallel loop, here. trib_segs = None From 724b400381d3e9ebab410c078ce79a2c0b47dbee Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 10 Feb 2023 16:33:21 +0000 Subject: [PATCH 53/54] edit to work with redesigned initial_warmstate_preprocess function --- test/unit_test_hyfeature/unittest_hyfeature.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit_test_hyfeature/unittest_hyfeature.yaml b/test/unit_test_hyfeature/unittest_hyfeature.yaml index 5ecaeb3ca..1adcb0de8 100644 --- a/test/unit_test_hyfeature/unittest_hyfeature.yaml +++ b/test/unit_test_hyfeature/unittest_hyfeature.yaml @@ -45,7 +45,7 @@ compute_parameters: #wrf_hydro_waterbody_restart_file: restart/HYDRO_RST.2020-08-26_00:00_DOMAIN1 #wrf_hydro_waterbody_ID_crosswalk_file : domain/LAKEPARM_NWMv2.1.nc #wrf_hydro_waterbody_crosswalk_filter_file: domain/LAKEPARM_NWMv2.1.nc - hyfeature_channel_restart_file: restart/201512010000NEXOUT.csv + start_datetime: "2015-12-01_00:00:00" hybrid_parameters: run_hybrid_routing: True diffusive_domain : domain/coastal_domain.yaml From a2dca4621cca3671c68d79311b67e8b36252de98 Mon Sep 17 00:00:00 2001 From: Sean Horvath Date: Fri, 10 Feb 2023 18:37:26 +0000 Subject: [PATCH 54/54] move build_forcing_sets() to AbstractNetwork. abstractnetwork_preprocess.py is no longer needed --- src/troute-network/troute/AbstractNetwork.py | 206 ++++++++++++++++++- src/troute-nwm/src/nwm_routing/__main__.py | 18 +- 2 files changed, 206 insertions(+), 18 deletions(-) diff --git a/src/troute-network/troute/AbstractNetwork.py b/src/troute-network/troute/AbstractNetwork.py index 9941f39ef..3c9bcc7fd 100644 --- a/src/troute-network/troute/AbstractNetwork.py +++ b/src/troute-network/troute/AbstractNetwork.py @@ -1,14 +1,18 @@ from abc import ABC, abstractmethod from functools import partial import pandas as pd +import numpy as np from datetime import datetime, timedelta -from collections import defaultdict +import os +import pathlib import time import logging +import pyarrow as pa +import pyarrow.parquet as pq from troute.nhd_network import extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition -from troute.nhd_network_utilities_v02 import organize_independent_networks, build_channel_initial_state, build_refac_connections +from troute.nhd_network_utilities_v02 import organize_independent_networks import troute.nhd_io as nhd_io from .AbstractRouting import MCOnly, MCwithDiffusive, MCwithDiffusiveNatlXSectionNonRefactored, MCwithDiffusiveNatlXSectionRefactored @@ -654,3 +658,201 @@ def _try_parsing_date(text): "channel initial states complete in %s seconds."\ % (time.time() - start_time) ) + + def build_forcing_sets(self,): + + forcing_parameters = self.forcing_parameters + supernetwork_parameters = self.supernetwork_parameters + + run_sets = forcing_parameters.get("qlat_forcing_sets", None) + qlat_input_folder = forcing_parameters.get("qlat_input_folder", None) + nts = forcing_parameters.get("nts", None) + max_loop_size = forcing_parameters.get("max_loop_size", 12) + dt = forcing_parameters.get("dt", None) + + geo_file_type = supernetwork_parameters.get('geo_file_type') + + try: + qlat_input_folder = pathlib.Path(qlat_input_folder) + assert qlat_input_folder.is_dir() == True + except TypeError: + raise TypeError("Aborting simulation because no qlat_input_folder is specified in the forcing_parameters section of the .yaml control file.") from None + except AssertionError: + raise AssertionError("Aborting simulation because the qlat_input_folder:", qlat_input_folder,"does not exist. Please check the the nexus_input_folder variable is correctly entered in the .yaml control file") from None + + forcing_glob_filter = forcing_parameters.get("qlat_file_pattern_filter", "*.NEXOUT") + + if forcing_glob_filter=="nex-*": + print("Reformating qlat nexus files as hourly binary files...") + binary_folder = forcing_parameters.get('binary_nexus_file_folder', None) + qlat_files = qlat_input_folder.glob(forcing_glob_filter) + + #Check that directory/files specified will work + if not binary_folder: + raise(RuntimeError("No output binary qlat folder supplied in config")) + elif not os.path.exists(binary_folder): + raise(RuntimeError("Output binary qlat folder supplied in config does not exist")) + elif len(list(pathlib.Path(binary_folder).glob('*.parquet'))) != 0: + raise(RuntimeError("Output binary qlat folder supplied in config is not empty (already contains '.parquet' files)")) + + #Add tnx for backwards compatability + qlat_files_list = list(qlat_files) + list(qlat_input_folder.glob('tnx*.csv')) + #Convert files to binary hourly files, reset nexus input information + qlat_input_folder, forcing_glob_filter = nex_files_to_binary(qlat_files_list, binary_folder) + forcing_parameters["qlat_input_folder"] = qlat_input_folder + forcing_parameters["qlat_file_pattern_filter"] = forcing_glob_filter + + # TODO: Throw errors if insufficient input data are available + if run_sets: + #FIXME: Change it for hyfeature + ''' + # append final_timestamp variable to each set_list + qlat_input_folder = pathlib.Path(qlat_input_folder) + for (s, _) in enumerate(run_sets): + final_chrtout = qlat_input_folder.joinpath(run_sets[s]['qlat_files' + ][-1]) + final_timestamp_str = nhd_io.get_param_str(final_chrtout, + 'model_output_valid_time') + run_sets[s]['final_timestamp'] = \ + datetime.strptime(final_timestamp_str, '%Y-%m-%d_%H:%M:%S') + ''' + elif qlat_input_folder: + # Construct run_set dictionary from user-specified parameters + + # get the first and seconded files from an ordered list of all forcing files + qlat_input_folder = pathlib.Path(qlat_input_folder) + all_files = sorted(qlat_input_folder.glob(forcing_glob_filter)) + first_file = all_files[0] + second_file = all_files[1] + + # Deduce the timeinterval of the forcing data from the output timestamps of the first + # two ordered CHRTOUT files + if forcing_glob_filter=="*.CHRTOUT_DOMAIN1": + t1 = nhd_io.get_param_str(first_file, "model_output_valid_time") + t1 = datetime.strptime(t1, "%Y-%m-%d_%H:%M:%S") + t2 = nhd_io.get_param_str(second_file, "model_output_valid_time") + t2 = datetime.strptime(t2, "%Y-%m-%d_%H:%M:%S") + else: + df = read_file(first_file) + t1_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + t1 = datetime.strptime(t1_str,"%Y-%m-%d_%H:%M:%S") + df = read_file(second_file) + t2_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + t2 = datetime.strptime(t2_str,"%Y-%m-%d_%H:%M:%S") + + + dt_qlat_timedelta = t2 - t1 + dt_qlat = dt_qlat_timedelta.seconds + + # determine qts_subdivisions + qts_subdivisions = dt_qlat / dt + if dt_qlat % dt == 0: + qts_subdivisions = int(dt_qlat / dt) + # make sure that qts_subdivisions = dt_qlat / dt + forcing_parameters['qts_subdivisions']= qts_subdivisions + + # the number of files required for the simulation + nfiles = int(np.ceil(nts / qts_subdivisions)) + + # list of forcing file datetimes + #datetime_list = [t0 + dt_qlat_timedelta * (n + 1) for n in + # range(nfiles)] + # ** Correction ** Because qlat file at time t is constantly applied throughout [t, t+1], + # ** n + 1 should be replaced by n + datetime_list = [self.t0 + dt_qlat_timedelta * (n) for n in + range(nfiles)] + datetime_list_str = [datetime.strftime(d, '%Y%m%d%H%M') for d in + datetime_list] + + # list of forcing files + forcing_filename_list = [d_str + forcing_glob_filter[1:] for d_str in + datetime_list_str] + + # check that all forcing files exist + for f in forcing_filename_list: + try: + J = pathlib.Path(qlat_input_folder.joinpath(f)) + assert J.is_file() == True + except AssertionError: + raise AssertionError("Aborting simulation because forcing file", J, "cannot be not found.") from None + + # build run sets list + run_sets = [] + k = 0 + j = 0 + nts_accum = 0 + nts_last = 0 + while k < len(forcing_filename_list): + run_sets.append({}) + + if k + max_loop_size < len(forcing_filename_list): + run_sets[j]['qlat_files'] = forcing_filename_list[k:k + + max_loop_size] + else: + run_sets[j]['qlat_files'] = forcing_filename_list[k:] + + nts_accum += len(run_sets[j]['qlat_files']) * qts_subdivisions + if nts_accum <= nts: + run_sets[j]['nts'] = int(len(run_sets[j]['qlat_files']) + * qts_subdivisions) + else: + run_sets[j]['nts'] = int(nts - nts_last) + + final_qlat = qlat_input_folder.joinpath(run_sets[j]['qlat_files'][-1]) + if forcing_glob_filter=="*.CHRTOUT_DOMAIN1": + final_timestamp_str = nhd_io.get_param_str(final_qlat,'model_output_valid_time') + else: + df = read_file(final_qlat) + final_timestamp_str = pd.to_datetime(df.columns[1]).strftime("%Y-%m-%d_%H:%M:%S") + + run_sets[j]['final_timestamp'] = \ + datetime.strptime(final_timestamp_str, '%Y-%m-%d_%H:%M:%S') + + nts_last = nts_accum + k += max_loop_size + j += 1 + + return run_sets + +def nex_files_to_binary(nexus_files, binary_folder): + for f in nexus_files: + # read the csv file + df = pd.read_csv(f, usecols=[1,2], names=['Datetime','qlat']) + + # convert and reformat datetime column + df['Datetime']= pd.to_datetime(df['Datetime']).dt.strftime("%Y%m%d%H%M") + + # reformat the dataframe + df['feature_id'] = get_id_from_filename(f) + df = df.pivot(index="feature_id", columns="Datetime", values="qlat") + df.columns.name = None + + for col in df.columns: + table_new = pa.Table.from_pandas(df.loc[:, [col]]) + + if not os.path.exists(f'{binary_folder}/{col}NEXOUT.parquet'): + pq.write_table(table_new, f'{binary_folder}/{col}NEXOUT.parquet') + + else: + table_old = pq.read_table(f'{binary_folder}/{col}NEXOUT.parquet') + table = pa.concat_tables([table_old,table_new]) + pq.write_table(table, f'{binary_folder}/{col}NEXOUT.parquet') + + nexus_input_folder = binary_folder + forcing_glob_filter = '*NEXOUT.parquet' + + return nexus_input_folder, forcing_glob_filter + +def get_id_from_filename(file_name): + id = os.path.splitext(file_name)[0].split('-')[1].split('_')[0] + return int(id) + +def read_file(file_name): + extension = file_name.suffix + if extension=='.csv': + df = pd.read_csv(file_name) + elif extension=='.parquet': + df = pq.read_table(file_name).to_pandas().reset_index() + df.index.name = None + + return df \ No newline at end of file diff --git a/src/troute-nwm/src/nwm_routing/__main__.py b/src/troute-nwm/src/nwm_routing/__main__.py index b4f0d906b..6d646864d 100644 --- a/src/troute-nwm/src/nwm_routing/__main__.py +++ b/src/troute-nwm/src/nwm_routing/__main__.py @@ -4,7 +4,6 @@ import asyncio import logging from datetime import datetime, timedelta -from collections import defaultdict from pathlib import Path import concurrent.futures @@ -25,12 +24,9 @@ from .output import nwm_output_generator from .log_level_set import log_level_set from troute.routing.compute import compute_nhd_routing_v02, compute_diffusive_routing -import troute.nhd_network as nhd_network + import troute.nhd_io as nhd_io import troute.nhd_network_utilities_v02 as nnu -import troute.routing.diffusive_utils as diff_utils -import troute.hyfeature_network_utilities as hnu -import troute.abstractnetwork_preprocess as abs_prep LOG = logging.getLogger('') @@ -106,17 +102,7 @@ def main_v04(argv): task_times['network_creation_time'] = network_end_time - network_start_time # Create run_sets: sets of forcing files for each loop - run_sets = abs_prep.build_forcing_sets( - supernetwork_parameters, - forcing_parameters, - network.t0 - ) - ''' - if supernetwork_parameters["geo_file_type"] == 'NHDNetwork': - run_sets = nnu.build_forcing_sets(forcing_parameters, network.t0) - elif supernetwork_parameters["geo_file_type"] == 'HYFeaturesNetwork': - run_sets = hnu.build_forcing_sets(forcing_parameters, network.t0) - ''' + run_sets = network.build_forcing_sets() # Create da_sets: sets of TimeSlice files for each loop if "data_assimilation_parameters" in compute_parameters: