Source code for spatiocoexistence.inventory._inventory

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)