Chapter 3: Growth and Yield
===========================
Learning Objectives
-------------------
After reading this chapter, you should be able to:
- Explain how growth curves are fitted to forest inventory data
- Use the :py:class:`ws3.core.Curve` class to define and manipulate growth curves
- Interpolate between curve points
- Perform arithmetic operations on curves (add, subtract, multiply, divide)
- Validate growth curves for biological plausibility
What Are Growth Curves?
-----------------------
**Growth curves** describe how forest attributes change over time. They
are the engine of wood supply models — without them, the model would
just track area without understanding how the forest grows.
Common attributes tracked by growth curves:
- **Volume**: Total merchantable volume (m³/ha)
- **Basal area**: Cross-sectional area of tree trunks (m²/ha)
- **Height**: Dominant height (m)
- **Biomass**: Total above-ground biomass (tonnes/ha)
- **Value**: Dollar value per unit volume ($/m³)
Growth curves typically follow a sigmoidal (S-shaped) pattern:
.. mermaid::
graph TD
YOUNG["Young stands
Slow growth"] --> MATURE["Mature stands
Rapid growth"] --> OLD["Old stands
Asymptoting"]
The curve starts slowly (young trees growing), accelerates (rapid
middle-age growth), and then plateaus (trees reach physiological limits).
The Curve Class
---------------
ws3's :py:class:`ws3.core.Curve` class provides a flexible interface
for defining and manipulating growth curves.
Defining a Curve
~~~~~~~~~~~~~~~~
.. code-block:: python
from ws3.core import Curve
# Define a volume curve for Douglas-fir on Site Index 50
volume_curve = Curve(
label="DF-SI50_volume",
is_volume=True,
points=[(0, 0), (10, 5), (20, 25), (30, 65), (40, 120),
(50, 200), (60, 300), (70, 400), (80, 470),
(90, 500), (100, 510)]
)
The ``x`` values are ages (years), and the ``y`` values are attribute
values (e.g., volume in m³/ha).
Interpolation
~~~~~~~~~~~~~
Curves support interpolation between defined points:
.. code-block:: python
# Get volume at age 25 (interpolated between ages 20 and 30)
vol_25 = volume_curve(25)
print(f"Volume at age 25: {vol_25:.1f} m³/ha")
# Get volume at age 55
vol_55 = volume_curve(55)
print(f"Volume at age 55: {vol_55:.1f} m³/ha")
Arithmetic Operations
~~~~~~~~~~~~~~~~~~~~~
Curves support arithmetic operations, which is useful for combining
curves or calculating differences:
.. code-block:: python
# Create a value curve (volume * price)
price_curve = Curve(
label="DF-SI50_value",
points=[(0, 0), (10, 250), (20, 1250), (30, 3250), (40, 6000),
(50, 10000), (60, 15000), (70, 20000), (80, 23500),
(90, 25000), (100, 25500)]
)
# Net value = value curve - harvesting cost curve
cost_curve = Curve(
label="harvest_cost",
points=[(0, 0), (10, 100), (20, 400), (30, 900), (40, 1600),
(50, 2500), (60, 3600), (70, 4900), (80, 6400),
(90, 8100), (100, 10000)]
)
net_value = price_curve - cost_curve
print(f"Net value at age 50: ${net_value(50):,.0f}")
Curve Algebra
~~~~~~~~~~~~~
The Curve class supports all basic arithmetic operations:
.. list-table::
:header-rows: 1
:widths: 20 80
* - Operator
- Description
* - ``curve1 + curve2``
- Element-wise addition
* - ``curve1 - curve2``
- Element-wise subtraction
* - ``curve1 * curve2``
- Element-wise multiplication
* - ``curve1 / curve2``
- Element-wise division
* - ``curve * scalar``
- Scale all values by a constant
* - ``curve / scalar``
- Divide all values by a constant
Fitting Growth Curves
---------------------
Growth curves are typically fitted to field data using statistical methods.
Common approaches include:
1. **Polynomial regression**: Simple but can produce unrealistic curves
2. **Logistic growth**: Captures sigmoidal shape well
3. **Richards function**: Flexible sigmoidal model
4. **Provincial yield tables**: Government-published curves for common species
Using Provincial Yield Tables
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In British Columbia, the Ministry of Forests publishes growth-and-yield
tables for common species. These tables provide volume estimates for
different species, site indices, and ages.
.. code-block:: python
# Example: Load a provincial yield table
# (This is a simplified example — actual tables are more complex)
yield_data = {
"species": "Pseudotsuga menziesii",
"site_index": 50,
"ages": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
"volumes": [0, 5, 25, 65, 120, 200, 300, 400, 470, 500, 510]
}
curve = Curve(
label=f"{yield_data['species']}-SI{yield_data['site_index']}_volume",
is_volume=True,
points=list(zip(yield_data["ages"], yield_data["volumes"]))
)
Validating Growth Curves
------------------------
Before using a growth curve in a model, validate it:
.. code-block:: python
def validate_curve(curve, min_age=0, max_age=500):
"""Validate a growth curve for biological plausibility."""
# Check that x values are increasing
x = curve.x
if not all(x[i] < x[i+1] for i in range(len(x)-1)):
raise ValueError("x values must be strictly increasing")
# Check that y values are non-negative
if any(y < 0 for y in curve.y):
raise ValueError("y values must be non-negative")
# Check that curve starts at zero (or near zero)
if curve.y[0] > 10:
raise ValueError("Curve should start near zero volume")
# Check for reasonable maximum values
max_vol = max(curve.y)
if max_vol > 10000: # 10,000 m³/ha is unrealistically high
raise ValueError(f"Maximum volume {max_vol} m³/ha is unrealistic")
# Check that curve eventually plateaus (optional)
if len(curve.y) > 10:
recent_growth = curve.y[-1] - curve.y[-2]
if recent_growth > 100: # More than 100 m³/ha growth in one period
print("Warning: Curve may not be plateauing")
validate_curve(volume_curve)
Common Growth Curve Shapes
--------------------------
Different species have characteristic growth curve shapes:
.. list-table::
:header-rows: 1
:widths: 25 75
* - Species
- Growth Pattern
* - Douglas-fir
- Fast initial growth, peaks around age 80-100, then declines
* - Western red cedar
- Slow initial growth, continues increasing slowly past age 200
* - Sitka spruce
- Very rapid growth, peaks early (age 40-60), then declines sharply
* - Lodgepole pine
- Moderate growth, peaks around age 60-80, relatively flat plateau
Exercises
---------
**Exercise 1 (Easy)**: Create a volume curve for Sitka spruce on Site
Index 45 and interpolate to find the volume at age 35.
**Exercise 2 (Medium)**: Write a function that takes two curves (volume
and price) and returns a net value curve (price * volume - cost).
**Exercise 3 (Hard)**: Fit a logistic growth curve to the following
data points using scipy.optimize.curve_fit:
.. code-block:: python
ages = [10, 20, 30, 40, 50, 60, 70, 80]
volumes = [10, 40, 90, 150, 210, 260, 300, 330]
Further Reading
---------------
- :doc:`ch02_forest_inventory` — Preparing inventory data
- :doc:`/howto/defining-growth-curves` — Detailed curve definition guide
- :doc:`/reference/contracts/data_contracts` — Data contracts and module boundaries