Skip to content

Commit b9b6349

Browse files
authored
Merge pull request #15 from ClimateImpactLab/bugfix/surge-edgecase
Better handle edges of surge lookup matrix and catch issues
2 parents 9e3c8a5 + 8f2505d commit b9b6349

File tree

4 files changed

+73
-18
lines changed

4 files changed

+73
-18
lines changed

pyCIAM/io.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,14 +310,16 @@ def _load_lslr_for_ciam(
310310
.set_index(scen_mc=ix_names)
311311
)
312312

313-
# interpolate to yearly
313+
# add on base year where slr is 0
314314
slr_out = slr_out.reindex(
315315
year=np.concatenate(([slr_0_year], slr.year.values)),
316316
fill_value=0,
317317
)
318318

319+
# interpolate to desired years
319320
if interp_years is not None:
320321
slr_out = slr_out.interp(year=interp_years)
322+
321323
return slr_out
322324

323325

pyCIAM/run.py

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,33 @@ def calc_costs(
332332
# interpolation with 0's
333333
surge_noadapt = []
334334
surge = []
335+
336+
def _check_vals(rh_diff_arr, lslr_arr, seg, check_rh_diff_max=True):
337+
# for surge_noadapt, rh_diff can be greater than max. This happens when
338+
# a segment chooses retreat10000 in their refA (initial adaptation) and
339+
# also has a declining no-climate-change SLR scenario (e.g. for
340+
# no-climate-change scenarios). However, we automatically assign 0
341+
# expected storm impacts for retreat10000, so this is acceptable and
342+
# does not need to exist in the surge lookup table.
343+
error_str = (
344+
"{0} value is {1} than {2} in storm damage lookup "
345+
f"table for segment {seg}. Please investigate why this is. You may "
346+
"need to re-generate the surge lookup table (or perhaps the `refA` "
347+
"(initial adaptation) table if it was generated for a different "
348+
"set of SLR scenarios or years."
349+
)
350+
# lslr is allowed to be below min surge lookup value b/c we created
351+
# lookup such that < min value will be 0 impact
352+
if (
353+
check_rh_diff_max
354+
and rh_diff_arr.max() > this_surge_lookup.rh_diff.max()
355+
):
356+
raise ValueError(error_str.format("rh_diff", "higher", "maximum"))
357+
if rh_diff_arr.min() < this_surge_lookup.rh_diff.min():
358+
raise ValueError(error_str.format("rh_diff", "lower", "minimum"))
359+
if lslr_arr.max() > this_surge_lookup.lslr.max():
360+
raise ValueError(error_str.format("lslr", "higher", "maximum"))
361+
335362
for seg in inputs.seg.values:
336363
this_surge_lookup = (
337364
surge_lookup.sel(seg=seg)
@@ -341,32 +368,58 @@ def calc_costs(
341368
)
342369
if this_surge_lookup.sum() == 0:
343370
continue
344-
surge_noadapt.append(
371+
372+
this_rh_diff_noadapt = rh_diff_noadapt.sel(seg=seg, drop=True)
373+
this_lslr = lslr.sel(seg=seg, drop=True)
374+
_check_vals(
375+
this_rh_diff_noadapt, this_lslr, seg, check_rh_diff_max=False
376+
)
377+
378+
lslr_too_low = this_lslr < this_surge_lookup.lslr.min()
379+
380+
this_surge_noadapt = (
345381
this_surge_lookup.sel(adapttype="retreat", drop=True)
346382
.interp(
347-
lslr=lslr.sel(seg=seg),
348-
rh_diff=rh_diff_noadapt.sel(seg=seg),
383+
lslr=this_lslr,
384+
rh_diff=this_rh_diff_noadapt,
349385
assume_sorted=True,
350-
kwargs={"fill_value": 0},
351386
)
352387
.reset_coords(drop=True)
353388
.expand_dims(seg=[seg])
354389
)
355390

391+
# ensure nans are only at the low end of LSLR or high end of rh_diff
392+
rh_diff_too_high = (
393+
this_rh_diff_noadapt > this_surge_lookup.rh_diff.max()
394+
)
395+
assert (
396+
this_surge_noadapt.notnull() | lslr_too_low | rh_diff_too_high
397+
).all(), seg
398+
399+
surge_noadapt.append(this_surge_noadapt.fillna(0))
400+
356401
surge_adapt = []
402+
403+
this_rh_diff_adapt = rh_diff.sel(seg=seg, drop=True)
404+
_check_vals(this_rh_diff_adapt, this_lslr, seg)
405+
357406
for adapttype in this_surge_lookup.adapttype.values:
358-
surge_adapt.append(
407+
this_surge_adapt = (
359408
this_surge_lookup.sel(adapttype=adapttype)
360409
.interp(
361-
lslr=lslr.sel(seg=seg),
362-
rh_diff=rh_diff.sel(
363-
adapttype=adapttype, seg=seg, drop=True
410+
lslr=this_lslr,
411+
rh_diff=this_rh_diff_adapt.sel(
412+
adapttype=adapttype, drop=True
364413
),
365414
assume_sorted=True,
366-
kwargs={"fill_value": 0},
367415
)
368416
.reset_coords(drop=True)
369417
)
418+
419+
# ensure nans are only at the low end of lslr
420+
assert (this_surge_adapt.notnull() | lslr_too_low).all(), seg
421+
422+
surge_adapt.append(this_surge_adapt.fillna(0))
370423
surge.append(
371424
xr.concat(surge_adapt, dim=this_surge_lookup.adapttype).expand_dims(
372425
seg=[seg]
@@ -1066,7 +1119,6 @@ def execute_pyciam(
10661119
# determine whether to check for finished jobs
10671120
if output_path is None:
10681121
check = False
1069-
tmp_output_path = None
10701122
else:
10711123
check = True
10721124

@@ -1149,7 +1201,7 @@ def execute_pyciam(
11491201
storage_options=storage_options,
11501202
)
11511203
# block on this calculation
1152-
wait(surge_futs)
1204+
client.gather(surge_futs)
11531205

11541206
###############################
11551207
# define temporary output store
@@ -1372,7 +1424,7 @@ def execute_pyciam(
13721424
###############################
13731425
# Rechunk and save final
13741426
###############################
1375-
wait(ciam_futs_2.tolist())
1427+
client.gather(ciam_futs_2.tolist())
13761428
assert [f.status == "finished" for f in ciam_futs_2.tolist()]
13771429
client.cancel(ciam_futs_2)
13781430
del ciam_futs_2
@@ -1465,7 +1517,9 @@ def get_refA(
14651517
**params.refA_scenario_selectors,
14661518
)
14671519
slr = slr.unstack("scen_mc")
1468-
slr = slr.squeeze(drop=True)
1520+
slr = slr.squeeze(
1521+
[d for d in slr.dims if len(slr[d]) == 1 and d != "seg"], drop=True
1522+
)
14691523

14701524
costs, refA = calc_costs(
14711525
inputs, slr, surge_lookup=surge, return_year0_hts=True, **model_kwargs

pyCIAM/surge/lookup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ def _get_lslr_rhdiff_range(
130130
mins.append(min_lslr)
131131
maxs.append(max_lslr)
132132

133-
min_lslr = xr.concat(mins, dim="tmp").min("tmp")
134-
max_lslr = xr.concat(maxs, dim="tmp").max("tmp")
133+
min_lslr = xr.concat(mins, dim="tmp").min("tmp") - 2 * np.finfo("float32").eps
134+
max_lslr = xr.concat(maxs, dim="tmp").max("tmp") + 2 * np.finfo("float32").eps
135135

136136
at = _get_planning_period_map(lslr.year, at_start)
137137

pyCIAM/utils.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,7 @@ def _get_lslr_plan_data(
9494
if diaz_negative_retreat:
9595
RH_heights = _pos(RH_heights)
9696
else:
97-
for i in range(1, len(RH_heights.at)):
98-
RH_heights[{"at": i}] = RH_heights.isel(at=slice(None, i + 1)).max("at")
97+
RH_heights = RH_heights.cumulative("at").max()
9998

10099
# set initial RH_heights to 0 (e.g.
101100
# assuming no retreat or protection anywhere such that both w/ and w/o climate

0 commit comments

Comments
 (0)