Skip to content

Commit 59fb486

Browse files
authored
Merge pull request #13 from OpenGeoscience/api
Refactor to create a Python API and to use Click for the CLI
2 parents f5ee43b + 86d6c41 commit 59fb486

File tree

9 files changed

+217
-182
lines changed

9 files changed

+217
-182
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ maintainers = [
1414
{ name = "August Posch", email = "[email protected]" },
1515
]
1616
dependencies = [
17+
"click",
1718
"matplotlib",
1819
"numpy<2",
1920
"pandas",

uvdat_flood_sim/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .run import run_sim
2+
from .save_results import write_multiframe_geotiff

uvdat_flood_sim/__main__.py

Lines changed: 110 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,175 +1,123 @@
1-
import argparse
2-
import json
3-
from datetime import datetime
1+
import logging
42
from pathlib import Path
3+
import time
4+
from typing import Literal
5+
6+
import click
57

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
108
from .animate_results import animate as animate_results
9+
from .run import run_sim
1110
from .save_results import write_multiframe_geotiff
12-
from .utils import download_file
13-
1411

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((
27100
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=}'
30103
))
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,
43114
)
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
83115

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.')
87118

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)
173121

174122

175123
if __name__ == '__main__':

uvdat_flood_sim/animate_results.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
import matplotlib.pyplot as plt
44
import matplotlib.animation as ani
55

6-
from .constants import OUTPUTS_FOLDER
76

8-
def animate(results):
9-
OUTPUTS_FOLDER.mkdir(parents=True, exist_ok=True)
7+
def animate(results, output_folder):
108
n_frames = results.shape[0]
119
vmin, vmax = numpy.min(results), numpy.max(results)
1210

@@ -25,7 +23,7 @@ def update(i):
2523

2624
animation = ani.FuncAnimation(fig, update, n_frames, interval=1000)
2725
animation.save(
28-
OUTPUTS_FOLDER / 'animation.gif',
26+
output_folder / 'animation.gif',
2927
writer=ani.PillowWriter(fps=2)
3028
)
3129

uvdat_flood_sim/constants.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
from datetime import timedelta
12
from pathlib import Path
23

34

45
DOWNLOADS_FOLDER = Path('downloads')
5-
OUTPUTS_FOLDER = Path('outputs')
66

77
DOWNSCALING_MODEL_URL = 'https://data.kitware.com/api/v1/item/68c463cb7d52b0d5b570f348/download'
88
DOWNSCALING_MODEL_PATH = DOWNLOADS_FOLDER / 'downscaling_model.pkl'
@@ -31,23 +31,23 @@
3131
}
3232

3333
HYDROGRAPHS = dict(
34-
short_charles=[
34+
short_charles=(
3535
0.006, 0.026, 0.066, 0.111, 0.138, 0.143,
3636
0.128, 0.102, 0.080, 0.054, 0.038, 0.030,
3737
0.021, 0.016, 0.012, 0.008, 0.006, 0.005,
3838
0.003, 0.002, 0.002, 0.001, 0.001, 0.001,
39-
],
40-
long_charles=[
39+
),
40+
long_charles=(
4141
0.003, 0.008, 0.029, 0.048, 0.072, 0.092,
4242
0.102, 0.104, 0.097, 0.088, 0.071, 0.063,
4343
0.046, 0.035, 0.028, 0.024, 0.018, 0.015,
4444
0.012, 0.010, 0.007, 0.006, 0.005, 0.004,
45-
],
45+
),
4646
)
4747

4848
WATERSHED_AREA_SQ_M = 656.38 * 1e6
4949
CUBIC_METERS_TO_CUBIC_FEET = 35.31467
50-
SECONDS_PER_DAY = 86400
50+
SECONDS_PER_DAY = int(timedelta(days=1).total_seconds())
5151

5252
GEOSPATIAL_PROJECTION = 'epsg:4326'
5353
GEOSPATIAL_BOUNDS = [

uvdat_flood_sim/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)