Show:

/**
 * The thinning module include activities thinning operations.
 *
 * @class thinning
 * @memberof abe-lib
 */


lib.thinning = {};

Globals.include(lib.path.dir + '/thinning/selective.js');


/**
 * Thinning from below
 * @method fromBelow
 * @param {object} options
 *    @param {string} options.id A unique identifier for the thinning activity (default: 'ThinningFromBelow').
 *    @param {object} options.schedule schedule for the repeater (default: {min: 30, opt: 40, max: 50}).
 *    @param {number} options.interval interval between repeated thinnings (default: 5).
 *    @param {number} options.times number of times to repeat the thinning (default: 5).
 *    @param {boolean} options.block block other activities in repeater (default: true).
 *    @param {string} options.mode mode of thinning, either 'simple' or 'dynamic' (default: 'simple').
 *    @param {number|function} options.thinningShare share of trees to be thinned, can be a number or a function returning a number (default: 0.1).
 *    @param {number[]} options.thinningClasses array defining how much of each thinning class should be removed (default: [60, 25, 15, 0, 0]).
 *    @param {number|undefined} options.speciesSelectivity species list, which species should have a lower probability to get harvested (default: undefined).
 *    @param {string} options.ranking ranking string for filtering trees, e.g. 'volume' (default: 'volume').
 *    @param {string} options.sendSignal signal for triggering the thinning (default: 'thinning_execute').
 *    @param {string|undefined} options.constraint constraint (default: undefined).
 * @return {object} program - An object describing the thinning program
 * @example
 *     lib.thinning.fromBelow({
 *         schedule: { min: 25, opt: 35, max: 45 },
 *         interval: 10,
 *         thinningShare: 0.3
 *     });
 */
lib.thinning.fromBelow = function(options) {
	// Problem TODO: thinning from below doesn't take as much volume as intended but!
    // 1. Default Options
    const defaultOptions = {
		id: 'ThinningFromBelow',
		schedule: {min: 30, opt: 40, max: 50}, // opt for Broadleaves: (35, 55, 85), Conifers: (25, 40, 55), more or less adapted from 'Waldbau auf soziologisch-ökologischer Grundlage', H. Mayer, p. 235
		interval: 5,
		times: 5,
		block: true,
		mode: 'simple',
        thinningShare: 0.1, // per thinning action
		thinningClasses: [80, 15, 4, 0.9, 0.1], // how much of which class should be removed?
		speciesSelectivity: undefined,
		ranking: 'basalarea', 
		sendSignal: 'thinning_execute',
		constraint: undefined,
		
        // ... add other default thinning parameters
    };
    const opts = lib.mergeOptions(defaultOptions, options || {});
	
	// tree filter for thinning
	var treeFilter = undefined;/*
	if (opts.speciesSelectivity !== undefined) {
		lib.dbg(`Stand flag: ${stand.flag('targetSpecies')}`);
		//opts.speciesSelectivity = opts.speciesSelectivity.call(opts);
		//treeFilter = lib.buildRareSpeciesFilter(opts.speciesSelectivity, 0.2);
	};*/
	
	// dynamic parameters of selective thinning
	function dynamic_thinningShare() {
		// retrieve thinning share from stand flag during runtime
		var value = stand.flag('thinningShare');
		if (value === undefined) value = opts.thinningShare;
		return value;
	};

	// changing parameters if mode is dynamic
	if (opts.mode == 'dynamic') {
		opts.thinningShare = dynamic_thinningShare;
	};

	var program = {};

	program["init"] = {
		id: opts.id + '_init',
		type: 'general',
		schedule: opts.schedule,
		action: function() {
			var treeFilter = undefined;
			if (opts.speciesSelectivity !== undefined) {
				lib.dbg(`Stand flag: ${stand.flag('targetSpecies')}`);
				var speciesObject = opts.speciesSelectivity.call();
				treeFilter = lib.buildRareSpeciesFilter(speciesObject, speciesObject);
			};
			lib.dbg(`Thinning from below initialized.`);
		},
	};

	program["thinning_repeat"] = lib.repeater({ 
		id: opts.id + "_repeater",
		schedule: opts.schedule, 
		signal: opts.sendSignal, 
		count: opts.times, 
		interval: opts.interval,
		block: opts.block
	});
	
	program["thinning"] = {
		//id: opts.id + '_thinning',
		type: 'thinning',
		schedule: { signal: opts.sendSignal},
		constraint: ["stand.age>30 and stand.age<70"],
        thinning: 'custom',
        targetValue: opts.thinningShare * 100, 
		targetVariable: opts.ranking, 
		targetRelative: true,
		filter: treeFilter,
        minDbh: 0,
        classes: opts.thinningClasses,
		onCreate: function(act) { 
							  act.scheduled=false; /* this makes sure that evaluate is also called when invoked by a signal */ 
							  console.log(`onCreate: ${opts.id}: `);
							  printObj(this);
							  console.log('---end---');							  
							  },
		onExecuted: function() {
			lib.dbg(`Thinning from below executed. filter: `+ this.filter);
			//lib.activityLog('thinning_from_below'); 
		},
	}

	if (opts.constraint !== undefined) program.constraint = opts.constraint;
	
	//program.description = `A simple repeating harvest operation (every ${opts.times} years), that removes all trees above a target diameter ( ${opts.TargetDBH} cm)).`;
						
	return program;	
}


/**
 * Tending operation
 * @method tending
 * @param {object} options
 *    @param {string} options.id A unique identifier for the tending activity (default: 'Tending').
 *    @param {object|undefined} options.schedule schedule for the tending (default: undefined).
 *    @param {number} options.times number of tending actions (default: 3).
 *    @param {number} options.interval interval between tending actions (default: 2).
 *    @param {object} options.speciesSelectivity object defining species selectivity, e.g. { 'fasy': 0.9, 'abal': 0.8, 'rest': 0.2 } (default: { 'rest': 0.9 }).
 *    @param {number} options.intensity intensity of tending (default: 10).
 *    @param {boolean} options.block block other activities in repeater (default: true).
 *    @param {string} options.sendSignal signal for triggering the tending (default: 'tending_execute').
 *    @param {string|undefined} options.constraint constraint (default: undefined).
 * @return {object} program - An object describing the tending program
 * @example
 *     lib.thinning.tending({
 *         times: 5,
 *         interval: 3,
 *         speciesSelectivity: { 'piab': 0.7, 'lade': 0.6 }
 *     });
 */
lib.thinning.tending = function(options) {
    // 1. Default Options
    const defaultOptions = {
		id: 'Tending',
		schedule: undefined,
		times: 3,
		interval: 2,
        speciesSelectivity: { 'rest': 0.3 },
        intensity: 10,
		block: true,
		sendSignal: 'tending_execute',
		constraint: undefined,		
        // ... add other default thinning parameters
    };
    const opts = lib.mergeOptions(defaultOptions, options || {});
	
    //if (typeof opts.speciesSelectivity === 'function')
    //    opts.speciesSelectivity = opts.speciesSelectivity.call(opts);
	//Globals.alert("Hello World");

	var program = {};

	program["tending_repeat"] = lib.repeater({ 
		id: opts.id + "_repeater",
		schedule: opts.schedule, 
		signal: opts.sendSignal, 
		count: opts.times, 
		interval: opts.interval,
		block: opts.block
	});
	
	program["tending"] = {
		id: opts.id + '_tending',
		type: 'thinning',
		schedule: { signal: opts.sendSignal},
		thinning: 'tending',
		speciesSelectivity: opts.speciesSelectivity,
		intensity: opts.intensity,
		onCreate: function(act) { 
							  act.scheduled=false; /* this makes sure that evaluate is also called when invoked by a signal */ 
							  console.log(`onCreate: ${opts.id}: `);
							  printObj(this);
							  console.log('---end---');							  
							  },
		onExecuted: function() {
			//Globals.alert("Tending in stand " + stand.id + " executed.");
			//lib.activityLog('thinning_tending'); 
		},
	}

	if (opts.constraint !== undefined) program.constraint = opts.constraint;
	
	program.description = `A tending activity, that starts at age ${opts.schedule.optRel} and does ${opts.times} tending operations every ${opts.interval} years.`;
						
	return program;	
}


/**
 * Plenter thinning operation
 * @method plenter
 * @param {object} options
 *    @param {string} options.id A unique identifier for the thinning activity (default: 'Plenter').
 *    @param {object|undefined} options.schedule schedule for the thinning (default: undefined).
 *    @param {number} options.times number of thinning actions (default: 1000).
 *    @param {number} options.interval interval between thinning actions (default: 5).
 *    @param {object} options.plenterCurve object defining target diameter distribution i.e. plenter curve.
 *    @param {number} options.dbhSteps width of dbh classes to compare with plenterCurve (default: 5).
 *    @param {number} options.intensity intensity of thinning activity, specifying which share of trees to remove should be removed (default: 1).
 *    @param {boolean} options.block block other activities in repeater (default: true).
 *    @param {string} options.sendSignal signal for triggering the thinning (default: 'plenter_execute').
 *    @param {string|undefined} options.constraint constraint (default: undefined).
 * @return {object} program - An object describing the plenter thinning program
 * @example
 *     lib.thinning.plenter({
 *         interval: 5,
 *         plenterCurve: { 10: 350, 20: 150, 30: 80, 40: 40, 50: 20 },
 * 		   dbhSteps: 10
 *     });
 */
lib.thinning.plenter = function(options) {
	// 1. Default Options
    const defaultOptions = {
		id: 'Plenter',
		schedule: undefined,
		times: 1000, // repeat for a long time!
		interval: 5,
		plenterCurve: { 10: 350, 15: 220, 20: 150, 25: 105, 30: 80, 35: 50, 40: 40, 45: 25, 50: 20 }, // target number of tress per dbh class
		dbhSteps: 5, // dbh class width
		intensity: 1,
		block: true,
		sendSignal: 'plenter_execute',
		constraint: undefined,		
        // ... add other default thinning parameters
    };

    const opts = lib.mergeOptions(defaultOptions, options || {});
	
	// check if any two dbh classes in PlenterCurve are closer than dbhSteps
	var dbhClasses = Object.keys(opts.plenterCurve);
	for (var i = 0; i < dbhClasses.length - 1; i++) {
		if (dbhClasses[i + 1] - dbhClasses[i] < opts.dbhSteps) {
			throw new Error(`Plenter thinning: dbh classes in 'plenterCurve' are too close together! Change parameter 'dbhSteps' or 'plenterCurve'.`);
		}
	}

	var program = {};

	program["plenter_repeat"] = lib.repeater({ 
		id: opts.id + "_repeater",
		schedule: opts.schedule, 
		signal: opts.sendSignal, 
		count: opts.times, 
		interval: opts.interval,
		block: opts.block
	});

	program["plenter_thinning"] = {
		id: opts.id + '_thinning',
		type: 'scheduled', 
		schedule: { signal: opts.sendSignal },
		onEvaluate: function() {
			return true; 
		},
		onExecute: function() {
			// Compare current and target distributions and mark trees for removal (example)
			for (var i in dbhClasses) {
				var dbh = dbhClasses[i];
				// load all trees of dbh class [dbh-dbhSteps, dbh]
				var treesInClass = stand.trees.load('dbh>' + (dbh - opts.dbhSteps) + ' and dbh<=' + dbh);
				var targetCount = opts.plenterCurve[dbh] * stand.area;
				lib.dbg(`plenter activity - dbh class: ${dbh}: ${treesInClass} trees in Class, ${targetCount} trees target.`);

				var N = stand.trees.filterRandomExclude((treesInClass - targetCount) * opts.intensity);
				stand.trees.harvest();

				stand.trees.removeMarkedTrees();

				lib.dbg(`plenter activity - dbh class: ${dbh}: ${treesInClass - N} trees removed.`);
			}
	
			//Remove the marked trees
			stand.trees.removeMarkedTrees();
		},
		// onExecuted: function() {
		// 	lib.activityLog('plenter_thinning'); 
		// },
		onExit: function() {
            if (opts.sendSignal !== undefined) {
			  	lib.dbg(`Signal: ${opts.sendSignal} emitted.`);
			  	stand.stp.signal(opts.sendSignal);
			};
        }
	};

	if (opts.constraint !== undefined) program.constraint = opts.constraint;
	
	program.description = `A plenter thinning activity, that removes every ${opts.interval} years a certain number of trees per dbh class according to a target distribution (plenter curve).`;
						
	return program;	
}