pyforestry.base.competition package#

Submodules#

pyforestry.base.competition.api module#

Compute competition indices for real tree records.

competition_indices() is the entry point: give it a list of Tree, a CircularPlot or a Stand, say which indices you want and how competitors should be chosen, and get one TreeCompetition back per tree.

Two neighbourhoods per tree#

The distance-independent indices are stand descriptors: BAL is the basal area per hectare standing in trees larger than the subject, over the whole plot. Restricting them to the trees a spatial selector happened to keep would make them shrink with the search radius and stop being the published quantity. Each subject therefore gets two neighbourhoods: the selector’s competitors drive the spatially explicit indices, and every other tree on the plot drives the distance-independent ones. The selector never touches the latter.

A plot is one population#

A Stand is processed one plot at a time. Stem coordinates are plot-local, plot-level quantities (basal area per hectare, QMD, stem density) describe a single plot, and trees on different plots do not compete. Results come back in plot order, then tree order.

weight_n and occlusion#

Every per-hectare quantity honours Tree.weight_n, so a record standing for ten stems counts as ten, and CircularPlot.occlusion, so a plot that only observed 1 - occlusion of its area expands accordingly – the same conventions the plot aggregation in pyforestry.base.helpers.stand uses. The spatially explicit indices are per-stem geometry and ignore weight_n: a representation tree has one position, and pretending it has ten would place ten stems on one point.

Edge effect#

A subject tree near the plot boundary has part of its competition zone outside the plot, so its distance-dependent indices come out too low. Each result carries observed_zone_fraction, the share of the competition zone that fell inside the plot, computed as a circle-circle overlap. With edge_correction="proportional_area" (the default for a plot of known geometry) the additive spatial indices are divided by that fraction, which is exactly the assumption that the unobserved sector holds competition at the same areal density as the observed part. Pass edge_correction=None to get raw values and apply your own weighting; observed_zone_fraction is reported either way, so a caller can always filter on it instead.

Two limits on that correction, both deliberate:

  • Only indices that are plain sums over competitors scale with the observed share. SBAr is a ratio and Almdg a weight-normalised mean of areas – halving the competitor set leaves both unchanged – so dividing them by the fraction would invent competition. They are reported raw and flagged by CompetitionIndex.additive.

  • The correction needs a competition zone that was fixed before the data were seen. Selectors that have no such zone (SearchCone, NearestNeighbours, BitterlichBAF) report zone_radius_m=None and are left uncorrected, because deriving the zone from the competitors that were actually found is circular: a truncated neighbourhood looks smaller, so the correction it implies is too small.

Non-spatial indices are never edge-corrected: they are plot-level sums that do not depend on where in the plot the subject sits.

class pyforestry.base.competition.api.TreeCompetition(tree: Tree, indices: Dict[str, float]=<factory>, n_competitors: int = 0, zone_radius_m: float | None = None, observed_zone_fraction: float = 1.0, edge_corrected: bool = False, skipped: Dict[str, str]=<factory>)[source]#

Bases: object

Competition indices computed for one subject tree.

Variables:
  • tree (pyforestry.base.helpers.tree.Tree) – The subject tree.

  • indices (Dict[str, float]) – Index value by Table 2 abbreviation. An index that could not be computed is absent from this mapping and appears in skipped.

  • n_competitors (int) – How many trees the selector kept for the spatially explicit indices. The distance-independent indices always see every other tree on the plot and are unaffected by this number.

  • zone_radius_m (float | None) – The competition-zone radius applied (m), if the selector defines one ahead of the data. None for the variable-reach selectors, which is also why those get no edge correction.

  • observed_zone_fraction (float) – Share of the competition zone inside the plot, in (0, 1]. 1.0 when no plot geometry was supplied, in which case no edge statement is being made.

  • edge_corrected (bool) – Whether the additive spatial indices were divided by observed_zone_fraction.

  • skipped (Dict[str, str]) – Why each unavailable index could not be computed.

edge_corrected: bool = False#
n_competitors: int = 0#
observed_zone_fraction: float = 1.0#
zone_radius_m: float | None = None#
pyforestry.base.competition.api.competition_indices(source: Sequence[Tree] | CircularPlot | object, *, indices: Sequence[str] | None = None, selector: CompetitorSelector | None = None, plot_area_ha: float | None = None, dominant_height_m: float | None = None, crown_radius: float | Callable[[Tree], float | None] | None = None, edge_correction: str | None = 'proportional_area') → List[TreeCompetition][source]#

Compute competition indices for every tree in source.

Parameters:
  • source – A sequence of Tree, a CircularPlot, or a Stand, whose plots are processed independently.

  • indices – Table 2 abbreviations to compute, e.g. ("Heg", "BAL"). Defaults to every index that the available data supports.

  • selector – How to choose competitors for the spatially explicit indices. Defaults to FixedRadius at 10 m. Ignored for the distance-independent indices, which always use the whole plot.

  • plot_area_ha – Plot area (ha), before occlusion. Taken from the plot when not given, and applied to every plot of a stand.

  • dominant_height_m – Dominant height (m), needed only for BALMOD’s relative spacing term.

  • crown_radius – Crown/influence-zone radius for the overlap indices – a constant, a callable f(tree) -> radius | None, or None to read Tree.crown_radius_m.

  • edge_correction – "proportional_area" (default) divides the additive spatial indices by the observed share of the competition zone; None leaves them raw. Either way the share is reported.

Returns:

One TreeCompetition per usable tree, in input order (plot order first for a stand).

Raises:
  • KeyError – If indices names an index that does not exist.

  • ValueError – If edge_correction is not a recognised mode.

pyforestry.base.competition.api.zone_area_m2(radius_m: float) → float[source]#

Area of a circular competition zone (m^2).

pyforestry.base.competition.geometry module#

Circle geometry shared by the influence-zone indices and the edge correction.

Two operations are needed repeatedly:

  • the area two overlapping circles share (crown/influence-zone overlap, O_ij);

  • the fraction of a circle that lies inside another circle (the proportional-area edge weight for a subject tree whose competition zone extends past the plot boundary).

Both are closed-form; no numerical integration is involved.

pyforestry.base.competition.geometry.circle_intersection_area(radius_a_m: float, radius_b_m: float, distance_m: float) → float[source]#

Area shared by two circles (m^2).

The standard circular-segment result: for centres d apart and radii r_a, r_b the lens area is the sum of the two circular segments cut off by the radical line.

Parameters:
  • radius_a_m – Radius of the first circle (m).

  • radius_b_m – Radius of the second circle (m).

  • distance_m – Distance between the two centres (m).

Returns:

0.0 when the circles are disjoint, and the area of the smaller circle when one contains the other.

Return type:

The shared area in m^2

Raises:

ValueError – If a radius or the distance is negative.

pyforestry.base.competition.geometry.circle_overlap_length(radius_a_m: float, radius_b_m: float, distance_m: float) → float[source]#

Linear overlap of two circles along the line joining their centres (m).

r_a + r_b - d, clamped at zero. This is the quantity Staebler (1951) sums: the length by which a competitor’s influence zone reaches into the subject tree’s zone.

Parameters:
  • radius_a_m – Radius of the subject tree’s influence zone (m).

  • radius_b_m – Radius of the competitor’s influence zone (m).

  • distance_m – Distance between the two stems (m).

Returns:

The overlap length in m, or 0.0 when the zones do not meet.

pyforestry.base.competition.geometry.fraction_of_circle_inside_circle(centre_offset_m: float, circle_radius_m: float, boundary_radius_m: float) → float[source]#

Fraction of a circle that falls inside a bounding circle.

Used for proportional-area edge weighting on a circular plot: the subject tree sits centre_offset_m from the plot centre and carries a competition zone of circle_radius_m; the plot has radius boundary_radius_m. The returned fraction is the share of that competition zone that was actually observable.

Parameters:
  • centre_offset_m – Distance from the plot centre to the subject tree (m).

  • circle_radius_m – Radius of the subject tree’s competition zone (m).

  • boundary_radius_m – Radius of the plot (m).

Returns:

A fraction in [0, 1]. 1.0 when the zone lies wholly inside the plot. For a tree that is itself inside the plot the result is always positive, since the zone is centred on it; 0.0 is only reachable for a centre further out than boundary_radius_m + circle_radius_m, which means the point and the boundary do not describe the same thing. Callers that divide by this must handle that case – competition_indices() refuses to make an edge statement about a tree outside its plot.

Raises:

ValueError – If any argument is negative, or the zone radius is zero.

pyforestry.base.competition.geometry.mean_spacing_m(stems_per_ha: float) → float[source]#

Mean distance between neighbouring stems (m) for a given density.

sqrt(10000 / N), the square-spacing equivalent used by Lee & Gadow (1997) to scale a dynamic competition-zone radius.

Parameters:

stems_per_ha – Stem density (trees/ha). Must be positive.

Returns:

Mean spacing in metres.

Raises:

ValueError – If stems_per_ha is not positive.

pyforestry.base.competition.indices module#

Eighteen individual-tree competition indices, each attributed to its own author.

Seven distance-independent and eleven spatially explicit indices, spanning Staebler (1951) to Schröder & Gadow (1999). Every index is a pure function of a Neighbourhood, and every one carries the citation of the paper that proposed it – reachable as index_source() or INDEX_REGISTRY[name].source. The set was assembled and compared by Maleki, Kiviste & Korjus (2015), who are credited for that collection in INDEX_SET_REVIEW and for nothing else.

Symbols follow the usual convention: d_i subject diameter (cm), d_j competitor diameter (cm), l_ij stem-to-stem distance (m), g basal area (m^2), G plot basal area (m^2/ha), S plot area (ha), CZR competition-zone radius (m), CZ competition-zone area (m^2), O_ij influence-zone overlap (m^2).

Where the transcription departs from the 2015 review’s Table 2 it does so to follow the original, and says so at the point of use:

  • Martin & Ek (1984), ``Sdrl2`` – the exponent is negative. Table 2 prints exp(+16*l_ij/(d_i+d_j)), as does Wang et al. (2012) Table 1, which would make a competitor’s contribution grow without bound with distance: at d_i+d_j = 40 cm one 10 m away would count 24 times one at 2 m. See martin_ek_sdrl2().

  • Rouvinen & Kuuluvainen – Table 2 dates the work 1977; it is 1997.

  • The angular indices (SAng1, SAng2, SdrAng) convert diameters from cm to m before the arctangent. As printed the ratio mixes cm with m, putting a 20 cm stem 5 m away at 76 degrees instead of 2.3. See lin_sang1().

class pyforestry.base.competition.indices.CompetitionIndex(abbreviation: str, compute: Callable[[Neighbourhood], float], source: SourceReference, spatial: bool, additive: bool)[source]#

Bases: object

One competition index: its formula, its provenance and what it needs.

Variables:
  • abbreviation (str) – The short name used in the literature, e.g. "Heg".

  • compute (Callable[[pyforestry.base.competition.neighbourhood.Neighbourhood], float]) – The kernel, a pure function of a Neighbourhood.

  • source (pyforestry.base.contracts.SourceReference) – The publication that proposed this index. Not the review it was collected from – see pyforestry.base.competition.sources.INDEX_SET_REVIEW for that.

  • spatial (bool) – Whether the index belongs to Table 2’s spatially explicit group, i.e. whether its value depends on a distance-based competitor selection. True for every index in SPATIAL_INDICES and false for every index in NON_SPATIAL_INDICES, so the flag and the grouping never disagree. SBAr is spatial on this reading even though its arithmetic has no distance term: its competitor set is distance-defined, and it is the one index whose formula and grouping come apart.

  • additive (bool) – Whether the index is a plain sum over competitors, and so scales with the share of the competition zone that was observed. Only additive indices can be edge-corrected by dividing by observed_zone_fraction; SBAr (a ratio) and Almdg (a weight-normalised mean of areas) are unchanged when half the competitors are lost, so dividing them would invent competition.

pyforestry.base.competition.indices.INDEX_REGISTRY: Dict[str, CompetitionIndex] = {'Almdg': CompetitionIndex(abbreviation='Almdg', compute=<function alemdag_almdg>, source=SourceReference(author='Alemdag, I.S.', year=1978, title='Evaluation of some competition indexes for the prediction of diameter increment in planted white spruce', appendix='', note='Canadian Forestry Service, Forest Management Institute, Information Report FMR-X-108, Ottawa.'), spatial=True, additive=False), 'BA-gj': CompetitionIndex(abbreviation='BA-gj', compute=<function basal_area_sum_ba_gj>, source=SourceReference(author='Steneker, G.A. & Jarvis, J.M.', year=1963, title='A preliminary study to assess competition in a white spruce-trembling aspen stand', appendix='', note='The Forestry Chronicle 39(3):334-336. doi:10.5558/tfc39334-3'), spatial=False, additive=False), 'BAL': CompetitionIndex(abbreviation='BAL', compute=<function basal_area_of_larger_bal>, source=SourceReference(author='Wykoff, W.R., Crookston, N.L. & Stage, A.R.', year=1982, title="User's guide to the Stand Prognosis Model", appendix='', note='USDA Forest Service, Intermountain Forest and Range Experiment Station, General Technical Report INT-133, Ogden, Utah.'), spatial=False, additive=False), 'BALMOD': CompetitionIndex(abbreviation='BALMOD', compute=<function balmod>, source=SourceReference(author='Schröder, J. & von Gadow, K.', year=1999, title='Testing a new competition index for Maritime pine in northwestern Spain', appendix='', note='Canadian Journal of Forest Research 29:280-283.'), spatial=False, additive=False), 'BALr': CompetitionIndex(abbreviation='BALr', compute=<function bal_ratio_balr>, source=SourceReference(author='Vanclay, J.K.', year=1991, title='Aggregating tree species to develop diameter increment equations for tropical rainforests', appendix='', note='Forest Ecology and Management 42:143-168.'), spatial=False, additive=False), 'BAr': CompetitionIndex(abbreviation='BAr', compute=<function basal_area_ratio_bar>, source=SourceReference(author='Corona, P. & Ferrara, A.', year=1989, title='Individual competition indices for conifer plantations', appendix='', note='Agriculture, Ecosystems and Environment 27:429-437.'), spatial=False, additive=False), 'Heg': CompetitionIndex(abbreviation='Heg', compute=<function hegyi>, source=SourceReference(author='Hegyi, F.', year=1974, title='A simulation model for managing jack-pine stands', appendix='', note='In: Fries, J. (ed.) Growth models for tree and stand simulation. Royal College of Forestry, Stockholm, pp. 74-90.'), spatial=True, additive=True), 'SAng1': CompetitionIndex(abbreviation='SAng1', compute=<function lin_sang1>, source=SourceReference(author='Lin, J.Y.', year=1974, title='Stand growth simulation models for Douglas-fir and western hemlock in the northwestern United States', appendix='', note='In: Fries, J. (ed.) Growth models for tree and stand simulation. Royal College of Forestry, Stockholm. Citation as given by Maleki et al. (2015) Table 2; not verified against an independent record.'), spatial=True, additive=True), 'SAng2': CompetitionIndex(abbreviation='SAng2', compute=<function rouvinen_kuuluvainen_sang2>, source=SourceReference(author='Rouvinen, S. & Kuuluvainen, T.', year=1997, title='Structure and asymmetry of tree crowns in relation to local competition in a natural mature Scots pine forest', appendix='', note='Canadian Journal of Forest Research 27:890-902. Maleki et al. (2015) Table 2 dates this 1977; the work is 1997.'), spatial=True, additive=True), 'SBAr': CompetitionIndex(abbreviation='SBAr', compute=<function daniels_sbar>, source=SourceReference(author='Daniels, R.F., Burkhart, H.E. & Clason, T.R.', year=1986, title='A comparison of competition measures for predicting growth of loblolly pine trees', appendix='', note='Canadian Journal of Forest Research 16:1230-1237.'), spatial=True, additive=False), 'SOdr': CompetitionIndex(abbreviation='SOdr', compute=<function bella_sodr>, source=SourceReference(author='Bella, I.E.', year=1971, title='A new competition model for individual trees', appendix='', note='Forest Science 17:364-372.'), spatial=True, additive=True), 'SOr': CompetitionIndex(abbreviation='SOr', compute=<function gerrard_sor>, source=SourceReference(author='Gerrard, D.J.', year=1969, title='Competition quotient: a new measure of the competition affecting individual forest trees', appendix='', note='Michigan State University, Agricultural Experiment Station, Research Bulletin 20.'), spatial=True, additive=True), 'Sdr': CompetitionIndex(abbreviation='Sdr', compute=<function sum_diameter_ratio_sdr>, source=SourceReference(author='Lorimer, C.G.', year=1983, title='Tests of age-independent competition indices for individual trees in natural hardwood stands', appendix='', note='Forest Ecology and Management 6:343-360.'), spatial=False, additive=False), 'SdrAng': CompetitionIndex(abbreviation='SdrAng', compute=<function rouvinen_kuuluvainen_sdrang>, source=SourceReference(author='Rouvinen, S. & Kuuluvainen, T.', year=1997, title='Structure and asymmetry of tree crowns in relation to local competition in a natural mature Scots pine forest', appendix='', note='Canadian Journal of Forest Research 27:890-902. Maleki et al. (2015) Table 2 dates this 1977; the work is 1997.'), spatial=True, additive=True), 'Sdrl1': CompetitionIndex(abbreviation='Sdrl1', compute=<function lorimer_sdrl1>, source=SourceReference(author='Lorimer, C.G.', year=1983, title='Tests of age-independent competition indices for individual trees in natural hardwood stands', appendix='', note='Forest Ecology and Management 6:343-360.'), spatial=True, additive=True), 'Sdrl2': CompetitionIndex(abbreviation='Sdrl2', compute=<function martin_ek_sdrl2>, source=SourceReference(author='Martin, G.L. & Ek, A.R.', year=1984, title='A comparison of competition measures and growth models for predicting plantation red pine diameter and height growth', appendix='', note='Forest Science 30(3):731-743. The distance term decays: implemented as exp(-16*l/(d_i+d_j)), against the positive exponent printed by Maleki et al. (2015) Table 2 and Wang et al. (2012) Table 1.'), spatial=True, additive=True), 'Sl': CompetitionIndex(abbreviation='Sl', compute=<function staebler_sl>, source=SourceReference(author='Staebler, G.R.', year=1951, title='Growth and spacing in an even-aged stand of Douglas-fir', appendix='', note='MSc thesis, University of Michigan, Ann Arbor.'), spatial=True, additive=True), 'drg': CompetitionIndex(abbreviation='drg', compute=<function diameter_ratio_drg>, source=SourceReference(author='Hamilton, D.A.', year=1986, title='A logistic model of mortality in thinned and unthinned mixed conifer stands of northern Idaho', appendix='', note='Forest Science 32(4):989-1000. Citation as given by Maleki et al. (2015) Table 2; not verified against an independent record.'), spatial=False, additive=False)}#

Every index, keyed by abbreviation.

pyforestry.base.competition.indices.NON_SPATIAL_INDICES: Dict[str, CompetitionIndex] = {'BA-gj': CompetitionIndex(abbreviation='BA-gj', compute=<function basal_area_sum_ba_gj>, source=SourceReference(author='Steneker, G.A. & Jarvis, J.M.', year=1963, title='A preliminary study to assess competition in a white spruce-trembling aspen stand', appendix='', note='The Forestry Chronicle 39(3):334-336. doi:10.5558/tfc39334-3'), spatial=False, additive=False), 'BAL': CompetitionIndex(abbreviation='BAL', compute=<function basal_area_of_larger_bal>, source=SourceReference(author='Wykoff, W.R., Crookston, N.L. & Stage, A.R.', year=1982, title="User's guide to the Stand Prognosis Model", appendix='', note='USDA Forest Service, Intermountain Forest and Range Experiment Station, General Technical Report INT-133, Ogden, Utah.'), spatial=False, additive=False), 'BALMOD': CompetitionIndex(abbreviation='BALMOD', compute=<function balmod>, source=SourceReference(author='Schröder, J. & von Gadow, K.', year=1999, title='Testing a new competition index for Maritime pine in northwestern Spain', appendix='', note='Canadian Journal of Forest Research 29:280-283.'), spatial=False, additive=False), 'BALr': CompetitionIndex(abbreviation='BALr', compute=<function bal_ratio_balr>, source=SourceReference(author='Vanclay, J.K.', year=1991, title='Aggregating tree species to develop diameter increment equations for tropical rainforests', appendix='', note='Forest Ecology and Management 42:143-168.'), spatial=False, additive=False), 'BAr': CompetitionIndex(abbreviation='BAr', compute=<function basal_area_ratio_bar>, source=SourceReference(author='Corona, P. & Ferrara, A.', year=1989, title='Individual competition indices for conifer plantations', appendix='', note='Agriculture, Ecosystems and Environment 27:429-437.'), spatial=False, additive=False), 'Sdr': CompetitionIndex(abbreviation='Sdr', compute=<function sum_diameter_ratio_sdr>, source=SourceReference(author='Lorimer, C.G.', year=1983, title='Tests of age-independent competition indices for individual trees in natural hardwood stands', appendix='', note='Forest Ecology and Management 6:343-360.'), spatial=False, additive=False), 'drg': CompetitionIndex(abbreviation='drg', compute=<function diameter_ratio_drg>, source=SourceReference(author='Hamilton, D.A.', year=1986, title='A logistic model of mortality in thinned and unthinned mixed conifer stands of northern Idaho', appendix='', note='Forest Science 32(4):989-1000. Citation as given by Maleki et al. (2015) Table 2; not verified against an independent record.'), spatial=False, additive=False)}#

Non-spatial indices, keyed by the abbreviation used in the literature. None of these is ever edge-corrected, so additive is irrelevant and set false.

pyforestry.base.competition.indices.SPATIAL_INDICES: Dict[str, CompetitionIndex] = {'Almdg': CompetitionIndex(abbreviation='Almdg', compute=<function alemdag_almdg>, source=SourceReference(author='Alemdag, I.S.', year=1978, title='Evaluation of some competition indexes for the prediction of diameter increment in planted white spruce', appendix='', note='Canadian Forestry Service, Forest Management Institute, Information Report FMR-X-108, Ottawa.'), spatial=True, additive=False), 'Heg': CompetitionIndex(abbreviation='Heg', compute=<function hegyi>, source=SourceReference(author='Hegyi, F.', year=1974, title='A simulation model for managing jack-pine stands', appendix='', note='In: Fries, J. (ed.) Growth models for tree and stand simulation. Royal College of Forestry, Stockholm, pp. 74-90.'), spatial=True, additive=True), 'SAng1': CompetitionIndex(abbreviation='SAng1', compute=<function lin_sang1>, source=SourceReference(author='Lin, J.Y.', year=1974, title='Stand growth simulation models for Douglas-fir and western hemlock in the northwestern United States', appendix='', note='In: Fries, J. (ed.) Growth models for tree and stand simulation. Royal College of Forestry, Stockholm. Citation as given by Maleki et al. (2015) Table 2; not verified against an independent record.'), spatial=True, additive=True), 'SAng2': CompetitionIndex(abbreviation='SAng2', compute=<function rouvinen_kuuluvainen_sang2>, source=SourceReference(author='Rouvinen, S. & Kuuluvainen, T.', year=1997, title='Structure and asymmetry of tree crowns in relation to local competition in a natural mature Scots pine forest', appendix='', note='Canadian Journal of Forest Research 27:890-902. Maleki et al. (2015) Table 2 dates this 1977; the work is 1997.'), spatial=True, additive=True), 'SBAr': CompetitionIndex(abbreviation='SBAr', compute=<function daniels_sbar>, source=SourceReference(author='Daniels, R.F., Burkhart, H.E. & Clason, T.R.', year=1986, title='A comparison of competition measures for predicting growth of loblolly pine trees', appendix='', note='Canadian Journal of Forest Research 16:1230-1237.'), spatial=True, additive=False), 'SOdr': CompetitionIndex(abbreviation='SOdr', compute=<function bella_sodr>, source=SourceReference(author='Bella, I.E.', year=1971, title='A new competition model for individual trees', appendix='', note='Forest Science 17:364-372.'), spatial=True, additive=True), 'SOr': CompetitionIndex(abbreviation='SOr', compute=<function gerrard_sor>, source=SourceReference(author='Gerrard, D.J.', year=1969, title='Competition quotient: a new measure of the competition affecting individual forest trees', appendix='', note='Michigan State University, Agricultural Experiment Station, Research Bulletin 20.'), spatial=True, additive=True), 'SdrAng': CompetitionIndex(abbreviation='SdrAng', compute=<function rouvinen_kuuluvainen_sdrang>, source=SourceReference(author='Rouvinen, S. & Kuuluvainen, T.', year=1997, title='Structure and asymmetry of tree crowns in relation to local competition in a natural mature Scots pine forest', appendix='', note='Canadian Journal of Forest Research 27:890-902. Maleki et al. (2015) Table 2 dates this 1977; the work is 1997.'), spatial=True, additive=True), 'Sdrl1': CompetitionIndex(abbreviation='Sdrl1', compute=<function lorimer_sdrl1>, source=SourceReference(author='Lorimer, C.G.', year=1983, title='Tests of age-independent competition indices for individual trees in natural hardwood stands', appendix='', note='Forest Ecology and Management 6:343-360.'), spatial=True, additive=True), 'Sdrl2': CompetitionIndex(abbreviation='Sdrl2', compute=<function martin_ek_sdrl2>, source=SourceReference(author='Martin, G.L. & Ek, A.R.', year=1984, title='A comparison of competition measures and growth models for predicting plantation red pine diameter and height growth', appendix='', note='Forest Science 30(3):731-743. The distance term decays: implemented as exp(-16*l/(d_i+d_j)), against the positive exponent printed by Maleki et al. (2015) Table 2 and Wang et al. (2012) Table 1.'), spatial=True, additive=True), 'Sl': CompetitionIndex(abbreviation='Sl', compute=<function staebler_sl>, source=SourceReference(author='Staebler, G.R.', year=1951, title='Growth and spacing in an even-aged stand of Douglas-fir', appendix='', note='MSc thesis, University of Michigan, Ann Arbor.'), spatial=True, additive=True)}#

Spatially explicit indices, keyed by the abbreviation used in the literature.

pyforestry.base.competition.indices.alemdag_almdg(n: Neighbourhood) → float[source]#

Almdg: potentially available growing space. Alemdag (1978).

sum(pi * [(l_ij * d_i) / (d_i + d_j)]^2 * (d_j / l_ij) / sum(d_j / l_ij)).

The bracketed term is the radius (m) at which the subject and competitor exert equal influence along the line joining them, so each summand is an area weighted by that competitor’s share of the total size-over-distance.

Like dr_g and SBAr, a high value means less competition: it is the space left to the subject, not the pressure on it. The weights sum to one, so this is a mean rather than a sum – losing half the competitors to a plot boundary leaves it unchanged, which is why it is not edge-corrected.

Parameters:

n – The neighbourhood. Needs distances_m.

Returns:

Growing space in m^2.

Raises:

ValueError – If there are no competitors.

pyforestry.base.competition.indices.bal_ratio_balr(n: Neighbourhood) → float[source]#

BALr: BAL as a share of plot basal area. Vanclay (1991).

sum(g_j for d_j > d_i) / G. Both terms are per-hectare, so the result is dimensionless and bounded by 1 – it is the proportion of the stand’s basal area held in trees larger than the subject.

Parameters:

n – The neighbourhood. Needs plot_area_ha and plot_basal_area_m2_ha.

Returns:

A dimensionless share in [0, 1].

pyforestry.base.competition.indices.balmod(n: Neighbourhood) → float[source]#

BALMOD: BALr scaled by relative spacing. Schröder & Gadow (1999).

[sum(g_j for d_j > d_i) / G] / RS with RS = sqrt(S/N) / H_dom (S in m^2). Dividing by relative spacing lets the index distinguish stands that share a BALr but differ in how crowded they are.

Parameters:

n – The neighbourhood. Needs plot_area_ha, plot_basal_area_m2_ha and relative_spacing.

Returns:

The modified BAL ratio.

pyforestry.base.competition.indices.basal_area_of_larger_bal(n: Neighbourhood) → float[source]#

BAL: basal area of trees larger than the subject. Wykoff et al. (1982).

sum(g_j for d_j > d_i) / S. The single most widely used distance- independent index.

Parameters:

n – The neighbourhood. Needs plot_area_ha.

Returns:

Basal area of larger trees in m^2/ha.

pyforestry.base.competition.indices.basal_area_ratio_bar(n: Neighbourhood) → float[source]#

BAr: summed basal-area ratio per hectare. Corona & Ferrara (1989).

((sum(g_j)) / g_i) / S – the basal-area analogue of Sdr.

Parameters:

n – The neighbourhood. Needs plot_area_ha.

Returns:

The index in ha^-1.

pyforestry.base.competition.indices.basal_area_sum_ba_gj(n: Neighbourhood) → float[source]#

BA-gj: basal area of the neighbours per hectare. Steneker & Jarvis (1963).

sum(g_j) / S – every neighbour counts, regardless of size.

Parameters:

n – The neighbourhood. Needs plot_area_ha.

Returns:

Competitor basal area in m^2/ha.

pyforestry.base.competition.indices.bella_sodr(n: Neighbourhood) → float[source]#

SOdr: Gerrard’s quotient weighted by diameter ratio. Bella (1971).

sum((O_ij * d_j) / (CZ * d_i)).

Parameters:

n – The neighbourhood. Needs distances_m, subject_crown_radius_m and competitor_crown_radius_m.

Returns:

The dimensionless index.

pyforestry.base.competition.indices.compute_index(name: str, neighbourhood: Neighbourhood) → float[source]#

Compute one index by its abbreviation.

Parameters:
  • name – The abbreviation, e.g. "Heg" or "BAL". Case-insensitive.

  • neighbourhood – The subject tree and its competitors.

Returns:

The index value.

Raises:

KeyError – If name is not one of the eighteen indices.

pyforestry.base.competition.indices.daniels_sbar(n: Neighbourhood) → float[source]#

SBAr: subject basal area against the competitors’. Daniels et al. (1986).

(d_i^2 * N_c) / sum(d_j^2). Like dr_g, a high value means the subject dominates its neighbours, i.e. less competition.

Parameters:

n – The neighbourhood.

Returns:

The dimensionless ratio.

Raises:

ValueError – If there are no competitors (the denominator would be zero).

pyforestry.base.competition.indices.diameter_ratio_drg(n: Neighbourhood) → float[source]#

dr_g: subject diameter over the plot quadratic mean diameter. Hamilton (1986).

d_i / d_g. Above 1 the subject is larger than the average tree, so unlike every other index here a high value means less competition.

Parameters:

n – The neighbourhood. Needs plot_qmd_cm.

Returns:

The dimensionless ratio.

pyforestry.base.competition.indices.gerrard_sor(n: Neighbourhood) → float[source]#

SOr: overlap area as a share of the influence zone. Gerrard (1969).

sum(O_ij / CZ) with CZ = pi * CZR_i^2. Gerrard’s competition quotient: how many times over the subject’s growing space is claimed by neighbours.

Parameters:

n – The neighbourhood. Needs distances_m, subject_crown_radius_m and competitor_crown_radius_m.

Returns:

The dimensionless quotient.

pyforestry.base.competition.indices.hegyi(n: Neighbourhood) → float[source]#

Heg: the Hegyi (1974) size-ratio distance-weighted index.

sum(d_j / (d_i * l_ij)). The most widely used distance-dependent index.

Parameters:

n – The neighbourhood. Needs distances_m.

Returns:

The index in m^-1.

pyforestry.base.competition.indices.index_source(name: str) → SourceReference[source]#

Return the publication that proposed the named index.

Parameters:

name – The abbreviation, e.g. "Heg". Case-insensitive.

Returns:

The index’s own primary citation – Hegyi (1974) for "Heg", not the review the set was collected from.

Raises:

KeyError – If name is not one of the eighteen indices.

pyforestry.base.competition.indices.lin_sang1(n: Neighbourhood) → float[source]#

SAng1: summed angular size of the competitors. Lin (1974).

2 * sum(arctan(d_j / (2 * l_ij))) – the horizontal angle each competitor stem subtends at the subject.

Diameters are converted from cm to m first. Table 2 prints d_j in cm against l_ij in m, which is dimensionally inconsistent: a 20 cm stem 5 m away would come out at 76 degrees rather than the correct 2.3.

Parameters:

n – The neighbourhood. Needs distances_m.

Returns:

The summed angle in radians.

pyforestry.base.competition.indices.lorimer_sdrl1(n: Neighbourhood) → float[source]#

Sdrl1: diameter ratio damped by the square root of relative distance. Lorimer (1983).

sum((d_j / d_i) / sqrt(l_ij / CZR)).

Parameters:

n – The neighbourhood. Needs distances_m and competition_zone_radius_m.

Returns:

The dimensionless index.

pyforestry.base.competition.indices.martin_ek_sdrl2(n: Neighbourhood) → float[source]#

Sdrl2: diameter ratio with exponential distance decay. Martin & Ek (1984).

sum((d_j / d_i) * exp(-16 * l_ij / (d_i + d_j))) with l_ij in m and diameters in cm – the constant 16 assumes that mixture.

The exponent is negative here and positive in the source table. Maleki et al. (2015) Table 2 and Wang et al. (2012) Table 1 both print exp(+16*...). That cannot be right: the index is a distance-decay weighting, and a positive exponent makes far competitors dominate near ones without bound (at d_i + d_j = 40 cm, a competitor at 10 m would weigh 24 times one at 2 m). The negative form is the one in general use and is what Martin & Ek describe as an “exponential weighting scheme”.

Parameters:

n – The neighbourhood. Needs distances_m.

Returns:

The dimensionless index.

pyforestry.base.competition.indices.resolve_index(name: str) → CompetitionIndex[source]#

Look an index up by abbreviation, case-insensitively.

Parameters:

name – The abbreviation, e.g. "Heg" or "heg".

Returns:

The registry entry, carrying the kernel, its citation and its flags.

Raises:

KeyError – If name is not one of the eighteen indices.

pyforestry.base.competition.indices.rouvinen_kuuluvainen_sang2(n: Neighbourhood) → float[source]#

SAng2: summed horizontal angle. Rouvinen & Kuuluvainen (1997).

sum(arctan(d_j / l_ij)), diameters converted to m (see lin_sang1()).

Parameters:

n – The neighbourhood. Needs distances_m.

Returns:

The summed angle in radians.

pyforestry.base.competition.indices.rouvinen_kuuluvainen_sdrang(n: Neighbourhood) → float[source]#

SdrAng: horizontal angle weighted by diameter ratio. Rouvinen & Kuuluvainen (1997).

sum((d_j / d_i) * arctan(d_j / l_ij)), diameters converted to m in the arctangent (the d_j / d_i ratio is unit-free either way).

Parameters:

n – The neighbourhood. Needs distances_m.

Returns:

The weighted angle sum in radians.

pyforestry.base.competition.indices.staebler_sl(n: Neighbourhood) → float[source]#

Sl: summed linear overlap of influence zones. Staebler (1951).

sum(l_ij) where l_ij is here the overlap length max(0, CZR_i + CZR_j - distance), not the stem distance. Table 2 reuses the symbol l_ij for both; the paper’s own text places this index in the “influence-zone overlap” group, which fixes the reading.

Parameters:

n – The neighbourhood. Needs distances_m, subject_crown_radius_m and competitor_crown_radius_m.

Returns:

Total overlap length in m.

pyforestry.base.competition.indices.sum_diameter_ratio_sdr(n: Neighbourhood) → float[source]#

Sdr: summed diameter ratio per hectare. Lorimer (1983).

((sum(d_j)) / d_i) / S.

Parameters:

n – The neighbourhood. Needs plot_area_ha.

Returns:

The index in ha^-1.

pyforestry.base.competition.neighbourhood module#

The input a competition index is computed from: a subject tree and its competitors.

Every index in pyforestry.base.competition.indices takes a single Neighbourhood. Keeping one uniform input lets the indices be looked up by name and compared against each other on identical data, which is the whole point of the review this module transcribes: an index value is only meaningful alongside the competitor-selection rule that produced it.

Fields a given index does not need are left None; each index raises a clear MissingNeighbourhoodData naming what it needed rather than failing on an arithmetic error deep inside a sum.

exception pyforestry.base.competition.neighbourhood.MissingNeighbourhoodData[source]#

Bases: ValueError

A competition index was asked for without the inputs it requires.

class pyforestry.base.competition.neighbourhood.Neighbourhood(subject_dbh_cm: float, competitor_dbh_cm: Tuple[float, ...], distances_m: Tuple[float, ...] | None = None, subject_crown_radius_m: float | None = None, competitor_crown_radius_m: Tuple[float, ...] | None = None, plot_area_ha: float | None = None, plot_basal_area_m2_ha: float | None = None, plot_qmd_cm: float | None = None, relative_spacing: float | None = None, competition_zone_radius_m: float | None = None, competitor_weights: Tuple[float, ...] | None = None)[source]#

Bases: object

A subject tree and the competitors selected for it.

Variables:
  • subject_dbh_cm (float) – Diameter at breast height of the subject tree (cm).

  • competitor_dbh_cm (Tuple[float, ...]) – Diameters of the selected competitors (cm), in the same order as every other per-competitor sequence here.

  • competitor_weights (Tuple[float, ...] | None) – How many stems each competitor record stands for (Tree.weight_n). None means one stem each. Used only by the per-hectare distance-independent indices: the spatially explicit ones are per-stem geometry and a record standing for ten stems still has one position.

  • distances_m (Tuple[float, ...] | None) – Distance from the subject stem to each competitor stem (m). Required by the spatially explicit indices; None for a non-spatial neighbourhood.

  • subject_crown_radius_m (float | None) – Radius of the subject tree’s influence zone (m), required by the influence-zone overlap indices.

  • competitor_crown_radius_m (Tuple[float, ...] | None) – Influence-zone radii of the competitors (m).

  • plot_area_ha (float | None) – Area of the plot the competitors were drawn from (ha).

  • plot_basal_area_m2_ha (float | None) – Total basal area of the plot (m^2/ha).

  • plot_qmd_cm (float | None) – Quadratic mean diameter of the plot (cm).

  • relative_spacing (float | None) – Relative spacing index of the plot, sqrt(S/N)/H_dom with S in m^2 (Schröder & Gadow’s RS).

  • competition_zone_radius_m (float | None) – Radius of the competition zone the competitors were selected within (m), used by Lorimer’s Sdrl1.

competition_zone_radius_m: float | None = None#
competitor_basal_areas_m2() → Tuple[float, ...][source]#

Basal area of each competitor record (m^2), one stem each.

competitor_crown_radius_m: Tuple[float, ...] | None = None#
competitor_weights: Tuple[float, ...] | None = None#
distances_m: Tuple[float, ...] | None = None#
classmethod from_sequences(subject_dbh_cm: float, competitor_dbh_cm: Sequence[float], distances_m: Sequence[float] | None = None, **kwargs: object) → Neighbourhood[source]#

Build a Neighbourhood from any sequences, coercing them to tuples.

Parameters:
  • subject_dbh_cm – Subject diameter (cm).

  • competitor_dbh_cm – Competitor diameters (cm).

  • distances_m – Competitor distances (m), if spatial.

  • **kwargs – Any other Neighbourhood field.

Returns:

The frozen neighbourhood.

larger_competitor_mask() → Tuple[bool, ...][source]#

Which competitors are strictly larger in diameter than the subject.

property n_competitors: int#

Number of selected competitor records, ignoring their weights.

plot_area_ha: float | None = None#
plot_basal_area_m2_ha: float | None = None#
plot_qmd_cm: float | None = None#
relative_spacing: float | None = None#
require(*fields: str) → None[source]#

Raise if any named field is unset.

Parameters:

*fields – Attribute names the calling index depends on.

Raises:

MissingNeighbourhoodData – Naming every field that is missing.

property subject_basal_area_m2: float#

Basal area of the subject tree (m^2).

subject_crown_radius_m: float | None = None#
weighted_competitor_basal_areas_m2() → Tuple[float, ...][source]#

Basal area each competitor record contributes (m^2), weight_n included.

property weights: Tuple[float, ...]#

Stems each competitor record stands for; all ones when unweighted.

pyforestry.base.competition.neighbourhood.basal_area_m2(diameter_cm: float) → float[source]#

Cross-sectional area at breast height (m^2) for a diameter in cm.

pyforestry.base.competition.selection module#

Rules for deciding which neighbours count as competitors.

A competition index value is only meaningful alongside the rule that chose its competitors: the same formula on the same stand ranks trees differently under different rules. The formula and the rule are therefore separate, composable objects here. That framing, and this particular set of four approaches, is from Maleki, Kiviste & Korjus (2015); the rules themselves are their own authors’.

Every number in these rules is a parameter with a literature default, not a fixed value – the defaults are one study’s choices, and are meant to be varied:

MeanHeightRadius(fraction=0.25)          # not stuck at 0.4
LeeGadowRadius(k=3.0)                    # the paper tests k = 2 and 3
BitterlichBAF(basal_area_factor=4.0)     # 1, 2 and 4 in the paper
SearchCone(opening_angle_deg=80.0)       # 100, 80 and 60 in the paper
FixedRadius(8.0, min_size_ratio=0.3, elimination_angle_deg=30.0)
  1. FixedRadius – plain geometry, no citation. MeanHeightRadius – a fraction of stand mean height, CZR = 0.4 * h after Sims et al. (2009). The influence-zone concept it rests on is Staebler (1951).

  2. LeeGadowRadius – a dynamic radius k * sqrt(10000 / N), i.e. a multiple of mean spacing, after Lee & von Gadow (1997).

  3. BitterlichBAF – variable-radius selection after Bitterlich (1952): a tree competes when it falls inside its own angle-count limiting distance 50 * d_j / sqrt(BAF), d_j in metres.

  4. SearchCone – the reversed search-cone / angular-height method after Pretzsch (2009), with the apex at the subject’s stem base or crown base.

  5. NearestNeighbours – the n closest stems; plain geometry.

The radius-based rules also take a competition elimination angle: a neighbour standing in the shadow of a nearer competitor, within a cone of elimination_angle_deg, is dropped. The 2015 comparison applies this at 30 degrees alongside the d_j >= 0.3 * d_i size screen for its approaches 1 and 2, so reproducing those needs both.

Each rule that implements somebody’s published proposal exposes it as .source; selector_source() reads it, returning None for the two that are plain geometry.

Only the radius rules (1 and 2) define a competition zone – a circle fixed before the data are seen. The variable-reach rules (3, 4, 5) report Selection.zone_radius_m = None, which means Lorimer’s Sdrl1 and the proportional-area edge correction do not apply to them; see Selection.

class pyforestry.base.competition.selection.BitterlichBAF(basal_area_factor: float = 2.0, min_size_ratio: float = 0.0, elimination_angle_deg: float = 0.0)[source]#

Bases: object

Variable-radius competitor selection. Bitterlich (1952).

An angle-count sweep is taken from the subject tree, and a neighbour competes when its own stem subtends at least the gauge angle, i.e. when

l_ij <= 50 * d_j / sqrt(BAF) with d_j in metres

equivalently l_ij <= d_j / (2 * sqrt(BAF)) for d_j in cm. A tree sits exactly on the limit when BAF = 10000 * (d_j / (2 * l_ij))^2. The limiting distance therefore belongs to the competitor, not to the subject: at BAF 2 a 60 cm neighbour competes out to 21.2 m while a 10 cm one stops at 3.5 m, which is the whole point of a variable-radius sample – large trees are counted from further away.

Two departures from the 2015 comparison’s Table 1, both to follow Bitterlich:

  • It prints the subject’s diameter in the limiting distance. That gives every neighbour one radius set by the subject’s size, which inverts the method: the 60 cm neighbour above would be excluded and the 10 cm one kept.

  • It writes the diameter in cm against a footnote defining d in cm, which only balances once the diameter is converted – at BAF 2 a 20 cm stem reaches 7.07 m, not 1.0 m. The paper’s own BAF-to-angle table corroborates the conversion: BAF 1, 2 and 4 correspond to gauge angles of 1.15, 1.62 and 2.30 degrees, which is 2 * arcsin(sqrt(BAF / 10000)).

Because each neighbour carries its own limiting distance, the selected zone is not a circle and Selection.zone_radius_m is None; see Selection for what that costs.

Variables:
  • basal_area_factor (float) – The BAF in m^2/ha; the 2015 comparison tests 1, 2, 4.

  • min_size_ratio (float) – Minimum d_j / d_i for a neighbour to compete.

  • elimination_angle_deg (float) – Shadow angle; see FixedRadius.

basal_area_factor: float = 2.0#
elimination_angle_deg: float = 0.0#
property gauge_angle_deg: float#

The angle-count gauge angle this BAF corresponds to (degrees).

limiting_distance_m(diameter_cm: float) → float[source]#

Farthest distance at which a stem of diameter_cm is still tallied (m).

min_size_ratio: float = 0.0#
select(subject_index: int, candidates: Candidates, context: SelectionContext) → Selection[source]#

Select every tree inside its own angle-count limiting distance.

property source#

Bitterlich (1952).

Type:

The publication this selection rule comes from

class pyforestry.base.competition.selection.Candidates(diameters_cm: Tuple[float, ...], distances_m: Tuple[float, ...], bearings_rad: Tuple[float, ...] | None = None, heights_m: Tuple[float | None, ...] | None = None, crown_base_heights_m: Tuple[float | None, ...] | None = None)[source]#

Bases: object

The per-tree data a selector may look at, for one subject’s neighbourhood.

Bundled rather than passed as loose arrays because different rules need different columns: the size screen needs diameters, the elimination angle needs bearings, and the search cone needs heights.

Variables:
  • diameters_cm (Tuple[float, ...]) – Diameter of every candidate tree (cm).

  • distances_m (Tuple[float, ...]) – Distance from the subject to every candidate (m); the subject’s own entry is 0.

  • bearings_rad (Tuple[float, ...] | None) – Compass bearing from the subject to every candidate (radians). Required by elimination_angle_deg.

  • heights_m (Tuple[float | None, ...] | None) – Height of every candidate (m), where known. Required by SearchCone.

  • crown_base_heights_m (Tuple[float | None, ...] | None) – Crown base height of every candidate (m), where known. Required by SearchCone with apex="crown_base".

bearings_rad: Tuple[float, ...] | None = None#
crown_base_heights_m: Tuple[float | None, ...] | None = None#
heights_m: Tuple[float | None, ...] | None = None#
class pyforestry.base.competition.selection.CompetitorSelector(*args, **kwargs)[source]#

Bases: Protocol

Chooses competitors for a subject tree from a set of candidates.

select(subject_index: int, candidates: Candidates, context: SelectionContext) → Selection[source]#

Return the competitors for subject_index.

Parameters:
  • subject_index – Index of the subject tree within candidates.

  • candidates – Per-tree data for the neighbourhood.

  • context – Stand-level quantities for radius-sizing selectors.

Returns:

The chosen competitors.

class pyforestry.base.competition.selection.FixedRadius(radius_m: float, min_size_ratio: float = 0.0, elimination_angle_deg: float = 0.0)[source]#

Bases: object

Competitors are the trees within a fixed radius of the subject.

Variables:
  • radius_m (float) – The competition-zone radius (m).

  • min_size_ratio (float) – Keep a neighbour only when d_j >= ratio * d_i. 0.0 keeps everything; the 2015 comparison uses 0.3.

  • elimination_angle_deg (float) – Drop a neighbour standing within this angle of a nearer competitor’s bearing. 0.0 disables; the 2015 comparison uses 30.0.

elimination_angle_deg: float = 0.0#
min_size_ratio: float = 0.0#
select(subject_index: int, candidates: Candidates, context: SelectionContext) → Selection[source]#

Select every tree inside radius_m.

class pyforestry.base.competition.selection.LeeGadowRadius(k: float = 2.0, min_size_ratio: float = 0.0, elimination_angle_deg: float = 0.0)[source]#

Bases: object

Dynamic influence-zone radius scaled to mean spacing. Lee & von Gadow (1997).

CZR = k * sqrt(10000 / N), where the square root is the mean distance between neighbours at N stems/ha. The 2015 comparison tests k = 2 and k = 3.

Variables:
  • k (float) – Multiple of mean spacing.

  • min_size_ratio (float) – Minimum d_j / d_i for a neighbour to compete.

  • elimination_angle_deg (float) – Shadow angle; see FixedRadius.

elimination_angle_deg: float = 0.0#
k: float = 2.0#
min_size_ratio: float = 0.0#
select(subject_index: int, candidates: Candidates, context: SelectionContext) → Selection[source]#

Select every tree inside k * mean spacing.

property source#

Lee & von Gadow (1997).

Type:

The publication this radius rule comes from

class pyforestry.base.competition.selection.MeanHeightRadius(fraction: float = 0.4, min_size_ratio: float = 0.0, elimination_angle_deg: float = 0.0)[source]#

Bases: object

Influence-zone radius set as a fraction of stand mean height.

CZR = fraction * mean_height; fraction=0.4 is the value tested by Sims et al. (2009), not a fixed constant.

Variables:
  • fraction (float) – Multiple of mean height to use as the radius.

  • min_size_ratio (float) – Minimum d_j / d_i for a neighbour to compete.

  • elimination_angle_deg (float) – Shadow angle; see FixedRadius.

elimination_angle_deg: float = 0.0#
fraction: float = 0.4#
min_size_ratio: float = 0.0#
select(subject_index: int, candidates: Candidates, context: SelectionContext) → Selection[source]#

Select every tree inside fraction * mean_height_m.

property source#

Sims et al. (2009).

Type:

The publication this radius rule comes from

class pyforestry.base.competition.selection.NearestNeighbours(n: int = 4, min_size_ratio: float = 0.0, elimination_angle_deg: float = 0.0)[source]#

Bases: object

The n closest trees, irrespective of distance.

Variables:
  • n (int) – How many neighbours to take.

  • min_size_ratio (float) – Minimum d_j / d_i, applied before taking the closest n so the screen cannot be swamped by suppressed stems.

  • elimination_angle_deg (float) – Shadow angle; see FixedRadius. Applied before the count, so n counts unshadowed neighbours.

elimination_angle_deg: float = 0.0#
min_size_ratio: float = 0.0#
n: int = 4#
select(subject_index: int, candidates: Candidates, context: SelectionContext) → Selection[source]#

Select the n nearest qualifying trees.

class pyforestry.base.competition.selection.SearchCone(opening_angle_deg: float = 80.0, apex: str = 'stem_base', min_size_ratio: float = 0.0, elimination_angle_deg: float = 0.0)[source]#

Bases: object

Reversed search-cone / angular-height selection. Pretzsch (2009).

A cone opens upward from the subject tree with opening angle beta; a neighbour competes when its top penetrates that cone:

l_ij < h_j / tan(90 - beta/2)                    apex at the stem base
l_ij < (h_j - cbh_i) / tan(90 - beta/2)          apex at the crown base

so a tall neighbour competes from further away than a short one, and a suppressed neighbour close by may not compete at all.

The 2015 comparison prints h_i (the subject’s height) in both equations. That cannot be the intent: the accompanying text defines competitors as “neighbouring trees whose heights are greater than” the critical value, and with h_i the neighbour’s own size plays no part at all, collapsing the cone into a plain fixed radius. The competitor’s height is used here, which is also the method as Pretzsch defines it.

Variables:
  • opening_angle_deg (float) – The cone’s opening angle beta; the 2015 comparison tests 100, 80 and 60 degrees.

  • apex (str) – "stem_base" (equation 4) or "crown_base" (equation 5), the latter needing the subject’s crown base height.

  • min_size_ratio (float) – Minimum d_j / d_i for a neighbour to compete.

  • elimination_angle_deg (float) – Shadow angle; see FixedRadius.

apex: str = 'stem_base'#
elimination_angle_deg: float = 0.0#
min_size_ratio: float = 0.0#
opening_angle_deg: float = 80.0#
select(subject_index: int, candidates: Candidates, context: SelectionContext) → Selection[source]#

Select the neighbours whose tops fall inside the cone.

Raises:

ValueError – If heights are missing, or apex="crown_base" without a crown base height for the subject.

property source#

Pretzsch (2009).

Type:

The publication this selection rule comes from

class pyforestry.base.competition.selection.Selection(indices: Tuple[int, ...], distances_m: Tuple[float, ...], zone_radius_m: float | None = None, reach_m: float | None = None)[source]#

Bases: object

The competitors a selector picked for one subject tree.

Variables:
  • indices (Tuple[int, ...]) – Positions of the selected competitors in the caller’s tree list.

  • distances_m (Tuple[float, ...]) – Distance from the subject to each selected competitor (m).

  • zone_radius_m (float | None) – The competition-zone radius that was applied (m), or None for a selector whose zone is not a circle fixed ahead of the data. Lorimer’s Sdrl1 and the proportional-area edge correction both need such a radius, so both are unavailable when it is None – substituting the reach below would make them circular, since a neighbourhood truncated by the plot boundary reaches less far and would report itself as needing less correction.

  • reach_m (float | None) – Distance to the farthest retained competitor (m), or None when nothing was retained. Descriptive only: it says how far this subject’s selection actually went.

reach_m: float | None = None#
zone_radius_m: float | None = None#
class pyforestry.base.competition.selection.SelectionContext(stems_per_ha: float | None = None, mean_height_m: float | None = None)[source]#

Bases: object

Stand-level quantities a selector may need to size its search radius.

Variables:
  • stems_per_ha (float | None) – Stem density (trees/ha), for LeeGadowRadius.

  • mean_height_m (float | None) – Arithmetic mean height (m), for MeanHeightRadius.

mean_height_m: float | None = None#
stems_per_ha: float | None = None#
pyforestry.base.competition.selection.selector_source(selector: object)[source]#

Return the publication a selector implements, if it has one.

Parameters:

selector – Any competitor selector.

Returns:

Its SourceReference, or None for the rules that are plain geometry rather than somebody’s proposal (FixedRadius, NearestNeighbours).

pyforestry.base.competition.sources module#

Primary citation for every competition index and competitor-selection rule.

Each index is attributed to the paper that proposed it, not to the review that tabulated it. Maleki, Kiviste & Korjus (2015) are the reason this particular set of eighteen is collected here and are cited for that – see INDEX_SET_REVIEW – but they are not the source of any of the science.

A handful of entries carry note text recording that the citation is as given by that review and has not been checked against an independent record; the rest were verified against the publisher or an indexing service.

pyforestry.base.competition.sources.INDEX_SOURCES = {'Almdg': SourceReference(author='Alemdag, I.S.', year=1978, title='Evaluation of some competition indexes for the prediction of diameter increment in planted white spruce', appendix='', note='Canadian Forestry Service, Forest Management Institute, Information Report FMR-X-108, Ottawa.'), 'BA-gj': SourceReference(author='Steneker, G.A. & Jarvis, J.M.', year=1963, title='A preliminary study to assess competition in a white spruce-trembling aspen stand', appendix='', note='The Forestry Chronicle 39(3):334-336. doi:10.5558/tfc39334-3'), 'BAL': SourceReference(author='Wykoff, W.R., Crookston, N.L. & Stage, A.R.', year=1982, title="User's guide to the Stand Prognosis Model", appendix='', note='USDA Forest Service, Intermountain Forest and Range Experiment Station, General Technical Report INT-133, Ogden, Utah.'), 'BALMOD': SourceReference(author='Schröder, J. & von Gadow, K.', year=1999, title='Testing a new competition index for Maritime pine in northwestern Spain', appendix='', note='Canadian Journal of Forest Research 29:280-283.'), 'BALr': SourceReference(author='Vanclay, J.K.', year=1991, title='Aggregating tree species to develop diameter increment equations for tropical rainforests', appendix='', note='Forest Ecology and Management 42:143-168.'), 'BAr': SourceReference(author='Corona, P. & Ferrara, A.', year=1989, title='Individual competition indices for conifer plantations', appendix='', note='Agriculture, Ecosystems and Environment 27:429-437.'), 'Heg': SourceReference(author='Hegyi, F.', year=1974, title='A simulation model for managing jack-pine stands', appendix='', note='In: Fries, J. (ed.) Growth models for tree and stand simulation. Royal College of Forestry, Stockholm, pp. 74-90.'), 'SAng1': SourceReference(author='Lin, J.Y.', year=1974, title='Stand growth simulation models for Douglas-fir and western hemlock in the northwestern United States', appendix='', note='In: Fries, J. (ed.) Growth models for tree and stand simulation. Royal College of Forestry, Stockholm. Citation as given by Maleki et al. (2015) Table 2; not verified against an independent record.'), 'SAng2': SourceReference(author='Rouvinen, S. & Kuuluvainen, T.', year=1997, title='Structure and asymmetry of tree crowns in relation to local competition in a natural mature Scots pine forest', appendix='', note='Canadian Journal of Forest Research 27:890-902. Maleki et al. (2015) Table 2 dates this 1977; the work is 1997.'), 'SBAr': SourceReference(author='Daniels, R.F., Burkhart, H.E. & Clason, T.R.', year=1986, title='A comparison of competition measures for predicting growth of loblolly pine trees', appendix='', note='Canadian Journal of Forest Research 16:1230-1237.'), 'SOdr': SourceReference(author='Bella, I.E.', year=1971, title='A new competition model for individual trees', appendix='', note='Forest Science 17:364-372.'), 'SOr': SourceReference(author='Gerrard, D.J.', year=1969, title='Competition quotient: a new measure of the competition affecting individual forest trees', appendix='', note='Michigan State University, Agricultural Experiment Station, Research Bulletin 20.'), 'Sdr': SourceReference(author='Lorimer, C.G.', year=1983, title='Tests of age-independent competition indices for individual trees in natural hardwood stands', appendix='', note='Forest Ecology and Management 6:343-360.'), 'SdrAng': SourceReference(author='Rouvinen, S. & Kuuluvainen, T.', year=1997, title='Structure and asymmetry of tree crowns in relation to local competition in a natural mature Scots pine forest', appendix='', note='Canadian Journal of Forest Research 27:890-902. Maleki et al. (2015) Table 2 dates this 1977; the work is 1997.'), 'Sdrl1': SourceReference(author='Lorimer, C.G.', year=1983, title='Tests of age-independent competition indices for individual trees in natural hardwood stands', appendix='', note='Forest Ecology and Management 6:343-360.'), 'Sdrl2': SourceReference(author='Martin, G.L. & Ek, A.R.', year=1984, title='A comparison of competition measures and growth models for predicting plantation red pine diameter and height growth', appendix='', note='Forest Science 30(3):731-743. The distance term decays: implemented as exp(-16*l/(d_i+d_j)), against the positive exponent printed by Maleki et al. (2015) Table 2 and Wang et al. (2012) Table 1.'), 'Sl': SourceReference(author='Staebler, G.R.', year=1951, title='Growth and spacing in an even-aged stand of Douglas-fir', appendix='', note='MSc thesis, University of Michigan, Ann Arbor.'), 'drg': SourceReference(author='Hamilton, D.A.', year=1986, title='A logistic model of mortality in thinned and unthinned mixed conifer stands of northern Idaho', appendix='', note='Forest Science 32(4):989-1000. Citation as given by Maleki et al. (2015) Table 2; not verified against an independent record.')}#

Primary citation for each index, keyed by its abbreviation.

pyforestry.base.competition.sources.SELECTOR_SOURCES = {'BitterlichBAF': SourceReference(author='Bitterlich, W.', year=1952, title='Die Winkelzählmessung', appendix='', note="Allgemeine Forst- und Holzwirtschaftliche Zeitung 63:33-36. Variable-radius competitor selection. Citation as given by Maleki et al. (2015); not verified against an independent record. The angle-count principle itself is Bitterlich (1948), 'Die Winkelzählprobe'. The limiting distance here is the competitor's, against the subject's as printed by Maleki et al. (2015) Table 1 -- see BitterlichBAF."), 'LeeGadowRadius': SourceReference(author='Lee, W.K. & von Gadow, K.', year=1997, title='Iterative Bestimmung der Konkurrenzbäume in Pinus densiflora Beständen', appendix='', note='Allgemeine Forst- und Jagdzeitung 168(3-4):41-44.'), 'MeanHeightRadius': SourceReference(author='Sims, A., Kiviste, A., Hordo, M., Laarmann, D. & von Gadow, K.', year=2009, title='Estimating tree survival: a study based on the Estonian Forest Research Plots Network', appendix='', note='Annales Botanici Fennici 46:336-352. Source of the 0.4 * mean height influence-zone radius; the influence-zone concept itself is Staebler (1951).'), 'SearchCone': SourceReference(author='Pretzsch, H.', year=2009, title='Forest dynamics, growth and yield: from measurement to model', appendix='', note="Springer, Berlin. The reversed search-cone method; Maleki et al. (2015) also cite Richards et al. (2008) for the equivalent angular-height method. Their equations 4 and 5 print the subject's height where the competitor's is meant -- see SearchCone.")}#

Primary citation for each competitor-selection rule that has one.

Module contents#

Individual-tree competition indices.

Eighteen indices – seven distance-independent and eleven spatially explicit – each attributed to the paper that proposed it, from Staebler (1951) through to Schröder & Gadow (1999). Alongside them are the competitor-selection rules they are used with, because an index value is only interpretable next to the rule that chose its competitors.

>>> from pyforestry.base.competition import competition_indices, FixedRadius
>>> results = competition_indices(plot, indices=["Heg", "BAL"],
...                               selector=FixedRadius(8.0, min_size_ratio=0.3))
>>> results[0].indices["Heg"]

Every number in a selection rule is a parameter with a literature default, not a fixed value. The defaults are one study’s choices and are meant to be varied:

MeanHeightRadius(fraction=0.25)              # 0.4 is Sims et al.'s, not a law
LeeGadowRadius(k=3.0)                        # k = 2 and 3 both tested
BitterlichBAF(basal_area_factor=4.0)         # 1, 2 and 4 tested
SearchCone(opening_angle_deg=60.0)           # 100, 80 and 60 tested
SearchCone(80.0, apex="crown_base")          # apex at the crown base instead
FixedRadius(8.0, min_size_ratio=0.3, elimination_angle_deg=30.0)

The last of those reproduces the size screen and the shadow screen that the 2015 comparison applies together for its approaches 1 and 2: a neighbour is dropped when it is under 0.3 * d_i, or when it stands within 30 degrees of the bearing of a nearer competitor.

Every index carries its own citation:

>>> from pyforestry.base.competition import index_source
>>> index_source("Heg")     # Hegyi (1974), not the review it was collected from
>>> index_source("Sdrl2")   # Martin & Ek (1984)

Three layers:

The distance-independent indices always see the whole plot: BAL is the basal area per hectare in trees larger than the subject, and restricting it to the selector’s competitors would make it shrink with the search radius and stop being the published quantity. Each subject therefore gets two neighbourhoods – the selector’s for the spatial indices, the plot’s for the rest. Every per-hectare quantity honours Tree.weight_n and CircularPlot.occlusion, and a Stand is processed one plot at a time, since stem coordinates are plot-local.

Distance-dependent indices are biased low for trees near a plot boundary, whose competition zone is partly unobserved. Every result reports observed_zone_fraction, and by default the additive spatial indices are divided by it (proportional-area weighting). SBAr and Almdg are left alone: a ratio and a weight-normalised mean do not scale with the observed share. So are selections with no zone fixed in advance (SearchCone, NearestNeighbours, BitterlichBAF), whose reach is an outcome of the data rather than a radius to correct against.

This package holds no science of its own. The set of eighteen was assembled and compared by Maleki, Kiviste & Korjus (2015), which is why these particular indices are here; see INDEX_SET_REVIEW. Per-index provenance is in INDEX_SOURCES.