The FeNEU Package - an Introduction

library(FeNEU)           # Attach FeNEU
library(ForestElementsR) # FeNEU builds on it, see the note below
library(dplyr)           # used in the code examples below

options(fe_spec_lang = "eng") # display species names in English

1 Introduction

FeNEU is an R package for evaluating inventory data from larger forest management units in order to support professional forest planning in practice. The package is designed to cover sample inventories as well as stand-wise forest inventories, and it is by design open for new data sources and evaluation methods. This first version does not claim to be able to do “everything” but provides a first reliable collection of central functions and workflows; i.e. a solid fundament, on which we can build up further development. FeNEU makes extensive use of the objects and functions provided by the package ForestElementsR (Biber and Toraño Caicoya 2026). We recommend attaching that package alongside FeNEU, as we do in the setup above. FeNEU calls it internally either way, but the inventory plots themselves are ForestElementsR objects, and the methods that display and plot them become available only once the package is attached.

Before we proceed, let us put out the same Important Warning as we do with ForestElementsR: While the functions and objects we provide with FeNEU behave exactly as described in the documentation, professional training in the fields of forestry, forest management, forest science is required to apply them correctly and to understand what their output actually means. If you are a hobby forester, you are more than welcome to make use of this package, but be sure to consult a professional before you draw important conclusions from what you get out of your evaluations. We also would like to point out another trivial wisdom that, however, seems to be ignored on a regular basis: If your data are bad, don’t expect to get good results.

Building on the object model and functions provided by ForestElementsR, FeNEU adds the evaluation and reporting layer that larger forest inventories require: i) Standardised import of several data formats, ii) reports of the status quo covered by an inventory (called ‘base tables’, ‘structure tables’), and iii) increment evaluations and estimates with different methods. In addition, the package provides functions for generating supporting information, e.g. inventory plot information sheets for supporting surveys in the field.

This vignette provides a brief overview of the most important concepts of the package, how to obtain it and how to work with it. For more detailed information we provide two additional vignettes, namely vignette("data_formats") and vignette("increment_estimation"). All examples shown below can be run as they stand; they use the example datasets that come with FeNEU (see ?data_examples_overview for the full list).

1.1 Installation

FeNEU itself installs like any other R package:

install.packages("FeNEU")

1.2 Setting up PDF output

Most evaluations in FeNEU end in a formatted PDF report, produced by the family of *_pdf() functions. These render through rmarkdown and kableExtra on top of two tools that live outside R and are therefore not installed along with the package: pandoc and a LaTeX distribution. All other functionality of FeNEU works without them; only the *_pdf() functions require them.

To find out where you stand, ask the package:

diagnose_pdf_toolchain()
#> PDF toolchain OK: pandoc and LaTeX are available.

The function reports each component separately and, for anything that is missing, prints what to do about it. If a LaTeX distribution is what you lack, setup_pdf_toolchain() installs TinyTeX for you (a small LaTeX distribution of about 300 MB; any pre-existing TeX Live or MiKTeX installation works equally well and is left untouched). Pandoc must be installed by hand from https://pandoc.org/installing.html — unless you work in RStudio, which ships its own copy and needs no further attention.

Setting this up is a one-time job per machine. Should you forget it, the *_pdf() functions stop early with a clear message instead of failing later somewhere deep inside the rendering toolchain.

2 Inventory types and levels of data

Basically FeNEU covers two types of forest inventories, i.e. sample inventories and stand-wise inventories. The former are typically designed as a grid overlaying the forest area, and a sample plot positioned at each gridpoint. Often, the plot locations are permanently marked in order to allow for repeated inventories of the sample plots. Standwise inventories, in contrast, collect information based on the pre-defined stands that together make up the forest area to be surveyed. Typically, several samples are taken at different positions in the stand without keeping their exact location. Usually, the measurements are less detailed compared to sample inventories. While both inventory types can be done with different methods, sample inventories do very often use a concentric circle plot design while stand-wise inventories often use angle count samples. Thus, the current version of FeNEU supports sample inventories with a concentric circle design and stand-wise inventories based on angle count sampling. This is, however, not a permanent restriction. In the contrary, FeNEU is technically not limited to these concepts, and more will be added if required.

At the input side FeNEU can deal with raw data and preprocessed data. Raw data are data in the shape as they have been collected in the field. Typically, at that stage, they are not yet in a format that is ideal for further evaluations, and they have undergone no or only sparse plausibility checks. FeNEU provides functions that read different raw data formats and transform them into corresponding preprocessed formats.

Preprocessed data, however, do not have to originate from FeNEU. The preprocessed formats are defined formats in their own right, and FeNEU reads them whichever way they were produced — by its own raw data import, by another program, or by an inventory service provider who delivers data in that shape already. This is why the import is deliberately split into two steps that can be used together or separately.

Whichever way the data enter, they all lead to the same destination: an object of the class fe_inventory. This is the uniform representation in which FeNEU holds inventory data internally, no matter which inventory type, survey method or input format they came from. Everything the package does afterwards works on that object rather than on the original files. The consequence is a useful guarantee: once the data have been successfully converted into an fe_inventory object, all subsequent evaluations are technically sound. Whether they are also sensible remains, as always, a question of the data and of professional judgement.

Currently, FeNEU supports three raw data formats and two preprocessed formats:

Note that the format numbers are assigned within an inventory type: the “Format 1” of the sample inventories has nothing to do with the “Format 1” of the stand-wise ones. The complete, column-level definition of all five formats — which columns are mandatory, which are optional, and what they mean — is the subject of the vignette Data Formats.

2.1 The fe_inventory class

Before we get into the workflows, we should mention the central object class in FeNEU, the S3 class fe_inventory. This class defines how a forest inventory is represented in FeNEU. Thus, the first and most crucial step when working with FeNEU is to transform the raw data into an fe_inventory object. After this has been accomplished, i.e. an fe_inventory object has been successfully constructed, it is guaranteed that all subsequent evaluations will (technically) work. In essence, an fe_inventory object is a tibble where every row represents an inventory unit. Let us have a look at an example fe_inventory object (data_ex3_sample_fe_inventory) that comes with the package FeNEU. Note that this example comprises only ten inventory units (plots) for quick demonstration purposes, while real inventories often come with several hundred up to several thousand plots. Also note that the example data_ex3_sample_fe_inventory is realistic, but not real. Do not expect to find anything related to the dummy geocoordinates stored in this object.

data_ex3_sample_fe_inventory |> is_fe_inventory()
#> [1] TRUE
data_ex3_sample_fe_inventory
#> # A tibble: 10 × 3
#>    plot_id              plot       area_rep_ha
#>  * <chr>                <list>           <dbl>
#>  1 900000002_1_99900002 <f_ccrcl_>        2.97
#>  2 900000015_1_99900015 <f_ccrcl_>        5.14
#>  3 900000020_1_99900020 <f_ccrcl_>        4.43
#>  4 900000029_1_99900029 <f_ccrcl_>        4.69
#>  5 900000043_1_99900043 <f_ccrcl_>        3.97
#>  6 900000046_1_99900046 <f_ccrcl_>        7.05
#>  7 900000061_1_99900061 <f_ccrcl_>        6.98
#>  8 900000075_1_99900075 <f_ccrcl_>        3.08
#>  9 900000086_1_99900086 <f_ccrcl_>        3.80
#> 10 900000100_1_99900100 <f_ccrcl_>        4.63

The object has three columns, the central column plot being the most important, because it contains the inventory units themselves. Each element of this column is an object of ForestElementsR’s fe_stand family. The column plot_id provides a unique id for every inventory unit, and the column area_rep_ha shows how much forest area (in hectares) is represented by each inventory unit. Let us check out the first inventory unit:

inv_unit <- data_ex3_sample_fe_inventory$plot[[1]]
class(inv_unit)
#> [1] "fe_ccircle_spatial" "fe_stand_spatial"   "fe_stand"
names(inv_unit)
#> [1] "stand_id"          "time_yr"           "circle_definition"
#> [4] "tree_positions"    "trees"             "small_trees"

An inventory unit is itself a list of slots: an identifier, the survey year, the geometry of the sample circles, the positions of the trees, and the two tree tables. The heart of it is trees, which holds one row per tree:

inv_unit$trees
#> # A tibble: 14 × 14
#>    tree_id species_id    layer_key time_yr age_yr dbh_cm height_m
#>    <chr>   <bavrn_state>     <dbl>   <dbl>  <dbl>  <dbl>    <dbl>
#>  1 3_1     Scots pine            4    2020     91   36.5     25.5
#>  2 4_1     Norway spruce         1    2020     53   36.5     NA  
#>  3 4_2     Norway spruce         1    2020     53   31.5     NA  
#>  4 4_3     Norway spruce         1    2020     53   23.5     NA  
#>  5 4_4     Norway spruce         1    2020     53   35.5     NA  
#>  6 4_5     Norway spruce         1    2020     53   13.5     NA  
#>  7 4_6     Norway spruce         1    2020     53   30.5     NA  
#>  8 4_7     Norway spruce         1    2020     53   31.5     NA  
#>  9 4_8     Norway spruce         1    2020     53   24.5     NA  
#> 10 4_9     Norway spruce         1    2020     53   31.5     NA  
#> 11 4_10    Norway spruce         1    2020     53   20.5     NA  
#> 12 4_11    Norway spruce         1    2020     53   33.5     NA  
#> 13 4_12    Norway spruce         1    2020     53   31.5     NA  
#> 14 4_13    Norway spruce         1    2020     53   20.5     NA  
#> # ℹ 7 more variables: crown_base_height_m <dbl>, crown_radius_m <dbl>,
#> #   removal <lgl>, ingrowth <lgl>, n_rep_ha <dbl>, tree_count <dbl>,
#> #   h_m_tree <lgl>

We can also obtain a graphical representation of the inventory plot. The dbh_scale argument draws the stems oversized, which makes them easier to see on a small map:

plot(inv_unit, dbh_scale = 4)

Map of an inventory plot with concentric circles

Two details of this map are worth knowing. The stems are shown in their true positions, but only trees from the second circle outwards carry coordinates at all — for the innermost circle, positions are not recorded. And that innermost circle is not concentric with the other two: it sits five metres north of the inventory point. That is the field convention these data follow, not a defect of the plot.

Evidently, this inventory unit is an object of class fe_ccircle_spatial which represents a sample inventory plot with a concentric circle design. This class is a child of fe_stand_spatial which in turn is a child of fe_stand (see the documentation of ForestElementsR for details about these classes). While it is not required that all elements of an fe_inventory object’s plot column have exactly the same class, as long as they are children of fe_stand, such a mix would be quite unusual and is not recommended. Some evaluations provided by FeNEU are only possible with special subtypes of fe_inventory objects. E.g. increment calculations from two repeated inventories are currently only possible when both inventories exclusively contain plots with a concentric circle design (i.e. fe_ccircle_spatial objects). In the following section, we will demonstrate how field data can be transformed into an fe_inventory object.

3 Importing Data

Field survey exports rarely come in the exact shape an fe_inventory object needs. The import therefore proceeds in two steps, each with its own function per inventory type. The first step, raw to pre, standardises the format; the second, pre to fe_inventory, builds the object. The following sketch shows how the pieces fit together:

  stage          sample inventory, concentric circles    stand-wise inventory, relascope
  -------------------------------------------------------------------------------------
  raw            Format 1            Format 2                 Format 1
                     |                   |                        |
                     v                   v                        v
                    import_sample_concentric_            import_standwise_relascope_
          format1_raw_to_pre() format2_raw_to_pre()      format1_raw_to_pre()
                     |                   |                        |
                     v                   v                        v
  pre             --- BaySF style format ---             Silvarith style format
                  (five tab-delimited files)             (one file, WZP_Daten.txt)
                     |                                            |
                    import_sample_concentric_            import_standwise_relascope_
                    pre_to_fe_inventory()                pre_to_fe_inventory()
                     |                                            |  
                     v                                            v
  fe_inventory   ------------------  fe_inventory  ---------------------

Read the sketch from top to bottom, but note that you may enter it at either level. If your data are already in one of the preprocessed formats, you skip the upper half entirely and start at pre.

The example datasets that come with FeNEU are placed in exactly this scheme. Each of them covers the stages that are useful for demonstrating it, which is why not every set exists at every stage:

Set Inventory type raw pre fe_inventory Scope
ex1 sample, concentric Format 1 10 points
ex2 sample, concentric Format 2 10 points
ex3 sample, concentric yes yes 10 points, plus an earlier survey
ex4 sample, concentric yes yes 100 points, plus an earlier survey
ex5 sample, concentric yes yes 10 simulated points, plus an earlier survey
ex6 stand-wise, relascope Format 1 yes yes 2 stands, 17 points
ex7 stand-wise, relascope yes yes 2 stands, 15 points

The sets that carry an earlier survey (ex3, ex4, ex5) are the ones we use for increment estimation from repeated inventories later on. Data at the fe_inventory stage are ready-made R objects, named like data_ex3_sample_fe_inventory; data at the raw and pre stages are text files, and you obtain the directory holding them with system.file(), as the examples below show. ?data_examples_overview describes every set in detail, down to the individual file.

3.1 From raw data to preprocessed data

The *_raw_to_pre() functions read the field data as tab-delimited text files, check them, and write the preprocessed files into a directory of your choosing. They are called for that side effect, and return a short report telling you how it went.

Let us convert the raw stand-wise example (ex6) that comes with the package:

raw_dir <- system.file("extdata", "data_ex6_standwise_raw", package = "FeNEU")
dir(raw_dir)
#> [1] "EingabedatenGesamt.txt" "Inventuraufnahmen.txt"

pre_dir <- file.path(tempdir(), "ex6_pre")

report <- import_standwise_relascope_format1_raw_to_pre(
  input_path = raw_dir,
  output_dir = pre_dir
)

report$ok
#> [1] TRUE
dir(pre_dir)
#> [1] "WZP_Daten.txt"

For a sample inventory it takes a little more, because the geometry of the concentric circles is not part of the field data — it belongs to the inventory design. It has to be supplied, as a pair of vectors giving the lower DBH threshold and the radius of each circle (as we do below). Alternatively, it can also be provided as a text file. This option is especially useful when differing circle definitions were applied throughout an inventory (see documentation of the import functions). Get it wrong and every figure is wrong with it, since these radii are what the representation factors of the sampled trees are computed from. The values used below mirror the design of inventory example ex1; ?data_examples_overview lists them for the raw example sets. Here is ex1, in raw Format 1:

raw_dir_ex1 <- system.file("extdata", "data_ex1_sample_raw", package = "FeNEU")
dir(raw_dir_ex1)
#> [1] "Baumschicht.txt" "Inv_punkt.txt"   "Verjuengung.txt"

pre_dir_ex1 <- file.path(tempdir(), "ex1_pre")

report_ex1 <- import_sample_concentric_format1_raw_to_pre(
  input_path           = raw_dir_ex1,
  output_dir           = pre_dir_ex1,
  small_trees_filename = "Verjuengung", # optional
  dbh_cm_from          = c(0, 12, 30, 48),
  radiuses_m           = c(2.82, 5.64, 11.28, 17.84),
  coord_sys            = "gk4",
  species_guess        = TRUE
)

report_ex1$ok
#> [1] TRUE
dir(pre_dir_ex1)
#> [1] "fcbestku.txt" "fdinvba.txt"  "fdinvbhd.txt" "fdinvkrs.txt" "fdvikrs.txt"

Note what the function has to say about these data:

report_ex1$warnings
#> [1] "Stand-age data are missing or incomplete. Analyses that need the (stand) age are therefore not possible (see the function documentation)."            
#> [2] "4 species abbreviation(s) were matched only by fuzzy/approximate rules and should be checked manually (see report$species_mapping, tier == 'review')."

Neither remark is fatal, and both are worth having. The first tells us that we will not be able to run some age-based evaluation on this inventory, especially increment estimates with yield tables or single tree growth functions (see vignette Increment Estimation). Age information might be incomplete due to errors, but also due to uneven-aged stands where an age is not defined. The second is a consequence of species_guess = TRUE, which lets the import resolve species abbreviations it does not know verbatim: four of them were matched only by approximate rules, and report_ex1$species_mapping shows which, so that the guesses can be checked rather than trusted. We recommend to use the species coding supported by the data format of choice instead of species namme abbreviations that have to be guessed afterwards. The report is a list with the elements ok, errors, warnings and paths. It is worth reading rather than discarding: ok is FALSE whenever the data could not be converted, and warnings is where the functions tell you what they had to do with your data in order to proceed.

Three properties of this step deserve mention, because they are what makes the difference between an import that works on real-world data and one that only works on tidy data:

Plausibility checks. The raw data import is the place where the data are scrutinised, and it distinguishes carefully between two kinds of problem. A defect that invalidates a whole inventory point — a duplicated plot identifier, or a plot whose centre coordinate is missing — stops the import immediately and with a clear message, because there is no defensible way to carry on. A defect confined to a single tree — a tree above the innermost circle that carries no position, a tree with only half a position, a living tree without a stand layer — leads to that one tree being excluded or its position dropped, while the rest of the plot survives. Whenever this happens, the affected trees are named individually in the warnings, by tree number where one exists and by DBH otherwise, so that you can look them up in your own data. What FeNEU never does is guess: the package does not invent a plausible value to keep going quietly.

Encoding robustness. Field data arrive in whatever encoding the exporting program happened to use, and the difference between UTF-8 and ISO-8859-1 usually shows up in exactly the places that matter — species abbreviations and column names carrying umlauts. Every import function takes an encoding argument that defaults to "auto", and the species and header matching additionally recognises the characteristic garbling that results when UTF-8 text is read as Latin-1, and repairs it.

Decimal separator robustness. German-language exports write decimals with a comma. Reading such a file with automatic type detection silently turns a DBH of 23,4 into 234, which is a data error no plausibility check further downstream can recognise as one. FeNEU therefore reads every input file as plain text and converts only those columns it actually uses, tolerant of both the comma and the point.

3.2 From preprocessed data to an fe_inventory object

The second step reads the preprocessed files (tab-delimited text) and assembles the fe_inventory object, which includes validating it. Continuing with the files we have just written:

inv_ex6 <- import_standwise_relascope_pre_to_fe_inventory(pre_dir)

inv_ex6
#> # A tibble: 17 × 3
#>    plot_id        plot       area_rep_ha
#>  * <chr>          <list>           <dbl>
#>  1 1_7- 2- 0- - 0 <fe_stand>       0.416
#>  2 1_7- 5- 0- - 0 <fe_stand>       0.546
#>  3 2_7- 2- 0- - 0 <fe_stand>       0.416
#>  4 2_7- 5- 0- - 0 <fe_stand>       0.546
#>  5 3_7- 2- 0- - 0 <fe_stand>       0.416
#>  6 3_7- 5- 0- - 0 <fe_stand>       0.546
#>  7 4_7- 2- 0- - 0 <fe_stand>       0.416
#>  8 4_7- 5- 0- - 0 <fe_stand>       0.546
#>  9 5_7- 2- 0- - 0 <fe_stand>       0.416
#> 10 5_7- 5- 0- - 0 <fe_stand>       0.546
#> 11 6_7- 2- 0- - 0 <fe_stand>       0.416
#> 12 6_7- 5- 0- - 0 <fe_stand>       0.546
#> 13 7_7- 2- 0- - 0 <fe_stand>       0.416
#> 14 7_7- 5- 0- - 0 <fe_stand>       0.546
#> 15 8_7- 2- 0- - 0 <fe_stand>       0.416
#> 16 8_7- 5- 0- - 0 <fe_stand>       0.546
#> 17 9_7- 5- 0- - 0 <fe_stand>       0.546

For a sample inventory it works exactly the same way. The package ships the preprocessed files of example ex3 in BaySF style format:

pre_path <- system.file("extdata", "data_ex3_sample_pre", package = "FeNEU")
dir(pre_path)
#> [1] "fcbestku.txt" "fdinvba.txt"  "fdinvbhd.txt" "fdinvkrs.txt" "fdvikrs.txt"

fe_inv <- import_sample_concentric_pre_to_fe_inventory(pre_path)

fe_inv
#> # A tibble: 10 × 3
#>    plot_id              plot       area_rep_ha
#>  * <chr>                <list>           <dbl>
#>  1 900000002_1_99900002 <f_ccrcl_>        2.97
#>  2 900000015_1_99900015 <f_ccrcl_>        5.14
#>  3 900000020_1_99900020 <f_ccrcl_>        4.43
#>  4 900000029_1_99900029 <f_ccrcl_>        4.69
#>  5 900000043_1_99900043 <f_ccrcl_>        3.97
#>  6 900000046_1_99900046 <f_ccrcl_>        7.05
#>  7 900000061_1_99900061 <f_ccrcl_>        6.98
#>  8 900000075_1_99900075 <f_ccrcl_>        3.08
#>  9 900000086_1_99900086 <f_ccrcl_>        3.80
#> 10 900000100_1_99900100 <f_ccrcl_>        4.63

The result is the very object that FeNEU also ships ready-made as data_ex3_sample_fe_inventory, and which we reuse in the workflows below:

isTRUE(all.equal(fe_inv, data_ex3_sample_fe_inventory))
#> [1] TRUE

Note that we did not have to tell the function anything about the coordinate reference system. Preprocessed data in BaySF style format declare their own coordinate system, and FeNEU uses that declaration. Should your files not carry it, the function will ask you for it explicitly through its coord_sys argument rather than assuming one — an assumption about coordinates is a particularly expensive way to be wrong.

3.3 A one-step shortcut for experienced users

For those cases in which the inventory type is not known until run time — a graphical user interface built on top of FeNEU is the typical example — the package also offers read_and_convert_data(). It covers the pre to fe_inventory step for all inventory types and dispatches on its inventory_type argument:

fe_inv_2 <- read_and_convert_data(pre_path, inventory_type = "sample_concentric")

isTRUE(all.equal(fe_inv_2, fe_inv))
#> [1] TRUE

This is an expert function, and the two-step route shown above remains the recommended one for everyday work: it names the inventory type in the function you call, which makes both your script and its error messages easier to read.

4 Status-quo Evaluation

A status-quo evaluation describes what an inventory currently holds: stem number, basal area and standing volume, broken down by species group and by size or age. FeNEU offers two such evaluations, base tables and structure tables, and produces each of them in three separate steps:

  aggregation          restructuring             rendering
  base_table_*()   ->  output_base_table()  ->   output_base_table_pdf()
  structure_table_*()  output_structure_table()  output_structure_table_pdf()

  the numbers          the table layout          the formatted report
  (R objects)          (an R data frame)         (a PDF file)

This separation is deliberate, and it matters more than it may look. The figures are computed first and handed back as plain R objects, so that you can work with them further — check them, combine them, feed them into your own graphics or into a report of your own making. Nothing forces you through the PDF, and the intermediate table stays language-neutral; only the last step commits to a language and a layout. If all you want is a number, you never have to render anything.

All evaluation functions work on a flat tree table rather than on the nested fe_inventory object. Preparing that table is the one step every evaluation in the rest of this vignette shares:

# The h_q fallback warning is expected here: a few species x layer groups in the
# example data carry no measured height, so their mean height is modelled.
trees_with_heights <- suppressWarnings(
  data_ex3_sample_fe_inventory |>
    pull_trees() |>
    height_complete_inventory()
)

trees <- data_ex3_sample_fe_inventory |>
  fill_heights_back(trees_with_heights) |>
  pull_trees() |>
  trees_add_essentials(method = "BaySF")
#>   |                                                                              |                                                                      |   0%  |                                                                              |=========                                                             |  12%  |                                                                              |==================                                                    |  25%  |                                                                              |==========================                                            |  38%  |                                                                              |===================================                                   |  50%  |                                                                              |============================================                          |  62%  |                                                                              |====================================================                  |  75%  |                                                                              |=============================================================         |  88%  |                                                                              |======================================================================| 100%

trees
#> # A tibble: 139 × 23
#>    plot_id     area_rep_ha layer_key species_id    species_group tree_id time_yr
#>    <chr>             <dbl>     <dbl> <bavrn_state> <bav_st_shrt> <chr>     <dbl>
#>  1 900000002_…        2.97         4 Scots pine    pine          3_1        2020
#>  2 900000002_…        2.97         1 Norway spruce spruce        4_1        2020
#>  3 900000002_…        2.97         1 Norway spruce spruce        4_2        2020
#>  4 900000002_…        2.97         1 Norway spruce spruce        4_3        2020
#>  5 900000002_…        2.97         1 Norway spruce spruce        4_4        2020
#>  6 900000002_…        2.97         1 Norway spruce spruce        4_5        2020
#>  7 900000002_…        2.97         1 Norway spruce spruce        4_6        2020
#>  8 900000002_…        2.97         1 Norway spruce spruce        4_7        2020
#>  9 900000002_…        2.97         1 Norway spruce spruce        4_8        2020
#> 10 900000002_…        2.97         1 Norway spruce spruce        4_9        2020
#> # ℹ 129 more rows
#> # ℹ 16 more variables: age_yr <dbl>, age_class <ord>, dbh_cm <dbl>,
#> #   dbh_class <ord>, height_m <dbl>, crown_base_height_m <dbl>,
#> #   crown_radius_m <dbl>, removal <lgl>, ingrowth <lgl>, n_rep_ha <dbl>,
#> #   h_m_tree <lgl>, dbh_trshld <dbl>, g_m2 <dbl>, v_m3 <dbl>, v_hub_m3 <dbl>,
#> #   standing_area_m2 <dbl>

Three things happen here. pull_trees() unnests the inventory into one row per tree. height_complete_inventory() estimates the heights of all trees that were not measured — in a typical inventory that is the large majority — and fill_heights_back() writes those estimates into the inventory object, so that the second pull_trees() delivers a tree table whose heights are complete. Finally trees_add_essentials() derives what the evaluations need: basal area, single-tree volume, species groups and size classes.

The detour through fill_heights_back() and a second pull_trees() is worth keeping to. It leaves the heights in the inventory object rather than in a loose table, and it is the form the rest of the package expects.

4.1 Base tables

A base table breaks the inventory down by species group and by one class variable, for which there is a choice: age classes or mean diameter classes. Which of the two to use is not a matter of taste. Age classes are the traditional grouping and work well as long as stands are reasonably even aged, so that a single stand age is a meaningful statement. In an enterprise that consists largely of structurally rich, uneven-aged stands, it is not: there is no age one could sensibly assign to such a stand. Mean diameter classes describe those forests better, because a diameter can always be measured. This is why FeNEU offers both throughout.

The second choice concerns the tree cohort: the whole inventory collective, or the main stand only. This distinction runs through the entire package:

The difference is not merely one of a filter. If per-hectare figures of species, age classes and other cohorts are desired, it is necessary to estimate the area share covered by the cohort of interest. This is only meaningful for the main stand. The all-layers tables therefore report absolute figures but no per-hectare values, while the main stand tables report both. Here is the main stand of ex3 by age class:

bt <- base_table_age_class_main_stand(trees)

names(bt)
#> [1] "detail"      "total"       "all_species" "all_total"

The result is a list of four data frames, and they are the levels of aggregation the printed table is later assembled from: detail holds each species group by class, total each species group across all classes, all_species each class across all species groups, and all_total the grand total. Confidence intervals and mean diameters cannot be summed, which is why these levels are computed rather than added up:

bt$detail |>
  select(species_group, age_class, v_hub_m3, n, d_q_cm, n_plot, conf_95_per)
#> # A tibble: 18 × 7
#> # Groups:   species_group, age_class [18]
#>    species_group  age_class v_hub_m3       n d_q_cm n_plot conf_95_per
#>    <bav_st_shrt>  <ord>        <dbl>   <dbl>  <dbl>  <int>       <dbl>
#>  1 spruce         (20,40]      151.  86594.    3.72      1         NA 
#>  2 spruce         (40,60]      150.     99.6  43.5       1         NA 
#>  3 spruce         (60,80]       63.5    88.8  31.5       1         NA 
#>  4 spruce         (80,100]    1827.   1496.   35.6       3        165.
#>  5 spruce         (140,160]    553.    672.   28.1       1         NA 
#>  6 spruce         (160,999]   2071.   1112.   41.0       1         NA 
#>  7 pine           (60,80]     1653.   2076.   32.9       2        212.
#>  8 pine           (80,100]    2000.   2206.   33.5       3        288.
#>  9 pine           (100,120]    431.    309.   39.8       1         NA 
#> 10 pine           (160,999]   1167.    648.   43.9       1         NA 
#> 11 Douglas fir    (80,100]     173.     76.0  48.5       1         NA 
#> 12 beech          (80,100]     143.     61.9  51.5       1         NA 
#> 13 oak            (0,20]         0    2793.    5.5       1         NA 
#> 14 oak            (40,60]       54.8    99.6  30.5       1         NA 
#> 15 other hardwood (0,20]       168.   5587.   10.5       1         NA 
#> 16 other hardwood (40,60]      226.    498.   28.5       1         NA 
#> 17 other hardwood (80,100]     124.     61.9  47.5       1         NA 
#> 18 noble hardwood (40,60]      104.     99.6  35.5       1         NA

Two figures in this output deserve a comment, because both say something about reading inventory results.

The stem number of the youngest age class looks enormous next to the others. Extreme values often come with very small sample sizes as we have here (our demo example comprises ten plots only). As we can see in the n_plot column, the stem number and the other variables in the age class are upscaled from one single plot only. However, despite being very high, such a number is not impossible on a single plot. The trees are very small on average, and per hectare they amount to some 14 000 stems, which is a possible density for small trees coming from natural regeneration.

The column conf_95_per is the more important one. It gives the width of the 95 % confidence interval of the volume, as a percentage of the volume itself, and in this example it is wide, clearly due to the unrealistically small demo sample size of just ten plots. For the inventory as a whole it comes to roughly ± 70 %, and for individual cells it is even wider. Clearly, it cannot be computed if there is only one plot (therefore the NA values), and for small samples its value is itself imprecise. Typical sample inventories comprise several hundred to several thousand inventory points. The package also ships a hundred-point example, data_ex4_sample_fe_inventory, and the same figure there is about ± 14 %.

Nothing about the method changes between the two — only the sample size. It is worth keeping in view whenever an inventory is broken down into ever finer classes: every subdivision spends precision, and the confidence interval is where you can watch it being spent.

output_base_table() turns the four into the one table that gets printed:

bt |>
  output_base_table() |>
  head(12)
#> # A tibble: 12 × 12
#>    species_group variable      `(0,20]` `(20,40]` `(40,60]` `(60,80]` `(80,100]`
#>    <bav_st_shrt> <chr>            <dbl>     <dbl>     <dbl>     <dbl>      <dbl>
#>  1 spruce        v_hub_m3            NA    151.     150.       63.5      1827.  
#>  2 spruce        g_m2                NA     94.3     14.8       6.92      149.  
#>  3 spruce        n                   NA  86594.      99.6      88.8      1496.  
#>  4 spruce        d_q_cm              NA      3.72    43.5      31.5        35.6 
#>  5 spruce        h_q_m               NA      9.60    27        23.5        33.1 
#>  6 spruce        n_plot              NA      1        1         1           3   
#>  7 spruce        conf_95_per         NA     NA       NA        NA         165.  
#>  8 spruce        area_ha             NA      6.05     0.509     0.257       5.77
#>  9 spruce        v_hub_m3_ha         NA     25.0    294.      247.        316.  
#> 10 spruce        g_m2_ha             NA     15.6     29.0      27.0        25.7 
#> 11 spruce        n_ha                NA  14316.     195.      346.        259.  
#> 12 spruce        tot_area_cla…       NA      6.05     3.97     11.5        14.9 
#> # ℹ 5 more variables: `(100,120]` <dbl>, `(120,140]` <dbl>, `(140,160]` <dbl>,
#> #   `(160,999]` <dbl>, total <dbl>

For the diameter variant, the class limits are prepared once by back_table_dclass() and passed in:

d_classes <- back_table_dclass(trees)

base_table_d_q_class_main_stand(trees, dclass_back = d_classes) |>
  output_base_table() |>
  head(8)
#> # A tibble: 8 × 10
#>   species_group variable    `(0,10]` `(10,20]` `(20,30]` `(30,40]` `(40,50]`
#>   <bav_st_shrt> <chr>          <dbl>     <dbl>     <dbl>     <dbl>     <dbl>
#> 1 spruce        v_hub_m3      151.          NA   1770.     2745.     150.   
#> 2 spruce        g_m2           94.3         NA    143.      201.      14.8  
#> 3 spruce        n           86594.          NA   1434.     1934.      99.6  
#> 4 spruce        d_q_cm          3.72        NA     35.6      36.4     43.5  
#> 5 spruce        h_q_m           9.60        NA     33.4      35.8     27    
#> 6 spruce        n_plot          1           NA      2         3        1    
#> 7 spruce        conf_95_per    NA           NA    232.      373.      NA    
#> 8 spruce        area_ha         6.05        NA      5.51      3.22     0.509
#> # ℹ 3 more variables: `(50,60]` <dbl>, `(60,999]` <dbl>, total <dbl>

The final step writes the formatted report. It needs the PDF toolchain of section 1.2, and it is the only step that does:

bt |>
  output_base_table() |>
  output_base_table_pdf(
    tab_title  = "Example forest enterprise",
    output_dir = tempdir(),
    inventory  = data_ex3_sample_fe_inventory
  )

The table describes itself: which cohort and which class variable it holds is recorded when the table is built and travels with it, so the renderer names the file accordingly and states the cohort on the page. Passing inventory adds the survey year, the total area and the number of inventory points to the header.

4.2 Structure tables

A structure table adds one further level to the base table: within each species group and class, the trees are additionally broken down by the diameter of the individual tree. Where the base table states that a species group holds so much volume in a given age class, the structure table shows how that volume is distributed over the diameter range.

The functions mirror the base tables exactly, in both choices, so everything said above applies unchanged:

st <- structure_table_age_class_main_stand(trees)

st |>
  output_structure_table() |>
  head(10)
#> # A tibble: 10 × 17
#> # Groups:   species_group, age_class [1]
#>    species_group age_class variable     `(0,5]` `(5,10]` `(10,15]` `(15,20]`
#>    <bav_st_shrt> <ord>     <chr>          <dbl>    <dbl>     <dbl>     <dbl>
#>  1 spruce        (20,40]   v_hub_m3        0      41.5     110.           NA
#>  2 spruce        (20,40]   g_m2           42.8    22.5      29.0          NA
#>  3 spruce        (20,40]   n           78214.   5587.     2793.           NA
#>  4 spruce        (20,40]   d_q_cm          2.64    7.16     11.5          NA
#>  5 spruce        (20,40]   h_q_m           7.88   11.1      11            NA
#>  6 spruce        (20,40]   n_plot          1       1         1            NA
#>  7 spruce        (20,40]   conf_95_per    NA      NA        NA            NA
#>  8 spruce        (20,40]   area_ha         5.27    0.463     0.311        NA
#>  9 spruce        (20,40]   v_hub_m3_ha     0      89.5     353.           NA
#> 10 spruce        (20,40]   g_m2_ha         8.11   48.5      93.4          NA
#> # ℹ 10 more variables: `(20,25]` <dbl>, `(25,30]` <dbl>, `(30,35]` <dbl>,
#> #   `(35,40]` <dbl>, `(40,45]` <dbl>, `(45,50]` <dbl>, `(50,55]` <dbl>,
#> #   `(55,60]` <dbl>, `(60,999]` <dbl>, total <dbl>

and, as before, output_structure_table_pdf() renders it.

One restriction is worth stating clearly, because it is a matter of meaning rather than of technique. Structure tables are built for inventories that carry individual tree sizes. Currently, only the sample inventory formats we support provide that information. Stand-wise inventories can in principle provide it as well, but the formats we support convey mean tree sizes only. Structure tables will also run on such data, but the result should not be used. The diameter breakdown would then be an artefact of a single mean value, and would suggest a structure that was never measured. FeNEU warns when it suspects this case.

5 Increment Estimation

Volume increment cannot be read off an inventory the way standing volume can. FeNEU estimates it in three ways, and which of them is available depends on the data at hand:

Data required Reports
Repeated survey two surveys of the same plots any tree collective, with a confidence interval
BWI 3 growth functions one survey, with tree ages any tree collective
Yield tables one survey, with tree ages main stand only

The repeated survey is the most informative way: it rests on measurements taken twice, so the increment is observed rather than modelled, and it is the only one of the three that yields a confidence interval. It is currently supported for sample inventories only. The other two use models, and both need the age of the trees/stands. Where an inventory carries no or incomplete ages — which happens, and is legitimate, since age is an optional attribute — these two are not available, and entering a pragmatic figure might be a workaround.

The three are treated in detail in the vignette Increment Estimation. This section shows what each of them looks like from the outside.

5.1 Increment from a repeated survey

The package bundles the two ex3 surveys — the one used above and an earlier one — together with their prepared tree tables, so the example starts where section 4 left off:

inc <- inv_increment_repeated_survey(
  inv_1st       = data_ex3_previous_sample_fe_inventory,
  inv_2nd       = data_ex3_sample_fe_inventory,
  inv_1st_trees = data_ex3_previous_sample_trees_essentials,
  inv_2nd_trees = data_ex3_sample_trees_essentials,
  method        = "rep_classic",
  fill_option   = "standard",
  progress_bar  = FALSE
)

inc
#> <fe_increment_repsurv>
#>   Surveys      : 2009 -> 2020  (period 11 yr)
#>   Area / plots : 46.7 ha / 10
#>   Method       : rep_classic / fill: standard
#>   Trees        : 139
#>   Increment    : 9.57 m3/ha/yr (summaric balance)
#>   Steps kept   : no
#>   Use increment_base_table() / output_increment_overall() on this object.

One call covers the whole chain: the plots of the two surveys are matched, the trees within them are matched, an increment is derived for every tree, and the trees that could not be matched are either ingrown or removed/mortal and are dealt with accordingly. Each step is also available as a function of its own; the increment vignette lists them. For most purposes this single call is sufficient.

Two arguments deserve attention. method selects how a tree’s increment is derived from two measurements — there are four variants, and they differ in how they treat trees that changed sample circle between the surveys. fill_option governs what happens to trees that could not be matched at all. Both are subjects of the Increment Estimation vignette.

The result carries everything the later steps need:

names(inc)
#> [1] "trees"    "overview" "summaric" "meta"     "steps"

trees holds the per-tree increments, overview the aggregation by plot and enterprise, summaric the increment balance of the enterprise, meta the figures the reports print in their header, and steps the intermediate results of the chain, for anyone who wants to look inside.

From here there are two directions. Broken down by species group and class, in the layout of the status-quo tables:

increment_base_table_main_stand(inc, by_class = "age") |>
  output_increment_base_table() |>
  head(10)
#> # A tibble: 10 × 12
#>    species_group variable      `(0,20]` `(20,40]` `(40,60]` `(60,80]` `(80,100]`
#>    <bav_st_shrt> <chr>            <dbl>     <dbl>     <dbl>     <dbl>      <dbl>
#>  1 spruce        n_plot_class        NA     1         2         1           3   
#>  2 spruce        n_iv_total          NA     7        14         1          21   
#>  3 spruce        n_iv_filled         NA     7        14         0          10   
#>  4 spruce        iv_m3_yr_tot…       NA    12.0      34.4       5.77       64.7 
#>  5 spruce        ci95_iv_m3_y…       NA    25.7      69.3      13.2        92.0 
#>  6 spruce        iv_m3_ha_yr         NA     0.256     0.737     0.124       1.39
#>  7 spruce        ci95_iv_m3_h…       NA     0.549     1.48      0.282       1.97
#>  8 spruce        area_ha             NA     6.05      3.48      0.198       5.78
#>  9 spruce        iv_m3_ha_yr_…       NA     1.98      9.89     29.2        11.2 
#> 10 pine          n_plot_class        NA    NA        NA         2           3   
#> # ℹ 5 more variables: `(100,120]` <dbl>, `(120,140]` <dbl>, `(140,160]` <dbl>,
#> #   `(160,999]` <dbl>, total <dbl>

Or as the enterprise-level summary, which is the figure to quote when the question is what the whole forest grows:

ovr <- output_increment_overall(inc)

ovr$table_combined |>
  select(species_group, is_total_row, iv_m3_ha_yr_st, iv_m3_ha_yr_sum)
#> # A tibble: 8 × 4
#>   species_group  is_total_row iv_m3_ha_yr_st iv_m3_ha_yr_sum
#>   <bav_st_shrt>  <lgl>                 <dbl>           <dbl>
#> 1 spruce         FALSE                3.94            4.15  
#> 2 pine           FALSE                4.81            4.80  
#> 3 larch          FALSE                0.139           0.130 
#> 4 Douglas fir    FALSE                0.168           0.168 
#> 5 beech          FALSE                0.115           0.115 
#> 6 oak            FALSE                0.0476          0.0476
#> 7 other hardwood FALSE                0.503           0.532 
#> 8 NA             TRUE                 9.35            9.57

The last row is the enterprise as a whole. The two columns are two ways of arriving at it, and they differ slightly, here 9.35 against 9.57 m³ per hectare and year: _st adds up the increments of the individual trees, _sum is the balance of the two surveys over the whole measured population — standing volume of the second survey, minus that of the first, plus what was removed in between. For the enterprise level, that balance is the reference figure; the per-tree route is what allows the increment to be broken down into species and classes at all.

Besides table_combined, which covers all inventory points, the result holds table_matched for the points measured in both surveys and table_2nd_only for those the later survey added, plus a meta element for the report header. The confidence interval is reported in table_matched — for this example 9.80 ± 2.29 m³ per hectare and year. It is derived from the scatter between the inventory points, which requires an inventory point measured twice; the points the later survey added contribute an estimate instead, so the combined table carries no confidence interval.

Both have their *_pdf() counterpart, output_increment_base_table_pdf() and output_increment_overall_pdf(), and both follow the three-step pattern of section 4.

5.2 Increment from a single survey

Where only one survey exists, the increment has to be estimated from the current state of the trees. FeNEU offers two independent approaches.

The growth functions of the third German National Forest Inventory (BWI 3) (Riedel et al. 2017) estimate the increment of each tree from its own dimensions and age. They yield the same per-tree quantity the repeated survey produces, which is why the same aggregation functions apply from there on. inv_increment_gnfi3() covers the chain and returns the same kind of object as inv_increment_repeated_survey():

inc_bwi3 <- inv_increment_gnfi3(
  inv       = data_ex3_sample_fe_inventory,
  inv_trees = data_ex3_sample_trees_essentials,
  dt        = 5
)

inc_bwi3
#> <fe_increment_gnfi3>
#>   Survey       : 2020 
#>   Area / plots : 46.7 ha / 10
#>   Method       : BWI 3 growth functions, single inventory
#>   Trees        : 108 
#>   Increment    : 7.33 m3/ha/yr
#>   Steps kept   : no 
#>   Use increment_base_table() on this object.

dt is the span the estimate covers, in years. A positive value looks forward, a negative one back; five years is a deliberate default rather than a number worth changing lightly, since the further such an estimate reaches, the more it is affected by removals, mortality and ingrowth it cannot know about.

From here the routes of section 5.1 apply unchanged — the class tables, and the species-group overview:

increment_base_table(inc_bwi3, by_class = "age") |>
  output_increment_overview_gnfi3()
#> # A tibble: 9 × 4
#>   species_group  is_total_row zv_m3_yr zv_m3_ha_yr
#>   <bav_st_shrt>  <lgl>           <dbl>       <dbl>
#> 1 spruce         FALSE          163.        3.49  
#> 2 pine           FALSE          118.        2.53  
#> 3 larch          FALSE            7.35      0.157 
#> 4 Douglas fir    FALSE            5.91      0.126 
#> 5 beech          FALSE            3.88      0.0829
#> 6 oak            FALSE            6.92      0.148 
#> 7 other hardwood FALSE           34.0       0.728 
#> 8 noble hardwood FALSE            3.46      0.0741
#> 9 NA             TRUE           343.        7.33

This is an estimate for the forest evaluated with the repeated survey above, arrived at independently and from one survey alone: 7.3 against 9.4 m³ per hectare and year. Holding the two against each other is one of the reasons to have both.

Since the estimate ages every tree, it needs the age of every tree. Where a stand layer carries no ages — which happens, and is legitimate — the function stops rather than evaluating the part of the population that happens to have one. If ages exist for the main stand only, restricting the tree list to it beforehand (dplyr::filter(trees, layer_key == 1)) yields a main-stand evaluation of the whole enterprise; the reference area stays the full inventory area.

Yield tables take the other route. They work at stand level rather than on individual trees, which requires the area to be split into the shares of virtual monospecific stands. Unlike the other two, this approach therefore applies to the main stand only. FeNEU uses the yield table system of ForestElementsR, described in its own vignette (vignette("yield_tables", package = "ForestElementsR")).

inc_yt <- inv_increment_ytables(
  inv              = data_ex3_sample_fe_inventory,
  inv_trees        = data_ex3_sample_trees_essentials,
  ytable_selection = ytables_bavrn_state_var_1_feneu
)

inc_yt
#> <fe_increment_ytables>
#>   Survey       : 2020 
#>   Area / plots : 46.7 ha / 10
#>   Method       : yield tables, single inventory
#>   Collective   : Hauptbestand 
#>   Cohorts      : 22 (plot x species)
#>   Increment    : 4.19 m3/ha/yr
#>   Use increment_ytables_base_table() on this object.

inc_yt$overview
#> # A tibble: 9 × 7
#>   species_group  area_ha site_index zv_m3_ha_yr zv_m3_yr is_total_row
#>   <bav_st_shrt>    <dbl>      <dbl>       <dbl>    <dbl> <lgl>       
#> 1 spruce          15.3        0.895        6.58   101.   FALSE       
#> 2 pine            21.6        0.729        3.12    67.2  FALSE       
#> 3 Douglas fir      0.687      1.36         2.70     1.85 FALSE       
#> 4 beech            0.812      2.48         3.61     2.93 FALSE       
#> 5 oak              0.610      3            4.99     3.04 FALSE       
#> 6 other hardwood   4.24       1.30         3.84    16.3  FALSE       
#> 7 noble hardwood   0.563     -0.531        6.78     3.81 FALSE       
#> 8 NA               2.97      NA           NA       NA    FALSE       
#> 9 NA              46.7       NA            4.19   196.   TRUE        
#> # ℹ 1 more variable: is_no_main_stand_row <lgl>

The estimate is made for each cohort: one species in the main stand of one inventory plot, on the virtual area it occupies there. Its age and mean height give the site index, age and site index give the increment the yield table holds, and that figure is corrected by the stocking level — the cohort’s basal area over the basal area the table expects. A cohort has one age and one mean diameter, so grouping a report by age class or by diameter class labels the same estimate rather than changing it.

The overview reads accordingly: area_ha is the virtual area of each species group, site_index its mean site index, and zv_m3_ha_yr the increment on that virtual area. The total row is not an average of those figures — it relates the whole increment to the entire inventory area, which is the larger reference. Its site_index is empty because the index scales of different yield tables cannot be averaged. The row without a species group carries the area of inventory points that hold no living main stand, so that the areas add up to the inventory total.

ytables_bavrn_state_var_1_feneu assigns a yield table to each species code. It is the set FeNEU ships, not a fixed part of the method — the assignment can be replaced by one of your own, built from the yield tables ForestElementsR provides. The Increment Estimation vignette shows how, and what the choice implies.

6 Useful tools

Beside the chains of the previous chapters, FeNEU holds a few functions that are useful in their own right.

6.1 Reaching into an inventory

Section 4 used pull_trees() to get at the trees of an fe_inventory. Three further functions pull the other parts of it into a flat table: pull_centers() the plot centre coordinates, pull_circle_definitions() the circle geometry of each plot, and pull_sums() the stand sums and mean values per plot and layer.

pull_centers(data_ex3_sample_fe_inventory)
#> Simple feature collection with 10 features and 1 field
#> Geometry type: POINT
#> Dimension:     XY
#> Bounding box:  xmin: 729780 ymin: 5427422 xmax: 752949 ymax: 5457197
#> Projected CRS: ETRS89 / UTM zone 32N
#>                 plot_id               geometry
#> 1  900000002_1_99900002 POINT (729780 5427422)
#> 2  900000015_1_99900015 POINT (735935 5438876)
#> 3  900000020_1_99900020 POINT (742855 5438138)
#> 4  900000029_1_99900029 POINT (744306 5437209)
#> 5  900000043_1_99900043 POINT (752853 5457197)
#> 6  900000046_1_99900046 POINT (748622 5431646)
#> 7  900000061_1_99900061 POINT (752949 5445687)
#> 8  900000075_1_99900075 POINT (745965 5453085)
#> 9  900000086_1_99900086 POINT (742935 5443759)
#> 10 900000100_1_99900100 POINT (749610 5435061)

6.2 The figures that describe an inventory

inventory_meta() returns the survey year, the inventory area and the number of plots. These are the three figures the PDF reports print below their title, and they are read off the inventory object itself, so they also cover plots that carry no trees:

inventory_meta(data_ex3_sample_fe_inventory)
#> $year
#> [1] 2020
#> 
#> $area_ha
#> [1] 46.73489
#> 
#> $n_plots
#> [1] 10

get_inv_year() returns the survey year alone. For a pair of surveys, inv_period() adds the period between them — the reference period of a repeated-survey increment:

inv_period(data_ex3_previous_sample_fe_inventory, data_ex3_sample_fe_inventory)
#> $time_yr_1st
#> [1] 2009
#> 
#> $time_yr_2nd
#> [1] 2020
#> 
#> $period_yr
#> [1] 11

6.3 A sheet for a single inventory plot

plot_info_sheet_pdf() renders one inventory plot as a PDF sheet. It takes a single plot rather than the whole inventory, so it is applied to an element of the plot column:

data_ex3_sample_fe_inventory$plot[[1]] |>
  plot_info_sheet_pdf(output_dir = tempdir())

Like every *_pdf() function it needs the toolchain of section 1.2. dbh_scale oversizes the tree symbols, which keeps them visible on a plot drawn to scale.

6.4 Writing a circle definition

The concentric importers of section 3 require a circle definition. Where none is at hand, generate_circle_definition() writes one:

generate_circle_definition(
  output_dir  = tempdir(),
  dbh_cm_from = c(0, 12, 30),
  radiuses_m  = c(2.82, 5.64, 12.62)
)

It produces a uniform definition — the same circles for every plot. There is deliberately no default geometry: the circle sizes determine the representation factors and therefore every per-hectare figure that follows. An inventory whose plots fall into several circle classes needs a file written by hand, with one fe_ikl per class.

6.5 Standard errors of your own aggregations

Inventory plots represent different areas, so a mean over them is a ratio estimator rather than an arithmetic mean. se_area_weighted() computes it together with its standard error and confidence interval, from the per-hectare values of the plots and the areas they represent. It is what the confidence intervals of the increment reports rest on, and it is exported so that the same figure can be obtained for an aggregation of your own. se_area_weighted_grouped() does the same within groups.

6.6 A species key for the field

FeNEU attaches ForestElementsR, so its functions are available directly after library(FeNEU). One of them is worth a section of its own here, because it answers a question that comes up on every survey: which code stands for which species. fe_species_get_field_table() returns a coding as a compact key, one row per code, with the scientific, English and German name beside it:

fe_species_get_field_table("bavrn_state") |> head(10)
#> # A tibble: 10 × 6
#>    species_id name_sci                   name_eng         name_ger level is_tree
#>    <chr>      <chr>                      <chr>            <chr>    <int> <lgl>  
#>  1 10         Picea abies                Norway spruce    Fichte       0 TRUE   
#>  2 11         Picea omorika              Serbian spruce   Omorika…     0 TRUE   
#>  3 12         Picea sitchensis           Sitka spruce     Sitka-F…     0 TRUE   
#>  4 20         Pinus sylvestris           Scots pine       Kiefer       0 TRUE   
#>  5 21         Pinus strobus              eastern white p… Strobe       0 TRUE   
#>  6 22         Pinus nigra                black pine       Schwarz…     0 TRUE   
#>  7 23         Pinus mugo subsp. uncinata Swiss mountain … Spirke       0 TRUE   
#>  8 24         Pinus cembra               Arolla pine      Zirbe        0 TRUE   
#>  9 25         Pinus mugo subsp. mugo     dwarf maountain… Latsche      0 TRUE   
#> 10 30         Abies alba                 silver fir       Tanne        0 TRUE

This is the view to print for field work, and it is not the same as the full coding table: that one carries one row per elementary species, so a group code appears once per member species. All three name columns are returned whatever options("fe_spec_lang") is set to. The rows come in the coding’s own order, the finest codes first and the group codes after; level says which is which (0 = finest) and is_tree marks the categories that are not trees.

The function works for every coding ForestElementsR implements, so the key can be produced for whichever coding an inventory is recorded in.

6.7 Reference data

The species codings and the yield tables themselves belong to ForestElementsR; see vignette("tree_species_codings", package = "ForestElementsR") and vignette("yield_tables", package = "ForestElementsR").

FeNEU adds one dataset of its own, ytables_bavrn_state_var_1_feneu: the yield-table assignment of section 5.2, which covers all current bavrn_state species codes.

?data_examples_overview lists the example datasets used throughout this vignette, at all three levels of processing.

Acknowledgments

The authors would like to thank the Bavarian Ministry for Nutrition, Agriculture, Forestry, and Tourism for funding the projects FeNEU: Ein innovatives Instrument für die forstliche Planung in Bayern (E062) and Ertragskundliche Betreuung der langfristigen Versuche (W007). We also wish to thank our colleagues at the Bayerische Staatsforsten (BaySF) for their friendly cooperation. The development of FeNEU has benefited especially from exchange with experienced practitioners. We sincerely thank Sabine Andersen, Stefan Esser, Markus Fritsch, Christian Winter, Hans Stark, and Daniel Kraus, representing a much larger group. Among them is Rainer Dürr, author of the Silvarith software, who sadly died before the release of FeNEU.

References

Biber, Peter, and Astor Toraño Caicoya. 2026. ForestElementsR: Data Structures and Functions for Working with Forest Data. https://doi.org/10.32614/CRAN.package.ForestElementsR.
Riedel, T., P. Hennig, F. Kroiher, H. Polley, F. Schmitz, and Schwitzgebel F. 2017. Die Dritte Bundeswaldinventur (BWI 2012). Inventur- Und Auswertungsmethoden. Thuenen Institut fuer Waldoekosysteme.