|
1 | | -import argparse |
2 | | -import json |
3 | | -from datetime import datetime |
| 1 | +import logging |
4 | 2 | from pathlib import Path |
| 3 | +import time |
| 4 | +from typing import Literal |
| 5 | + |
| 6 | +import click |
5 | 7 |
|
6 | | -from .constants import PERCENTILES_URL, PERCENTILES_PATH, HYDROGRAPHS, SECONDS_PER_DAY |
7 | | -from .downscaling_prediction import downscale_boston_cesm |
8 | | -from .hydrological_prediction import calculate_discharge_from_precipitation |
9 | | -from .hydrodynamic_prediction import generate_flood_from_discharge |
10 | 8 | from .animate_results import animate as animate_results |
| 9 | +from .run import run_sim |
11 | 10 | from .save_results import write_multiframe_geotiff |
12 | | -from .utils import download_file |
13 | | - |
14 | 11 |
|
15 | | -def run_end_to_end( |
16 | | - time_period: str, # Two-decade future period whose climate we are interested in. |
17 | | - annual_probability: float, # Annual probability of a 1-day extreme precipitation event happening. |
18 | | - hydrograph: list[float], # List of 24 floats where each is a proportion of flood volume passing through in one hour. |
19 | | - potential_evapotranspiration: float, # This function takes PET in physical units, but the user never inputs those directly. |
20 | | - soil_moisture: float, # This function takes PET in physical units, but the user never inputs those directly. |
21 | | - ground_water: float, # This function takes PET in physical units, but the user never inputs those directly. |
22 | | - output_path: str | None, |
23 | | - animate: bool, |
24 | | - tiff_writer: str, |
25 | | -): |
26 | | - print(( |
| 12 | +logger = logging.getLogger('uvdat_flood_sim') |
| 13 | + |
| 14 | + |
| 15 | +def _ensure_dir_exists(ctx, param, value): |
| 16 | + value.mkdir(parents=True, exist_ok=True) |
| 17 | + return value |
| 18 | + |
| 19 | + |
| 20 | +@click.command(name='Dynamic Flood Simulation') |
| 21 | +@click.option( |
| 22 | + '--time-period', '-t', |
| 23 | + type=click.Choice(['2031-2050', '2041-2060']), |
| 24 | + default='2031-2050', |
| 25 | + help='The 20 year time period in which to predict a flood' |
| 26 | +) |
| 27 | +@click.option( |
| 28 | + '--annual-probability', '-p', |
| 29 | + type=click.FloatRange(min=0, min_open=True, max=1, max_open=True), |
| 30 | + default=0.04, |
| 31 | + help='The probability that a flood of this magnitude will occur in any given year' |
| 32 | +) |
| 33 | +@click.option( |
| 34 | + '--hydrograph-name', '-n', |
| 35 | + type=click.Choice(['short_charles', 'long_charles']), |
| 36 | + default='short_charles', |
| 37 | + help=( |
| 38 | + 'A selection of a 24-hour hydrograph. ' |
| 39 | + '"short_charles" represents a hydrograph for the main river and ' |
| 40 | + '"long_charles" represents a hydrograph for the main river plus additional upstream water sources.' |
| 41 | + ) |
| 42 | +) |
| 43 | +@click.option( |
| 44 | + '--hydrograph', '-g', |
| 45 | + type=float, |
| 46 | + nargs=24, |
| 47 | + help='A hydrograph expressed as a list of numeric values where each value represents a proportion of total discharge' |
| 48 | +) |
| 49 | +@click.option( |
| 50 | + '--pet-percentile', '-e', |
| 51 | + type=click.IntRange(min=0, max=100), |
| 52 | + default=25, |
| 53 | + help='Potential evapotranspiration percentile' |
| 54 | +) |
| 55 | +@click.option( |
| 56 | + '--sm-percentile', '-s', |
| 57 | + type=click.IntRange(min=0, max=100), |
| 58 | + default=25, |
| 59 | + help='Soil moisture percentile' |
| 60 | +) |
| 61 | +@click.option( |
| 62 | + '--gw-percentile', '-w', |
| 63 | + type=click.IntRange(min=0, max=100), |
| 64 | + default=25, |
| 65 | + help='Ground water percentile' |
| 66 | +) |
| 67 | +@click.option( |
| 68 | + '--output-path', '-o', |
| 69 | + type=click.Path(writable=True, file_okay=False, path_type=Path), |
| 70 | + default=Path.cwd() / 'outputs', |
| 71 | + callback=_ensure_dir_exists, |
| 72 | + help='Directory to write the flood simulation outputs' |
| 73 | +) |
| 74 | +@click.option( |
| 75 | + '--animation/--no-animation', |
| 76 | + default=True, |
| 77 | + help='Display result animation via matplotlib' |
| 78 | +) |
| 79 | +@click.option( |
| 80 | + '--tiff-writer', |
| 81 | + type=click.Choice(['rasterio', 'large_image']), |
| 82 | + default='rasterio', |
| 83 | + help='Library to use for writing result tiff' |
| 84 | +) |
| 85 | +def main( |
| 86 | + time_period: Literal['2031-2050', '2041-2060'], |
| 87 | + annual_probability: float, |
| 88 | + hydrograph_name: Literal['short_charles', 'long_charles'], |
| 89 | + hydrograph: tuple[float, ...], |
| 90 | + pet_percentile: int, |
| 91 | + sm_percentile: int, |
| 92 | + gw_percentile: int, |
| 93 | + output_path: Path, |
| 94 | + animation: bool, |
| 95 | + tiff_writer: Literal['rasterio', 'large_image'], |
| 96 | +) -> None: |
| 97 | + logging.basicConfig(level=logging.INFO) |
| 98 | + |
| 99 | + logger.info(( |
27 | 100 | f'Inputs: {time_period=}, {annual_probability=}, {hydrograph=}, ' |
28 | | - f'{potential_evapotranspiration=}, {soil_moisture=}, {ground_water=}, ' |
29 | | - f'{output_path=}, {animate=}' |
| 101 | + f'{pet_percentile=}, {sm_percentile=}, {gw_percentile=}, ' |
| 102 | + f'{output_path=}, {animation=}' |
30 | 103 | )) |
31 | | - start = datetime.now() |
32 | | - |
33 | | - # Obtain extreme precipitation level |
34 | | - level = downscale_boston_cesm(time_period, annual_probability) |
35 | | - print(f'Downscaling prediction: precipitation level = {level}') # Extreme precipitation level in millimeters |
36 | | - |
37 | | - # Obtain discharge |
38 | | - q = calculate_discharge_from_precipitation( |
39 | | - level, |
40 | | - potential_evapotranspiration, |
41 | | - soil_moisture, |
42 | | - ground_water, |
| 104 | + start = time.perf_counter() |
| 105 | + |
| 106 | + flood = run_sim( |
| 107 | + time_period=time_period, |
| 108 | + annual_probability=annual_probability, |
| 109 | + hydrograph_name=hydrograph_name, |
| 110 | + hydrograph=hydrograph if hydrograph else None, |
| 111 | + pet_percentile=pet_percentile, |
| 112 | + sm_percentile=sm_percentile, |
| 113 | + gw_percentile=gw_percentile, |
43 | 114 | ) |
44 | | - print(f'Hydrological prediction: discharge value = {q}') |
45 | | - # Discharge is in cubic feet per second, for the same 1 day as the precipitation. |
46 | | - |
47 | | - # Obtain flood simulation |
48 | | - flood = generate_flood_from_discharge(q * SECONDS_PER_DAY, hydrograph) # input q should be in cubic feet per day |
49 | | - # flood is a numpy array with 2 spatial dimensions and 1 time dimension |
50 | | - print(f'Hydrodynamic prediction: flood raster with shape {flood.shape}') |
51 | | - |
52 | | - print(f'Done in {(datetime.now() - start).total_seconds()} seconds.\n') |
53 | | - |
54 | | - # Convert flood to multiframe GeoTIFF |
55 | | - write_multiframe_geotiff(flood, output_path=output_path, writer=tiff_writer) |
56 | | - if animate: |
57 | | - animate_results(flood) |
58 | | - |
59 | | - |
60 | | -def validate_args(args): |
61 | | - time_period, annual_probability, hydrograph_name, hydrograph, output_path, animate = ( |
62 | | - args.time_period, args.annual_probability, |
63 | | - args.hydrograph_name, args.hydrograph, |
64 | | - args.output_path, args.no_animation, |
65 | | - ) |
66 | | - if annual_probability <= 0 or annual_probability >= 1: |
67 | | - raise Exception('Annual probability must be >0 and <1.') |
68 | | - |
69 | | - download_file(PERCENTILES_URL, PERCENTILES_PATH) |
70 | | - with open(PERCENTILES_PATH) as f: |
71 | | - percentiles = json.load(f) |
72 | | - |
73 | | - pet_percentile = int(args.pet_percentile) |
74 | | - sm_percentile = int(args.sm_percentile) |
75 | | - gw_percentile = int(args.gw_percentile) |
76 | | - |
77 | | - if any(p < 0 or p > 100 for p in [pet_percentile, sm_percentile, gw_percentile]): |
78 | | - raise Exception('Percentile values must be between 0 and 100 (inclusive).') |
79 | | - |
80 | | - potential_evapotranspiration = percentiles['pet'][pet_percentile] # Converts PET percentile into physical units |
81 | | - soil_moisture = percentiles['sm'][sm_percentile] # Converts SM percentile into physical units |
82 | | - ground_water = percentiles['gw'][gw_percentile] # Converts GW percentile into physical units |
83 | 115 |
|
84 | | - hydrograph = hydrograph or HYDROGRAPHS.get(hydrograph_name) |
85 | | - if output_path is not None: |
86 | | - output_path = Path(output_path) |
| 116 | + write_multiframe_geotiff(flood, output_path, writer=tiff_writer) |
| 117 | + logger.info(f'Done in {time.perf_counter() - start} seconds.') |
87 | 118 |
|
88 | | - return ( |
89 | | - time_period, |
90 | | - annual_probability, |
91 | | - hydrograph, |
92 | | - potential_evapotranspiration, |
93 | | - soil_moisture, |
94 | | - ground_water, |
95 | | - output_path, |
96 | | - animate, |
97 | | - args.tiff_writer, |
98 | | - ) |
99 | | - |
100 | | - |
101 | | -def main(): |
102 | | - parser = argparse.ArgumentParser( |
103 | | - prog='Dynamic Flood Simulation' |
104 | | - ) |
105 | | - parser.add_argument( |
106 | | - '--time_period', '-t', |
107 | | - help='The 20 year time period in which to predict a flood', |
108 | | - choices=['2031-2050', '2041-2060'], |
109 | | - type=str, |
110 | | - default='2031-2050', |
111 | | - ) |
112 | | - parser.add_argument( |
113 | | - '--annual_probability', '-p', |
114 | | - help='The probability that a flood of this magnitude will occur in any given year', |
115 | | - type=float, |
116 | | - default=0.04 |
117 | | - ) |
118 | | - parser.add_argument( |
119 | | - '--hydrograph-name', '-n', |
120 | | - help=( |
121 | | - 'A selection of a 24-hour hydrograph. ' |
122 | | - '"short_charles" represents a hydrograph for the main river and ' |
123 | | - '"long_charles" represents a hydrograph for the main river plus additional upstream water sources.' |
124 | | - ), |
125 | | - choices=['short_charles', 'long_charles'], |
126 | | - type=str, |
127 | | - default='short_charles' |
128 | | - ) |
129 | | - parser.add_argument( |
130 | | - '--hydrograph', '-g', |
131 | | - help='A hydrograph expressed as a list of numeric values where each value represents a proportion of total discharge', |
132 | | - nargs='*', |
133 | | - type=float, |
134 | | - ) |
135 | | - parser.add_argument( |
136 | | - '--pet_percentile', '-e', |
137 | | - help='Potential evapotranspiration percentile', |
138 | | - type=int, |
139 | | - default=25, |
140 | | - ) |
141 | | - parser.add_argument( |
142 | | - '--sm_percentile', '-s', |
143 | | - help='Soil moisture percentile', |
144 | | - type=int, |
145 | | - default=25, |
146 | | - ) |
147 | | - parser.add_argument( |
148 | | - '--gw_percentile', '-w', |
149 | | - help='Ground water percentile', |
150 | | - type=int, |
151 | | - default=25, |
152 | | - ) |
153 | | - parser.add_argument( |
154 | | - '--output_path', '-o', |
155 | | - help='Path to write the flood simulation tif file', |
156 | | - nargs='?', |
157 | | - type=str, |
158 | | - ) |
159 | | - parser.add_argument( |
160 | | - '--no_animation', |
161 | | - help='Disable display of result animation via matplotlib', |
162 | | - action='store_false' |
163 | | - ) |
164 | | - parser.add_argument( |
165 | | - '--tiff-writer', |
166 | | - help='Library to use for writing result tiff', |
167 | | - choices=['rasterio', 'large_image'], |
168 | | - type=str, |
169 | | - default='rasterio', |
170 | | - ) |
171 | | - args = parser.parse_args() |
172 | | - run_end_to_end(*validate_args(args)) |
| 119 | + if animation: |
| 120 | + animate_results(flood, output_path) |
173 | 121 |
|
174 | 122 |
|
175 | 123 | if __name__ == '__main__': |
|
0 commit comments