| Title: | Parameter Sets, Trait Trade-Offs and Environmental Drivers for the 'plant' Model |
|---|---|
| Description: | Sets up runs of the plant forest model by bundling a site's environment with a species' trait trade-offs and parameter sets. Holds named parameter sets for species and sites, the functions that derive model parameters from traits, and adapters that turn gridded and flux-tower climate into the daily driver series plant reads. |
| Authors: | Daniel Falster [aut, cre] (ORCID: <https://orcid.org/0000-0002-9814-092X>) |
| Maintainer: | Daniel Falster <[email protected]> |
| License: | AGPL (>= 3) |
| Version: | 0.0.0.9000 |
| Built: | 2026-11-13 01:29:23 UTC |
| Source: | https://github.com/traitecoevo/fieldtrial |
Writes the daily met table's columns into a TF24 environment's drivers, one knot per day at its middle, and turns its precipitation into rainfall:
apply_drivers( env, met, start_year = NULL, rainfall = c("pulse", "spline"), drivers = c("SWdown", "atm_vpd", "atm_vpd_diurnal_amp", "atm_kpa", "wind_speed", "ca", "rainfall"), until = NULL )apply_drivers( env, met, start_year = NULL, rainfall = c("pulse", "spline"), drivers = c("SWdown", "atm_vpd", "atm_vpd_diurnal_amp", "atm_kpa", "wind_speed", "ca", "rainfall"), until = NULL )
env |
A TF24-family |
met |
A daily met table (see |
start_year |
The calendar year at time 0. Defaults to the table's first year. |
rainfall |
|
drivers |
The drivers to set, from those above; |
until |
Drop rainfall pulses after this time: plant refuses an event
past the run's |
| met column | driver | conversion |
SWdown_noon |
SWdown |
none, W m-2 |
VPD_noon |
atm_vpd |
none, kPa |
VPD_amp |
atm_vpd_diurnal_amp |
only where the environment has it |
Psurf |
atm_kpa |
none, kPa |
Wind |
wind_speed |
none, m s-1 |
CO2air |
ca |
ppm to Pa at Psurf, or at the environment's atm_kpa |
Tair_noon |
leaf_temp |
only if "leaf_temp" is in drivers |
Precip |
rainfall | see rainfall
|
leaf_temp is left at plant's value unless asked for, because plant's TF24
runs its leaf at that temperature with the energy balance off, and a site's
air temperature changes every temperature response at once. When asked for,
it is the noon temperature, like every other driver here: the daily mean
would pair a noon VPD with a cooler, less saturable air.
Rainfall is delivered as pulses by default: each wet day's total, in
metres, at the middle of the day, as rows from plant::rainfall_pulse(), and
the continuous rainfall driver is set to zero so the water is not counted
twice. Pass the returned events to plant::run_scm() with
plant::events(). A daily total is a sum of discrete events; interpolating
it spreads each one over its neighbours. rainfall = "spline" sets the
continuous driver instead (m yr-1), which suits a mean year.
Before setting anything, the noon VPD is checked against the leaf's limit,
0 < VPD < e_s(T) at the leaf temperature it will meet (Tetens, as plant's
leaf computes it): plant refuses outside it, and it is cheaper to say so
here, with the dates.
Times are plant's: years from 1 January of start_year. plant's TF24 reads
the day of year from the fractional part of time, so start_year must be
the first calendar year of the run. Each day's value is a knot at the middle
of the day, and the first and last days hold to the start of the first and
the end of the last, so the drivers cover the table's whole days; a run must
not go past them.
A list: env; events, the rainfall pulses (or NULL); and
drivers, a data frame saying, for each driver, whether it was set from
the table or left at plant's value.
plant's TF24 family hands its leaf (phylloptim's Leaf) a set of traits
and numerical controls. A parameter set can only reach what plant exposes:
a strategy parameter, a strategy field, or a Control field. This lists
every trait in phylloptim::leaf_traits() and every control in
phylloptim::leaf_control() with where plant exposes it, so a new leaf
parameter that plant does not yet forward shows up as a gap to fix in
plant, not as a value a set silently cannot change.
check_passthrough(model = "TF24")check_passthrough(model = "TF24")
model |
A TF24-family model name. |
Two kinds of leaf parameter are listed as out of scope rather than gaps:
those of the leaf's other stomatal models (JS22_gamma, CMax_a,
CMax_b), which plant's TF24 does not select, and integration_tol, which
only sets the tolerance of a direct quadrature off the production path that
plant never calls. A plant field for either would be settable and reach
nothing.
A data frame with columns name, kind ("trait" or
"control") and where ("strategy pars", "strategy field",
"Control", "out of scope", or NA for a gap).
met_climatology() collapses a record to its mean year: each column
averaged by day of year over the complete years, with 29 February dropped,
dated in the non-leap year 2001. met_repeat() repeats whole years of a
table end to end to cover a longer run, since plant's drivers cannot be
evaluated outside the times they were given.
met_climatology(met) met_repeat(met, n_years)met_climatology(met) met_repeat(met, n_years)
met |
A daily met table. |
n_years |
How many years the result spans. |
A mean year's rainfall falls a little on every day. Deliver it as a spline
(apply_drivers(rainfall = "spline")), or draw pulses from the record with
pulse_series_fit() instead.
A daily met table carrying met's site attributes (latitude,
longitude, vpd_peak_hour, filled, source, climatology_of).
met_climatology() sets climatology_of to the first and last years
averaged.
met <- data.frame(date = seq(as.Date("2001-01-01"), as.Date("2003-12-31"), by = "day")) met$Tair_mean <- 20 + 8 * cos(2 * pi * seq_len(nrow(met)) / 365) nrow(met_climatology(met)) nrow(met_repeat(met_climatology(met), 10))met <- data.frame(date = seq(as.Date("2001-01-01"), as.Date("2003-12-31"), by = "day")) met$Tair_mean <- 20 + 8 * cos(2 * pi * seq_len(nrow(met)) / 365) nrow(met_climatology(met)) nrow(met_repeat(met_climatology(met), 10))
Aggregates a tower or reanalysis record to the daily met table, fitting the noon values and VPD amplitude plant's TF24 rebuilds a day from:
met_daily_from_subdaily( x, latitude, longitude, utc_offset, timestamp = c("start", "middle", "end"), vpd_peak_hour = 14.5, max_amp = 0.99, vpd_floor = 0.01 )met_daily_from_subdaily( x, latitude, longitude, utc_offset, timestamp = c("start", "middle", "end"), vpd_peak_hour = 14.5, max_amp = 0.99, vpd_floor = 0.01 )
x |
A data frame with a |
latitude, longitude
|
Site location, degrees; south and west negative. |
utc_offset |
Hours the timestamps are ahead of UTC (Alice Springs standard time: 9.5). Daylight saving is not followed. |
timestamp |
Whether |
vpd_peak_hour |
Local solar hour of the daily VPD peak. |
max_amp, vpd_floor
|
Bounds applied to the VPD amplitude and the noon VPD (kPa). |
SWdown_noon is the noon value of a clear-sky-shaped day holding the
day's observed shortwave energy (swdown_noon_from_mean()), so the day
plant rebuilds integrates to what was measured.
VPD_amp and VPD_noon come from a least-squares fit of
VPD(h) = c0 * (1 + A * cos(2 * pi * (h - vpd_peak_hour) / 24)) to the
day's record, in local solar time.
Tair_noon comes from the same fit applied to air temperature (with no
amplitude bound); the day's mean, maximum and minimum are kept too.
Precipitation is the day's total; wind and CO2 their means while the sun is up; pressure its mean.
Records are put into days by local solar time, worked out from the timestamps' fixed offset from UTC, the longitude and the equation of time. A day is kept only if it is complete (a solar day may hold one step more or fewer than 24 clock hours), so the partial days at each end of a record are dropped.
Two corrections keep the table inside what plant accepts, and are counted in
the result's attributes: an amplitude outside [0, max_amp] (a day whose
VPD peaks at night, say) is clamped, and a noon VPD below vpd_floor is
raised to it, since the leaf refuses a saturated atmosphere.
A daily met table (see met_schema()) with attributes latitude,
longitude, vpd_peak_hour, filled (empty: every column is from the
record), amp_clamped and vpd_floored (the days corrected).
Turns monthly climate, either normals (one row per month) or a series (one row per year and month), into the daily met table. Every monthly source in fieldtrial (WorldClim, and later CRU TS and AGCD) goes through this, so they share one set of assumptions:
met_from_monthly( monthly, latitude, longitude = NA_real_, elevation = NULL, peak_hour = 14.5, max_amp = 0.99, vpd_floor = 0.01, source = NULL )met_from_monthly( monthly, latitude, longitude = NA_real_, elevation = NULL, peak_hour = 14.5, max_amp = 0.99, vpd_floor = 0.01, source = NULL )
monthly |
A data frame with |
latitude, longitude
|
Site location, degrees. |
elevation |
Site elevation, m, for |
peak_hour |
Local solar hour of the daily temperature and VPD peak. |
max_amp, vpd_floor
|
Bounds on the VPD amplitude and noon VPD (kPa), as
in |
source |
Recorded as |
Daily values are interpolated linearly between mid-month values, wrapping December to January for normals. Precipitation and shortwave are then rescaled month by month so each month keeps its total exactly; temperatures, vapour pressure and wind keep their mid-month values but not exactly their monthly means.
The day's temperature follows one cosine between Tair_min and
Tair_max, peaking at peak_hour; Tair_noon is read from it.
VPD through the day is e_s(T(h)) - e_a, with the actual vapour
pressure e_a constant over the day; VPD_noon and VPD_amp are the
least-squares cosine fit to it that tower records get, bounded the same
way.
Noon shortwave is the noon value of a clear-sky-shaped day holding the
daily mean (swdown_noon_from_mean()).
Pressure is from elevation by the standard atmosphere, if given.
The columns estimated by an assumption rather than read from the source are
named in attr(, "filled").
A daily met table (see met_schema()). Normals are dated in the
non-leap year 2001 and carry climatology_of if monthly has that
attribute.
normals <- data.frame(month = 1:12, Tair_min = c(22, 21, 18, 13, 8, 5, 4, 6, 10, 15, 18, 21), Tair_max = c(36, 35, 32, 28, 23, 20, 19, 22, 27, 31, 34, 35), Precip_month = c(40, 45, 30, 15, 15, 12, 8, 7, 8, 20, 25, 40), SWdown_mean = c(310, 290, 255, 210, 170, 150, 165, 200, 245, 280, 300, 315), vapour_pressure = c(1.4, 1.5, 1.3, 1.0, 0.9, 0.8, 0.7, 0.7, 0.7, 0.9, 1.0, 1.2)) normals$Tair_mean <- (normals$Tair_min + normals$Tair_max) / 2 met <- met_from_monthly(normals, latitude = -22.28, longitude = 133.25, elevation = 600) head(met)normals <- data.frame(month = 1:12, Tair_min = c(22, 21, 18, 13, 8, 5, 4, 6, 10, 15, 18, 21), Tair_max = c(36, 35, 32, 28, 23, 20, 19, 22, 27, 31, 34, 35), Precip_month = c(40, 45, 30, 15, 15, 12, 8, 7, 8, 20, 25, 40), SWdown_mean = c(310, 290, 255, 210, 170, 150, 165, 200, 245, 280, 300, 315), vapour_pressure = c(1.4, 1.5, 1.3, 1.0, 0.9, 0.8, 0.7, 0.7, 0.7, 0.9, 1.0, 1.2)) normals$Tair_mean <- (normals$Tair_min + normals$Tair_max) / 2 met <- met_from_monthly(normals, latitude = -22.28, longitude = 133.25, elevation = 600) head(met)
WorldClim 2.1 gives monthly climate normals for 1970-2000 on a global grid
(Fick and Hijmans 2017). met_from_worldclim() reads them at a site and
makes a daily mean year with met_from_monthly(); worldclim_monthly()
returns the monthly values themselves.
met_from_worldclim( latitude, longitude, res = 10, path = fieldtrial_cache("worldclim"), ... ) worldclim_monthly( latitude, longitude, res = 10, path = fieldtrial_cache("worldclim") )met_from_worldclim( latitude, longitude, res = 10, path = fieldtrial_cache("worldclim"), ... ) worldclim_monthly( latitude, longitude, res = 10, path = fieldtrial_cache("worldclim") )
latitude, longitude
|
Site location, degrees. |
res |
Grid resolution in arc-minutes: 10, 5, 2.5 or 0.5. |
path |
Folder to keep the downloads in. |
... |
Passed to |
The grids are downloaded once with the geodata package and kept in
path. A download is the whole globe for each of seven variables plus
elevation, so the resolution sets its size: about 200 MB at 10 arc-minutes
(the default, about 18 km), 700 MB at 5, 2.5 GB at 2.5. At 0.5 (30
arc-seconds, about 1 km) the 30-degree tile around the site is fetched
instead.
What WorldClim gives and what is assumed: temperatures, precipitation,
shortwave (srad, converted from kJ m-2 d-1 to W m-2), wind (a 24-hour mean,
used where the table wants a daytime mean) and vapour pressure are read;
the noon temperature, noon VPD and its amplitude come from the daily cosine
assumptions in met_from_monthly(); surface pressure comes from elevation.
There is no CO2. The result is a mean year with no interannual variation, so
its rainfall belongs on a spline (apply_drivers(rainfall = "spline")).
met_from_worldclim(): a daily met table for the year 2001, with
climatology_of = c(1970, 2000). worldclim_monthly(): a data frame of
12 months in met_from_monthly()'s columns, with an elevation
attribute.
Fick, S.E. and Hijmans, R.J. (2017) WorldClim 2: new 1-km spatial resolution climate surfaces for global land areas. International Journal of Climatology 37(12), 4302-4315.
## Not run: met <- met_from_worldclim(-22.283, 133.249) # Alice Springs Mulga sum(met$Precip) # mean annual rainfall, mm ## End(Not run)## Not run: met <- met_from_worldclim(-22.283, 133.249) # Alice Springs Mulga sum(met$Precip) # mean annual rainfall, mm ## End(Not run)
Every climate source in fieldtrial produces the same daily table, and
apply_drivers() turns it into plant's drivers. met_schema() describes its
columns; validate_met() checks a table against it and returns it, or stops
saying what is wrong.
met_schema() validate_met(met)met_schema() validate_met(met)
met |
A data frame. |
The table holds noon values, not daytime means: plant's TF24 reads
SWdown and atm_vpd as their values at solar noon and rebuilds the day
from them, with a VPD amplitude and the latitude. Tair_noon is the air
temperature the same noon, and the one a noon VPD must be read against. Only date is required;
a source supplies the columns it can. Site metadata travel as attributes:
latitude, longitude and vpd_peak_hour (local solar time of the VPD
peak), and filled, naming the columns that were estimated rather than
observed.
met_schema(): a data frame of columns, units, bounds and
meanings. validate_met(): met, invisibly.
met_schema()met_schema()
Years from 1 January of start_year, placed at the middle of each day.
plant's TF24 reads the day of year as 365 * (time - floor(time)), so
start_year must be the run's first calendar year for the seasons to line
up.
met_time(date, start_year)met_time(date, start_year)
date |
Dates, as |
start_year |
The calendar year at time 0. |
Numeric times, in years.
met_time(as.Date(c("2010-01-01", "2010-07-02", "2011-01-01")), 2010)met_time(as.Date(c("2010-01-01", "2010-07-02", "2011-01-01")), 2010)
A parameter set bundles everything one run of plant needs for a species at
a site: its traits and the trade-offs that derive its other parameters, any
parameters set directly, the soil, the climate drivers and the run's
settings, each scientific value with the source it came from. Sets live as
YAML files in the package (list_sets()) or anywhere on disk.
parameter_set( set, check = TRUE, drivers = TRUE, driver_path = fieldtrial_cache("worldclim") ) check_set(set, allow_version_drift = FALSE) run_set(set, ...) list_sets()parameter_set( set, check = TRUE, drivers = TRUE, driver_path = fieldtrial_cache("worldclim") ) check_set(set, allow_version_drift = FALSE) run_set(set, ...) list_sets()
set |
A set name from |
check |
Whether to run |
drivers |
Whether to build the set's drivers. |
driver_path |
Where downloaded climate data are kept, for sets whose drivers come from a gridded source. |
allow_version_drift |
Accept a set built against another plant model version. |
... |
Passed to |
parameter_set() reads a set and builds the four inputs of
plant::run_scm(): p (a plant::Parameters with one strategy), env
(a plant::Environment with its soil and drivers), ctrl (a
plant::Control) and events (rainfall pulses, or NULL). run_set()
runs them. Nothing a set names is dropped: a key that reaches none of these
inputs is refused.
A set records the plant model version it was built against
(plant_scientific_version, e.g. "TF24@v15"). check_set() compares it
with the installed plant and refuses a mismatch, since the same numbers can
mean a different plant after a scientific change.
parameter_set(): a list of class fieldtrial_set with p, env,
ctrl, events, tradeoffs (the trade-off function used), met (the
daily met table) and meta (the parsed file). run_set(): the result of
plant::run_scm(). check_set(): set, invisibly. list_sets(): the
names of the sets in the package.
Series of resource pulses fitted to a daily record
pulse_series_fit(date, amount, wet_threshold = 0, min_wet = 30) pulse_series_markov(from, to, fit, days_per_pulse = 1)pulse_series_fit(date, amount, wet_threshold = 0, min_wet = 30) pulse_series_markov(from, to, fit, days_per_pulse = 1)
date |
Dates of the record's days, as |
amount |
Amount on each day, |
wet_threshold |
A day is wet when its amount is above this. |
min_wet |
The fewest days a month and class needs for a fit of its own. |
from, to
|
The span the series covers: pulses fall in |
fit |
A fit from |
days_per_pulse |
Days whose amounts make one pulse: 1 for a pulse each wet day, 7 for one each week. |
pulse_series_fit() fits a seasonal weather generator to an observed
daily record, and pulse_series_markov() draws a series of any length
from the fit. Where pulse_series_record() repeats a record, which
makes the series periodic, a fitted series is stationary, and a different
realisation for each seed.
The fit uses the record's complete years only: those with an observation on
every day. Each year is put in a class by its total – dry, normal or wet,
split at the terciles of the annual totals. For each month and class, the
fit keeps the chance that a day is wet after a wet day and after a dry one
(a first-order Markov chain), and the empirical quantiles of the amounts on
wet days. A month and class with fewer than min_wet wet days, or
fewer than min_wet days after a wet or a dry day, takes that month's
values pooled over the classes.
pulse_series_markov() draws each year in turn: its total from the
quantiles of the annual totals, and so its class; the wet days from that
class's chain, month by month; and each wet day's amount from that month's
quantiles. It then scales the year's amounts to the drawn total. A model
year has 365 days and starts on 1 January, at from. Each day's
amount is a pulse at the middle of its day, or, with days_per_pulse
above 1, the amounts of that many days are one pulse at the middle of the
days. A pulse of zero is left out. The draws come from R's random number
stream, so set.seed() fixes the series.
pulse_series_fit(): a list with the quantile levels
probs, the quantiles of the annual totals annual, their
terciles, the 12 x 3 (month by class) matrices p_ww and
p_wd of the chance of a wet day after a wet and a dry day, the
12 x 3 x length(probs) array depth of quantiles of the
amounts on wet days, and the complete years used.
pulse_series_markov(): a data frame with columns time and
amount.
pulse_series_poisson, rainfall_pulse
set.seed(1) date <- seq(as.Date("1991-01-01"), as.Date("2020-12-31"), by = "day") wet <- runif(length(date)) < ifelse(format(date, "%m") %in% c("12", "01", "02"), 0.5, 0.1) amount <- ifelse(wet, rexp(length(date), rate = 1 / 0.01), 0) fit <- pulse_series_fit(date, amount) s <- pulse_series_markov(0, 100, fit, days_per_pulse = 7) sum(s$amount) / 100 # about the record's mean annual totalset.seed(1) date <- seq(as.Date("1991-01-01"), as.Date("2020-12-31"), by = "day") wet <- runif(length(date)) < ifelse(format(date, "%m") %in% c("12", "01", "02"), 0.5, 0.1) amount <- ifelse(wet, rexp(length(date), rate = 1 / 0.01), 0) fit <- pulse_series_fit(date, amount) s <- pulse_series_markov(0, 100, fit, days_per_pulse = 7) sum(s$amount) / 100 # about the record's mean annual total
Series of resource pulses
pulse_series_poisson(from, to, rate, mean_amount) pulse_series_regular(from, to, interval, amount) pulse_series_record( time, amount, from = 0, to = from + period, period = ceiling(max(time)) )pulse_series_poisson(from, to, rate, mean_amount) pulse_series_regular(from, to, interval, amount) pulse_series_record( time, amount, from = 0, to = from + period, period = ceiling(max(time)) )
from, to
|
The span the series covers: pulses fall in |
rate |
Mean number of pulses per year. |
mean_amount |
Mean amount per pulse, in the units the pulse constructor takes (TF24: metres of water). |
interval |
Years between pulses. |
amount |
Amount per pulse ( |
time |
Times of the recorded pulses, in years from the start of the
record, in |
period |
The record's length in years, the spacing at which it repeats. Defaults to its last time rounded up to a whole year. |
Three ways to build a series of pulses for a run, each returning the pulses'
times and amounts as a data frame, sorted by time, to hand to a model's pulse
constructor: for TF24, rainfall_pulse(s$time, s$amount).
pulse_series_poisson() draws a stationary marked Poisson process:
pulses arrive at rate per year, and each one's amount is exponential
with mean mean_amount, so the long-run supply is rate *
mean_amount per year. It draws from R's random number stream, so
set.seed() fixes the series, and one realisation shared by every run
that is compared is what makes a comparison of fitness a comparison of
traits (common random numbers). It draws the count, then the times, then the
amounts, so a series over a longer span does not contain a shorter one.
pulse_series_regular() puts a pulse of amount every
interval years.
pulse_series_record() takes an observed record – each wet day a
pulse, say, with times in years – and, given to beyond the record's
end, repeats it end to end to cover a longer run: a 100-year record behind a
burn-in and a window of several centuries. Repeating a record makes the
series periodic, which a window shorter than the record does not see.
A data frame with columns time and amount.
set.seed(1) s <- pulse_series_poisson(0, 100, rate = 20, mean_amount = 0.014) nrow(s) # about 2000 pulses sum(s$amount) / 100 # about 0.28 m of water a year p <- plant::add_strategies(plant::scm_base_parameters("TF24"), plant::trait_matrix(0.0825, "lma")) ev <- plant::events(plant::events_default(p), plant::rainfall_pulse(s$time, s$amount))set.seed(1) s <- pulse_series_poisson(0, 100, rate = 20, mean_amount = 0.014) nrow(s) # about 2000 pulses sum(s$amount) / 100 # about 0.28 m of water a year p <- plant::add_strategies(plant::scm_base_parameters("TF24"), plant::trait_matrix(0.0825, "lma")) ev <- plant::events(plant::events_default(p), plant::rainfall_pulse(s$time, s$amount))
Builds a vector of layer widths for
set_soil_layer_widths() on a TF24 environment in plant: n layers
spanning depth, the first of width top and each subsequent one a
fixed factor thicker. The motivating case is soil evaporation, which needs a
very thin layer at the top of the profile – a 30 cm surface layer
averages away the wetting and drying that drives it.
soil_widths_graded(depth = 1.5, n = 5, top = 0.02)soil_widths_graded(depth = 1.5, n = 5, top = 0.02)
depth |
Total column depth, m. |
n |
Number of layers. |
top |
Width of the surface layer, m. Must be less than
|
The ratio is solved so the widths sum to depth exactly; the last width
is then taken as the remainder rather than computed, so rounding cannot leave
the column a hair shallower or deeper than asked for.
A vector of n layer widths, summing to depth, each at
least as thick as the one above it.
Two things, neither of which plant corrects for you.
The column gets stiff. The soil water balance's Jacobian diagonal is
(dK/dtheta)/dz, so it scales as 1/dz: measured 1.8e4 yr^-1 at the
0.3 m default and 2.6e5 at 2 cm, where the characteristic time is about two
minutes. It only bites when the layer is wet – just after rain, which is
exactly when an evaporation layer matters. Below about 2.6 mm the stable step
falls under Control()'s ode_step_size_min and the step controller
cannot satisfy stability at all.
Runoff is recalibrated. The infiltration term keys off the surface layer
alone, through (theta_0/theta_sat)^b_infil with b_infil = 8. A
thin layer 0 fills far faster and so holds that switch shut for longer:
replacing the uniform profile with a 2 cm top layer moved annual runoff from
13.4\
changed. a_infil and b_infil were calibrated against a 30 cm
surface layer.
plant's environment refuses, in set_soil_layer_widths(), a profile in which any layer is
thinner than the one above it (equal widths are fine, so a uniform profile is
legal). Refining towards the surface is the standard and is what this function
builds; refining at depth has no counterpart in TF24 – a contrast in soil
properties is per-layer parameters, not a thin layer, and TF24 has no
water table – while it does reach a failure the model cannot absorb, driving
the size-density equations to overflow. Every profile this function returns
satisfies the rule by construction.
⚠️ A thickening profile is not automatically well resolved either. The error
in the usual Richards discretisation vanishes on a uniform grid and grows with
the contrast between adjacent thicknesses whichever way it runs, and
grading fast with depth is equivalent to a coarser column. So a small
top at a fixed n buys surface resolution at the price of a
coarse base: at top = 0.005 the deepest of five layers is 1.11 m.
Prefer more layers to a steeper ladder.
soil_widths_graded(depth = 1.5, n = 5, top = 0.02) sum(soil_widths_graded(depth = 1.5, n = 5, top = 0.02)) # 1.5soil_widths_graded(depth = 1.5, n = 5, top = 0.02) sum(soil_widths_graded(depth = 1.5, n = 5, top = 0.02)) # 1.5
solar_declination() gives the sun's declination, and solar_day() the
terms of the sun's elevation through a day at a latitude: sin(elevation) at
solar hour h is a + b * cos(pi * (h - 12) / 12), and the photoperiod is
where that is positive. These are the forms plant's TF24 uses when it
rebuilds a day from noon values, so drivers made here and the day plant
integrates share one sun.
solar_declination(time) solar_day(time, latitude)solar_declination(time) solar_day(time, latitude)
time |
Time in years from 1 January, as plant counts it: the day of
year is |
latitude |
Latitude in degrees, south negative. |
The declination is asin(0.39785 * sin(...)). plant's older R solar model
(kept for FF16's trade-offs) uses the inner expression as the angle itself,
which is about 0.6 degrees low at the solstices.
solar_declination(): radians. solar_day(): a data frame with
columns a, b and daylength (hours), one row per time. A day with
no sunrise or no sunset (polar day or night) is refused.
solar_day(c(0, 0.5), latitude = -22.28) # midsummer and midwinter at Alice Springssolar_day(c(0, 0.5), latitude = -22.28) # midsummer and midwinter at Alice Springs
Shortwave on a horizontal surface under a clear sky at a solar hour, for a
location- and season-dependent light driver where radiation was not
observed (traitecoevo/plant#701). The beam is S0 * transmissivity^m at air
mass m = 1 / sin(elevation), projected onto the horizontal.
swdown_clear_sky(time, latitude, hour = 12, transmissivity = 0.75, S0 = 1360)swdown_clear_sky(time, latitude, hour = 12, transmissivity = 0.75, S0 = 1360)
time |
Time in years from 1 January, as plant counts it: the day of
year is |
latitude |
Latitude in degrees, south negative. |
hour |
Local solar hour; 12 is noon. |
transmissivity |
Fraction of the beam that crosses one air mass, about 0.7 to 0.8 under clear skies. |
S0 |
Shortwave outside the atmosphere, W m-2. |
This is an estimate of an upper bound, not of a real day: it has no clouds,
no diffuse light and no aerosol. Where a gridded product gives daily
radiation, use that through swdown_noon_from_mean() instead.
W m-2, zero when the sun is down.
t <- (seq_len(365) - 0.5) / 365 plot(t, swdown_clear_sky(t, latitude = -22.28), type = "l")t <- (seq_len(365) - 0.5) / 365 plot(t, swdown_clear_sky(t, latitude = -22.28), type = "l")
A day's shortwave follows the sun's elevation, scaled so that its noon value
is SWdown_noon; plant's TF24 rebuilds the day this way. These convert
between that noon value and the 24-hour mean the same day carries, so a day
rebuilt from SWdown_noon holds exactly the observed energy.
swdown_noon_from_mean(SWdown_mean, time, latitude) swdown_mean_from_noon(SWdown_noon, time, latitude)swdown_noon_from_mean(SWdown_mean, time, latitude) swdown_mean_from_noon(SWdown_noon, time, latitude)
SWdown_mean |
Shortwave averaged over 24 hours, W m-2. |
time |
Time in years from 1 January, as plant counts it: the day of
year is |
latitude |
Latitude in degrees, south negative. |
SWdown_noon |
Shortwave at solar noon, W m-2. |
The other quantity, W m-2.
swdown_noon_from_mean(250, time = 0.05, latitude = -22.28)swdown_noon_from_mean(250, time = 0.05, latitude = -22.28)
The coefficients that parameterise a model's trait trade-offs, read from the package's coefficient file for that model's family. TF24, TF24f, TF24t and TF24w share one set.
tradeoff_coefficients(model) tradeoff_table(model)tradeoff_coefficients(model) tradeoff_table(model)
model |
A plant model name, such as |
tradeoff_coefficients(): a named list of numbers.
tradeoff_table(): a data frame with one row per coefficient and columns
name, value, units, description, source and reference.
tradeoff_coefficients("TF24")$B_Hv1 head(tradeoff_table("FF16"))tradeoff_coefficients("TF24")$B_Hv1 head(tradeoff_table("FF16"))
Builds the function plant uses to derive model parameters from traits: it
takes a trait matrix m (one row per strategy, named columns) and a
strategy s, and returns m with the derived parameters appended. Pass it
to plant::add_strategies(), plant::generate_strategy() or
plant::stand_gradient() as hyperpar.
tradeoffs(model, coefficients = list())tradeoffs(model, coefficients = list())
model |
A plant model name: |
coefficients |
A named list of coefficients that replace the defaults
in |
A trait missing from m takes the strategy's value. With filter = TRUE,
a derived column equal to the strategy's own value in every row is left out,
so the strategy keeps its default.
These are the recommended trade-offs. plant keeps its own frozen copy for testing; on any one platform the two give identical numbers.
A function f(m, s, filter = TRUE), carrying attr(f, "model"),
attr(f, "coefficients") and attr(f, "traits") (every trait-matrix
column it reads). FF16's also carries attr(f, "input_traits"), naming
the traits it reads that are not parameters of the model.
f <- tradeoffs("TF24", coefficients = list(B_Hv2 = -0.25)) attr(f, "coefficients")$B_Hv2f <- tradeoffs("TF24", coefficients = list(B_Hv2 = -0.25)) attr(f, "coefficients")$B_Hv2