from __future__ import annotations
import numpy as np
from numpy.typing import NDArray
from pathlib import Path
from ._tools import (
read_initial_inventory,
create_initial_inventory,
_check_inventory_data,
size_class,
)
from ..crowding import crowding_indices
import matplotlib.pyplot as plt
from ..model._processes import (
_calculate_reduced_growth,
_calculate_survival_rate,
_calculate_recruitment_rate,
)
from ._cy_utils import (
_species_abundance,
_count_saplings,
_count_reproductives,
_species_area_relationship,
)
from ._plotting import (
plot_inventory_on_ax,
plot_kff_vs_abundance_ax,
plot_sad_ax,
plot_reduced_growth_ax,
plot_reduced_survival_ax,
plot_reduced_recruitment_ax,
plot_ci_cs_hist_ax,
plot_ci_hs_hist_ax,
plot_size_mean_ci_cs_ax,
plot_size_mean_ci_hs_ax,
plot_size_counts_ax,
plot_size_survival_ax,
display,
plot_BA_per_size_class_ax,
plot_rank_abundance_distribution,
)
from ._tools import mean_count_sum_size_class
[docs]
class Inventory:
"""
This inventory class stores forest data from the simulation and the ForestGEO network.
Instance methods offer statistics for analyzing the data directly.
Some statistics require crowding indices, which is why they are calculated if they are not provided in the data.
:no-index:
"""
# Default directory where plots will be saved if enabled.
# Can be overridden per instance via ``inventory.plot_saving_path = "..."``.
plot_saving_path: Path | str | None = None
def _init_inventory(self, data: np.ndarray, radius: float = 10.0):
self.radius = radius
self._data = data
# Instance-level override for plot saving path (optional)
self.plot_saving_path = self.__class__.plot_saving_path
[docs]
@classmethod
def from_data(
cls,
data: np.ndarray | Path | Inventory,
radius: float = 10.0,
):
"""
Create an Inventory instance from existing data, a file path, or another Inventory.
If an Inventory is provided, its data and radius are copied.
If a Path is provided, the file is read and validated.
If a numpy array is provided, it is validated for correct structure and types.
:param data: Structured numpy array, file path, or Inventory to initialize from.
:param radius: Neighborhood radius for crowding indices (default: 10.0).
:param returns: A new Inventory instance initialized from the provided data.
"""
if isinstance(data, Inventory):
arr = data.data
radius = data.radius
elif isinstance(data, Path):
arr = read_initial_inventory(data)
else:
arr = _check_inventory_data(data, radius)
obj = cls.__new__(cls)
obj._init_inventory(arr, radius)
return obj
[docs]
@classmethod
def from_random(
cls,
n_species: int,
dim_x: float,
dim_y: float,
radius: float = 10.0,
num_threads: int = 1,
):
"""
Create an Inventory instance from randomly spread trees. These trees will have the same density
as trees in the BCI plot.
:param n_species: Number of different species to include in the inventory.
:param dim_x: Width of the plot area.
:param dim_y: Height of the plot area.
:param radius: Neighborhood radius for crowding indices (default: 10.0).
:param num_threads: Number of threads to use for inventory creation (default: 1).
:returns: A new Inventory instance with randomly generated data.
"""
arr = create_initial_inventory(
n_species=n_species,
dim_x=dim_x,
dim_y=dim_y,
radius=radius,
num_threads=num_threads,
)
obj = cls.__new__(cls)
obj._init_inventory(arr, radius)
return obj
@property
def data(self) -> np.ndarray:
"""
The full structured numpy array representing the inventory.
Read-only: do not assign directly, use field setters or methods.
"""
return self._data
@property
def x(self):
"""X-coordinates of individuals (np.ndarray, dtype float64).
This property can be updated/set. Setting it will automatically recalculate all crowding indices.
"""
return self.data["x"]
@x.setter
def x(self, value: np.ndarray[np.float64]) -> None:
if not (isinstance(value, np.ndarray) and value.dtype == np.float64):
raise TypeError("x must be a numpy.ndarray of dtype float64")
self.data["x"] = value
self._update_crowding_indices()
@property
def y(self):
"""Y-coordinates of individuals (np.ndarray, dtype float64).
This property can be updated/set. Setting it will automatically recalculate all crowding indices.
"""
return self.data["y"]
@y.setter
def y(self, value: np.ndarray[np.float64]) -> None:
if not (isinstance(value, np.ndarray) and value.dtype == np.float64):
raise TypeError("y must be a numpy.ndarray of dtype float64")
self.data["y"] = value
self._update_crowding_indices()
@property
def dbh(self):
"""Diameter at breast height (np.ndarray, dtype float64).
This property can be updated/set. Setting it will automatically recalculate all crowding indices.
"""
return self.data["dbh"]
@dbh.setter
def dbh(self, value: np.ndarray[np.float64]) -> None:
if not (isinstance(value, np.ndarray) and value.dtype == np.float64):
raise TypeError("dbh must be a numpy.ndarray of dtype float64")
self.data["dbh"] = value
self._update_crowding_indices()
@property
def species(self):
"""Species IDs (np.ndarray, dtype int64).
This property can be updated/set. Setting it will automatically recalculate all crowding indices.
"""
return self.data["species"]
@species.setter
def species(self, value: np.ndarray[np.int64]) -> None:
if not (isinstance(value, np.ndarray) and value.dtype == np.int64):
raise TypeError("species must be a numpy.ndarray of dtype int64")
self.data["species"] = value
self._update_crowding_indices()
@property
def status(self):
"""Status codes (np.ndarray, dtype int32).
This property can be updated/set. Setting it will automatically recalculate all crowding indices.
"""
return self.data["status"]
@status.setter
def status(self, value: np.ndarray[np.int32]) -> None:
if not (isinstance(value, np.ndarray) and value.dtype == np.int32):
raise TypeError("status must be a numpy.ndarray of dtype int32")
self.data["status"] = value
self._update_crowding_indices()
@property
def CI_CS(self):
"""Conspecific crowding index."""
return self.data["CI_CS"]
@property
def CI_HS(self):
"""Heterospecific crowding index."""
return self.data["CI_HS"]
@property
def CI_CS_d(self):
"""Conspecific crowding index (distance-weighted)."""
return self.data["CI_CS_d"]
@property
def CI_HS_d(self):
"""Heterospecific crowding index (distance-weighted)."""
return self.data["CI_HS_d"]
@property
def CI_C(self):
"""Conspecific crowding index (unweighted)."""
return self.data["CI_C"]
@property
def CI_H(self):
"""Heterospecific crowding index (unweighted)."""
return self.data["CI_H"]
@property
def CI_C_d(self):
"""Conspecific crowding index (distance-weighted, unweighted)."""
return self.data["CI_C_d"]
@property
def CI_H_d(self):
"""Heterospecific crowding index (distance-weighted, unweighted)."""
return self.data["CI_H_d"]
def _update_crowding_indices(self):
"""
Recalculate crowding indices in-place for the current inventory data.
"""
# Calculate all crowding indices and update fields
CI_CS, CI_HS, CI_CS_d, CI_HS_d = crowding_indices(
self.x,
self.y,
self.species,
self.status,
self.radius,
dbh=self.dbh,
)
CI_C, CI_H, CI_C_d, CI_H_d = crowding_indices(
self.x,
self.y,
self.species,
self.status,
self.radius,
)
self.data["CI_CS"] = CI_CS
self.data["CI_HS"] = CI_HS
self.data["CI_CS_d"] = CI_CS_d
self.data["CI_HS_d"] = CI_HS_d
self.data["CI_C"] = CI_C
self.data["CI_H"] = CI_H
self.data["CI_C_d"] = CI_C_d
self.data["CI_H_d"] = CI_H_d
[docs]
def get_BA_and_k(self) -> tuple[
NDArray[np.float64],
NDArray[np.float64],
NDArray[np.float64],
NDArray[np.float64],
NDArray[np.float64],
NDArray[np.float64],
NDArray[np.int_],
NDArray[np.int_],
]:
"""
Calculates summary statistics for BA and k using inventory data.
BA stands for basal area, and k represents aggregation.
ff is for comparing a focal species with conspecifics.
fh is for comparing a focal species with heterospecifics.
:returns: Tuple of arrays (BA_ff, BA_fh, n_BA_ff, n_BA_fh, k_ff, k_fh, abundance_con, abundance_het).
:rtype: tuple
"""
CI_CS = self.CI_CS
CI_C = self.CI_C
CI_HS = self.CI_HS
CI_H = self.CI_H
species = self.species
dbh = self.dbh
unique_species = np.unique(species)
n_species = unique_species.size
BA_ff = np.zeros(n_species)
BA_fh = np.zeros(n_species)
k_ff = np.zeros(n_species)
k_fh = np.zeros(n_species)
n_BA_ff = np.zeros(n_species)
n_BA_fh = np.zeros(n_species)
abundance_con = np.zeros(n_species, dtype=int)
abundance_het = np.zeros(n_species, dtype=int)
max_x = np.max(self.x) if self.x.size else 0.0
max_y = np.max(self.y) if self.y.size else 0.0
c = (
2
* np.pi
* self.radius
/ (int(max_x + 0.5) * int(max_y + 0.5) if (max_x > 0 and max_y > 0) else 1)
)
all_individuals = np.arange(len(species))
for idx, f in enumerate(unique_species):
conspecifics = np.where(species == f)[0]
heterospecifics = np.delete(all_individuals, conspecifics)
CI_CS_mean = np.mean(CI_CS[conspecifics]) if conspecifics.size else 0.0
CI_C_mean = np.mean(CI_C[conspecifics]) if conspecifics.size else 0.0
CI_HS_mean = np.mean(CI_HS[conspecifics]) if conspecifics.size else 0.0
CI_H_mean = np.mean(CI_H[conspecifics]) if conspecifics.size else 0.0
dbh_C_mean = np.mean(dbh[conspecifics]) if conspecifics.size else 0.0
dbh_H_mean = np.mean(dbh[heterospecifics]) if heterospecifics.size else 0.0
# Use denominator guards to avoid divide-by-zero and spurious zeros
ff = (CI_CS_mean / CI_C_mean) if CI_C_mean != 0 else 0.0
fh = (CI_HS_mean / CI_H_mean) if CI_H_mean != 0 else 0.0
BA_ff[idx] = ff
n_BA_ff[idx] = ff / dbh_C_mean if dbh_C_mean != 0 else 0.0
k_ff[idx] = (
CI_C_mean / (c * len(conspecifics))
if len(conspecifics) > 0 and c != 0
else 0.0
)
BA_fh[idx] = fh
n_BA_fh[idx] = fh / dbh_H_mean if dbh_H_mean != 0 else 0.0
k_fh[idx] = (
CI_H_mean / (c * len(heterospecifics))
if len(heterospecifics) > 0 and c != 0
else 0.0
)
abundance_con[idx] = len(conspecifics)
abundance_het[idx] = len(heterospecifics)
return BA_ff, BA_fh, n_BA_ff, n_BA_fh, k_ff, k_fh, abundance_con, abundance_het
[docs]
def reduced_growth(self, beta_gr: float | None = None) -> NDArray[np.float64]:
"""
Calculate the reduced growth using the formula from Cython.
Reduced means, to only account for the exponential part of the growth function.
:param beta_gr: Growth reduction parameter.
:returns: Array of reduced growth values.
:rtype: NDArray[np.float64]
"""
return _calculate_reduced_growth(self.CI_CS, self.CI_HS, beta_gr)
[docs]
def reduced_survival(self, reduced: bool = True) -> NDArray[np.float64]:
"""
Calculate the reduced survival probabilities using the formula from mortality in cython.
Reduced means, to only account for the exponential part of the survival probability.
:param reduced: If True, calculate reduced survival; if False, apply background mortality.
:returns: Array of survival probabilities.
:rtype: NDArray[np.float64]
"""
return _calculate_survival_rate(
self.CI_CS,
self.CI_HS,
self.CI_CS_d,
self.CI_HS_d,
self.dbh,
apply_bg=(not reduced),
)
[docs]
def reduced_recruitment(self) -> NDArray[np.float64]:
"""
Calculate the recruitment probability using the formula from recruitment in cython.
Reduced means, to only account for the exponential part of the recruitment probability.
:returns:
Array of recruitment probabilities.
"""
return _calculate_recruitment_rate(
self.CI_CS,
self.CI_HS,
self.CI_CS_d,
self.CI_HS_d,
self.dbh,
self.species,
)
[docs]
def species_abundance_distribution(self, abundance_class: np.ndarray) -> tuple:
"""
Get the amount of species per abundance class.
:param abundance_class: Array of abundance class boundaries (bins).
:returns: Distribution of species counts per abundance class.
"""
_, counts = np.unique(self.species, return_counts=True)
return np.histogram(counts, bins=abundance_class)
[docs]
def get_CI_CS_distribution(self, bins: np.ndarray) -> np.ndarray:
"""
Get distribution counts for conspecific crowding index (CI_CS).
:param bins: Array of bin edges for distribution.
:returns: Distribution of CI_CS.
"""
counts, _ = np.histogram(self.CI_CS, bins=bins)
return counts
[docs]
def get_CI_HS_distribution(self, bins: np.ndarray) -> np.ndarray:
"""
Get distribution counts for heterospecific crowding index (CI_HS).
:param bins: Array of bin edges for distribution.
:returns: Distribution of CI_HS.
"""
counts, _ = np.histogram(self.CI_HS, bins=bins)
return counts
[docs]
def get_reduced_growth_distribution(
self, bins: np.ndarray, beta_gr: float = 0.084
) -> np.ndarray:
"""
Get distribution counts for reduced growth values.
:param bins: Array of bin edges for distribution.
:param beta_gr: Growth reduction parameter.
:returns: Distribution of reduced growth.
"""
vals = self.reduced_growth(beta_gr=beta_gr)
counts, _ = np.histogram(vals, bins=bins)
return counts
[docs]
def get_survival_distribution(self, bins: np.ndarray) -> np.ndarray:
"""
Get distribution counts for survival probabilities.
:param bins: Array of bin edges for distribution.
:returns: Distribution of survival probabilities.
"""
vals = self.reduced_survival()
counts, _ = np.histogram(vals, bins=bins)
return counts
[docs]
def get_recruitment_distribution(self, bins: np.ndarray) -> np.ndarray:
"""
Get distribution counts for recruitment probabilities.
:param bins: Array of bin edges for distribution.
:returns: Distribution of recruitment probabilities.
"""
vals = self.reduced_recruitment()
counts, _ = np.histogram(vals, bins=bins)
return counts
[docs]
def get_SAD_distribution(self, bins: np.ndarray) -> np.ndarray:
"""
Get distribution counts for species abundance distribution (SAD).
:param bins: Array of bin edges for abundance classes.
:returns: Distribution of SAD.
"""
species = self.species
unique_species, species_counts = np.unique(species, return_counts=True)
sad_hist, _ = np.histogram(
species_counts,
bins=bins,
weights=(
np.ones_like(species_counts) * 1 / len(unique_species)
if len(unique_species) > 0
else None
),
)
return sad_hist
[docs]
def count_reproductives(
self, reproductive_size: float
) -> tuple[np.ndarray[np.int32], np.ndarray[np.int32]]:
"""
Count the number of reproductive trees.
All individuals are checked to be alive and larger or equally large than reproductive_size.
:param reproductive_size: The size upon which an individual can reproduce.
:returns: Amount of reproductives per species, indices of reproductive trees.
"""
return _count_reproductives(
self.dbh, self.species, self.status, reproductive_size
)
[docs]
def count_saplings(self, reproductive_size: float) -> int:
"""
Count the number of saplings.
All individuals are checked to be alive and smaller than reproductive_size.
:param reproductive_size: The size upon which an individual can reproduce.
:returns: The total amount of saplings.
"""
return _count_saplings(self.dbh, self.status, reproductive_size)
[docs]
def species_abundance(self) -> np.ndarray[np.int32]:
"""
Count the amount of individuals per species.
:returns: Array of individual counts per species.
"""
n_species = np.max(self.species)
return _species_abundance(self.species, n_species)
[docs]
def extend_inventory(self, other: Inventory | np.ndarray) -> None:
"""
Extend the current inventory by appending another inventory's data.
This method concatenates all fields from the other inventory to the current one
and recalculates all crowding indices for the combined dataset.
:param other: Another Inventory object or structured numpy array to append.
:raises TypeError: If other is not an Inventory or compatible structured array.
:raises ValueError: If the data structures are incompatible.
"""
# Convert other to data array if it's an Inventory
if isinstance(other, Inventory):
other_data = other.data
elif isinstance(other, np.ndarray):
# Validate that it's a structured array with the right fields
other_data = _check_inventory_data(other, self.radius)
else:
raise TypeError(
"other must be an Inventory object or a structured numpy array"
)
# Check that both arrays have the same dtype
if self._data.dtype != other_data.dtype:
raise ValueError(
f"Cannot extend: dtype mismatch. "
f"Current: {self._data.dtype}, Other: {other_data.dtype}"
)
# Concatenate the structured arrays
combined_data = np.concatenate([self._data, other_data])
# Update internal data
self._data = combined_data
# Recalculate all crowding indices for the combined dataset
self._update_crowding_indices()
[docs]
def apply_focus(
self,
focus: str | None = None,
reproductive_size: float = 1.0,
) -> None:
"""Filter this Inventory in-place to a subset of trees based on *focus*.
- "rep": reproductive trees, ``dbh >= reproductive_size``
- "rec": recruits, ``status == 1``
- "sap": saplings, ``dbh < reproductive_size``
- "dead": dead trees, ``status == -1``
- ``None``: no filtering; do nothing.
The inventory's internal data array is updated in-place; no new
:class:`Inventory` instance is created.
:param focus: Which trees to focus on ("rep", "rec", "sap", "dead" or ``None``).
:param reproductive_size: DBH threshold separating saplings from reproductives.
:raises ValueError: if *focus* is not one of the allowed values.
:return: None.
"""
if focus is None:
return
dbh = self.dbh
status = self.status
if focus == "rep":
focused_trees = dbh >= reproductive_size
elif focus == "rec":
focused_trees = status == 1
elif focus == "sap":
focused_trees = dbh < reproductive_size
elif focus == "dead":
focused_trees = status == -1
else:
raise ValueError(
f"Invalid focus={focus!r}. Expected one of 'rep', 'rec', 'sap', 'dead' or None."
)
# Apply mask in-place
self._data = self.data[focused_trees]
# Recalculate crowding indices for the focused subset
self._update_crowding_indices()
[docs]
def get_size_distribution(self) -> tuple[np.ndarray, np.ndarray]:
"""
Return counts of individuals per DBH size class.
:returns: The midpoints of the size classes,
and the counts of individuals per size class.
"""
sc = size_class()
# histogram-style counts using the same bin edges as in the plot
counts = mean_count_sum_size_class(sc, self.dbh, self.dbh, option="count")
valid = counts > 0
centers = np.log(sc[:-1][valid])
return centers, counts[valid]
[docs]
def get_BA_distribution(self) -> tuple[np.ndarray, np.ndarray]:
"""
Return the mean basal area (BA) per size class.
Only size classes that contain at least one individual are
returned.
:returns: A tuple ``(centers, counts)`` where ``centers`` are the
log-transformed midpoints of the occupied size classes
and ``counts`` are the mean basal area per occupied
size class.
"""
sc = size_class()
BA = self.dbh**2 * np.pi / 4
mean_BA_sc = mean_count_sum_size_class(sc, BA, self.dbh, option="sum")
valid = mean_BA_sc > 0
centers = np.log(sc[:-1][valid])
counts = mean_BA_sc[valid]
return centers, counts
[docs]
def plot_BA_distribution(
self,
reference_inventory: Inventory | None = None,
figsize: tuple[float, float] | None = None,
show: bool = False,
save: bool = False,
filename: str = "BA_distribution.png",
):
"""
Plot mean basal area (BA) per size class for this inventory.
:param reference_inventory: Optional inventory whose BA per size
class is overlaid as a reference curve (in red) using the
same size-class bins.
:param figsize: Optional figure size used. If *None*, a default size is used.
:param show: If ``True``, immediately display the figure.
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
if figsize is None:
figsize = (6.0, 4.0)
sc = size_class()
fig, ax = plt.subplots(figsize=figsize)
# Current inventory
plot_BA_per_size_class_ax(ax, dbh=self.dbh, size_classes=sc, ref=False)
# Optional reference inventory, re-using the same size classes
if reference_inventory is not None:
plot_BA_per_size_class_ax(
ax,
dbh=reference_inventory.dbh,
size_classes=sc,
ref=True,
)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot(
self,
threshold: int = 50,
filter: float = 1.0,
scale: float = 7.5,
focus: str | None = None,
show: bool = False,
figsize: tuple = (22, 10),
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "inventory_overview.png",
):
"""
Plot the current state of the inventory.
:param threshold: Minimum abundance of conspecifics for summary statistics.
:param filter: Filter out all trees below that size for inventory plot.
:param scale: Scaling factor for point size based on DBH (default 7.5).
:param figsize: Figure size tuple (width, height).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose statistics are
plotted as reference (in red) where applicable.
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
fig, axs = plt.subplots(5, 5, figsize=figsize)
bins = None
if reference_inventory is not None:
# Overlay reference inventory using the same composite helper.
bins = display(
reference_inventory,
threshold=threshold,
filter=filter,
scale=scale,
abundances=reference_inventory.species_abundance(),
axs=axs,
focus=focus,
ref=True,
)
display(
self,
threshold=threshold,
filter=filter,
scale=scale,
abundances=self.species_abundance(),
axs=axs,
focus=focus,
bins_dict=bins,
ref=False,
)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_map(
self,
filter: float = 1.0,
scale: float = 7.5,
figsize: tuple | None = None,
show_axes: bool = True,
show: bool = False,
save: bool = False,
filename: str = "inventory_map.png",
title: str = "Inventory",
):
"""
Plot only the spatial map of the inventory (trees as scatter plot).
:param filter: Minimum DBH of trees to be plotted.
:param scale: Scaling factor for point size based on DBH.
:param figsize: Figure size tuple (width, height). If None, auto-calculated based on plot dimensions.
:param show_axes: If False, hide axis frame, ticks, labels and grid for clean map-only plot (default True).
:param show: Whether to display the plot (default True).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
:param title: Title of the plot, default = 'Inventory'.
"""
if figsize is None:
figsize = (10, int(10 * np.max(self.y) / np.max(self.x)))
fig, ax = plt.subplots(figsize=figsize)
plot_inventory_on_ax(
ax=ax,
inventory=self,
filter=filter,
scale=scale,
show_axes=show_axes,
title=title,
)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_k_vs_abundance(
self,
threshold: int = 50,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "k_vs_abundance.png",
) -> None:
"""
Plot conspecific aggregation (k_ff) against species abundance as a standalone figure.
:param threshold: Minimum abundance threshold for plotting (default 50).
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose k_ff vs abundance
relationship is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
BA_ff, BA_fh, _, _, k_ff, k_fh, abundance_con, _ = self.get_BA_and_k()
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_kff_vs_abundance_ax(
ax,
abundance_con=abundance_con,
k_ff=k_ff,
threshold=threshold,
ref=False,
)
if reference_inventory is not None:
BA_ff_r, BA_fh_r, _, _, k_ff_r, k_fh_r, abundance_con_r, _ = (
reference_inventory.get_BA_and_k()
)
plot_kff_vs_abundance_ax(
ax,
abundance_con=abundance_con_r,
k_ff=k_ff_r,
threshold=threshold,
ref=True,
)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_SAD(
self,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "SAD.png",
) -> None:
"""
Plot the species abundance distribution (SAD) as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose SAD is plotted as
reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
abundance_class = np.arange(0, 33) * 37
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_sad_ax(ax, self.species, abundance_class)
if reference_inventory is not None:
plot_sad_ax(ax, reference_inventory.species, abundance_class, ref=True)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_reduced_growth(
self,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "reduced_growth.png",
) -> None:
"""
Plot the reduced growth histogram as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose reduced growth
distribution is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
bins = np.linspace(0, 1, 31)
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_reduced_growth_ax(ax, self.reduced_growth(), bins)
if reference_inventory is not None:
plot_reduced_growth_ax(
ax,
reference_inventory.reduced_growth(),
bins,
ref=True,
)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_reduced_survival(
self,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "reduced_survival.png",
) -> None:
"""
Plot the reduced survival histogram as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose reduced survival
distribution is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
bins = np.linspace(0, 1, 31)
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_reduced_survival_ax(ax, self.reduced_survival(), bins)
if reference_inventory is not None:
plot_reduced_survival_ax(
ax,
reference_inventory.reduced_survival(),
bins,
ref=True,
)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_reduced_recruitment(
self,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "reduced_recruitment.png",
) -> None:
"""
Plot the reduced recruitment histogram as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose reduced
recruitment distribution is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
bins = np.linspace(0, 1, 31)
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_reduced_recruitment_ax(ax, self.reduced_recruitment(), bins)
if reference_inventory is not None:
plot_reduced_recruitment_ax(
ax,
reference_inventory.reduced_recruitment(),
bins,
ref=True,
)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_CI_CS_distribution(
self,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "CI_CS_distribution.png",
) -> None:
"""
Plot CI_CS distribution as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose CI_CS
distribution is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
"""
if reference_inventory is not None:
ci_cs_max = float(np.percentile(reference_inventory.CI_CS, 99))
else:
ci_cs_max = float(np.percentile(self.CI_CS, 99))
bins_cs = np.linspace(0, ci_cs_max)
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_ci_cs_hist_ax(ax, self.CI_CS, bins_cs)
if reference_inventory is not None:
plot_ci_cs_hist_ax(ax, reference_inventory.CI_CS, bins_cs, ref=True)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_CI_HS_distribution(
self,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "CI_HS_distribution.png",
) -> None:
"""
Plot CI_HS distribution as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose CI_HS
distribution is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
"""
if reference_inventory is not None:
ci_hs_max = float(np.percentile(reference_inventory.CI_HS, 98))
else:
ci_hs_max = float(np.percentile(self.CI_HS, 98))
bins_hs = np.linspace(0, ci_hs_max)
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_ci_hs_hist_ax(ax, self.CI_HS, bins_hs)
if reference_inventory is not None:
plot_ci_hs_hist_ax(ax, reference_inventory.CI_HS, bins_hs, ref=True)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_size_mean_CI_CS(
self,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "size_mean_CI_CS.png",
) -> None:
"""
Plot mean CI_CS per size class as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True)
:param reference_inventory: Optional inventory whose mean CI_CS per
size class is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
sc = size_class()
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_size_mean_ci_cs_ax(ax, self.CI_CS, self.dbh, sc)
if reference_inventory is not None:
plot_size_mean_ci_cs_ax(
ax,
reference_inventory.CI_CS,
reference_inventory.dbh,
sc,
ref=True,
)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_size_mean_CI_HS(
self,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "size_mean_CI_HS.png",
) -> None:
"""
Plot mean CI_HS per size class as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose mean CI_HS per
size class is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
sc = size_class()
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_size_mean_ci_hs_ax(ax, self.CI_HS, self.dbh, sc)
if reference_inventory is not None:
plot_size_mean_ci_hs_ax(
ax,
reference_inventory.CI_HS,
reference_inventory.dbh,
sc,
ref=True,
)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_size_distribution(
self,
reference_inventory: Inventory | None = None,
figsize: tuple[float, float] | None = None,
show: bool = False,
save: bool = False,
filename: str = "size_distribution.png",
) -> None:
"""
Plot size-class frequency distribution as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose size-class
distribution is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
sc = size_class()
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_size_counts_ax(ax, self.dbh, sc)
if reference_inventory is not None:
plot_size_counts_ax(ax, reference_inventory.dbh, sc, ref=True)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_size_mortality(
self,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "size_mortality.png",
) -> None:
"""
Plot mortality vs size class as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose mortality vs
size-class relationship is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
sc = size_class()
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
plot_size_survival_ax(ax, self.reduced_survival(), self.dbh, sc)
if reference_inventory is not None:
plot_size_survival_ax(
ax,
reference_inventory.reduced_survival(),
reference_inventory.dbh,
sc,
ref=True,
)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def plot_rank_abundance_distribution(
self,
figsize: tuple[float, float] | None = None,
show: bool = False,
reference_inventory: Inventory | None = None,
save: bool = False,
filename: str = "rank_abundance.png",
) -> None:
"""Plot the rank–abundance distribution as a standalone figure.
:param figsize: Figure size tuple (width, height). If None, defaults to (6, 4).
:param show: Whether to display the plot (default True).
:param reference_inventory: Optional inventory whose rank–abundance
distribution is plotted as reference (in red).
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
"""
if figsize is None:
figsize = (6, 4)
fig, ax = plt.subplots(figsize=figsize)
# species_abundance returns abundance per species; use it directly.
abundances = self.species_abundance()
plot_rank_abundance_distribution(ax, abundances)
if reference_inventory is not None:
abundances_ref = reference_inventory.species_abundance()
plot_rank_abundance_distribution(ax, abundances_ref, ref=True)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)
[docs]
def species_area_relationship(
self,
cell_size: float,
n_steps: int = 50,
n_repeats: int = 100,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Compute the species–area relationship (SAR) for this inventory.
The species–area relationship is estimated by repeatedly sampling
square boxes of increasing side length over the plot under
periodic boundary conditions, and counting the number of species
present in each box.
The implementation delegates the heavy computation to the Cython
helper :func:`_species_area_relationship`, which uses the
spatial grid built by :func:`cy_create_grid`.
:param cell_size: Grid cell size used for the internal spatial
index. A value on the order of the typical inter-tree
distance is usually a good choice.
:param n_steps: Number of different box side lengths (area
sizes) to evaluate (default: 50).
:param n_repeats: Number of random box placements per side
length used to estimate mean and standard deviation of
species richness (default: 100).
:returns: A tuple ``(area, sar_mean, sar_std)`` where ``area``
is the sampled box area, ``sar_mean`` is the mean species
richness, and ``sar_std`` the standard deviation of
richness across repeats for each area.
"""
return _species_area_relationship(
self.x,
self.y,
self.species,
self.status,
cell_size,
n_steps,
n_repeats,
)
[docs]
def plot_species_area_relationship(
self,
cell_size: float,
n_steps: int = 50,
n_repeats: int = 100,
reference_inventory: Inventory | None = None,
loglog: bool = True,
figsize: tuple[float, float] | None = None,
show: bool = False,
save: bool = False,
filename: str = "species_area_relationship.png",
) -> None:
"""Plot the species–area relationship (SAR) for this inventory.
The SAR is obtained from :meth:`species_area_relationship` and
plotted as species richness versus area. Optionally, a reference
inventory can be overlaid for comparison.
:param cell_size: Grid cell size passed to
:meth:`species_area_relationship`.
:param n_steps: Number of different box areas to evaluate
(default: 50).
:param n_repeats: Number of random boxes per area size (default:
100).
:param reference_inventory: Optional second inventory whose SAR
is plotted as a reference curve using the same parameters
(drawn in red).
:param loglog: If ``True`` (default), use log–log axes for the
SAR plot; if ``False``, use linear axes.
:param figsize: Optional figure size passed to
:func:`matplotlib.pyplot.subplots`. If *None*, a default of
``(6.0, 4.0)`` is used.
:param show: If ``True``, immediately display the figure using
:func:`matplotlib.pyplot.show`.
:param save: If ``True``, save the figure to
``self.plot_saving_path / filename`` (if
``plot_saving_path`` is set) or to ``filename`` in the
current working directory.
:param filename: File name used when *save* is ``True``.
:return: ``None``.
"""
if figsize is None:
figsize = (6.0, 4.0)
area, sar_mean, sar_std = self.species_area_relationship(
cell_size=cell_size,
n_steps=n_steps,
n_repeats=n_repeats,
)
fig, ax = plt.subplots(figsize=figsize)
if loglog:
ax.loglog(area, sar_mean, label="Inventory")
else:
ax.plot(area, sar_mean, label="Inventory")
if reference_inventory is not None:
area_ref, sar_mean_ref, _ = reference_inventory.species_area_relationship(
cell_size=cell_size,
n_steps=n_steps,
n_repeats=n_repeats,
)
if loglog:
ax.loglog(area_ref, sar_mean_ref, "r--", label="Reference")
else:
ax.plot(area_ref, sar_mean_ref, "r--", label="Reference")
ax.set_xlabel("Area")
ax.set_ylabel("Species richness")
ax.legend()
ax.grid(True, which="both", ls=":", alpha=0.5)
plt.tight_layout()
if save:
if self.plot_saving_path:
out_path = Path(self.plot_saving_path) / filename
else:
out_path = Path(filename)
fig.savefig(out_path)
if show:
plt.show()
plt.close(fig)