diff --git a/controls.py b/controls.py index 46cb196e4..ca1bff577 100644 --- a/controls.py +++ b/controls.py @@ -22,7 +22,7 @@ def Text(id, default): dbc Input component with text inputs. """ return [ dbc.Input(id=id, type="text", - size="sm", className="m-1 d-inline-block w-auto",debounce=True, value=default) ] + size="sm", className="m-1 d-inline-block w-auto", debounce=True, value=default) ] def Number(id, default, min=None, max=None, html_size=None): @@ -158,7 +158,7 @@ def Sentence(*elems): return dbc.Form(groups) -def Block(title, *body, is_on=True, width="100%"): #width of the block in its container +def Block(title, *body, is_on=True, width="100%", border_color="grey"): #width of the block in its container """Separates out components in individual Cards Auto-generates a formatted block with a card header and body. @@ -185,10 +185,14 @@ def Block(title, *body, is_on=True, width="100%"): #width of the block in its co the_display = "inline-block" else: the_display = "none" - return dbc.Card([ - dbc.CardHeader(title), - dbc.CardBody(body), - ], className="mb-4 ml-4 mr-4", style={"display": the_display, "width": width}) + return dbc.Card( + [ + dbc.CardHeader(title), + dbc.CardBody(body), + ], + className="mb-4 ml-4 mr-4", + style={"display": the_display, "width": width, "border-color": border_color, "line-height": 1}, + ) def Options(options,labels=None): """ Creates options for definition of different Dash components. diff --git a/enacts/wat_bal/agronomy.py b/enacts/wat_bal/agronomy.py index 85737c457..9ed5151b0 100644 --- a/enacts/wat_bal/agronomy.py +++ b/enacts/wat_bal/agronomy.py @@ -301,7 +301,13 @@ def soil_plant_water_balance( planting_date = (peffective[time_dim][-1].drop_vars(time_dim) - (planted_since - np.timedelta64(1, "D")) ) - return sm, drainage, et_crop, et_crop_red, planting_date + return ( + sm.rename("sm"), + drainage.rename("drainage"), + et_crop.rename("et_crop"), + et_crop_red.rename("et_crop_red"), + planting_date.rename("planting_date"), + ) def api_runoff( diff --git a/enacts/wat_bal/layout_monit.py b/enacts/wat_bal/layout_monit.py index 0d0bc9d06..b273e2b11 100644 --- a/enacts/wat_bal/layout_monit.py +++ b/enacts/wat_bal/layout_monit.py @@ -39,7 +39,10 @@ def app_layout(): # Initialization rr_mrg = calc.read_zarr_data(RR_MRG_ZARR) - center_of_the_map = [((rr_mrg["Y"][int(rr_mrg["Y"].size/2)].values)), ((rr_mrg["X"][int(rr_mrg["X"].size/2)].values))] + center_of_the_map = [ + ((rr_mrg["Y"][int(rr_mrg["Y"].size/2)].values)), + ((rr_mrg["X"][int(rr_mrg["X"].size/2)].values)), + ] lat_res = np.around((rr_mrg["Y"][1]-rr_mrg["Y"][0]).values, decimals=10) lat_min = np.around((rr_mrg["Y"][0]-lat_res/2).values, decimals=10) lat_max = np.around((rr_mrg["Y"][-1]+lat_res/2).values, decimals=10) @@ -48,6 +51,10 @@ def app_layout(): lon_max = np.around((rr_mrg["X"][-1]+lon_res/2).values, decimals=10) lat_label = str(lat_min)+" to "+str(lat_max)+" by "+str(lat_res)+"˚" lon_label = str(lon_min)+" to "+str(lon_max)+" by "+str(lon_res)+"˚" + first_year = rr_mrg["T"][0].dt.year.values + one_to_last_year = rr_mrg["T"][-367].dt.year.values + last_year = rr_mrg["T"][-1].dt.year.values + year_label = str(first_year)+" to "+str(last_year) return dbc.Container( [ @@ -56,7 +63,18 @@ def app_layout(): dbc.Row( [ dbc.Col( - controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label), + controls_layout( + lat_min, + lat_max, + lon_min, + lon_max, + lat_label, + lon_label, + first_year, + one_to_last_year, + last_year, + year_label, + ), sm=12, md=4, style={ @@ -166,7 +184,18 @@ def navbar_layout(): ) -def controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label): +def controls_layout( + lat_min, + lat_max, + lon_min, + lon_max, + lat_label, + lon_label, + other_year_min, + other_year_default, + other_year_max, + year_label +): return dbc.Container( [ html.Div( @@ -183,26 +212,35 @@ def controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label): ), dcc.Loading(html.P(id="map_description"), type="dot"), html.P( - f""" + dcc.Markdown(""" The soil-plant-water balance algorithm estimates soil moisture and other characteristics of the soil and plants since planting date of the current season and up to now. It is driven by rainfall and the crop cultivars Kc - that can be changed in the Control Panel below. - """ + that can be changed in the _Controls Panel_ below. + """) ), html.P( - f""" - Map another day of the simulation using the Date control on the top bar, + dcc.Markdown(""" + Map another day of the simulation using the _Date_ control in the top bar, or by clicking a day of interest on the time series graph.. You can pick a day between planting and today (or last day of available data). - """ + """) ), html.P( - f""" + dcc.Markdown(""" Pick another point to monitor evolution since planting - with the controls below or by clicking on the map. - """ + with the _Pick a point_ controls or by clicking on the map. + """) + ), + html.P( + dcc.Markdown(""" + The current evolution (blue) is put in context by comparing it + to another situation (dashed red) that can be altered + by picking another planting date and/or + another crop (Kc parameters) and/or + another year through the _Compare to..._ panel. + """) ), html.H5("Water Balance Outputs"), ]+[ @@ -218,6 +256,14 @@ def controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label): {GLOBAL_CONFIG["institution"]}’s archive with satellite rainfall estimates. """ ), + html.P( + f""" + Total Available Water (TAW) regridded on rainfall data from SoilGrids's + absolute total available water capacity (mm), + aggregated over the Effective Root Zone Depth for Maize + data product. + """ + ), ], style={"position":"relative","height":"30%", "overflow":"scroll"}, ), @@ -234,6 +280,7 @@ def controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label): min=lat_min, max=lat_max, type="number", + style={"height": "auto", "padding-bottom": "0px"}, ), dbc.Label("Latitude", style={"font-size": "80%"}), dbc.Tooltip( @@ -250,6 +297,7 @@ def controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label): min=lon_min, max=lon_max, type="number", + style={"height": "auto", "padding-bottom": "0px"}, ), dbc.Label("Longitude", style={"font-size": "80%"}), dbc.Tooltip( @@ -259,7 +307,7 @@ def controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label): ) ]), ), - dbc.Button(id="submit_lat_lng", children='Submit'), + dbc.Button(id="submit_lat_lng", children='Submit', color="secondary"), ], ), ), @@ -271,6 +319,7 @@ def controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label): {"label": val["menu_label"], "value": key} for key, val in CONFIG["map_text"].items() ], + style={"padding-top": "0px", "padding-bottom": "0px"}, ), ), Block( @@ -309,7 +358,67 @@ def controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label): Sentence( Number("kc_end", CONFIG["kc_v"][4], min=0, max=2, html_size=4), ), - dbc.Button(id="submit_kc", children='Submit'), + dbc.Button( + id="submit_kc", + children='Submit', + color="light", + style={"color": "green", "border-color": "green"}, + ), + border_color="green", + ), + Block( + "Compare to...", + Sentence( + "Planting Date", + DateNoYear("planting2_", 1, CONFIG["planting_month"]), + "", + Number( + "planting2_year", + other_year_default, + min=other_year_min, + max=other_year_max, + html_size=5 + ), + ), + Sentence( + "for", + Text("crop2_name", CONFIG["crop_name"]), + "crop cultivars: initiated at", + ), + Sentence( + Number("kc2_init", CONFIG["kc_v"][0], min=0, max=2, html_size=4), + "through", + Number("kc2_init_length", CONFIG["kc_l"][0], min=0, max=99, html_size=2), + "days of initialization to", + ), + Sentence( + Number("kc2_veg", CONFIG["kc_v"][1], min=0, max=2, html_size=4), + "through", + Number("kc2_veg_length", CONFIG["kc_l"][1], min=0, max=99, html_size=2), + "days of growth to", + ), + Sentence( + Number("kc2_mid", CONFIG["kc_v"][2], min=0, max=2, html_size=4), + "through", + Number("kc2_mid_length", CONFIG["kc_l"][2], min=0, max=99, html_size=2), + "days of mid-season to", + ), + Sentence( + Number("kc2_late", CONFIG["kc_v"][3], min=0, max=2, html_size=4), + "through", + Number("kc2_late_length", CONFIG["kc_l"][3], min=0, max=99, html_size=2), + "days of late-season to", + ), + Sentence( + Number("kc2_end", CONFIG["kc_v"][4], min=0, max=2, html_size=4), + ), + dbc.Button( + id="submit_kc2", + children='Submit', + color="light", + style={"color": "blue", "border-color": "green"}, + ), + border_color="blue", ), ], style={"position":"relative","height":"60%", "overflow":"scroll"}, diff --git a/enacts/wat_bal/maproom_monit.py b/enacts/wat_bal/maproom_monit.py index 6cdb4d625..0b47c41a9 100644 --- a/enacts/wat_bal/maproom_monit.py +++ b/enacts/wat_bal/maproom_monit.py @@ -14,6 +14,7 @@ import pandas as pd import numpy as np import urllib +import datetime import xarray as xr import agronomy as ag @@ -301,11 +302,79 @@ def pick_location(n_clicks, click_lat_lng, latitude, longitude): return [lat, lng], lat, lng +def wat_bal_ts( + precip, + map_choice, + taw, + planting_day, + planting_month, + kc_init_length, + kc_veg_length, + kc_mid_length, + kc_late_length, + kc_init, + kc_veg, + kc_mid, + kc_late, + kc_end, + planting_year=None, + time_coord="T", +): + + kc_periods = pd.TimedeltaIndex( + [0, kc_init_length, kc_veg_length, kc_mid_length, kc_late_length], unit="D" + ) + kc_params = xr.DataArray(data=[ + kc_init, kc_veg, kc_mid, kc_late, kc_end + ], dims=["kc_periods"], coords=[kc_periods]) + p_d = calc.sel_day_and_month( + precip[time_coord], planting_day, planting_month + ) + p_d = (p_d[-1] if planting_year is None else p_d.where( + p_d.dt.year == planting_year, drop=True + )).squeeze(drop=True).rename("p_d") + precip = precip.where( + (precip["T"] >= p_d) & (precip["T"] < (p_d + np.timedelta64(365, "D"))), + drop=True, + ) + precip.load() + try: + water_balance_outputs = ag.soil_plant_water_balance( + precip, + et=5, + taw=taw, + sminit=taw/3., + kc_params=kc_params, + planting_date=p_d, + ) + for wbo in water_balance_outputs: + if (wbo.name == map_choice): + ts = wbo + except TypeError: + ts = None + return ts + + +def plot_scatter(ts, name, color, dash=None, customdata=None): + hovertemplate = "%{y} on %{x}" + if customdata is not None: + hovertemplate = hovertemplate + " %{customdata}" + return pgo.Scatter( + x=ts["T"].dt.strftime("%-d %b"), + y=ts.values, + customdata=customdata, + hovertemplate=hovertemplate, + name=name, + line=pgo.scatter.Line(color=color, dash=dash), + connectgaps=False, + ) + @APP.callback( Output("wat_bal_plot", "figure"), Input("loc_marker", "position"), Input("map_choice", "value"), Input("submit_kc", "n_clicks"), + Input("submit_kc2", "n_clicks"), State("planting_day", "value"), State("planting_month", "value"), State("crop_name", "value"), @@ -318,11 +387,25 @@ def pick_location(n_clicks, click_lat_lng, latitude, longitude): State("kc_late", "value"), State("kc_late_length", "value"), State("kc_end", "value"), + State("planting2_day", "value"), + State("planting2_month", "value"), + State("planting2_year", "value"), + State("crop2_name", "value"), + State("kc2_init", "value"), + State("kc2_init_length", "value"), + State("kc2_veg", "value"), + State("kc2_veg_length", "value"), + State("kc2_mid", "value"), + State("kc2_mid_length", "value"), + State("kc2_late", "value"), + State("kc2_late_length", "value"), + State("kc2_end", "value"), ) def wat_bal_plots( marker_pos, map_choice, n_clicks, + n2_clicks, planting_day, planting_month, crop_name, @@ -335,70 +418,96 @@ def wat_bal_plots( kc_late, kc_late_length, kc_end, + planting2_day, + planting2_month, + planting2_year, + crop2_name, + kc2_init, + kc2_init_length, + kc2_veg, + kc2_veg_length, + kc2_mid, + kc2_mid_length, + kc2_late, + kc2_late_length, + kc2_end, ): + + first_year = rr_mrg.precip["T"][0].dt.year.values + last_year = rr_mrg.precip["T"][-1].dt.year.values + if planting2_year is None: + return pingrid.error_fig( + error_msg=f"Planting date must be between {first_year} and {last_year}" + ) + lat = marker_pos[0] lng = marker_pos[1] - kc_periods = pd.TimedeltaIndex( - [0, int(kc_init_length), int(kc_veg_length), int(kc_mid_length), int(kc_late_length)], unit="D" - ) - kc_params = xr.DataArray(data=[ - float(kc_init), float(kc_veg), float(kc_mid), float(kc_late), float(kc_end) - ], dims=["kc_periods"], coords=[kc_periods]) - precip = rr_mrg.precip.isel({"T": slice(-366, None)}) - p_d = calc.sel_day_and_month( - precip["T"], int(planting_day), calc.strftimeb2int(planting_month) - ).squeeze(drop=True).rename("p_d") - #p_d = precip["T"].where( - # lambda x: (x.dt.day == int(planting_day)) - # & (x.dt.month == calc.strftimeb2int(planting_month)), - # drop=True - #).squeeze(drop=True).rename("p_d") - precip = precip.where(precip["T"] >= p_d, drop=True) try: - precip = pingrid.sel_snap(precip, lat, lng) - isnan = np.isnan(precip).any() - if isnan: - error_fig = pingrid.error_fig(error_msg="Data missing at this location") - return error_fig + taw = pingrid.sel_snap(xr.open_dataarray(Path(CONFIG["taw_file"])), lat, lng) except KeyError: - error_fig = pingrid.error_fig(error_msg="Grid box out of data domain") - return error_fig - precip.load() - taw = pingrid.sel_snap(xr.open_dataarray(Path(CONFIG["taw_file"])), lat, lng) - try: - sm, drainage, et_crop, et_crop_red, planting_date = ag.soil_plant_water_balance( - precip, - et=5, - taw=taw, - sminit=taw/3., - kc_params=kc_params, - planting_date=p_d, - ) - except TypeError: - error_fig = pingrid.error_fig( + return pingrid.error_fig(error_msg="Grid box out of data domain") + precip = pingrid.sel_snap(rr_mrg.precip, lat, lng) + if np.isnan(precip).all(): + return pingrid.error_fig(error_msg="Data missing at this location") + + ts = wat_bal_ts( + precip, + map_choice, + taw, + int(planting_day), + calc.strftimeb2int(planting_month), + int(kc_init_length), + int(kc_veg_length), + int(kc_mid_length), + int(kc_late_length), + float(kc_init), + float(kc_veg), + float(kc_mid), + float(kc_late), + float(kc_end), + ) + if (ts is None): + return pingrid.error_fig( error_msg="Please ensure all input boxes are filled for the calculation to run." ) - return error_fig - if map_choice == "sm": - ts = sm - elif map_choice == "drainage": - ts = drainage - elif map_choice == "et_crop": - ts = et_crop - wat_bal_graph = pgo.Figure() - wat_bal_graph.add_trace( - pgo.Scatter( - x=ts["T"].dt.strftime("%-d %b %y"), - y=ts.values, - hovertemplate="%{y} on %{x}", - name="", - line=pgo.scatter.Line(color="blue"), - ) + + ts2 = wat_bal_ts( + precip, + map_choice, + taw, + int(planting2_day), + calc.strftimeb2int(planting2_month), + int(kc2_init_length), + int(kc2_veg_length), + int(kc2_mid_length), + int(kc2_late_length), + float(kc2_init), + float(kc2_veg), + float(kc2_mid), + float(kc2_late), + float(kc2_end), + planting_year=int(planting2_year) ) - wat_bal_graph.update_traces( - mode="lines", - connectgaps=False, + if (ts2 is None): + return pingrid.error_fig( + error_msg="Please ensure all input boxes are filled for the calculation to run." + ) + + p_d2 = calc.sel_day_and_month( + precip["T"], int(planting2_day), calc.strftimeb2int(planting2_month) ) + p_d2 = p_d2.where( + abs(ts["T"][0] - p_d2) == abs(ts["T"][0] - p_d2).min(), drop=True + ).squeeze(drop=True).rename("p_d2") + ts2 = ts2.assign_coords({"T": pd.date_range(datetime.datetime( + p_d2.dt.year.values, p_d2.dt.month.values, p_d2.dt.day.values + ), periods=ts2["T"].size)}) + + ts, ts2 = xr.align(ts, ts2, join="outer") + + wat_bal_graph = pgo.Figure() + wat_bal_graph.add_trace(plot_scatter(ts, "Current", "green", customdata=ts["T"].dt.strftime("%Y"))) + wat_bal_graph.add_trace(plot_scatter(ts2, "Comparison", "blue", dash="dash")) wat_bal_graph.update_layout( xaxis_title="Time", yaxis_title=f"{CONFIG['map_text'][map_choice]['menu_label']} [{CONFIG['map_text'][map_choice]['units']}]",