
Build the full nitrogen balance: inputs, outputs, losses and NUE.
Source:R/n_balance.R
build_nitrogen_balance.RdAssembles build_n_inputs()'s long-format nitrogen inputs into the
aggregate N_input_* sums, adds the output side (crop production,
residue use/burning, grazed weeds, soil organic-matter sequestration),
runs the nitrogen-loss cascade (calculate_nh3(), calculate_soil_n2o(),
calculate_n_leaching(), calculate_indirect_n2o_nh3()), closes the
balance (N_input_full - N_output_full), applies the N-limitation cap on
SOM sequestration, and derives nutrient-use-efficiency (NUE) indicators
plus a GWP/CO2e indicator for the nitrous-oxide streams. Ported from the
balance and indicator equations of an earlier regional historical
reconstruction.
Arguments
- methods
A named list of method choices:
nh3(forwarded tocalculate_nh3(), default"manner"),n2o(forwarded tocalculate_soil_n2o(), default"ipcc2019", the IPCC 2019 Tier 1 wet/dry factors of Table 11.1 (Updated) p. 11.13;"ipcc2019_atl_med","aguilera"and"ipcc2006"are selectable),n2o_indirect(forwarded tocalculate_indirect_n2o_nh3(), default"ipcc2019", the wet/dry EF4 of Table 11.3 (Updated) p. 11.26, or"ipcc2019_atl_med"),leaching(forwarded tocalculate_n_leaching(), default"meisinger_drainage", or"ipcc_fracleach", the wet/dry FracLEACH-(H) of Section 11.2.2.2 p. 11.25 applied to the N added, or"fracleach_surplus", the same fractions on the surplus) andregime, how each grid row is split into a rainfed and an irrigated part (see Details):"yield_split"(default),"area_split"or"none". The IPCC 2019 wet/dry methods readmoisture_regimefrom the driver tables indata.- resolution
"grid"(default) or"polity", as inbuild_n_inputs();"polity"sums every term (and re-derives every indicator) over cells.- polity_validity
What to do with a row whose
(area_code, year)resolves to a polity that did not exist in that year (the cell-polity crosswalk has no year dimension, so an early-20th-century cell is labelled with its present-day territory)."keep"(default) keeps every row, which is the historical behaviour, and warns naming the rows, years and area codes involved."flag"keeps them and adds the per-row logicalreporting_polity_out_of_span, marking exactly which rows are stand-ins."drop"removes them. All three warn; only"drop"changes the numbers. Seepolity_coverage_gaps(), which reports the same rows for an already-built table.- data
Named list of pre-loaded upstream inputs.
n_inputs(build_n_inputs()'s output) is used directly when supplied, elsebuild_n_inputs()is called with this SAMEdatalist (so its nested readers, e.g.bnf_input/npp_n_input/livestock_intake, propagate through). Also required:npp_n_input: shared withbuild_n_inputs()'s"recycling"term (seecalculate_npp_carbon_nitrogen()); used here forprod_n_t.ag_land_support: thebuild_ag_land_support()table, supplyingarea_ha(the per-hectare boundary denominator) on a physical-land basis. Derived natively when absent and derivable, and shared withbuild_n_inputs()so both see one table; when neither supplied nor derivable,area_hafalls back to the harvested area the NPP input carries.residue_destiny_input:calculate_residue_destinies()'s required input (item_prod_code,residue_dm_t, plus whatever the chosenresidue_destiny_methodneeds; the polity'sregion_krausmannandregion_un_subare taken fromarea_codewhen absent), forused_residue_n_t/bedding_residue_n_t/burnt_residue_n_t.residue_destiny_methodselects the method (default"recovery_regional"),residue_destiny_recoveryits recovery-rate variant (default"wirsenius", the package default),residue_beddingthe bedding rule (default"wirsenius", 12.4% of the recovered cereal straw) and, underresidue_bedding = "fraction",residue_bedding_fractionthe share of the recovered non-feed residue used as bedding (default0; seecalculate_residue_destinies()). The same arguments split the residue ofnpp_n_inputfor the"recycling"input ofbuild_n_inputs(), which returns to the soil only the residue left on the field (recycling_residue_basis, default"residue_soil_returned";"total_residue"recycles the gross residue and is recorded inmethod_recycling_n). Annpp_n_inputthat already carriesresidue_soil_dm_tkeeps it: the recycling then uses the caller's destinies while the removal outputs useresidue_destiny_inputand these arguments, so the soil-returned and removed residue add up to the gross residue only when both come from the package's own destiny split. The bedded straw also comes back as a manure input, throughbuild_n_inputs(), read from the same split asbedding_residue_n_t.residue_feed_shareselects that split'sfeed_share(default"wirsenius").regime_areas,regime_ratio,regime_production,regime_shares: the rainfed/irrigated split's inputs (see Details), each read or built when absent: the rainfed and irrigated hectares per cell and production item (lon,lat,area_code,item_prod_code,year,rainfed_ha,irrigated_ha; built withbuild_gridded_landuse()), thebuild_regime_yield_ratio()table (built on the NPP rows; pass the tablebuild_carbon_inputs()reads to make both chains apply one ratio), and theget_primary_production()table the yield bounds ofsplit_regime_yield()are pooled from.regime_sharesreplaces all three with pre-built shares per grid key (lon,lat,area_code,item_cbs_code,year,irrigated_area_share,irrigated_yield_share), which carry no production item: the residue terms then follow the production share.livestock_intake: shared withbuild_n_inputs()'s manure term; its"grass"feed_qualityrows drivegrazed_weeds_n_t.carbon_balance: shared withbuild_n_inputs()'s"som_ mineralization"term; itsson_change_kgn_hadrivessom_ sequestration_n_t.n_balance_driversandn_balance_leaching_drivers: the loss drivers. At grid resolution each one leftNULLis built bybuild_n_loss_drivers()on the split balance's own rows (the IPCC 2019 wet/dry class and the ATL/MED drivers mapped from it; see its table of which methods it supplies), andmethod_driverssays so. That needs the rainfed/irrigated split (methods$regimeother than"none");leaching = "meisinger_drainage"and the MANNERnh3methods read drivers it does not supply and are refused before any input is assembled unless their table is supplied. At polity resolution both must be supplied.n_loss_driver_inputs: optional list passed asdatatobuild_n_loss_drivers()when it runs (climate,crop_depth,lpjml_years,lpjml_grid,regime_areas,moisture_regime); the balance's owncountry_gridis shared with it.n_balance_drivers: additional per-(key, fert_type)driver columnscalculate_nh3()/calculate_soil_n2o()/calculate_indirect_n2o_nh3()need beyondn_input_t/fert_type(moisture_regimefor the IPCC 2019 wet/dry methods,climateandirrig_typefor the ATL/MED ones, plus every MANNER driver column whenmethods$nh3 == "manner"), joined by the balance key ANDfert_type(the Title-case vocabulary of the loss functions:"Synthetic","Solid","Liquid","Excreta","Human","Recycling","SOM"). The excreta rows' species mix,excreta_cpp_share, comes frombuild_n_inputs(), N-weighted over the rows pooled into each loss row. A second set of balance-key-only driver columns (moisture_regime,fert_type,climate,irrig_cat,tillage,som_share,cn_input,land_use) forcalculate_n_leaching()is read fromn_balance_leaching_drivers, which must hold at most one row per balance key (a many-to-one join aborts on duplicate keys rather than fanning the rows out and misaligningdrainage_mm). Missing required drivers abort inside the calledcalculate_*()function, naming the exact column; for the MANNERnh3methods the check runs onn_balance_driversbefore any input is assembled, because no function in the package supplies those drivers (onlywindspeed_mshas a reader,read_lpjml_wind(), not wired in).drainage_mm: annual drainage (mm) forcalculate_n_leaching(), as a numeric vector aligned to the balance-key rows, or already present as adrainage_mmcolumn vian_balance_leaching_drivers. At grid resolution the rainfed/irrigated split doubles the rows, so an aligned vector only fits withmethods$regime = "none"; otherwise supply drainage as a column ofn_balance_leaching_drivers.
- gwp
100-year global warming potential standard for N2O,
"ar6"(default),"ar5"or"ar4", matchingbuild_crop_soil_n2o_extension().- example
If
TRUE, return a small fixture instead of assembling real data. Defaults toFALSE.
Value
A tibble keyed by year/area_code/item_cbs_code (plus
lon/lat at resolution = "grid", and water_regime, "rainfed" or
"irrigated", at grid resolution unless methods$regime = "none") with
area_ha (each crop's harvested hectares in the cell, summed over cells
at
resolution = "polity"; used downstream to convert tonnes N to a
per-hectare rate), the input aggregates
(n_input_full_t, n_input_full_nosom_t, n_input_std_t,
n_input_som_t, n_input_for_n2o_t), the output aggregates
(n_output_residues_t, n_output_som_t, n_output_useful_t,
n_output_std_t, n_output_full_t), the loss terms (nh3_n_t,
n2o_direct_n_t, no3_n_t, denitrification_n_t,
retained_profile_n_t, n2o_indirect_no3_n_t, n2o_indirect_nh3_n_t;
no3_n_t + denitrification_n_t + retained_profile_n_t is the surplus,
see calculate_n_leaching()), the (post-cap)
som_sequestration_n_t, n_balance_t, surplus_t, surplus_share,
the five NUE ratios (nue_std, nue_residues, nue_som,
nue_useful, nue_full), total_gwp_co2e_kg, and the method_nh3/
method_soil_n2o/method_n2o_indirect_nh3/method_leaching
provenance columns (the per-row stamps of the loss functions; a balance
row pooling loss rows priced differently lists them joined by "|",
and a row with no N2O-relevant input, which emits no direct or
NH3-indirect N2O, carries the method chosen suffixed "_no_loss_row",
e.g. "ipcc2019_no_loss_row"),
method_drivers ("whep_built", "supplied" or
"supplied|whep_built"), and, when the leaching drivers carry them (as
built ones do), the row's wet/dry moisture_regime, how it was obtained
(method_moisture_regime) and climate_fill_km,
method_regime
and method_regime_split when the split ran ("yield_ratio";
"area_no_production" where the crop's production could not be
expressed in fresh weight and the area share was used for everything;
"regime_shares" for shares supplied as data$regime_shares; or
"no_regime_share" for a row booked wholly rainfed because the regime
layer does not cover it), and method_residue_regime, how the row's
residue terms were split, the rules of its production items joined by
"|": "part_yield" (re-evaluated at a rainfed part's yield),
"part_yield_irrigation_terms" (re-evaluated at an irrigated part's
yield, with the model's irrigation terms), "crop_density" (the item's
per-hectare values on the part's area: no part yield, or
"area_split") or "yield_share" (the production share, for shares
supplied without production items), NA on a row with no residue
term, plus the polity columns below. When the
supplied n_inputs carry them, the
method_recycling_n, method_synthetic, method_deposition,
method_deposition_scope, method_human_population,
method_human_kgn_cap, method_unsupported, method_manure and
method_unattributed stamps from
build_n_inputs() are carried through as well, so a balance names the
input conventions that produced it. Gains
reporting_polity_out_of_span when polity_validity = "flag".
Details
Rainfed/irrigated split. At grid resolution each crop row is split
into a rainfed and an irrigated part before the losses are computed, so
regime-specific loss drivers can attach. Two shares per row drive it:
the irrigated share of the crop's cell area, from the spatialization
chain's rainfed/irrigated hectares (build_gridded_landuse()), and the
irrigated share of its production, from split_regime_yield() on the
ratio of build_regime_yield_ratio(). Synthetic N and production N split
by the production share, on the assumption that N availability per unit
of yield is the same in both regimes; every other input, the harvested
area, grazed weeds and SOM sequestration split by the area share. The
residue terms – the residue and root N returned to soil
("recycling") and the used, bedding and burnt residue N – are
evaluated per part: for each production item of the row, the parts'
yields keep its production (Y_r = Y / (1 + s (R - 1)), Y_i = R Y_r),
and each part carries the item's residue and root N per hectare times
the response of calculate_crop_residues() and calculate_crop_roots()
from the item's mean yield to the part's yield, in the part's water
regime, so the model's irrigation terms (a lower residue ratio for the
crop groups that respond, a lower root:shoot ratio) apply to the
irrigated part. On the default chains, whose NPP input is built with the
polity's region_hanpp and without n_input_kg_ha, the item's residue
and root per hectare are the model's at its mean yield, so each part's
are the model's at the part's own yield; with any other NPP input they
are the item's own density scaled by the model's response. Residue and
root are affine in yield, so a high-yield hectare carries less than its
yield share. Summing the two parts
reproduces the unsplit balance for every additive column except the
residue terms and the totals that contain them, which sum to the parts'
evaluation instead: with the irrigation terms, the residue and root N of
irrigated land fall. The bedded straw the manure input brings back is
the parts' bedding: when the balance builds its own inputs and beds any
straw, each production item's bedding is scaled by its factor over the
two parts before it reaches the manure engine, so the manure columns,
too, differ from an unsplit run by the bedding the irrigation terms take
off, and the bedded N that leaves the field is still the bedded N that
returns. A non-zero residue term at a key the per-item
evaluation does not cover means the balance's inputs and the NPP-N table
disagree, and aborts. "area_split" uses the area share for everything,
residue terms included, and "none" returns the unsplit balance.
Loss-driver tables without a water_regime column apply to both parts.
polity_validity is forwarded to build_n_inputs() – which forwards it in
turn to build_ag_land_support(), build_n_deposition(), build_human_n()
and spatialize_country_n_to_crops() – and then applied to the balance rows
themselves, so one choice governs the whole build (whep#727). A
data$n_inputs table supplied directly is left alone: it was built by its
own call with its own choice, and the balance rows derived from it still get
this argument's fate at the tail.
Grazing excreta. The "excreta" input is the N grazing animals deposit
on pasture, range and paddock, and its N2O is in n2o_direct_n_t,
n2o_indirect_nh3_n_t and total_gwp_co2e_kg, as the field loss it is.
calculate_livestock_emissions() prices the same emission and reports it
as manure_n2o_direct_pasture and manure_n2o_indirect_pasture, and that
is the chain WHEP's GHG footprint takes it from. So these balance columns
are not to be added to livestock manure N2O: subtract the pasture columns
from one of the two first (whep#1524).
Loss drivers
Which driver columns each loss method reads, and whether
build_n_loss_drivers() supplies them. At grid resolution that function
builds each driver table left NULL; at polity resolution both tables must
be supplied.
| Method | Driver columns | Supplied | Status |
nh3: "ipcc" | fert_type | yes | built |
nh3: "manner", "manner_default" | MANNER's | no | note 1 |
n2o: "ipcc2019" | moisture_regime | yes | built |
n2o: "ipcc2019_atl_med" | climate | yes | built |
n2o: "aguilera", "ipcc2006" | climate, irrig_type | yes | built |
n2o_indirect: "ipcc2019" | moisture_regime | yes | built |
n2o_indirect: "ipcc2019_atl_med" | climate, irrig_type | yes | built |
leaching: "ipcc_fracleach", "fracleach_surplus" | moisture_regime, water_regime | yes | built |
leaching: "meisinger_drainage" | Meisinger's | no | note 2 |
Note 1: MANNER reads
manner_fertiliser,soil_ph,rate_kg_ha,rainfall_mm,irrigated,temp_c,temp_c_annual_mean,windspeed_ms,technique,system,incorporation_delay_handspecies(seecalculate_nh3()). Drivers must be supplied; global drivers planned.Note 2: Meisinger reads
fert_type,climate,irrig_cat,land_use,cn_input,tillage,som_shareanddrainage_mm(seecalculate_n_leaching()). Drivers must be supplied; global drivers planned.
build_nitrogen_balance() refuses a method of note 1 or note 2 before
assembling any input unless its driver table is supplied.
Polity columns
Every area-keyed output carries the polity its area_code resolves to in
that row's year:
polity_area_code: The numeric key rows are AGGREGATED on, for the matrix workflows. It is a bucket, not an identity: usereporting_polity_codeto say which territory a row belongs to.reporting_polity_code: The polity itself, e.g.ESP-1846-1914. It is year-aware, so the samearea_coderesolves to different polities in different years, which is the point of the crosswalk.reporting_polity_name: Its name. It can differ from the area's own name where the area folds into an aggregate.reporting_polity_has_geometry: Whether the polity has a polygon in the WHEP polity database, for callers that need to map or intersect it.FALSEis a documented gap upstream, not an error.
Rows whose area_code resolves to no polity keep the columns with NA
rather than being dropped, so a gap is visible instead of silent.
Rows before the back-cast anchor year resolve to the polity live in that
anchor year rather than to the polity live in the row's own year, because
WHEP's pre-anchor series are back-cast onto the anchor-year territory. See
add_polity_code() for the reasoning. Where that polity is not live in the
row's own year – 41.5% of the pre-1961 (area, year) cells –
add_polity_code() says so as mapping_status == "backcast_anchor", and
polity_coverage_gaps() reports it as gap_kind == "backcast_anchor".
These columns do not say so either way.
A row whose year no mapped period covers is resolved to the NEAREST period of
the same area instead, so reporting_polity_code can name a polity that did
not exist in that row's year – FAOSTAT bucket 206 "Sudan (former)" keeps
reporting after SUD-1956-2011 ends, and its post-2011 rows carry that code.
These columns do not say so: add_polity_code() reports such a row as
mapping_status == "out_of_span", and that column is dropped here so that
adding it does not change the schema of every area-keyed output at once.
polity_coverage_gaps() reports the stand-in rows of a built table, and
options(whep.polity_mapping_status = "flag") (or "status") carries the
signal on the outputs themselves. Both are opt-in; the default is no extra
column.
Examples
build_nitrogen_balance(example = TRUE)
#> # A tibble: 1 × 45
#> year area_code polity_area_code reporting_polity_code reporting_polity_name
#> <int> <int> <int> <chr> <chr>
#> 1 2020 10 10 AUS-1901-2025 Australia
#> # ℹ 40 more variables: reporting_polity_has_geometry <lgl>,
#> # item_cbs_code <int>, area_ha <dbl>, n_input_full_t <dbl>,
#> # n_input_full_nosom_t <dbl>, n_input_std_t <dbl>, n_input_som_t <dbl>,
#> # n_input_for_n2o_t <dbl>, prod_n_t <dbl>, used_residue_n_t <dbl>,
#> # bedding_residue_n_t <dbl>, burnt_residue_n_t <dbl>, grazed_weeds_n_t <dbl>,
#> # som_sequestration_n_t <dbl>, n_output_residues_t <dbl>,
#> # n_output_som_t <dbl>, n_output_useful_t <dbl>, n_output_std_t <dbl>, …