diff --git a/.gitignore b/.gitignore index d5a0a0d03..e5a182082 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,7 @@ bower_components/ .grunt/ src/vendor/ dist/ +data/ +output +test_compose.yaml +src/app/.ruff_cache diff --git a/build.sh b/build.sh new file mode 100644 index 000000000..d60aacf93 --- /dev/null +++ b/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +if ! docker login --username ${GH_USERNAME} --password ${GH_TOKEN} ghcr.io; then + echo "Error: Failed to login to ghcr.io. Please check your GH_USERNAME and GH_TOKEN env vars" + exit 1 +fi + +if ! docker build -t ghcr.io/NOAA-OWP/t-route/t-route-api:${TAG} -f Dockerfile.troute_api .; then + echo "Error: Failed to build Docker image. Please check your Dockerfile and build context." + exit 1 +fi + +if ! docker push ghcr.io/NOAA-OWP/t-route/t-route-api:${TAG}; then + echo "Error: Failed to push Docker image. Please check your TAG env var" + exit 1 +fi + +echo "Successfully built and pushed Docker image with tag: ${TAG}" diff --git a/doc/api/api_docs.md b/doc/api/api_docs.md new file mode 100644 index 000000000..051361905 --- /dev/null +++ b/doc/api/api_docs.md @@ -0,0 +1,102 @@ +# T-Route FastAPI + +The following doc is meant to explain the T-Route FastAPI implementation using docker compose and shared volumes. + +## Why an API? + +T-Route is used in many contexts for hydrological river routing: +- NGEN +- Scientific Python Projects +- Replace and Route (RnR) + +In the latest PR for RnR (https://github.com/NOAA-OWP/hydrovis/pull/865), there is an requirement to run T-Route as a service. This service requires an easy way to dynamically create config files, restart flow from Initial Conditions, and run T-Route. To satisfy this requirement, a FastAPI endpoint was created in `/src/app` along with code to dynamically create t-route endpoints. + +## Why use shared volumes? + +Since T-Route is running in a docker container, there has to be a connection between the directories on your machine and the directories within the container. We're sharing the following folders by default: +- `data/rfc_channel_forcings` + - For storing RnR RFC channel domain forcing files (T-Route inputs) +- `data/rfc_geopackage_data` + - For storing HYFeatures gpkg files + - Indexed by the NHD COMID, also called hf_id. Ex: 2930769 is the hf_id for the CAGM7 RFC forecast point. +- `data/troute_restart` + - For storing TRoute Restart files +- `data/troute_output` + - For outputting results from the T-Route container + +## Quickstart +1. From the Root directory, run: +```shell +docker compose --env-file ./compose.env up +``` + +This will start the T-Route container and run the API on localhost:8004. To view the API spec and swagger docs, visit localhost:8004/docs + +2. Submit a request +```shell +curl -X 'GET' \ + 'http://localhost:8004/api/v1/flow_routing/v4/?lid=CAGM7&feature_id=2930769&hy_id=1074884&initial_start=0&start_time=2024-08-24T00%3A00%3A00&num_forecast_days=5' \ + -H 'accept: application/json' +``` + +This curl command is pinging the flow_routing v4 endpoint `api/v1/flow_routing/v4/` with the following metadata: +``` +lid=CAGM7 +feature_id=2930769 +hy_id=1074884 +initial_start=0 +start_time=2024-08-24T00:00:00 +num_forecast_days=5 +``` + +which informs T-Route which location folder to look at, what feature ID to read a gpkg from, the HY feature_id where flow is starting, the initial start flow for flow restart, start-time of the routing run, and the number of days to forecast. + +You can also run the following args from the swagger endpoint: + +![alt text](swagger_endpoints.png) + +The result for a successful routing is a status 200: +```json +{ + "message": "T-Route run successfully", + "lid": "CAGM7", + "feature_id": "2930769" +} +``` + +and an internal 500 error if there is something wrong. + +## Building and pushing to a container registry + +To ensure Replace and Route is using the correct version of T-Route, it is recommended a docker container be built, and then pushed to a registry (Dockerhub, GitHub Container Registry, etc). To do this manually for the GitHub container registry, the following commands should be used within a terminal. + +```shell +docker login --username ${GH_USERNAME} --password ${GH_TOKEN} ghcr.io +``` +- This command will log the user into the GitHub container registry using their credentials + +```shell +docker build -t ghcr.io/NOAA-OWP/t-route/t-route-api:${TAG} -f docker/Dockerfile.troute_api +``` +- This command builds the T-Route API container using a defined version `${TAG}` + +```shell +docker push ghcr.io/NOAA-OWP/t-route/t-route-api:${TAG} +``` +- This commands pushes the built T-Route API container to the NOAA-OWP/t-route container registry + + +The following env variables are used: +- `${GH_USERNAME}` + - your github username +- `${GH_TOKEN}` + - your github access token +- `${TAG} ` + - the version tag + - ex: 0.0.2 + +If you want to build this off a forked version, change the container registry (`/NOAA-OWP/t-route/`) to your user accounts container registry. + +## Testing: + +See `test/api/README.md` for testing information diff --git a/doc/api/api_spec.png b/doc/api/api_spec.png new file mode 100644 index 000000000..dc13df869 Binary files /dev/null and b/doc/api/api_spec.png differ diff --git a/doc/api/swagger_endpoints.png b/doc/api/swagger_endpoints.png new file mode 100644 index 000000000..801f45592 Binary files /dev/null and b/doc/api/swagger_endpoints.png differ diff --git a/docker/Dockerfile.compose b/docker/Dockerfile.compose new file mode 100644 index 000000000..306f762aa --- /dev/null +++ b/docker/Dockerfile.compose @@ -0,0 +1,26 @@ +FROM rockylinux:9.2 as rocky-base +RUN yum install -y epel-release +RUN yum install -y netcdf netcdf-fortran netcdf-fortran-devel netcdf-openmpi + +RUN yum install -y git cmake python python-devel pip + +WORKDIR /t-route/ +COPY . /t-route/ + +RUN ln -s /usr/lib64/gfortran/modules/netcdf.mod /usr/include/openmpi-x86_64/netcdf.mod + +ENV PYTHONPATH=/t-route:$PYTHONPATH +RUN pip install uv==0.2.5 +RUN uv venv + +ENV VIRTUAL_ENV=/t-route/.venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +RUN uv pip install --no-cache-dir -r /t-route/requirements.txt + +RUN ./compiler.sh no-e + +RUN uv pip install --no-cache-dir -r /t-route/requirements-app.txt + +WORKDIR "/t-route/src/" +RUN mkdir -p /t-route/data/troute_restart/ diff --git a/docker/Dockerfile.troute_api b/docker/Dockerfile.troute_api new file mode 100644 index 000000000..2de584c30 --- /dev/null +++ b/docker/Dockerfile.troute_api @@ -0,0 +1,39 @@ +FROM rockylinux:9.2 as rocky-base + +RUN yum install -y epel-release +RUN yum install -y netcdf netcdf-fortran netcdf-fortran-devel netcdf-openmpi + +RUN yum install -y git cmake python python-devel pip + +WORKDIR "/t-route/" + +# Copy the contents of the parent directory (repository root) into the container +COPY . /t-route/ + +RUN ln -s /usr/lib64/gfortran/modules/netcdf.mod /usr/include/openmpi-x86_64/netcdf.mod + +ENV PYTHONPATH=/t-route:$PYTHONPATH +RUN pip install uv==0.2.5 +RUN uv venv + +ENV VIRTUAL_ENV=/t-route/.venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +RUN uv pip install --no-cache-dir -r /t-route/requirements.txt + +RUN ./compiler.sh no-e + +RUN uv pip install --no-cache-dir -r /t-route/requirements-app.txt + +WORKDIR "/t-route/src/" +RUN mkdir -p /t-route/data/troute_restart/ + +# Create volume mount points +RUN mkdir -p ${OUTPUT_VOLUME_TARGET} ${DATA_VOLUME_TARGET} ${CORE_VOLUME_TARGET} /t-route/test + +# Set the command to run the application +CMD sh -c ". /t-route/.venv/bin/activate && uvicorn app.main:app --host 0.0.0.0 --port ${PORT}" + +# Add healthcheck +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD curl --fail -I http://localhost:${PORT}/health || exit 1 diff --git a/docker/compose.yaml b/docker/compose.yaml new file mode 100644 index 000000000..3b805c001 --- /dev/null +++ b/docker/compose.yaml @@ -0,0 +1,27 @@ +services: + troute: + build: + context: .. + dockerfile: docker/Dockerfile.compose + ports: + - "${PORT}:${PORT}" + volumes: + - type: bind + source: ${OUTPUT_VOLUME_SOURCE} + target: ${OUTPUT_VOLUME_TARGET} + - type: bind + source: ${DATA_VOLUME_SOURCE} + target: ${DATA_VOLUME_TARGET} + - type: bind + source: ${CORE_VOLUME_SOURCE} + target: ${CORE_VOLUME_TARGET} + - type: bind + source: ${TEST_SOURCE} + target: ${TEST_TARGET} + command: sh -c ". /t-route/.venv/bin/activate && uvicorn app.main:app --host 0.0.0.0 --port ${PORT}" + healthcheck: + test: curl --fail -I http://localhost:${PORT}/health || exit 1 + interval: 30s + timeout: 5s + retries: 3 + start_period: 5s diff --git a/docker/rnr_compose.env b/docker/rnr_compose.env new file mode 100644 index 000000000..988d903e7 --- /dev/null +++ b/docker/rnr_compose.env @@ -0,0 +1,26 @@ +# Port mapping +#------------- +# The following port will be used for spinning up the API server + +PORT=8000 + +# Volume bindings +# --------------- +# The following variables are used in the compose.yaml file to define the shared volume mount with T-Route + +# For saving output from the container +OUTPUT_VOLUME_SOURCE=../data/troute_output +OUTPUT_VOLUME_TARGET=/t-route/output + +# For mounting the data directory +DATA_VOLUME_SOURCE=../data +DATA_VOLUME_TARGET=/t-route/data + +# For mounting all core files within T-Route (Used for sharing template config files) +CORE_VOLUME_SOURCE=../src/app/core +CORE_VOLUME_TARGET=/t-route/src/app/core + +# For uploading test data scripts +TEST_SOURCE=../test +TEST_TARGET=/t-route/test + diff --git a/docker/test_troute_api.env b/docker/test_troute_api.env new file mode 100644 index 000000000..3a333877d --- /dev/null +++ b/docker/test_troute_api.env @@ -0,0 +1,25 @@ +# Port mapping +#------------- +# The following port will be used for spinning up the API server + +PORT=8000 + +# Volume bindings +# --------------- +# The following variables are used in the compose.yaml file to define the shared volume mount with T-Route + +# For saving output from the container +OUTPUT_VOLUME_SOURCE=../test/api/data/troute_output +OUTPUT_VOLUME_TARGET=/t-route/output + +# For mounting the data directory +DATA_VOLUME_SOURCE=../test/api/data +DATA_VOLUME_TARGET=/t-route/data + +# For mounting all core files within T-Route (Used for sharing template config files) +CORE_VOLUME_SOURCE=../src/app/core +CORE_VOLUME_TARGET=/t-route/src/app/core + +# For uploading test data scripts +TEST_SOURCE=../test +TEST_TARGET=/t-route/test diff --git a/requirements-app.txt b/requirements-app.txt new file mode 100644 index 000000000..1668d40ca --- /dev/null +++ b/requirements-app.txt @@ -0,0 +1,3 @@ +fastapi==0.110.1 +uvicorn==0.30.1 +pydantic<=1.10 \ No newline at end of file diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/api/__init__.py b/src/app/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/api/router.py b/src/app/api/router.py new file mode 100644 index 000000000..07d00150e --- /dev/null +++ b/src/app/api/router.py @@ -0,0 +1,12 @@ +"""Author: Tadd Bindas""" + +from fastapi import APIRouter + +from app.api.routes import troute_v4 + +api_router = APIRouter() + +# The following "router" is an entrypoint used to build/call T-route to do hydrologic routing +api_router.include_router( + troute_v4.router, prefix="/flow_routing/v4", tags=["Troute v4 Flow Routing"] +) diff --git a/src/app/api/routes/__init__.py b/src/app/api/routes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/api/routes/troute_v4.py b/src/app/api/routes/troute_v4.py new file mode 100644 index 000000000..4b90622a1 --- /dev/null +++ b/src/app/api/routes/troute_v4.py @@ -0,0 +1,130 @@ +import json +from datetime import datetime +from pathlib import Path +from typing import Annotated + +import yaml +from fastapi import APIRouter, Depends, HTTPException +from nwm_routing import main_v04 as t_route +from pydantic import conint +from troute.config import Config + +from app.api.services.initialization import (create_initial_start_file, + create_params, edit_yaml) +from app.api.services.utils import update_test_paths_with_prefix +from app.core import get_settings +from app.core.settings import Settings +from app.schemas import HttpStatusCode, TestStatus, TRouteStatus + +router = APIRouter() + + +@router.get("/", response_model=TRouteStatus) +async def get_gauge_data( + lid: str, + feature_id: str, + hy_id: str, + initial_start: float, + start_time: datetime, + num_forecast_days: conint(ge=1, le=30), + settings: Annotated[Settings, Depends(get_settings)], +) -> TRouteStatus: + """An API call for running T-Route within the context of replace and route + + Parameters: + ---------- + lid: str + The Location of the RFC Point + feature_id: str + The COMID associated with the LID + hy_id: str + The HY_ID associated with the LID + initial_start: float + The initial start for T-Route + start_time: str + The start time for the forecast + num_forecast_days: int + The number of days in the forecast + + Returns: + ------- + TRouteOutput + A successful T-Route run + """ + base_config = settings.base_config + params = create_params( + lid, feature_id, hy_id, initial_start, start_time, num_forecast_days, settings + ) + restart_file = create_initial_start_file(params, settings) + yaml_file_path = edit_yaml(base_config, params, restart_file) + try: + t_route(["-f", yaml_file_path.__str__()]) + except Exception as e: + raise HTTPException( + status_code=500, + detail=str(e), + ) + + yaml_file_path.unlink() + + return TRouteStatus( + status_code=HttpStatusCode.CREATED, + message="T-Route run successfully", + lid=lid, + feature_id=feature_id, + ) + + +@router.post("/tests/LowerColorado", response_model=TestStatus) +async def run_lower_colorado_tests( + settings: Annotated[Settings, Depends(get_settings)], + config_path: str = "test/LowerColorado_TX_v4/test_AnA_V4_HYFeature.yaml", +) -> TRouteStatus: + """An API call for running the LowerColorado T-Route test using a predefined config file + + Parameters: + ---------- + config_path: str + Path to the YAML configuration file for the test + + Returns: + -------- + TRouteStatus + The status of the T-Route run + """ + base = "/t-route" + path_to_test_dir = Path(f"{base}/{config_path}").parent + yaml_path = Path(f"{base}/{config_path}") + + with open(yaml_path) as custom_file: + data = yaml.load(custom_file, Loader=yaml.SafeLoader) + + # Updating paths to work in docker + data = update_test_paths_with_prefix(data, path_to_test_dir, settings.lower_colorado_paths_to_update) + + troute_configuration = Config.with_strict_mode(**data) + + tmp_yaml = path_to_test_dir / "tmp.yaml" + + dict_ = json.loads(troute_configuration.json()) + + # converting timeslice back to string (Weird pydantic 1.10 workaround) + dict_["compute_parameters"]["restart_parameters"]["start_datetime"] = data["compute_parameters"]["restart_parameters"]["start_datetime"] + + with open(tmp_yaml, 'w') as file: + yaml.dump(dict_, file) + + try: + t_route(["-f", tmp_yaml.__str__()]) + return TestStatus( + status_code=HttpStatusCode.CREATED, + message="T-Route run successfully using defined configuration", + ) + except Exception as e: + raise HTTPException( + status_code=500, + detail=str(e), + ) + finally: + if tmp_yaml.exists(): + tmp_yaml.unlink() diff --git a/src/app/api/services/__init__.py b/src/app/api/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/api/services/initialization.py b/src/app/api/services/initialization.py new file mode 100644 index 000000000..e1217a93c --- /dev/null +++ b/src/app/api/services/initialization.py @@ -0,0 +1,166 @@ +"""Author: Tadd Bindas""" + +from datetime import datetime +from pathlib import Path +from typing import Dict + +import geopandas as gpd +import numpy as np +import pandas as pd +import yaml + +from app.core.settings import Settings + + +def edit_yaml(original_file: Path, params: Dict[str, str], restart_file: Path) -> Path: + """A function to dynamically edit the T-Route config + + Parameters: + ----------- + original_file: Path + The path to the base yaml config file + params: Dict[str, str] + The parameters that will be added to the base config file + restart_file: path + The location to the restart_file path + + Returns: + -------- + Path: + The path to the dynamically generated config + """ + tmp_yaml = original_file.with_name( + original_file.stem + "_tmp_" + params["lid"] + original_file.suffix + ) + with open(original_file, "r") as file: + data = yaml.safe_load(file) + + output_dir = Path( + data["output_parameters"]["stream_output"]["stream_output_directory"].format( + params["lid"] + ) + ) + output_dir.mkdir(exist_ok=True) + + data["network_topology_parameters"]["supernetwork_parameters"]["geo_file_path"] = ( + params["geo_file_path"] + ) + + data["compute_parameters"]["restart_parameters"]["start_datetime"] = params[ + "start_datetime" + ] + data["compute_parameters"]["restart_parameters"]["lite_channel_restart_file"] = ( + restart_file.__str__() + ) + data["compute_parameters"]["forcing_parameters"]["nts"] = params["nts"] + data["compute_parameters"]["forcing_parameters"]["qlat_input_folder"] = params[ + "qlat_input_folder" + ] + + data["output_parameters"]["stream_output"]["stream_output_directory"] = ( + output_dir.__str__() + ) + + with open(tmp_yaml, "w") as file: + yaml.dump(data, file) + + return tmp_yaml + + +def create_params( + lid: str, + feature_id: str, + hy_id: str, + initial_start: float, + start_time: str, + num_forecast_days: int, + settings: Settings, +) -> Dict[str, str]: + """Generating the parameters to be added to the T-Route config files + + Parameters: + ----------- + lid: str + The location ID for the RFC point + feature_id: str + The feature ID, or COMID/hf_id, from the RFC point + hy_id: str + The HYFeatures ID from the RFC point + initial_start: float + The initial starting flow (m3/s) for Routing + start_time: str + The starting time ("%Y-%m-%dT%H:%M:%S") for when routing begins + num_forecast_days: int + The number of days we are going to route + settings: Settings + The T-route BaseSettings + + Returns: + -------- + Dict[str, str]: + The parameters to be added to the t-route config file + """ + start_datetime = start_time.strftime("%Y-%m-%d_%H:%M") + + nts = 288 * num_forecast_days + + geo_file_path = settings.geofile_path.format(feature_id) + qlat_input_folder = settings.qlat_input_path.format(lid) + return { + "lid": lid, + "hy_id": hy_id, + "initial_start": initial_start, + "start_datetime": start_datetime, + "geo_file_path": geo_file_path, + "nts": nts, + "qlat_input_folder": qlat_input_folder, + } + + +def create_initial_start_file(params: Dict[str, str], settings: Settings) -> Path: + """Creating the initial start/restart files + + Parmeters: + ---------- + params: Dict[str, str] + The parameters from the API to be added to the t-route config file + settings: Settings + The T-route BaseSettings + + Returns: + -------- + Path: + The path to the t-route restart file + """ + start_datetime = datetime.strptime(params["start_datetime"], "%Y-%m-%d_%H:%M") + formatted_datetime = start_datetime.strftime("%Y-%m-%d_%H:%M") + + gdf = gpd.read_file(params["geo_file_path"], layer="network") + mask = gdf["divide_id"].isna() + keys = [int(val.split("-")[1]) for val in set(gdf[~mask]["divide_id"].values.tolist())] + + discharge_upstream = np.zeros([len(keys)]) + discharge_downstream = np.zeros([len(keys)]) + height = np.zeros([len(keys)]) + idx = keys.index(int(params["hy_id"])) + discharge_upstream[idx] = float(params["initial_start"]) + + time_array = np.array( + [pd.to_datetime(formatted_datetime, format="%Y-%m-%d_%H:%M")] * len(keys) + ) + + df = pd.DataFrame( + { + "time": time_array, + "key": np.array(keys), + "qu0": discharge_upstream, + "qd0": discharge_downstream, + "h0": height, + } + ) + df.set_index("key", inplace=True) + restart_path = Path(settings.restart_path.format(params["lid"])) + restart_path.mkdir(exist_ok=True) + restart_full_path = restart_path / settings.restart_file.format(formatted_datetime) + df.to_pickle(restart_full_path) + return restart_full_path diff --git a/src/app/api/services/utils.py b/src/app/api/services/utils.py new file mode 100644 index 000000000..740b8967e --- /dev/null +++ b/src/app/api/services/utils.py @@ -0,0 +1,33 @@ +from pathlib import Path +from typing import Dict, List + + +def update_test_paths_with_prefix(data: Dict[str, str], prefix: Path, paths_to_update: List[List[str]]) -> Dict[str, str]: + """Update specific paths inside of a config dictionary with the given prefix, if they exist. + + Parameters: + ----------- + data: Dict[str, str] + The data dictionary read from the yaml config + prefix: Path + The path prefix we want to append + paths_to_update: List[str] + The list of paths to update from the config + + Returns: + -------- + Dict[str, str] + The updated data dictionary + """ + + for keys in paths_to_update: + current = data + for key in keys[:-1]: + if key in current: + current = current[key] + else: + break + if keys[-1] in current: + current[keys[-1]] = (prefix / current[keys[-1]]).__str__() + + return data diff --git a/src/app/core/__init__.py b/src/app/core/__init__.py new file mode 100644 index 000000000..624b3fba3 --- /dev/null +++ b/src/app/core/__init__.py @@ -0,0 +1,12 @@ +from app.core.settings import Settings + + +def get_settings() -> Settings: + """Instantiating the Settings object for FastAPI + + Returns + ------- + Settings + The Settings config object + """ + return Settings() diff --git a/src/app/core/base_config.yaml b/src/app/core/base_config.yaml new file mode 100644 index 000000000..133052fc8 --- /dev/null +++ b/src/app/core/base_config.yaml @@ -0,0 +1,99 @@ +# $ python -m nwm_routing -f -V4 test_rfc.yaml +#-------------------------------------------------------------------------------- +log_parameters: + #---------- + showtiming: True + log_level : DEBUG +#-------------------------------------------------------------------------------- +network_topology_parameters: + #---------- + supernetwork_parameters: + #---------- + network_type: HYFeaturesNetwork + geo_file_path: ??? + columns: + key: 'id' + downstream: 'toid' + dx : 'length_m' + n : 'n' + ncc : 'nCC' + s0 : 'So' + bw : 'BtmWdth' + waterbody : 'rl_NHDWaterbodyComID' + gages : 'rl_gages' + tw : 'TopWdth' + twcc : 'TopWdthCC' + musk : 'MusK' + musx : 'MusX' + cs : 'ChSlp' + alt: 'alt' + mainstem: 'mainstem' + waterbody_parameters: + #---------- + break_network_at_waterbodies: True +#-------------------------------------------------------------------------------- +compute_parameters: + #---------- + parallel_compute_method: by-subnetwork-jit-clustered # serial + compute_kernel : V02-structured + assume_short_ts : True + subnetwork_target_size : 10000 + cpu_pool : 36 + restart_parameters: + #---------- + start_datetime : ??? + lite_channel_restart_file : #restart/RESTART.2020082600_DOMAIN1 + lite_waterbody_restart_file : #restart/waterbody_restart_202006011200 + 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 : /data/rfc_channel_forcings + qlat_file_pattern_filter : "*.CHRTOUT_DOMAIN1.csv" + coastal_boundary_input_file : #boundary_forcing + nts : 1440 # 288 for 1day; 2592 for 9 days; + max_loop_size : 24 # [hr] + data_assimilation_parameters: + #---------- + # usgs_timeslices_folder : usgs_timeslices/ + # usace_timeslices_folder : usace_timelices/ + streamflow_da: + #---------- + streamflow_nudging : False + diffusive_streamflow_nudging : False + lastobs_file : #lastobs/nudgingLastObs.2023-04-02_00:00:00.nc + reservoir_da: + #---------- + reservoir_persistence_da: + #---------- + reservoir_persistence_usgs : False + reservoir_persistence_usace : False + reservoir_rfc_da: + #---------- + reservoir_rfc_forecasts : False + reservoir_rfc_forecasts_time_series_path: rfc_timeseries/ + reservoir_rfc_forecasts_lookback_hours : 28 + reservoir_rfc_forecasts_offset_hours : 28 +#-------------------------------------------------------------------------------- +output_parameters: + #---------- +# test_output : output/lcr_flowveldepth.pkl +# lite_restart: +# #---------- +# lite_restart_output_directory: restart/ +# lakeout_output: lakeout/ + stream_output : + stream_output_directory: /t-route/output/{} + stream_output_time: 1 #[hr] + stream_output_type: '.nc' #please select only between netcdf '.nc' or '.csv' or '.pkl' + stream_output_internal_frequency: 60 #[min] it should be order of 5 minutes. For instance if you want to output every hour put 60 + \ No newline at end of file diff --git a/src/app/core/settings.py b/src/app/core/settings.py new file mode 100644 index 000000000..c8936ce3c --- /dev/null +++ b/src/app/core/settings.py @@ -0,0 +1,65 @@ +from pathlib import Path +from typing import List + +from pydantic import BaseSettings + + +class Settings(BaseSettings): + """ + Configuration settings for the application. + + This class uses Pydantic's BaseSettings to manage any variables, + allowing for easy integration with environment variables and + configuration files. + + Attributes + ---------- + api_v1_str : str + The base API string. + base_config: str + The path to the base_config file that we build custom config files from + project_name : str + The project's name for the OPENAPI spec + qlat_input_path: str + The path to the docker folders where rfc_channel_forcings live in the shared volume + restart_path: str + The path to where restart files will be saved to in the shared volume + restart_file: str + The regex string for finding restart files + geofile_path: str + The path to the docker folders where the geopackage is located in the shared volume + paths_to_update: str + The entries in a config that have relative paths. Used for changig in services/utils.py + """ + + api_v1_str: str = "/api/v1" + base_config: Path = "/t-route/src/app/core/base_config.yaml" + project_name: str = "T-Route" + qlat_input_path: str = "/t-route/data/rfc_channel_forcings/{}/" + restart_path: str = "/t-route/data/troute_restart/{}/" + restart_file: str = "HYDRO_RST_{}_DOMAIN1" + geofile_path: str = "/t-route/data/rfc_geopackage_data/{}/downstream.gpkg" + lower_colorado_paths_to_update: List[List[str]] = [ + ["network_topology_parameters", "supernetwork_parameters", "geo_file_path"], + ["compute_parameters", "hybrid_parameters", "diffusive_domain"], + ["compute_parameters", "hybrid_parameters", "topobathy_domain"], + ["compute_parameters", "hybrid_parameters", "coastal_boundary_domain"], + ["compute_parameters", "forcing_parameters", "qlat_input_folder"], + ["compute_parameters", "data_assimilation_parameters", "usace_timeslices_folder"], + ["compute_parameters", "data_assimilation_parameters", "usgs_timeslices_folder"], + ["compute_parameters", "data_assimilation_parameters", "canada_timeslices_folder"], + ["compute_parameters", "data_assimilation_parameters", "LakeOntario_outflow"], + ["compute_parameters", "data_assimilation_parameters", "reservoir_da", "reservoir_rfc_da", "reservoir_rfc_forecasts_time_series_path"] + ] + lower_colorado_paths_to_update: List[List[str]] = [ + ["network_topology_parameters", "supernetwork_parameters", "geo_file_path"], + ["compute_parameters", "hybrid_parameters", "diffusive_domain"], + ["compute_parameters", "hybrid_parameters", "topobathy_domain"], + ["compute_parameters", "hybrid_parameters", "coastal_boundary_domain"], + ["compute_parameters", "forcing_parameters", "qlat_input_folder"], + ["compute_parameters", "data_assimilation_parameters", "usace_timeslices_folder"], + ["compute_parameters", "data_assimilation_parameters", "usgs_timeslices_folder"], + ["compute_parameters", "data_assimilation_parameters", "canada_timeslices_folder"], + ["compute_parameters", "data_assimilation_parameters", "LakeOntario_outflow"], + ["compute_parameters", "data_assimilation_parameters", "reservoir_da", "reservoir_rfc_da", "reservoir_rfc_forecasts_time_series_path"] + ] diff --git a/src/app/main.py b/src/app/main.py new file mode 100644 index 000000000..e6ec5ad1d --- /dev/null +++ b/src/app/main.py @@ -0,0 +1,20 @@ +"""Author: Tadd Bindas""" + +from fastapi import FastAPI, status +from fastapi.responses import Response + +from app.api.router import api_router +from app.core.settings import Settings + +settings = Settings() + +app = FastAPI( + title=settings.project_name, +) + +app.include_router(api_router, prefix=settings.api_v1_str) + + +@app.head("/health") +async def health_check(): + return Response(status_code=status.HTTP_200_OK) diff --git a/src/app/schemas.py b/src/app/schemas.py new file mode 100644 index 000000000..05b5ed77c --- /dev/null +++ b/src/app/schemas.py @@ -0,0 +1,48 @@ +from enum import IntEnum + +from pydantic import BaseModel + + +class HttpStatusCode(IntEnum): + OK = 200 + CREATED = 201 + ACCEPTED = 202 + BAD_REQUEST = 400 + UNAUTHORIZED = 401 + FORBIDDEN = 403 + NOT_FOUND = 404 + INTERNAL_SERVER_ERROR = 500 + + +class TRouteStatus(BaseModel): + """A schema to define successful t-route output + + Attributes: + ----------- + status_code: HttpStatusCode + The HTTP status code output from the code run + message: str + The output message from T-Route + lid : str + The location ID belonging to the point being routed + feature_id : str + The COMID, or hf_id, belonging to the point being routed + """ + status_code: HttpStatusCode + message: str + lid: str + feature_id: str + + +class TestStatus(BaseModel): + """A schema for output from t-route test cases + + Attributes: + ----------- + status_code: HttpStatusCode + The HTTP status code output from the code run + message: str + The output message from T-Route + """ + status_code: HttpStatusCode + message: str diff --git a/src/troute-nwm/src/nwm_routing/__init__.py b/src/troute-nwm/src/nwm_routing/__init__.py index e69de29bb..8cc8a247e 100644 --- a/src/troute-nwm/src/nwm_routing/__init__.py +++ b/src/troute-nwm/src/nwm_routing/__init__.py @@ -0,0 +1 @@ +from .__main__ import main_v04 diff --git a/test/api/README.md b/test/api/README.md new file mode 100644 index 000000000..eae48f709 --- /dev/null +++ b/test/api/README.md @@ -0,0 +1,54 @@ +## Testing: + +### Set up Using a Dockerfile: + +To build the T-Route api from a dockerfile, you can use the following commands from the t-route root repo: + +```shell +docker build -t troute_api -f docker/Dockerfile.troute_api . +``` + +```shell +docker run -p 8000:8000 \ +--env-file docker/test_troute_api.env \ +-v ${OUTPUT_VOLUME_SOURCE}:${OUTPUT_VOLUME_TARGET} \ +-v ${DATA_VOLUME_SOURCE}:${DATA_VOLUME_TARGET} \ +-v ${CORE_VOLUME_SOURCE}:${CORE_VOLUME_TARGET} \ +-v ${TEST_SOURCE}:${TEST_TARGET} troute_api +``` + +### Set up Docker Compose: + +Docker Compose uses a YAML file to configure docker containers and execution. To install compose, you can follow the examples on docker's docs: https://docs.docker.com/compose/install/linux/ + +To run compose, you can use the following command from the root directory: + +```shell +docker compose --env-file docker/test_troute_api.env -f docker/compose.yaml up --build +``` + +if you want to build a compose container to mimic what is used in RnR, you can run the following steps +```shell +docker compose --env-file docker/rnr_compose.env -f docker/compose.yaml up --build +``` + +#### Testing the RnR endpoint in the API: +The following folder contains data files that are to be used to test the T-Route FastAPI code within src/app + +1. Follow the steps above to build/run either the docker container, or docker compose +2. visit `localhost:8000/docs` in your browser +3. Enter the following parameters into the `/api/v1/flow_routing/v4` endpoint +- lid=CAGM7 +- feature_id=2930769 +- hy_id=1074884 +- initial_start=0 +- start_time=2024-08-24T00:00:00 +- num_forecast_days=5 +4. Click execute +5. A Status 201 code means the run ran, and test/api/data/troute_output will be populated in the `{lid}/` folder + +#### Testing the LowerColorado test cases through docker compose: +1. Follow the steps above to build/run either the docker container, or docker compose +2. visit `localhost:8000/docs` in your browser +3. Execute the `/api/v1/flow_routing/v4/tests/LowerColorado` endpoint using the default parameter file path for LowerColorado_TX_v4 +4. A Status 201 code means the run ran, and the defined yaml output will be populated diff --git a/test/api/data/rfc_channel_forcings/.gitkeep b/test/api/data/rfc_channel_forcings/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408211800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408211800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..00a89e76d --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408211800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408211800 +1074884,2690.10042624 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408220000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408220000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..4adef092e --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408220000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408220000 +1074884,2690.10042624 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408220600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408220600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..706aecce3 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408220600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408220600 +1074884,2650.4568410112 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408221200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408221200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..b47173bb2 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408221200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408221200 +1074884,2610.8132557824 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408221800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408221800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..070cb9cc5 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408221800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408221800 +1074884,2571.1696705536 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408230000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408230000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..55dfe4ab6 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408230000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408230000 +1074884,2531.5260853248 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408230600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408230600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..0ddfbecdc --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408230600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408230600 +1074884,2491.882500096 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408231200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408231200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..21aa12c0c --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408231200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408231200 +1074884,2452.2389148672 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408231800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408231800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..b232664d8 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408231800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408231800 +1074884,2412.5953296384 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408240000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408240000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..f65b37e9a --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408240000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408240000 +1074884,2372.9517444096 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408240600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408240600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..4eb4f1766 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408240600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408240600 +1074884,2372.9517444096 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408241200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408241200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..397446dc1 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408241200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408241200 +1074884,2333.3081591808 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408241800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408241800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..4dcfd5d83 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408241800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408241800 +1074884,2333.3081591808 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408250000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408250000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..ae30d271b --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408250000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408250000 +1074884,2293.664573952 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408250600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408250600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..1510b52dc --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408250600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408250600 +1074884,2293.664573952 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408251200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408251200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..5ec0a0d8a --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408251200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408251200 +1074884,2293.664573952 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408251800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408251800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..ab9779e61 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408251800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408251800 +1074884,2259.6843580416003 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408260000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408260000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..f0d04014a --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408260000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408260000 +1074884,2259.6843580416003 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408260600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408260600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..f067169c5 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408260600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408260600 +1074884,2225.7041421312 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408261200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408261200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..da60cbe56 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408261200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408261200 +1074884,2225.7041421312 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408261800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408261800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..c8391c1dd --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408261800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408261800 +1074884,2191.7239262208 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408270000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408270000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..bf3bb045a --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408270000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408270000 +1074884,2191.7239262208 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408270600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408270600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..0dacb875b --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408270600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408270600 +1074884,2157.7437103104003 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408271200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408271200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..d8ce46a71 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408271200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408271200 +1074884,2157.7437103104003 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408271800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408271800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..6ed3f74ae --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408271800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408271800 +1074884,2123.7634944 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408280000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408280000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..72307873f --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408280000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408280000 +1074884,2123.7634944 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408280600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408280600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..ec02da818 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408280600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408280600 +1074884,2123.7634944 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408281200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408281200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..72921fb22 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408281200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408281200 +1074884,2089.7832784896 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408281800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408281800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..cf1bc3ba3 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408281800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408281800 +1074884,2089.7832784896 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408290000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408290000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..9994d9205 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408290000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408290000 +1074884,2089.7832784896 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408290600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408290600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..bed96c567 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408290600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408290600 +1074884,2089.7832784896 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408291200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408291200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..5ae695b21 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408291200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408291200 +1074884,2089.7832784896 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408291800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408291800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..4c34835d7 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408291800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408291800 +1074884,2089.7832784896 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408300000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408300000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..32fb6e794 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408300000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408300000 +1074884,2055.8030625792 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408300600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408300600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..1f29d8fcf --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408300600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408300600 +1074884,2055.8030625792 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408301200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408301200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..83a148cce --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408301200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408301200 +1074884,2055.8030625792 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408301800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408301800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..b836ef168 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408301800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408301800 +1074884,2055.8030625792 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408310000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408310000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..ec125eb0c --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408310000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408310000 +1074884,2055.8030625792 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408310600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408310600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..82218e629 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408310600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408310600 +1074884,2021.8228466688001 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408311200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408311200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..127e1b7ed --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408311200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408311200 +1074884,2021.8228466688001 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202408311800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202408311800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..a93592e6c --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202408311800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202408311800 +1074884,2021.8228466688001 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409010000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409010000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..226ecf399 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409010000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409010000 +1074884,1987.8426307584 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409010600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409010600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..85c14a02b --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409010600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409010600 +1074884,1987.8426307584 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409011200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409011200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..7604a6216 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409011200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409011200 +1074884,1953.862414848 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409011800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409011800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..09523ec7d --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409011800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409011800 +1074884,1953.862414848 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409020000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409020000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..8df654491 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409020000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409020000 +1074884,1919.8821989376002 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409020600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409020600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..10d6ddeff --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409020600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409020600 +1074884,1919.8821989376002 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409021200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409021200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..6cf30802b --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409021200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409021200 +1074884,1885.9019830272 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409021800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409021800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..82ee2c6db --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409021800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409021800 +1074884,1885.9019830272 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409030000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409030000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..6ef1c73c7 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409030000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409030000 +1074884,1851.9217671168003 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409030600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409030600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..758340e11 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409030600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409030600 +1074884,1817.9415512064002 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409031200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409031200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..37bb4fa02 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409031200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409031200 +1074884,1817.9415512064002 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409031800.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409031800.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..c2d66be52 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409031800.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409031800 +1074884,1783.961335296 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409040000.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409040000.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..67fbd7975 --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409040000.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409040000 +1074884,1752.8128040448 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409040600.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409040600.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..91ac0c4cc --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409040600.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409040600 +1074884,1721.6642727936 diff --git a/test/api/data/rfc_channel_forcings/CAGM7/202409041200.CHRTOUT_DOMAIN1.csv b/test/api/data/rfc_channel_forcings/CAGM7/202409041200.CHRTOUT_DOMAIN1.csv new file mode 100644 index 000000000..599a35c7e --- /dev/null +++ b/test/api/data/rfc_channel_forcings/CAGM7/202409041200.CHRTOUT_DOMAIN1.csv @@ -0,0 +1,2 @@ +feature_id,202409041200 +1074884,1721.6642727936 diff --git a/test/api/data/rfc_geopackage_data/.gitkeep b/test/api/data/rfc_geopackage_data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test/api/data/rfc_geopackage_data/2930769/downstream.gpkg b/test/api/data/rfc_geopackage_data/2930769/downstream.gpkg new file mode 100644 index 000000000..625087207 Binary files /dev/null and b/test/api/data/rfc_geopackage_data/2930769/downstream.gpkg differ diff --git a/test/api/data/rfc_geopackage_data/2930769/subset.gpkg b/test/api/data/rfc_geopackage_data/2930769/subset.gpkg new file mode 100644 index 000000000..ec15f4b67 Binary files /dev/null and b/test/api/data/rfc_geopackage_data/2930769/subset.gpkg differ diff --git a/test/api/data/troute_output/.gitkeep b/test/api/data/troute_output/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test/api/data/troute_restart/.gitkeep b/test/api/data/troute_restart/.gitkeep new file mode 100644 index 000000000..e69de29bb