(root)/src/abe/actthinning.cpp - Rev 1222
Rev 1221 |
Blame |
Compare with Previous |
Last modification |
View Log
| RSS feed
/********************************************************************************************
** iLand - an individual based forest landscape and disturbance model
** http://iland.boku.ac.at
** Copyright (C) 2009- Werner Rammer, Rupert Seidl
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see <http://www.gnu.org/licenses/>.
********************************************************************************************/
#include "abe_global.h"
#include "actthinning.h"
#include "fmstand.h"
#include "fmtreelist.h"
#include "fmstp.h"
#include "forestmanagementengine.h"
#include "fomescript.h"
#include "model.h"
#include "speciesset.h"
#include "species.h"
#include "tree.h"
#include <QJSValueIterator>
namespace ABE {
/** @class ActThinning
@ingroup abe
The ActThinning class implements a very general interface to thinning activties.
*/
// statics
QStringList ActThinning::mSyntaxCustom;
ActThinning::ActThinning(FMSTP *parent): Activity(parent)
{
mBaseActivity.setIsScheduled(true); // use the scheduler
mBaseActivity.setDoSimulate(true); // simulate per default
mThinningType = Invalid;
if (mSyntaxCustom.isEmpty())
mSyntaxCustom = QStringList() << Activity::mAllowedProperties
<< "percentile" << "removal" << "thinning"
<< "relative" << "remainingStems" << "minDbh"
<< "filter" << "targetVariable" << "targetRelative"
<< "targetValue" << "classes" << "onEvaluate";
}
QString ActThinning::type() const
{
QString th;
switch (mThinningType) {
case Invalid: th="Invalid"; break;
case FromBelow: th="from below"; break;
case FromAbove: th="from above"; break;
case Custom: th = "custom"; break;
case Selection: th = "selection"; break;
}
return QString("thinning (%1)").arg(th);
}
void ActThinning::setup(QJSValue value)
{
Activity::setup(value); // setup base events
mThinningType = Invalid;
QString th_type = FMSTP::valueFromJs(value, "thinning").toString();
if (th_type=="fromBelow") mThinningType=FromBelow;
else if (th_type=="fromAbove") mThinningType=FromAbove;
else if (th_type=="custom") mThinningType=Custom;
else if (th_type=="selection") mThinningType=Selection;
else
throw IException(QString("Setup of thinning: invalid thinning type: %1").arg(th_type));
switch (mThinningType) {
case Custom: setupCustom(value); break;
case Selection: setupSelective(value); break;
default: throw IException("No setup defined for thinning type");
}
}
bool ActThinning::evaluate(FMStand *stand)
{
bool return_value = true;
switch (mThinningType) {
case Custom:
for (int i=0;i<mCustomThinnings.count();++i)
return_value = return_value && evaluateCustom(stand, mCustomThinnings[i]);
return return_value; // false if one fails
case Selection:
return evaluateSelective(stand);
default: throw IException("ActThinning::evaluate: not available for thinning type");
}
return false;
}
bool ActThinning::execute(FMStand *stand)
{
if (stand->trace()) qCDebug(abe) << stand->context() << "execute activity" << name() << ":" << type();
if (events().hasEvent(QStringLiteral("onExecute"))) {
// switch off simulation mode
stand->currentFlags().setDoSimulate(false);
// execute this event
bool result = Activity::execute(stand);
stand->currentFlags().setDoSimulate(true);
return result;
} else {
// default behavior: process all marked trees (harvest / cut)
if (stand->trace()) qCDebug(abe) << stand->context() << "activity" << name() << "remove all marked trees.";
FMTreeList trees(stand);
trees.removeMarkedTrees();
return true;
}
}
void ActThinning::setupCustom(QJSValue value)
{
events().setup(value, QStringList() << "onEvaluate");
mCustomThinnings.clear();
if (value.hasProperty("thinnings") && value.property("thinnings").isArray()) {
QJSValueIterator it(value.property("thinnings"));
while (it.hasNext()) {
it.next();
if (it.name()==QStringLiteral("length"))
continue;
mCustomThinnings.push_back(SCustomThinning());
setupSingleCustom(it.value(), mCustomThinnings.back());
}
} else {
mCustomThinnings.push_back(SCustomThinning());
setupSingleCustom(value, mCustomThinnings.back());
}
}
void ActThinning::setupSelective(QJSValue value)
{
mSelectiveThinning.N = FMSTP::valueFromJs(value, "N", "400").toInt();
}
// setup of the "custom" thinning operation
void ActThinning::setupSingleCustom(QJSValue value, SCustomThinning &custom)
{
FMSTP::checkObjectProperties(value, mSyntaxCustom, "setup of 'custom' thinning:" + name());
custom.usePercentiles = FMSTP::boolValueFromJs(value, "percentile", true);
custom.removal = FMSTP::boolValueFromJs(value, "removal", true);
custom.relative = FMSTP::boolValueFromJs(value, "relative", true);
custom.remainingStems = FMSTP::valueFromJs(value, "remainingStems", "0").toInt();
custom.minDbh = FMSTP::valueFromJs(value, "minDbh", "0").toNumber();
QJSValue filter = FMSTP::valueFromJs(value, "filter", "");
if (filter.isString())
custom.filter = filter.toString();
else
custom.filter = QString();
custom.targetVariable = FMSTP::valueFromJs(value, "targetVariable", "stems").toString();
if (custom.targetVariable != "stems" &&
custom.targetVariable != "basalArea" &&
custom.targetVariable != "volume")
throw IException(QString("setup of custom Activity: invalid targetVariable: %1").arg(custom.targetVariable));
custom.targetRelative = FMSTP::boolValueFromJs(value, "targetRelative", true);
custom.targetValue = FMSTP::valueFromJs(value, "targetValue", "30").toNumber();
if (custom.targetRelative && (custom.targetValue>100. || custom.targetValue<0.))
throw IException(QString("setup of custom Activity: invalid relative targetValue (0-100): %1").arg(custom.targetValue));
QJSValue values = FMSTP::valueFromJs(value, "classes", "", "setup custom acitvity");
if (!values.isArray())
throw IException("setup of custom activity: the 'classes' is not an array.");
custom.classValues.clear();
custom.classPercentiles.clear();
QJSValueIterator it(values);
while (it.hasNext()) {
it.next();
if (it.name()==QStringLiteral("length"))
continue;
custom.classValues.push_back(it.value().toNumber());
}
if (custom.classValues.size()==0)
throw IException("setup of custom thinnings: 'classes' has no elements.");
// check if sum is 100 for relative classes
if (custom.relative) {
double sum=0.;
for (int i=0;i<custom.classValues.size();++i)
sum+=custom.classValues[i];
if (fabs(sum-100.)>0.000001)
throw IException("setup of custom thinnings: 'classes' do not add up to 100 (relative=true).");
}
// span the range between 0..100: from e.g. 10,20,30,20,20 -> 0,10,30,60,80,100
double f = 100. / custom.classValues.size();
double p = 0.;
for (int i=0;i<custom.classValues.size();++i, p+=f)
custom.classPercentiles.push_back(qRound(p));
custom.classPercentiles.push_back(100);
}
bool ActThinning::evaluateCustom(FMStand *stand, SCustomThinning &custom)
{
// fire onEvaluate event and collect probabilities
QJSValue eval_result = events().run(QStringLiteral("onEvaluate"), stand);
if (eval_result.isBool() && eval_result.toBool()==false)
return false; // do nothing
bool species_selective = false;
if( eval_result.isObject()) {
// expecting a list of probabilities....
// create list if not present
if (mSpeciesSelectivity.isEmpty()) {
foreach(const Species *s, GlobalSettings::instance()->model()->speciesSet()->activeSpecies())
mSpeciesSelectivity[s] = 1.;
}
// fetch from javascript
double rest_val = eval_result.property(QStringLiteral("rest")).isNumber() ? eval_result.property(QStringLiteral("rest")).toNumber() : 1.;
foreach(const Species *s, mSpeciesSelectivity.keys()) {
mSpeciesSelectivity[s] = limit( eval_result.property(s->id()).isNumber() ? eval_result.property(s->id()).toNumber() : rest_val, 0., 1.);
}
species_selective = true;
}
FMTreeList trees(stand);
QString filter = custom.filter;
if (custom.minDbh>0.) {
if (!filter.isEmpty())
filter += " and ";
filter += QString("dbh>%1").arg(custom.minDbh);
}
if (!filter.isEmpty())
trees.load(filter);
else
trees.loadAll();
if (custom.remainingStems>0 && custom.remainingStems>=trees.trees().size())
return false;
if (trees.trees().size()==0)
return false;
// remove harvest flags.
clearTreeMarks(&trees);
// sort always by target variable (if it is stems, then simply by dbh)
bool target_dbh = custom.targetVariable=="stems";
if (target_dbh)
trees.sort("dbh");
else
trees.sort(custom.targetVariable);
// count trees and values (e.g. volume) in the defined classes
QVector<double> values = custom.classValues; // make a copy
QVector<double> tree_counts = custom.classValues; // make a copy
QVector<int> percentiles = custom.classPercentiles; // make a copy
QVector<QPair<Tree*, double> >::const_iterator it;
for (int i=0;i<values.count();++i) {
tree_counts[i]=0.;
}
int class_index = 0;
int n=0;
percentiles.first()=0;
double tree_count=trees.trees().count();
double total_value = 0.;
for (it = trees.trees().constBegin(); it!=trees.trees().constEnd(); ++it, ++n) {
if (n/tree_count*100. > custom.classPercentiles[class_index+1]) {
++class_index;
percentiles[class_index] = n; // then n'th tree
}
tree_counts[class_index]++;
total_value += target_dbh?1.:it->second; // e.g., sum of volume in the class, or simply count
}
while (++class_index<percentiles.size())
percentiles[class_index]=n+1;
double target_value=0.;
if (custom.targetRelative)
target_value = custom.targetValue * total_value / 100.;
else
target_value = custom.targetValue * stand->area();
if (!custom.relative) {
// TODO: does not work now!!! redo!!
// class values are given in absolute terms, e.g. 40m3/ha.
// this needs to be translated to relative classes.
// if the demand in a class cannot be met (e.g. planned removal of 40m3/ha, but there are only 20m3/ha in the class),
// then the "miss" is distributed to the other classes (via the scaling below).
for (int i=0;i<values.size();++i) {
if (values[i]>0){
if (values[i]<=custom.classValues[i]*stand->area()) {
values[i] = 1.; // take all from the class
} else {
values[i] = custom.classValues[i]*stand->area() / values[i];
}
}
}
// scale to 100
double sum=0.;
for (int i=0;i<values.size();++i)
sum+=values[i];
if (sum>0.){
for (int i=0;i<values.size();++i)
values[i] *= 100./sum;
}
}
// *****************************************************************
// *************** Main loop
// *****************************************************************
for (int i=0;i<values.size();++i)
values[i] = 0;
bool finished = false;
int cls;
double p;
int removed_trees = 0;
double removed_value = 0.;
int no_tree_found = 0;
bool target_value_reached=false;
do {
// look up a random number: it decides in which class to select a tree:
p = nrandom(0,100);
for (cls=0;cls<values.size();++cls) {
if (p < custom.classPercentiles[cls+1])
break;
}
// select a tree:
int tree_idx = selectRandomTree(&trees, percentiles[cls], percentiles[cls+1]-1, species_selective);
if (tree_idx>=0) {
// stop harvesting, when the target size is reached: if the current tree would surpass the limit,
// a random number decides whether the tree should be included or not.
double tree_value = target_dbh?1.:trees.trees()[tree_idx].second;
if (custom.targetValue>0.) {
if (removed_value + tree_value > target_value) {
if (drandom()>0.5 || target_value_reached)
break;
else
target_value_reached = true;
}
}
trees.remove_single_tree(tree_idx, true);
removed_trees++;
removed_value += tree_value;
values[cls]++;
} else {
// tree_idx = -1: no tree found in list, -2: tree found but is not selected
no_tree_found += tree_idx == -1 ? 100 : 1; // empty list counts much more
if (no_tree_found > 1000)
finished=true;
}
// stop harvesting, when the minimum remaining number of stems is reached
if (trees.trees().size()-removed_trees <= custom.remainingStems*stand->area())
finished = true;
if (custom.targetValue>0. && removed_value > target_value)
finished = true;
} while (!finished);
if (stand->trace()) {
qCDebug(abe) << stand->context() << "custom-thinning: removed" << removed_trees << ". Reached cumulative 'value' of:" << removed_value << "(planned value:" << target_value << "). #of no trees found:" << no_tree_found << "; stand-area:" << stand->area();
for (int i=0;i<values.count();++i)
qCDebug(abe) << stand->context() << "class" << i << ": removed" << values[i] << "of" << percentiles[i+1]-percentiles[i];
}
return true;
}
int ActThinning::selectRandomTree(FMTreeList *list, const int pct_min, const int pct_max, const bool selective)
{
// pct_min, pct_max: the indices of the first and last tree in the list to be looked for, including pct_max
// seek a tree in the class 'cls' (which has not already been removed);
int idx;
if (pct_max<pct_min)
return -1;
// search randomly for a couple of times
for (int i=0;i<5;i++) {
idx = irandom(pct_min, pct_max);
Tree *tree = list->trees().at(idx).first;
if (!tree->isDead() && !tree->isMarkedForHarvest() && !tree->isMarkedForCut())
return selectSelectiveSpecies(list, selective, idx);
}
// not found, now walk in a random direction...
int direction = 1;
if (drandom()>0.5) direction=-1;
// start in one direction from the last selected random position
int ridx=idx;
while (ridx>=pct_min && ridx<pct_max) {
Tree *tree = list->trees().at(ridx).first;
if (!tree->isDead() && !tree->isMarkedForHarvest() && !tree->isMarkedForCut())
return selectSelectiveSpecies(list, selective, ridx);
ridx+=direction;
}
// now look in the other direction
direction = -direction;
ridx = idx;
while (ridx>=pct_min && ridx<pct_max) {
Tree *tree = list->trees().at(ridx).first;
if (!tree->isDead() && !tree->isMarkedForHarvest() && !tree->isMarkedForCut())
return selectSelectiveSpecies(list, selective, ridx);
ridx+=direction;
}
// no tree found in the entire range
return -1;
}
int ActThinning::selectSelectiveSpecies(FMTreeList *list, const bool is_selective, const int index)
{
if (!is_selective)
return index;
// check probability for species [0..1, 0: 0% chance to take a tree of that species] against a random number
if (mSpeciesSelectivity[list->trees()[index].first->species()] < drandom())
return index; // take the tree
// a tree was found but is not going to be removed
return -2;
}
void ActThinning::clearTreeMarks(FMTreeList *list)
{
QVector<QPair<Tree*, double> >::const_iterator it;
for (it=list->trees().constBegin(); it!=list->trees().constEnd(); ++it) {
Tree *tree = it->first;
if (tree->isMarkedForHarvest())
tree->markForHarvest(false);
if (tree->isMarkedForCut())
tree->markForCut(false);
}
}
bool ActThinning::evaluateSelective(FMStand *stand)
{
markCropTrees(stand);
return true;
}
bool ActThinning::markCropTrees(FMStand *stand)
{
// tree list from current exeution context
FMTreeList *treelist = ForestManagementEngine::instance()->scriptBridge()->treesObj();
treelist->setStand(stand);
treelist->loadAll();
clearTreeMarks(treelist);
// get the 2x2m grid for the current stand
Grid<float> &grid = treelist->localGrid();
// clear (except the out of "stand" pixels)
for (float *p=grid.begin(); p!=grid.end(); ++p)
if (*p > -1.f)
*p = 0.f;
int target_n = mSelectiveThinning.N * stand->area();
if (target_n>=treelist->trees().count())
target_n = treelist->trees().count();
int max_target_n = qMax(target_n * 1.5, treelist->trees().count()/2.);
if (max_target_n>=treelist->trees().count())
max_target_n = treelist->trees().count();
// we have 2500 px per ha (2m resolution)
// if each tree dominates its Moore-neighborhood, 2500/9 = 267 trees are possible (/ha)
// if *more* trees should be marked, some trees need to be on neighbor pixels:
// pixels = 2500 / N; if 9 px are the Moore neighborhood, the "overprint" is N*9 / 2500.
// N*)/2500 -1 = probability of having more than zero overlapping pixels
double overprint = (mSelectiveThinning.N * 9) / double(cPxPerHectare) - 1.;
// order the list of trees according to tree height
treelist->sort("-height");
// start with a part of N and 0 overlap
int n_found = 0;
int tests=0;
for (int i=0;i<target_n/3;i++) {
float f=testPixel(treelist->trees().at(i).first->position(), grid); ++tests;
if (f==0.f) {
setPixel(treelist->trees().at(i).first->position(), grid);
treelist->trees()[i].first->markCropTree(true);
++n_found;
}
}
// continue with a higher probability --- incr
for (int run=0;run<4;++run) {
for (int i=0; i<max_target_n;++i) {
if (treelist->trees().at(i).first->isMarkedAsCropTree())
continue;
float f=testPixel(treelist->trees().at(i).first->position(), grid); ++tests;
if ( (f==0.f) ||
(f<=2.f && drandom()<overprint) ||
(run==1 && f<=4 && drandom()<overprint) ||
(run==2 && drandom()<overprint) ||
(run==3) ) {
setPixel(treelist->trees().at(i).first->position(), grid);
treelist->trees()[i].first->markCropTree(true);
++n_found;
if (n_found == target_n)
break;
}
}
if (n_found==target_n)
break;
}
// now mark the competitors:
// competitors are trees up to 75th percentile of the tree population that
int n_competitor=0;
for (int run=0;run<3;++run) {
for (int i=0; i<max_target_n;++i) {
Tree *tree = treelist->trees().at(i).first;
if (tree->isMarkedAsCropTree() || tree->isMarkedAsCropCompetitor())
continue;
float f=testPixel(treelist->trees().at(i).first->position(), grid); ++tests;
if ( (f>12.f) ||
(run==1 && f>8) ||
(run==2 && f>4) ) {
tree->markCropCompetitor(true);
n_competitor++;
}
}
}
if (FMSTP::verbose()) {
qCDebug(abe) << stand->context() << "Thinning::markCropTrees: marked" << n_found << "(plan:" << target_n << ") from total" << treelist->trees().count()
<< ". Tests performed:" << tests << "marked as competitors:" << n_competitor;
}
return n_found==target_n;
}
float ActThinning::testPixel(const QPointF &pos, Grid<float> &grid)
{
// check Moore neighborhood
int x=grid.indexAt(pos).x();
int y=grid.indexAt(pos).y();
float sum = 0.f;
sum += grid.isIndexValid(x-1,y-1) ? grid.valueAtIndex(x-1, y-1) : 0;
sum += grid.isIndexValid(x,y-1) ? grid.valueAtIndex(x, y-1) : 0;
sum += grid.isIndexValid(x+1,y-1) ? grid.valueAtIndex(x+1, y-1) : 0;
sum += grid.isIndexValid(x-1,y) ? grid.valueAtIndex(x-1, y) : 0;
sum += grid.isIndexValid(x,y) ? grid.valueAtIndex(x, y) : 0;
sum += grid.isIndexValid(x+1,y) ? grid.valueAtIndex(x+1, y) : 0;
sum += grid.isIndexValid(x-1,y+1) ? grid.valueAtIndex(x-1, y+1) : 0;
sum += grid.isIndexValid(x,y+1) ? grid.valueAtIndex(x, y+1) : 0;
sum += grid.isIndexValid(x+1,y+1) ? grid.valueAtIndex(x+1, y+1) : 0;
return sum;
}
void ActThinning::setPixel(const QPointF &pos, Grid<float> &grid)
{
// check Moore neighborhood
int x=grid.indexAt(pos).x();
int y=grid.indexAt(pos).y();
if (grid.isIndexValid(x-1,y-1)) grid.valueAtIndex(x-1, y-1)++;
if (grid.isIndexValid(x,y-1)) grid.valueAtIndex(x, y-1)++;
if (grid.isIndexValid(x+1,y-1)) grid.valueAtIndex(x+1, y-1)++;
if (grid.isIndexValid(x-1,y)) grid.valueAtIndex(x-1, y)++;
if (grid.isIndexValid(x,y)) grid.valueAtIndex(x, y) += 3; // more impact on center pixel
if (grid.isIndexValid(x+1,y)) grid.valueAtIndex(x+1, y)++;
if (grid.isIndexValid(x-1,y+1)) grid.valueAtIndex(x-1, y+1)++;
if (grid.isIndexValid(x,y+1)) grid.valueAtIndex(x, y+1)++;
if (grid.isIndexValid(x+1,y+1)) grid.valueAtIndex(x+1, y+1)++;
}
} // namespace