pyforestry.simulation.valuation package#
Submodules#
pyforestry.simulation.valuation.cashflow module#
What a run earned, when, and what that is worth at the start of it.
A projection that thins produces revenue in the year the thinning happened. Two things then stand between those figures and a number an analyst can compare against another scenario, and both are forcings:
What a cubic metre fetched that year. A
PRICEforcing scales each period’s revenue where it is earned, so the flows this module receives are already nominal — in the money of their own year.What a krona in that year is worth now. Discounting, at a rate that may itself vary year by year, which is a
DISCOUNTforcing.
Keeping them apart is the point. A single “real” rate folded together hides which half of a difference between two scenarios came from prices and which from time preference; the run manifest records both, separately, with their sources.
A discount rate is a stated preference, not a finding about the world, so it is
cited with the (none)/year-0 sentinel this package already uses for anything
authored rather than published — stated_choice()
builds one.
- pyforestry.simulation.valuation.cashflow.CASH_FLOWS_KEY = 'cash_flows'#
Where a run’s cash flows accumulate on the context, one entry per period that earned anything.
- class pyforestry.simulation.valuation.cashflow.CashFlow(year: float, amount: float, price_factor: float = 1.0, label: str = '')[source]#
Bases:
objectOne period’s revenue, in the money of the year it was earned.
- Variables:
year (float) – The calendar year the revenue was earned in.
amount (float) – The revenue, nominal – already scaled by whatever price forcing applied that year.
price_factor (float) – The price forcing that was applied, recorded so a reader can separate a change in prices from a change in what was cut.
label (str) – What produced it, e.g. the stage’s name.
- label: str = ''#
- price_factor: float = 1.0#
- pyforestry.simulation.valuation.cashflow.discount_factor(year: float, base_year: float, *, discount_rate: float | None = None, forcings: ForcingSet | None = None) float[source]#
Return what one unit of money in
yearis worth inbase_year.With a flat rate this is the textbook
1 / (1 + r) ** (year - base_year). With aDISCOUNTforcing it compounds year by year, so a rate that changes over the horizon discounts each year at its own – which a single exponent cannot express.- Parameters:
year – The year the money falls in.
base_year – The year to express it in. Usually the run’s first.
discount_rate – A flat annual rate, e.g.
0.03.forcings – The run’s forcings; a
DISCOUNTone overrides the flat rate.
- Returns:
The factor to multiply a nominal amount by.
1.0when the flow is in the base year, or when no rate applies at all.- Raises:
ValueError – If a rate of
-1or below applies, which makes money in the following year infinitely valuable.KeyError – If a
DISCOUNTseries does not cover every year betweenbase_yearandyear. Compounding needs a rate for each of them, so a term structure has to be dense over the horizon it is used on.
- pyforestry.simulation.valuation.cashflow.net_present_value(cash_flows: Iterable[CashFlow], *, base_year: float, discount_rate: float | None = None, forcings: ForcingSet | None = None) float[source]#
Return the discounted value of
cash_flows, expressed inbase_year.- Parameters:
cash_flows – What the run earned, and when.
base_year – The year to express the total in – the run’s first, usually.
discount_rate – A flat annual rate.
forcings – The run’s forcings. A
DISCOUNTforcing supplies a year-varying rate; aPRICEforcing has already been applied to the amounts, where they were earned.
- Returns:
The sum of each flow times its discount factor. Zero for no flows, which is the ordinary case for a projection that never harvested.
pyforestry.simulation.valuation.removals module#
What a run removed, in whatever detail the stand it came from could give.
Two kinds, because two kinds of model produce removals:
TreeRemoval– a stem, with a diameter and a height, which can be bucked into assortments and priced by grade. What a tree-list model gives.MeanTreeRemoval– the stand’s representative stem, and how many of them came out. What an aggregate model gives: Kuehne (2022) and its siblings step a basal area and a stem count, so a thinning there has no individual stems – but it does have a quadratic mean diameter, and that mean tree can be bucked like any other.
The second existed nowhere, which is the whole reason Norway had no valuation stage: its models are aggregate, the ledger could only hold stems, so there was nothing for a valuation to price. A stand-level model cannot be bucked stem by stem; it can be bucked once, at its mean tree.
- class pyforestry.simulation.valuation.removals.CohortRemoval(identifier: str, species: TreeName | str, metadata: MutableMapping[str, ~typing.Any]=<factory>, trees: list[TreeRemoval] = <factory>)[source]#
Bases:
objectGroup a collection of removed trees for a cohort.
- iter_trees() Iterator[TreeRemoval][source]#
Yield the recorded tree removals.
- record_tree(tree: Tree, *, weight: float | None = None, metadata: Mapping[str, Any] | None = None) TreeRemoval[source]#
Append a tree removal to the cohort.
- property total_weight: float#
Return the sum of removal weights in the cohort.
- property tree_count: int#
Return the number of recorded tree removals.
- class pyforestry.simulation.valuation.removals.MeanTreeRemoval(cohort_id: str, species: TreeName | str, diameter_cm: float, height_m: float, stems: float, volume_m3: float, metadata: Dict[str, ~typing.Any]=<factory>)[source]#
Bases:
objectRecord a removal as one representative stem, and how many came out.
What an aggregate model can say. There are no individual stems, but a stand has a quadratic mean diameter and a mean height, and the stem those describe can be bucked like any other – which is how a stand-level model gets an assortment split at all.
volume_m3is the total the model says left the stand, in the model’s own measure – m3sk for the Nordic stand volume functions – and it is what the run’s summary reports as harvested. It is deliberately not imposed on the logs:MeanTreeVolumeDescriptorbucks the mean stem and multiplies bystems, because a price list buys the narrower m3to and scaling the grades up to an m3sk total pays m3to prices on m3sk cubic metres. It is still the ceiling – logs cannot exceed the stem volume they came from – and the descriptor raises if they do.- Variables:
cohort_id (str) – Which removal event this belongs to.
species (pyforestry.base.helpers.tree_species.TreeName | str) – The species removed.
diameter_cm (float) – The representative stem’s diameter, normally the stand’s QMD.
height_m (float) – Its height. Lorey’s mean height where the stand has one.
stems (float) – How many such stems came out, per hectare.
volume_m3 (float) – The total volume removed, per hectare, from the model.
metadata (Dict[str, Any]) – Carried through to the piece records.
- property species_name: str#
The species’ full scientific name.
- class pyforestry.simulation.valuation.removals.StandRemovalLedger(stand_id: str | None = None, metadata: MutableMapping[str, ~typing.Any]=<factory>, cohorts: MutableMapping[str, ~pyforestry.simulation.valuation.removals.CohortRemoval]=<factory>, mean_trees: list[MeanTreeRemoval] = <factory>)[source]#
Bases:
objectTop-level container aggregating removal cohorts for a stand.
- add_cohort(identifier: str, *, species: TreeName | str, metadata: Mapping[str, Any] | None = None) CohortRemoval[source]#
Register a cohort and return it (existing cohorts are updated).
- extend(cohorts: Iterable[CohortRemoval]) None[source]#
Merge
cohortsinto the ledger.
- property is_empty: bool#
Return
Truewhen nothing at all was recorded.Both kinds count: a ledger holding only mean-tree removals is not empty, which is what lets an aggregate model’s thinning be priced.
- iter_cohorts() Iterator[CohortRemoval][source]#
Yield the registered cohorts.
- iter_mean_tree_removals() Iterator[MeanTreeRemoval][source]#
Yield mean-tree removals, in the order they were recorded.
- iter_tree_removals() Iterator[TreeRemoval][source]#
Yield tree removals across all cohorts.
- record_mean_tree(identifier: str, *, species: TreeName | str, diameter_cm: float, height_m: float, stems: float, volume_m3: float, metadata: Mapping[str, Any] | None = None) MeanTreeRemoval[source]#
Record a removal as a representative stem, for a stand holding no stems.
- Parameters:
identifier – The removal event, e.g.
"management@2035".species – What was removed.
diameter_cm – The representative stem’s diameter, normally the QMD.
height_m – Its height.
stems – How many came out, per hectare.
volume_m3 – The total volume removed, per hectare, from the model.
metadata – Carried through to the piece records.
- Returns:
The recorded removal.
- record_tree(identifier: str, tree: Tree, *, species: TreeName | str | None = None, weight: float | None = None, metadata: Mapping[str, Any] | None = None) TreeRemoval[source]#
Record a tree removal under
identifierand return the entry.
- stand_id: str | None = None#
- property total_volume_m3: float#
Return the volume recorded through mean-tree removals.
- property total_weight: float#
Return the total removal weight across cohorts.
- property tree_count: int#
Return the total number of tree removals.
- class pyforestry.simulation.valuation.removals.TreeRemoval(cohort_id: str, species: TreeName | str, tree: Tree, weight: float | None = None, metadata: MutableMapping[str, ~typing.Any]=<factory>)[source]#
Bases:
objectRecord the removal of an individual (possibly weighted) tree.
- property diameter_cm: float#
Return the tree diameter in centimetres.
- property height_m: float#
Return the tree height in metres.
- property species_name: str#
Return the lower-case
"genus species"representation.
- property stump_height_m: float#
Return the stump height in metres for the removal.
- weight: float | None = None#
pyforestry.simulation.valuation.step module#
Pricing removals as a step in a simulation pipeline.
This is what gives pyforestry.simulation.valuation a runtime. The ledger,
the connector and the piece records were a complete design with no caller,
because nothing harvested through a runtime; a pipeline that thins and then
prices what it removed is that caller.
- class pyforestry.simulation.valuation.step.ValuationStep(settings: ValuationSettings, connector: VolumeConnector = <factory>, forcings: ForcingSet = <factory>, name: str = 'valuation')[source]#
Bases:
objectPrice whatever the run has removed, and add it to the run’s cash.
The price list, taper and bucking config are constructor arguments, because they are properties of the pipeline a caller assembles and do not change between periods. Only the ledger comes off the context, at
ctx.attrs["removal_ledger"]– the one place – and the priced result goes back toctx.attrs["valuation"].They used to be looked for on
ctx.attrs["valuation_settings"], falling back to “the context itself”, which could not work: aSimulationContexthas no price list, so the documented default raisedAttributeErrorthe first time a run removed anything. A required argument cannot be forgotten.Does nothing when there is no ledger or it is empty, which is the ordinary case for a step that did not thin.
- CASH_KEY = 'cash'#
- LEDGER_KEY = 'removal_ledger'#
Where the ledger is read from and the result is written to.
- RESULT_KEY = 'valuation'#
- name: str = 'valuation'#
- run(ctx: SimulationContext, dt: float) None[source]#
Price this period’s removals into
ctx.attrs, then clear the ledger.Clearing is what makes the step correct over more than one period. The ledger accumulates as steps remove stems; pricing it without emptying it re-prices every earlier period’s removals again, so
cashcompounds what was already banked and the bucking cost grows with the square of the run length. Nothing ran this step for two periods until the scenario runtime did, so neither showed.
pyforestry.simulation.valuation.volume module#
Volume conversion utilities for valuation workflows.
Pricing a removal needs three things the ledger does not carry: a price list, a
taper function and a bucking configuration. They arrive as one typed
ValuationSettings.
Two routes, chosen by what the stand could report. A tree list is bucked stem by
stem and priced by grade (TreeVolumeDescriptor). An aggregate model has
no individual stems, but it has a quadratic mean diameter and a mean height, so
its representative stem is bucked once and scaled up
(MeanTreeVolumeDescriptor) – which gets a stand-level model a real
assortment split rather than pulpwood for everything.
That used to be a model_view: Any – a name left over from a subsystem this
package no longer has – resolved by getattr across four spellings
(pricelist or price_list, taper_class or get_taper_class(), each
optionally callable). It is the defect ValuationStep
was written to fix one layer up, where a ledger had been looked for in six
places: several conventions for one value is no convention, and the failure mode
is an object that implements a seventh spelling being read as implementing
nothing.
- class pyforestry.simulation.valuation.volume.EmptyVolumeDescriptor(*, ledger: StandRemovalLedger, metadata: MutableMapping[str, ~typing.Any]=<factory>, reason: str = 'empty')[source]#
Bases:
VolumeDescriptorDescriptor used when no removals are present.
- evaluate() VolumeResult[source]#
Return an empty result tagged with the reason for emptiness.
- reason: str = 'empty'#
- class pyforestry.simulation.valuation.volume.MeanTreeVolumeDescriptor(*, ledger: ~pyforestry.simulation.valuation.removals.StandRemovalLedger, metadata: ~typing.MutableMapping[str, ~typing.Any] = <factory>, removals: ~typing.Tuple[~pyforestry.simulation.valuation.removals.MeanTreeRemoval, ...], pricelist: ~pyforestry.base.pricelist.pricelist.Pricelist, taper_class: ~typing.Type[~pyforestry.base.taper.taper.Taper], bucking_config: ~pyforestry.base.helpers.bucking.BuckingConfig = <factory>, bucker_cls: ~typing.Type[~pyforestry.base.timber_bucking.nasberg_1985.Nasberg_1985_BranchBound] = <class 'pyforestry.base.timber_bucking.nasberg_1985.Nasberg_1985_BranchBound'>, min_diam_dead_wood: float = 0.0, timber_factory: ~typing.Callable[[~pyforestry.simulation.valuation.volume.StemDimensions], ~pyforestry.base.timber.timber_base.timber.Timber] = <function _default_timber_factory>)[source]#
Bases:
VolumeDescriptorBuck the stand’s representative stem, and scale it to the stems removed.
An aggregate model reports a basal area and a stem count, so a thinning from it has no individual stems to cut. It does have a quadratic mean diameter and a mean height, and the stem those describe is a real stem: bucking it once gives the proportions of butt, middle, top and pulp a stand of that mean size yields.
One stem’s worth of logs, multiplied by how many came out – the same arithmetic
TreeVolumeDescriptordoes with an expansion factor, so the two routes report volume and value on the same basis.This used to scale the grades so they summed to the model’s own volume figure, on the reasoning that how much came out is the model’s business and only the split is the mean tree’s. Those are not the same measure of volume. A stand volume function reports m3sk – stem volume over bark, on the wider Nordic definition; the price list buys m3to, top-measured log volume, as
TimberPricelist.volume_typedeclares. Scaling m3to grades up to an m3sk total pays m3to prices on m3sk cubic metres, about 21% more per removal than the stem yields. Nothing caught it because it made priced volume equal harvested volume by construction, and the tree-list route – which prices around 78% of what its own reporter counts – was the only place the difference showed.So the model’s figure is no longer imposed on the logs. It still says how much left the stand, which is what the summary’s
harvested_m3reports in m3sk; the logs are what the mean stem yields in m3to.share_of_removed_volume_soldis the ratio between them, so the conversion is a number a reader can see instead of an assumption.Three things it assumes, all worth knowing before comparing the result with a bucked inventory:
The mean tree’s grade split is the stand’s. It is not: value is convex in diameter, so a stand with the same mean but a wider spread yields more sawtimber than its mean tree suggests. This understates a heterogeneous stand and is exact only for a uniform one.
Whatever height the run supplied for the mean stem is the mean stem’s. A model that predicts only dominant height has none, and using that overstates the stem’s taper – which is why the height is the run’s to supply rather than something guessed here. That overstatement now shows up where it can be seen, in the share of removed volume sold, rather than being absorbed into a scale factor.
The mean stem stands for every stem removed. A thinning from below takes stems smaller than the mean, so pricing them all at the mean overstates the grade split of a low thinning.
- Raises:
ValueError – From
evaluate(), if a mean stem bucks to more log volume than the model says came out of the stand. m3to is a narrower measure than m3sk, so the logs cannot exceed the stem volume they came from; when they do, the representative stem is too large for the removal – most often a dominant height standing in for a mean one.
- evaluate() VolumeResult[source]#
Buck each mean stem and multiply its grades by the stems removed.
- Raises:
ValueError – If a mean stem’s logs exceed the volume the model says was removed, which no real stem can do.
- min_diam_dead_wood: float = 0.0#
- class pyforestry.simulation.valuation.volume.PieceRecord(cohort_id: str, species: str, quality: QualityType, length_m: float, top_diameter_cm: float, volume_m3: float, value: float, weight: float)[source]#
Bases:
objectRepresentation of one bucked piece aggregated across identical trees.
- class pyforestry.simulation.valuation.volume.StemDimensions(*args, **kwargs)[source]#
Bases:
ProtocolWhat a timber factory needs from a removal, of either kind.
TreeRemovalandMeanTreeRemovalboth satisfy it, so one factory serves both routes – a region whose taper needs its ownTimbersubclass writes it once.- property diameter_cm: float#
Breast-height diameter, in cm.
- property height_m: float#
Total height, in m.
- property species_name: str#
The species’ full scientific name.
- class pyforestry.simulation.valuation.volume.TreeVolumeDescriptor(*, ledger: ~pyforestry.simulation.valuation.removals.StandRemovalLedger, metadata: ~typing.MutableMapping[str, ~typing.Any] = <factory>, removals: ~typing.Tuple[~pyforestry.simulation.valuation.removals.TreeRemoval, ...], pricelist: ~pyforestry.base.pricelist.pricelist.Pricelist, taper_class: ~typing.Type[~pyforestry.base.taper.taper.Taper], bucking_config: ~pyforestry.base.helpers.bucking.BuckingConfig = <factory>, bucker_cls: ~typing.Type[~pyforestry.base.timber_bucking.nasberg_1985.Nasberg_1985_BranchBound] = <class 'pyforestry.base.timber_bucking.nasberg_1985.Nasberg_1985_BranchBound'>, min_diam_dead_wood: float = 0.0, timber_factory: ~typing.Callable[[~pyforestry.simulation.valuation.volume.StemDimensions], ~pyforestry.base.timber.timber_base.timber.Timber] = <function _default_timber_factory>)[source]#
Bases:
VolumeDescriptorDescriptor based on individual tree removals.
- evaluate() VolumeResult[source]#
Run bucking and valuation over the recorded tree removals.
- min_diam_dead_wood: float = 0.0#
- class pyforestry.simulation.valuation.volume.ValuationSettings(pricelist: ~pyforestry.base.pricelist.pricelist.Pricelist, taper_class: ~typing.Type[~pyforestry.base.taper.taper.Taper], bucking_config: ~pyforestry.base.helpers.bucking.BuckingConfig = <factory>, min_diam_dead_wood: float = 0.0, timber_factory: ~typing.Callable[[~pyforestry.simulation.valuation.volume.StemDimensions], ~pyforestry.base.timber.timber_base.timber.Timber] = <function _default_timber_factory>)[source]#
Bases:
objectWhat pricing a removal needs beyond the removal itself.
One typed object, validated when it is built, so a misconfiguration is a named error at the call site rather than an
AttributeErrorraised the first time a run happens to thin something.- Parameters:
pricelist – The price list to value the bucked assortments against.
taper_class – The
Tapersubclass the bucker takes stem dimensions from.bucking_config – Bucking options.
save_sectionsis forced on, because the piece records this package reports are the sections.min_diam_dead_wood – Minimum top diameter, in cm, below which wood is treated as dead and not merchandised.
timber_factory – Builds the
Timbera removal is bucked as, for either kind of removal – an individual stem or a stand’s mean tree. Defaults to the baseTimber; a region whose taper needs its own subclass supplies one, which is what Sweden’sEdgrenNylinder1949requires – it rejects anything that is not aSweTimber, so the default produced aValueErrorfrom inside the bucker rather than at configuration time.
- Raises:
TypeError – If any field is not of its declared type.
- min_diam_dead_wood: float = 0.0#
- class pyforestry.simulation.valuation.volume.VolumeConnector(*, bucker_cls: Type[Nasberg_1985_BranchBound] | None = None)[source]#
Bases:
objectResolve removal ledgers into volume descriptors and valuation results.
- connect(settings: ValuationSettings, ledger: StandRemovalLedger) VolumeResult[source]#
Return the evaluated volume result for
ledgerundersettings.
- describe(settings: ValuationSettings, ledger: StandRemovalLedger) VolumeDescriptor[source]#
Return the descriptor describing
ledgerpriced undersettings.- Raises:
TypeError – If
settingsorledgeris not of its declared type.
- class pyforestry.simulation.valuation.volume.VolumeDescriptor(*, ledger: StandRemovalLedger, metadata: MutableMapping[str, ~typing.Any]=<factory>)[source]#
Bases:
objectBase descriptor for conversion inputs.
- evaluate() VolumeResult[source]#
Return an empty result for descriptors that cannot produce volume.
- class pyforestry.simulation.valuation.volume.VolumeResult(descriptor: ~pyforestry.simulation.valuation.volume.VolumeDescriptor, pieces: ~typing.Tuple[~pyforestry.simulation.valuation.volume.PieceRecord, ...], total_value: float, volume_by_quality: ~typing.Mapping[~pyforestry.base.helpers.bucking.QualityType, float], metadata: ~typing.Mapping[str, ~typing.Any] = <factory>)[source]#
Bases:
objectContainer describing the outcome of a valuation conversion.
- property total_volume: float#
Return the total merchantable volume (m³).
Module contents#
Valuation helpers for converting removals into marketable products.