pyforestry package#

Subpackages#

Submodules#

pyforestry.catalog module#

Discover and search pyforestry’s scientific formula models.

Most formula modules publish a module-level DESCRIPTOR (see pyforestry.base.contracts.FormulaModuleDescriptor) describing the model’s identity, citation, species applicability, and units. This module aggregates those descriptors so a model can be found without already knowing its import path:

>>> from pyforestry import catalog
>>> catalog.find(domain="volume")                 # all volume models
>>> catalog.find(domain="growth", species="pinus")  # best-effort species filter
>>> catalog.search("bark")                         # by id / module / citation
>>> entry = catalog.describe("soderberg_1992_bark")
>>> entry.source.title

Discovery covers pyforestry.base as well as the regions, so region-independent models are findable too:

>>> catalog.find(region="base")                    # Näslund, García, Bitterlich, Näsberg

Discovery currently covers modules that expose a DESCRIPTOR; the set grows as more modules adopt the convention.

class pyforestry.catalog.ModelEntry(component_id: str, module: str, region: str, domain: str, source: SourceReference, species_groups: Mapping[str, frozenset[str]], units: Mapping[str, str], kernel_names: tuple[str, ...], kind: str = 'formula', composes: tuple[str, ...] = ())[source]#

Bases: object

A discoverable formula model and its introspection metadata.

composes: tuple[str, ...] = ()#
kind: str = 'formula'#
pyforestry.catalog.describe(identifier: str) → ModelEntry[source]#

Return the model matching identifier (component id or module path).

Falls back to a unique case-insensitive substring match on the component id.

Raises:

KeyError – if no model, or more than one, matches identifier.

pyforestry.catalog.discovery_errors() → dict[str, BaseException][source]#

Return the modules the last discovery pass could not import.

Empty when everything imported. A non-empty result means the catalog is incomplete and says exactly which modules are missing and why – which is what “this model does not exist” used to look like.

pyforestry.catalog.domains() → list[str][source]#

Return the sorted set of model domains (e.g. volume, mortality).

pyforestry.catalog.find(*, region: str | None = None, domain: str | None = None, species: str | None = None, units: str | None = None, kind: str | None = None) → list[ModelEntry][source]#

Return models matching every supplied filter (case-insensitive).

Parameters:
  • region – Region name, e.g. "sweden".

  • domain – Domain name, e.g. "volume". Returns both formula kernels and composed models for that domain unless kind is also given.

  • species – Substring matched against each model’s species identifiers. Best-effort, since identifiers are stored as TreeName strings.

  • units – Substring matched against the model’s unit names or values.

  • kind – "formula" (equation kernels) or "model" (composed, runnable model adapters).

Returns:

Matching ModelEntry objects, ordered by region/domain/id.

pyforestry.catalog.list_models() → list[ModelEntry][source]#

Return every discoverable model entry.

pyforestry.catalog.refresh() → None[source]#

Clear the discovery cache (e.g. after importing new model modules).

pyforestry.catalog.regions() → list[str][source]#

Return the sorted set of regions that publish discoverable models.

pyforestry.catalog.search(query: str) → list[ModelEntry][source]#

Return models whose id, module, domain, or citation contains query.

pyforestry.projection module#

One projection, in one call.

Running a projection used to mean knowing Eriksson1976Model, StandInit, ThinningProgram, build_context, mode_hint, SimulationSetup and Eriksson1976ManagementSchedule – seven concepts, one of which raised TypeError when used the way the repository’s own worked example used it.

import pyforestry as pf

result = pf.project(stand, model="elfving_2010", years=100, step=5, seed=42)

result.table        # a DataFrame, one row per step
result.stand        # the final state
result.provenance   # what was cited, and by what

The typed constructors are all still there; this is the ninety-per-cent path, not a replacement for them. model= resolves through pyforestry.catalog, which is the payoff for having built a discovery layer: the string a user finds with catalog.search("elfving") is the string they can run.

class pyforestry.projection.ProjectionResult(table: pd.DataFrame, stand: Stand, context: SimulationContext, provenance: Mapping[str, Any]=<factory>)[source]#

Bases: object

What a projection produced, and where it came from.

property model: GrowthModel#

The model that was stepped.

pyforestry.projection.available_models() → list[str][source]#

Return every name project() accepts for model=, sorted.

These are single-tree/stand growth models: give one a Stand and it advances it. The composite pipelines – which build their own stand from a site and run nine models around a growth model – are a different shape and are not listed here; see available_pipelines().

pyforestry.projection.available_pipelines() → list[str][source]#

Return every composite pipeline name, sorted, across all regions.

A pipeline is not something project() can run: it reconstructs its own stand from a site rather than advancing one you supply. Build it with the get_pipeline of the region that publishes it – Sweden’s is pyforestry.sweden.simulation.presets.get_pipeline() – and drive it with initialize(site=...) / run_projection(...).

pyforestry.projection.project(stand: Stand, *, model: str | 'GrowthModel', years: float, step: float | None = None, seed: int | None = None, policy: 'Policy' | None = None, attrs: Mapping[str, Any] | None = None, inputs: Any | None = None, pipeline: Sequence['Step'] | None = None) → ProjectionResult[source]#

Project stand forward and return the result.

Parameters:
  • stand – The inventory to project. Deep-copied first, so the caller’s stand is untouched and the same stand can be projected under several models or several seeds and compared. (GrowthModel.build_context copies only the plot containers and shares the Tree objects, because a model grows diameters in place. That is right for a model and wrong for a front door: it would make project(stand, ...) return something different the second time you called it.)

  • model – A name from available_models(), or a GrowthModel you built yourself.

  • years – Total length of the projection.

  • step – Length of one period. Defaults to the model’s declared native_step_years, and to 5 years for a model that declares none, so the common case needs no argument and no guessing.

  • seed – Root seed for every random stream the run uses. Required only if something in the run draws; without it, drawing raises rather than silently using an unseeded generator.

  • policy – A management policy – Callable[[ctx], Sequence[Action]] – run before growth in each period. Triggers, schedules and rulesets are all policies; see pyforestry.base.simulation.pipeline.

  • attrs – Site and history values the stand does not carry typed. Resolved into the model’s Inputs during the build, so a missing one is a named error before any time is simulated.

  • inputs – An already-built Inputs instance, which skips resolution.

  • pipeline – The ordered steps to run each period, overriding the default (management if a policy was given, then growth). Supply this to add a valuation step or reorder the phases.

Returns:

A ProjectionResult.

Raises:

ValueError – If model names nothing, if the stand cannot supply what the model needs, or if the clock arguments are not positive.

Module contents#

Top-level package for pyforestry.

The compact path to a projection – you have a stand, you want it advanced:

import pyforestry as pf

result = pf.project(stand, model="elfving_2010", years=100, step=5, seed=42)

pf.available_models() lists what model= accepts.

The other shape is a composite pipeline: you have a site rather than a stand, and want one reconstructed and grown through a whole published workflow (NYSKOG regeneration, young-stand growth, mortality, ingrowth, valuation). project cannot drive one, because a pipeline builds its own stand:

from pyforestry.sweden.simulation.presets import get_pipeline

pipeline = get_pipeline("elfving_2010_composite")
table = pipeline.run_projection(site=site, n_steps=20)

pf.available_pipelines() lists those. The two are named apart on purpose: "elfving_2010" is the growth model, "elfving_2010_composite" the whole workflow around it, and the same string used to mean both.

Everything else is lazily loaded on first access, so importing the package costs almost nothing until you reach for a region.