Show:

/**
 * The harvest library includes management operations related to harvesting.
 *
 * @class harvest
 * @memberof abe-lib
 */
lib.harvest = {};


Globals.include(lib.path.dir + '/harvest/femel.js');
Globals.include(lib.path.dir + '/harvest/salvage.js');


/**
 * No harvest management
 * @method noManagement
 * @return {object} act - An object describing the harvest activity
 * @example
 *     lib.harvest.noManagement();
 */
lib.harvest.noManagement = function() {
	var act = {
		type: 'general', 
		schedule: { repeat: true, repeatInterval: 100},
		action: function() {
		}
	};
	
	act.description = `No harvest management`;
						
	return act;
};

/**
 * Harvest all trees above a certain height threshold
 * @method HarvestAllBigTrees
 * @param {object} options
 *    @param {string} options.id A unique identifier for the harvest activity (default: 'HarvestAllBigTrees').
 *    @param {object} options.schedule schedule of the harvest (default: {absolute: true, opt: 3}).
 *    @param {string} options.preferenceFunction ranking string for filtering trees, e.g. 'height > 10' (default: 'height > 10').
 *    @param {string|undefined} options.sendSignal signal send out after activity (default: undefined).
 *    @param {string|undefined} options.constraint constraint (default: undefined).
 * @return {object} act - An object describing the harvest activity
 * @example
 *     lib.harvest.HarvestAllBigTrees({
 *         schedule: {absolute: true, opt: 5},
 *         preferenceFunction: 'height > 15'
 *     });
 */
lib.harvest.HarvestAllBigTrees = function(options) {
	// 1. Default Options
    const defaultOptions = { 
		id: 'HarvestAllBigTrees',
		schedule: {absolute: true, opt: 3},
		preferenceFunction: 'height > 10',
		sendSignal: undefined,
		constraint: undefined,
    };

    const opts = lib.mergeOptions(defaultOptions, options || {});
	
	var act = {
		id: opts.id,
		type: 'general', 
		schedule: opts.schedule,
		action: function() {
			stand.trees.load(opts.preferenceFunction);
			stand.trees.harvest();
			//lib.activityLog('HarvestAllBigTrees'); 
		},
		onExit: function() {
            if (opts.sendSignal !== undefined) {
                lib.dbg(`Signal: ${opts.sendSignal} emitted.`);
			  	stand.stp.signal(opts.sendSignal);
            }
        },
	};

	act.description = `Harvest all trees with ${opts.preferenceFunction} after ${opts.schedule.opt} years of simulation.`;
	return act;
};

/**
 * Clearcut operation, that removes all trees above a minimum diameter
 * @method clearcut
 * @param {object} options
 *    @param {string} options.id A unique identifier for the harvest activity (default: 'Clearcut').
 *    @param {object} options.schedule schedule of the harvest (default: { minRel: 0.8, optRel: 1, maxRel: 1.2, force: true }).
 *    @param {string} options.preferenceFunction ranking string for filtering trees, e.g. 'dbh > 10' (default: 'dbh > 0').
 *    @param {string|undefined} options.finalHarvest boolean variable to indeicate, if the activity should be interpreted as final harvest (default: true).
 *    @param {string|undefined} options.sendSignal signal send out after activity (default: undefined).
 *    @param {string|undefined} options.constraint constraint (default: undefined).
 * @return {object} act - An object describing the harvest activity
 * @example
 *     lib.harvest.clearcut({
 *         schedule: { minRel: 0.7, optRel: 0.9, maxRel: 1.1, force: true },
 *         preferenceFunction: `dbh > 5`
 *     });
 */
lib.harvest.clearcut = function(options) {
    // 1. Default Options
    const defaultOptions = { 
        id: 'Clearcut',
		schedule: { minRel: 0.8, optRel: 1, maxRel: 1.2, force: true }, 
        preferenceFunction: `dbh > 0`,
		finalHarvest: true,
		sendSignal: undefined,
		constraint: undefined
    };
    const opts = lib.mergeOptions(defaultOptions, options || {});

	const act = { 
		id: opts.id,
		type: "scheduled", 
		schedule: opts.schedule, 
		onCreate: function() { 
			if (opts.finalHarvest === true) {
				this.finalHarvest = true; 
			}
		}, 
		onSetup: function() { 
			lib.initStandObj(); 
		},
		onEvaluate: function() {				
			return true; 
		},
		onExecute: function() {
			stand.trace = true;
			let was_simulate = stand.trees.simulate;
			stand.trees.simulate = false;
			stand.trees.load(opts.preferenceFunction);
			stand.trees.harvest();
			stand.trees.simulate = was_simulate;

			//lib.activityLog(`Clearcut executed`);
		},
		onExit: function() {
			if (opts.sendSignal !== undefined) {
				lib.dbg(`Signal: ${opts.sendSignal} emitted.`);
				stand.stp.signal(opts.sendSignal);
			}
		}
	};

	if (opts.constraint !== undefined) act.constraint = opts.constraint;
    act.description = `A clearcut operation, that removes all trees with ${opts.preferenceFunction}.`;

	return act;  
};

/**
 * Shelterwood harvest system
 * @method shelterwood
 * @param {object} options
 *    @param {string} options.id A unique identifier for the harvest activity (default: 'Shelterwood').
 *    @param {object} options.schedule schedule of the harvest (default: { minRel: 0.7, optRel: 0.8, maxRel: 0.9, force: true }).
 *    @param {number} options.nTrees Number of seed trees to select (default: 40).
 *    @param {number} options.nCompetitors Number of other trees to select - should be all remaining trees and thus really high (default: 1000).
 *    @param {object} options.speciesSelectivity species selectivity object (default: {}).
 *    @param {string} options.preferenceFunction ranking string for selecting seed trees, e.g. 'height' (default: 'height').
 *    @param {number} options.interval interval between repeated harvests (default: 5).
 *    @param {number} options.times number of repeated harvests (default: 3).
 *    @param {string|undefined} options.finalHarvest boolean variable to indeicate, if the activity should be interpreted as final harvest (default: true).
 *    @param {string|undefined} options.internalSignal internal signal to start each shelterwood activity (default: 'Shelterwood_remove').
 *    @param {string|undefined} options.sendSignal signal send out after last shelterwood activity (default: undefined).
 *    @param {string|undefined} options.constraint constraint (default: undefined).
 * @return {object} program - An object describing the harvest program
 * @example
 *     lib.harvest.shelterwood({
 *         schedule: { minRel: 0.6, optRel: 0.7, maxRel: 0.8, force: true },
 *         nTrees: 50,
 *         speciesSelectivity: { 'pisy': 1, 'abal': 0.5 }
 *     });
 */
lib.harvest.shelterwood = function(options) {
    // 1. Default Options
    const defaultOptions = { 
        id: 'Shelterwood',
		schedule: { minRel: 0.7, optRel: 0.8, maxRel: 0.9, force: true }, // activity should start before the optimal rotation length is reached.
        nTrees: 40,
		nCompetitors: 1000,
		speciesSelectivity: {},
		preferenceFunction: 'height',
		interval: 5,
		times: 2, // number of harvests before the final harvest, so with e.g. times 2 it would be 2 pre-Harvests and lastly one final
		finalHarvest: true,
		internalSignal: 'Shelterwood_remove',
		sendSignal: undefined,
		constraint: undefined
    };
    const opts = lib.mergeOptions(defaultOptions, options || {});

	// program consists of first a selection treatment, where the seed trees are set to crop trees and all other trees to competitors
	// secondly it consists of a harvest treatment, within every tree gets harvest in multiple harvest activities
	const program = {};

	// select the remaining trees, that should be harvested last
	// those should be the trees, that are i) dominant and ii) the target species of the regeneration
    program["Shelterwood_selector"] = {
        id: opts.id +  "_select_trees",
        type: 'thinning',
        schedule: opts.schedule,
        thinning: 'selection',
        N: opts.nTrees,
        NCompetitors: opts.nCompetitors,
        speciesSelectivity: opts.speciesSelectivity,
        ranking: opts.preferenceFunction,

		onSetup: function() { 
			lib.initStandObj();
		},
        onEnter: function() {
            stand.obj.lib.shelterwoodHarvestCounter = 0;
        },
        onExit: function() {
            stand.stp.signal('Shelterwood_start_repeat');
        },
        description: `Select ${opts.nTrees} seed trees of the stand.`
    };

	program['Shelterwood_repeater'] = lib.repeater({ schedule: { signal: 'Shelterwood_start_repeat'},
											signal: opts.internalSignal,
											interval: opts.interval,
											count: opts.times // in final year all crop trees get removed
										});

	// harvest competitor trees in the first year and seed trees in the last harvest activity
	program["Shelterwood_remover"] = {
        id: opts.id + "_remove_trees",
        type: 'general',
        schedule: { signal: opts.internalSignal},
        action: function() {

            // first year. Save # of marked competitors
			if (stand.obj.lib.shelterwoodHarvestCounter == 0) {
				var marked = stand.trees.load('markcompetitor=true');
                stand.setFlag('compN', marked);
                lib.dbg(`selectiveThinning: start removal phase. ${marked} trees marked for removal.`);
            };

            // remove equal amount of non seed trees in each harvest
			var n = stand.flag('compN') / (opts.times - 1);
            var N_Competitors = stand.trees.load('markcompetitor=true');
			
			if ((N_Competitors - n) > 0) {
				stand.trees.filterRandomExclude(N_Competitors - n);
			}
			
            var harvested = stand.trees.harvest();
			stand.obj.lib.shelterwoodHarvestCounter = stand.obj.lib.shelterwoodHarvestCounter + 1;

            lib.dbg("Year: " + Globals.year + ", shelterwood harvest");
			lib.dbg(`shelterwood harvest: No. ${stand.obj.lib.shelterwoodHarvestCounter}, removed ${harvested} trees.`);

			//lib.activityLog(`shelterwood harvest No. ${stand.obj.lib.shelterwoodHarvestCounter}`);
        },

        description: `Shelterwood harvest (during ${opts.times * opts.interval} years), that removes all trees in ${opts.times} harvests.`
    }

	program['Shelterwood_final'] = { 
		type: "scheduled", 
		id: opts.id + "_final_harvest",
		schedule: { signal: 'Shelterwood_start_repeat', wait: (opts.interval * opts.times) }, 

		onCreate: function() { 
			if (opts.finalHarvest === true) {
				this.finalHarvest = true; 
			}
		},
		onEvaluate: function() { 
			return true
		},
		onExecute: function() {
			stand.trace = true;
			let was_simulate = stand.trees.simulate;
			stand.trees.simulate = false;
			stand.trees.load('markcompetitor=true or markcrop=true');
			var harvested = stand.trees.harvest();
			stand.trees.simulate = was_simulate;
			
			lib.dbg(`shelterwood final harvest: ${harvested} trees removed.`);
    		//lib.activityLog('Shelter wood final harvest'); 
		},
		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 shelterwood operation, that removes all trees in ${opts.times} harvests.`;
	
	return program;  
};


/**
 * Strip cut system
 * @method stripCut
 * @memberof abe-lib.harvest
 * @param {object} options
 *    @param {string} options.id A unique identifier for the harvest activity (default: 'StripCut').
 *    @param {number} options.harvestDirection direction of the strips in degrees (not working yet) (default: 120).
 *    @param {number} options.stripWidth width of each strip in meters (default: 30).
 *    @param {number} options.nStrips number of strips before the next strip is a "first" strip again (default: 5).
 *    @param {number[]} options.harvestIntensities array of harvest intensities for each harvest on the strip (default: [0.7, 0.5, 1]).
 *    @param {number} options.interval number of years between each harvest activity (default: 5).
 *    @param {string|undefined} options.sendSignal signal send out after each activity (default: undefined).
 *    @param {string} options.constraint constraint, which strips should be harvested e.g. "topHeight>30" (default: "stand.topHeight>30").
 * @return {object} act - An object describing the harvest activity
 * @example
 *     lib.harvest.stripCut2({
 *         harvestDirection: 180,
 *         stripWidth: 25,
 *         harvestIntensities: [0.5, 0.7, 1],
 *         constraint: "stand.basalArea>30"
 *     });
 */
lib.harvest.stripCut = function(options) {
    // 1. Default Options
    const defaultOptions = {
        id: 'StripCut',
		harvestDirection: 120, // harvest direction of the strips in degrees; strip orientation is +90°
        stripWidth: 30, // width of each strip
        nStrips: 5, // number of strips before the next strip is a "first" strip again

        harvestIntensities: [0.7, 0.5, 1], // harvest intensities of the harvests on the strip (e.g. first removing 70% of all trees in the strip, after 5 years remaining 50% of the rest, after 5 years remaining the rest)
        interval: 5, // number of years between each harvest activity
		sendSignal: undefined,
        constraint: "stand.topHeight>30", // gate keeper which strips should be harvested

        // ... add other default thinning parameters
    };
    const opts = lib.mergeOptions(defaultOptions, options || {});

    const act = {
		id: opts.id,
        type: "general",
        schedule: { repeat: true, repeatInterval: opts.interval },
        onSetup: function() {
            // initialise the patches
            // opts.harvestDirection and opts.stripWidth needed.
            stand.patches.list = stand.patches.createStrips(opts.stripWidth, /*horiziontal=*/ true);   //needs to be cooler

            initStandObj();
            stand.obj.act["stripcutHarvestCounter"] = 0;
            opts.nStrips = Math.min(stand.patches.list.length, opts.nStrips);

        },
        action: function() {
            var harvestTookPlace = 0;

            for (p of stand.patches.list) {
                //Globals.alert("Check patch " + p.id);
                // Step 1: check if harvest should take place... maybe with sth. like the following
                let stripType = ((p.id - 1) % opts.nStrips) + 1 // check the type of harvest (first, second, ..., last or no)

                // old let harvestType = Math.max(stand.obj.act["stripcutHarvestCounter"] + 2 - stripType, 0) * (stripType - 1 <= stand.obj.act["stripcutHarvestCounter"]) * ((stand.obj.act["stripcutHarvestCounter"] + 2 - stripType) <= opts.harvestIntensities.length);
                let harvestType = Math.max(stand.obj.act["stripcutHarvestCounter"] - stripType + 2, 0) * (stand.obj.act["stripcutHarvestCounter"] - stripType + 1 < opts.harvestIntensities.length);
                //Globals.alert("harvestType " + harvestType);

                if (harvestType !== 0) {
                    //Globals.alert("Harvest!" + " harvestIntensitie: " + opts.harvestIntensities[harvestType - 1])
                    // still to do: only harvest if mean height of trees in stand are above harvestHeightThreshold

                    stand.trees.load('patch=' + p.id);
                    //var treesToRemain = Math.round(stand.trees.count*(1-opts.harvestIntensities[harvestType - 1]));
                    var treesToHarvest = Math.round(stand.trees.count * (opts.harvestIntensities[harvestType - 1]));

                    stand.trees.filterRandomExclude(treesToHarvest);
                    //stand.trees.filter('incsum(1) <= ' + treesToHarvest);
                    stand.trees.harvest();
                    //lib.activityLog(`Stripcut executed`);
                    harvestTookPlace = 1;

                } else {
                    //Globals.alert("No Harvest")
                };
            }
            stand.obj.act["stripcutHarvestCounter"] = stand.obj.act["stripcutHarvestCounter"] + harvestTookPlace;
        },
		onExit: function() {
            if (opts.sendSignal !== undefined) {
			  	lib.dbg(`Signal: ${opts.sendSignal} emitted.`);
			  	stand.stp.signal(opts.sendSignal);
			};
        },
    };
    if (opts.constraint !== undefined) act.constraint = opts.constraint;

    act.description = `Stripcut system which first devides the stand into ${opts.stripWidth} meter wide stripes and harvests on these stripes every ${opts.interval} years.`;
    return act;
};


/**
 * Coppice with standards management system
 * @method CoppiceWithStandard
 * @param {object} options
 *    @param {number} options.targetDBH target DBH for harvesting (default: 80).
 *    @param {number} options.nStandards number of remaining trees per hectare (default: 30).
 *    @param {number} options.interval time interval between harvests (default: 20).
 *    @param {string} options.preferenceFunction ranking string for selecting standards, e.g. 'dbh+100*species=quro' (default: 'dbh').
 *    @param {string|undefined} options.sendSignal signal send out after each activity (default: undefined).
 *    @param {string|undefined} options.constraint constraint (default: undefined).
 * @return {object} act - An object describing the harvest activity
 * @example
 *     lib.harvest.CoppiceWithStandard({
 *         targetDBH: 70,
 *         nStandards: 40,
 *         species: 'fasy'
 *     });
 */
lib.harvest.CoppiceWithStandard = function(options) {
	// 1. Default Options
    const defaultOptions = { 
		id: "CoppiceWithStandard",
		targetDBH: 80, 
		nStandards: 30, // number of remaining trees per ha
		interval: 20,
		preferenceFunction: "dbh",
		sendSignal: undefined,
		constraint: undefined
    };
    const opts = lib.mergeOptions(defaultOptions, options || {});
	
	var act = {
		id: opts.id,
		type: 'general', 
		schedule: { repeat: true, repeatInterval: opts.interval},
		action: function() {
			// harvest all trees above the targetDBH threshold
			stand.trace = true;
			let was_simulate = stand.trees.simulate;
			stand.trees.simulate = false;
			stand.trees.load('dbh>='+opts.targetDBH);
			var harvested = stand.trees.harvest();
			
			// harvest all other trees but nStandards
			var nTreesToHarvest = stand.trees.load('dbh<'+opts.targetDBH) - opts.nStandards;
			stand.trees.sort(preferenceFunction);
			stand.trees.filter('incsum(1) <= ' + nTreesToHarvest);
			stand.trees.harvest();
			stand.trees.simulate = was_simulate;
			
			//lib.activityLog(`CoppiceWithStandard executed`);
		},
		onExit: function() {
            if (opts.sendSignal !== undefined) {
			  	lib.dbg(`Signal: ${opts.sendSignal} emitted.`);
			  	stand.stp.signal(opts.sendSignal);
			};
        }
	};
	if (opts.constraint !== undefined) act.constraint = opts.constraint;
	
	act.description = `Coppice with standard strategy: removing every ${opts.interval} years every but ${opts.nStandards} trees.`;
  return act;  
};


/**
 * Target diameter harvesting system
 * @method targetDBH
 * @param {object} options
 *    @param {object|undefined} options.schedule schedule for the harvest (default: undefined).
 *    @param {number} options.targetDBH target DBH for harvesting (default: 50).
 *    @param {number} options.times time interval between harvests (default: 5).
 *    @param {object} options.dbhList object with DBH thresholds per species (default: {}).
 *    @param {string|undefined} options.sendSignal signal send out after each activity (default: undefined).
 *    @param {string|undefined} options.constraint constraint (default: undefined).
 * @return {object} act - An object describing the harvest activity
 * @example
 *     lib.harvest.targetDBH({
 *         targetDBH: 60,
 *         times: 10,
 *         dbhList: { 'fasy': 50, 'abal': 55 }
 *     });
 */
lib.harvest.targetDBH = function(options) {
	// 1. Default Options
    const defaultOptions = { 
		id: 'targetDBH',
		schedule: undefined,
		targetDBH: 50, 
		times: 5, 
		dbhList: {"fasy":65, "frex":60, "piab":45, "quro":75, "pisy":45, "lade":65, "qupe":75, "psme":65, "abal":45, "acps":60, "pini":45}, //source: 'Waldbau auf ökologischer Grundlage', p.452
		sendSignal: undefined,
		constraint: undefined,
    };
    let opts = lib.mergeOptions(defaultOptions, options || {});
	
	opts.dbhList['rest'] = opts.targetDBH;  // set the rest to the overall target DBH	
	
	let excludeString = "";
	let isFirst = true;
	// in(species, piab, fasy, ...)=false 
	
	for (var species in opts.dbhList) {
		if (isFirst) {
			excludeString += "species <> ";
			excludeString += species;
			isFirst = false;
		} else {
			excludeString += " and species <> ";
			excludeString += species;
		}
	};
		
	var act = {
		id: opts.id,
		type: 'general', 
		schedule: opts.schedule,
		action: function() {
			lib.dbg(`Stand: ${stand.id}, Year: ${Globals.year}, targetDBH harvest`);
			for (var species in opts.dbhList) {
				var dbh = opts.dbhList[species]
				if (species === 'rest') {
					var N_Trees = stand.trees.load(excludeString + ' and dbh > '+ dbh);
				} else {
					var N_Trees = stand.trees.load('species = ' + species + ' and dbh > ' + dbh);
				};
				
				if (N_Trees > 0) {
					lib.dbg("Species: " + species + ", target DBH: " + dbh + ", Trees: " + N_Trees + " harvested.");
					stand.trees.harvest();
				};
			};
			//lib.activityLog(`Harvest targetDBH executed`);
		},
		onExit: function() {
            if (opts.sendSignal !== undefined) {
			  	lib.dbg(`Signal: ${opts.sendSignal} emitted.`);
			  	stand.stp.signal(opts.sendSignal);
			};
        }
	};
	
	if (opts.constraint !== undefined) act.constraint = opts.constraint;
	
	act.description = `A simple repeating harvest operation (every ${opts.times} years), that removes all trees above a target diameter ( ${opts.targetDBH} cm)).`;
						
	return act;
};

/**
 * Target diameter harvesting system for Norway Spruce (No)
 * @method targetDBHforNo3
 * @param {object} options
 *    @param {number} options.targetDBH target DBH for harvesting (default: 50).
 *    @param {number} options.times time interval between harvests (default: 5).
 *    @param {object} options.dbhList object with DBH thresholds per species (default: {}).
 *    @param {string|undefined} options.sendSignal signal send out after each activity (default: undefined).
 *    @param {string|undefined} options.constraint constraint (default: undefined).
 * @return {object} act - An object describing the harvest activity
 * @example
 *     lib.harvest.targetDBHforNo3({
 *         targetDBH: 55,
 *         times: 8,
 *         dbhList: { 'fasy': 45, 'abal': 60 }
 *     });
 */
lib.harvest.targetDBHforNo3 = function(options) {
	// 1. Default Options
    const defaultOptions = {
		id: 'TargetDBHforNo3', 
		targetDBH: 50, 
		times: 5, 
		dbhList: {},
		sendSignal: undefined,
		constraint: undefined,
    };
    let opts = lib.mergeOptions(defaultOptions, options || {});
	
	opts.dbhList['rest'] = opts.targetDBH;  // set the rest to the overall target DBH	
	
	let excludeString = "";
	let isFirst = true;
	// in(species, piab, fasy, ...)=false 
	
	for (var species in opts.dbhList) {
		if (isFirst) {
			excludeString += "species <> ";
			excludeString += species;
			isFirst = false;
		} else {
			excludeString += " and species <> ";
			excludeString += species;
		}
	};
		
	var act = {
		id: opts.id, 
		type: 'general', 
		schedule: { repeat: true, repeatInterval: opts.times},
		onSetup: function() { 
            lib.initStandObj();
			stand.obj.act["NHarvests"] = 0;
		},
		action: function() {
			lib.dbg("Year: " + Globals.year + ", targetDBH harvest");
			for (var species in opts.dbhList) {
				var dbh = opts.dbhList[species]
				if (species === 'rest') {
					stand.trees.load(excludeString + ' and dbh > '+ dbh);
				} else {
					stand.trees.load('species = ' + species + ' and dbh > ' + dbh);
				};
				lib.dbg("Species: " + species + ", target DBH: " + dbh + ", Trees: " + stand.trees.count)
				stand.obj.act["NHarvests"] = stand.obj.act["NHarvests"] + stand.trees.count;
				stand.trees.harvest();
				//lib.activityLog(`Harvest targetDBHforNo3 executed`);
			};
			if (stand.obj.act["NHarvests"] > 20) {
				fmengine.runPlanting(stand.id, {species: 'psme', height: 0.4, fraction:1, pattern:'rect2', spacing:10});
				Globals.alert("Planting!");	
				//lib.activityLog(`Planting of targetDBHforNo3 executed`);			
			};
			stand.obj.act["NHarvests"] = 0;
		},
		onExit: function() {
            if (opts.sendSignal !== undefined) {
			  	lib.dbg(`Signal: ${opts.sendSignal} emitted.`);
			  	stand.stp.signal(opts.sendSignal);
			};
        }
	};
	
	if (opts.constraint !== undefined) act.constraint = opts.constraint;
	
	act.description = `A simple repeating harvest operation (every ${opts.times} years), that removes all trees above a target diameter ( ${opts.targetDBH} cm)).`;
						
	return act;
};