Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add chill portion and chill units #1909

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9cb1833
Add chill_portion to _agro
saschahofmann Sep 9, 2024
2b84622
Merge branch 'main' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 9, 2024
f6846b1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 9, 2024
52819cb
Add make hourly data to helper function
saschahofmann Sep 9, 2024
a42979f
Merge branch 'chilling' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 9, 2024
d236173
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 9, 2024
e244e59
Fix typos
saschahofmann Sep 9, 2024
489cbb1
Merge branch 'chilling' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 9, 2024
90f2bad
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 9, 2024
7d522f6
Add utah model
saschahofmann Sep 9, 2024
264a9b0
Merge branch 'chilling' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 9, 2024
44ab60b
Update changelog
saschahofmann Sep 9, 2024
2abdbf7
Implement Zeitsperre comments to changelog and constants into subfunc…
saschahofmann Sep 9, 2024
c787fff
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 9, 2024
bc382eb
Use numpy pi
saschahofmann Sep 9, 2024
6ce1c78
Merge branch 'chilling' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 9, 2024
4d1b9d7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 9, 2024
c79eb83
Add chill_units test to test_indices
saschahofmann Sep 10, 2024
619edfa
Merge branch 'chilling' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 10, 2024
de45168
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 10, 2024
177bee0
Add make_hourly_temperature test
saschahofmann Sep 10, 2024
8845b59
Merge branch 'chilling' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 10, 2024
7dda632
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 10, 2024
4472be9
Merge branch 'main' into chilling
Zeitsperre Sep 16, 2024
3bbd693
Update descriptions
saschahofmann Sep 16, 2024
5676280
Merge branch 'chilling' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 16, 2024
0fcceb4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 16, 2024
6585a9c
Take maximum along time axis for chill_portions
saschahofmann Sep 16, 2024
2251b7b
Merge branch 'main' into chilling
saschahofmann Sep 17, 2024
4c0e3f0
Merge branch 'chilling' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 17, 2024
98900df
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 17, 2024
0a496be
Test chill portions
saschahofmann Sep 17, 2024
76fd7e8
Merge branch 'chilling' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 17, 2024
72dc980
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 17, 2024
f0f572e
Merge branch 'main' into chilling
Zeitsperre Sep 18, 2024
2baad27
Merge branch 'main' into chilling
Zeitsperre Sep 18, 2024
14b44bd
Apply suggestions from aulemahal
saschahofmann Sep 19, 2024
9d65ca1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 19, 2024
f266153
Add unitless to chill_units and abstract to chill_portion
saschahofmann Sep 19, 2024
0e539b8
Merge branch 'chilling' of github.com:saschahofmann/xclim into chilling
saschahofmann Sep 19, 2024
d0eea99
Apply more suggestions
saschahofmann Sep 19, 2024
0659e6a
Integrate two more comments
saschahofmann Sep 19, 2024
ad5a7ec
Use ResamplingIndicatorWithIndexing for chill_units
saschahofmann Sep 19, 2024
879fb98
Merge branch 'main' into chilling
saschahofmann Sep 19, 2024
6dc26ba
Add indexing to chill_portions
saschahofmann Sep 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions xclim/indices/_agro.py
Original file line number Diff line number Diff line change
Expand Up @@ -1524,3 +1524,74 @@ def hardiness_zones(

zones = zones.assign_attrs(units="")
return zones


# Chill portion constants Dynamic Model described in Luedeling et al. (2009)
E0 = 4153.5
E1 = 12888.8
A0 = 139500
A1 = 2.567e18
SLP = 1.6
TETMLT = 277
AA = A0 / A1
EE = E1 - E0
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved


def _accumulate_intermediate(prev_E, prev_xi, curr_xs, curr_ak1):
"""Accumulate the intermediate product based on the previous concentration and the current temperature."""
curr_S = np.where(prev_E < 1, prev_E, prev_E - prev_E * prev_xi)
return curr_xs - (curr_xs - curr_S) * np.exp(-curr_ak1)


def _chill_portion_one_season(tas_K):
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved
"""Computes the chill portion for a single season based on the dynamic model on a numpy array."""
ftmprt = SLP * TETMLT * (tas_K - TETMLT) / tas_K
sr = np.exp(ftmprt)
xi = sr / (1 + sr)
xs = AA * np.exp(EE / tas_K)
ak1 = A1 * np.exp(-E1 / tas_K)

inter_E = np.zeros_like(tas_K)
for i in range(1, tas_K.shape[-1]):
inter_E[..., i] = _accumulate_intermediate(
inter_E[..., i - 1], xi[..., i - 1], xs[..., i], ak1[..., i]
)
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved
delta = np.where(inter_E >= 1, inter_E * xi, 0)

return delta.cumsum(axis=-1)
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved


def _apply_chill_portion_one_season(tas_K):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would benefit from the work in #1848 ! Let's see if we can merge that other thing before.

"""Apply the chill portion function on to an xarray DataArray."""
tas_K = tas_K.chunk(time=-1)
return xarray.apply_ufunc(
_chill_portion_one_season,
tas_K,
input_core_dims=[["time"]],
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved
output_core_dims=[["time"]],
dask="parallelized",
)


@declare_units(tas="[temperature]")
def chill_portion(tas: xarray.DataArray, time_dim: str = "time") -> xarray.DataArray:
"""Chill portion based on the dynamic model

Chill portions are a measure to estimate the bud breaking potential of different crop.
The constants and functions are taken from Luedeling et al. (2009) which formalises
the method described in Fishman et al. (1987). The model computes the accumulation of
an intermediate product that is transformed to the final product once it exceeds a
certain concentration. The intermediate product can be broken down at higher temperatures
but the final product is stable even at higher temperature. Thus the dynamic model is
more accurate than the Utah model especially in moderate climates like Israel,
California or Spain.

Parameters
----------
tas : xr.DataArray
Hourly temperature.
time_dim : str, optional
The name of the time dimension (default: "time").
"""
tas_K = convert_units_to(tas, "K")
return tas_K.groupby(f"{time_dim}.year").apply(_apply_chill_portion_one_season)
131 changes: 131 additions & 0 deletions xclim/indices/helpers.py
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from __future__ import annotations

from collections.abc import Mapping
from datetime import timedelta
from inspect import stack
from math import pi
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved
from typing import Any, cast

import cf_xarray # noqa: F401, pylint: disable=unused-import
Expand Down Expand Up @@ -550,3 +552,132 @@ def _gather_lon(da: xr.DataArray) -> xr.DataArray:
"Try passing it explicitly (`lon=ds.lon`)."
)
raise ValueError(msg) from err


def _compute_daytime_temperature(
hour_after_sunrise: xr.DataArray,
tasmin: xr.DataArray,
tasmax: xr.DataArray,
daylength: xr.DataArray,
) -> xr.DataArray:
"""Compute daytime temperature based on a sinusoidal profile.

Minimum temperature is reached at sunrise and maximum temperature 2h before sunset.

Parameters
----------
hours_after_sunrise : xarray.DataArray
Hours after the last sunrise.
tasmin : xarray.DataArray
Daily minimum temperature.
tasmax : xarray.DataArray
Daily maximum temperature.
daylength : xarray.DataArray
Length of the day in hours.

Returns
-------
xarray.DataArray
Hourly daytime temperature.
"""
return (tasmax - tasmin) * np.sin(
(pi * hour_after_sunrise) / (daylength + 4)
) + tasmin


def _compute_nighttime_temperature(
hours_after_sunset: xr.DataArray,
tasmin: xr.DataArray,
tas_sunset: xr.DataArray,
daylength: xr.DataArray,
) -> xr.DataArray:
"""Compute nighttime temperature based on a logarithmic profile.

Temperature at sunset is computed from previous daytime temperature,
minimum temperature is reached at sunrise.

Parameters
----------
hours_after_sunset : xarray.DataArray
Hours after the last sunset.
tasmin : xarray.DataArray
Daily minimum temperature.
tas_sunset : xarray.DataArray
Temperature at last sunset.
daylength : xarray.DataArray
Length of the day in hours.

Returns
-------
xarray.DataArray
Hourly nighttime temperature.
"""
return tas_sunset - ((tas_sunset - tasmin) / np.log(24 - daylength)) * np.log(
hours_after_sunset
)


def make_hourly_temperature(tasmin: xr.DataArray, tasmax: xr.DataArray) -> xr.DataArray:
"""Compute hourly temperatures from tasmin and tasmax.

Based on the Linvill et al. "Calculating Chilling Hours and Chill Units from Daily
Maximum and Minimum Temperature Observations", HortScience, 1990
we assume a sinusoidal temperature profile during daylight and a logarithmic decrease after sunset
with tasmin reached at sunsrise and tasmax reached 2h before sunset.

For simplicity and because we do daily aggregation, we assume that sunrise globally happens at midnight
and the sunsets after `daylength` hours computed via the day_lengths function.
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
tasmin : xarray.DataArray
Daily minimum temperature.
tasmax : xarray.DataArray
Daily maximum temperature.

Returns
-------
xarray.DataArray
Hourly temperature.
"""
data = xr.merge([tasmin, tasmax])
# We add one more timestamp so the resample function includes the last day
data = xr.concat(
[
data,
data.isel(time=-1).assign_coords(
time=data.isel(time=-1).time + timedelta(days=1)
),
],
dim="time",
)

daylength = day_lengths(data.time, data.lat)
# Create daily chunks to avoid memory issues after the resampling
data = data.assign(
daylength=daylength,
sunset_temp=_compute_daytime_temperature(
daylength, data.tasmin, data.tasmax, daylength
),
next_tasmin=data.tasmin.shift(time=-1).ffill("time"),
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved
).chunk(time=1)
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved
# Compute hourly data by resampling and remove the last time stamp that was added earlier
hourly = data.resample(time="h").ffill().isel(time=slice(0, -1))
# To avoid "invalid value encountered in log" warning we set hours before sunset to 1
nighttime_hours = xr.where(
(hourly.time.dt.hour + 1 - hourly.daylength) > 0,
hourly.time.dt.hour + 1 - hourly.daylength,
1,
)
saschahofmann marked this conversation as resolved.
Show resolved Hide resolved
return xr.where(
hourly.time.dt.hour < hourly.daylength,
_compute_daytime_temperature(
hourly.time.dt.hour, hourly.tasmin, hourly.tasmax, hourly.daylength
),
_compute_nighttime_temperature(
nighttime_hours,
hourly.next_tasmin,
hourly.sunset_temp,
hourly.daylength - 1,
),
)
Loading