/** Example for the iLand spinup procedure see details: iland.boku.ac.at/spinup this script is based on the application of the spinup for the "Stubaital" in Austria. by Werner Rammer & Katharina Albrich, 2017 */ // Include the spinup library Globals.include("abe/spinuplib.js"); // create global objects which are defined in the spinup library: var DBHdist = new DBHDist('temp/dbhdist.txt', 'temp/stands.txt'); var lg = new Logger('temp/spinup.log'); var snapshot = new Snapshot('output/standsnapshot.sqlite', 'scripts/stands_carbon.txt'); var stand_map = Factory.newMap(); // the stand grid // global data structures var stand_ids = {}; var targets = {}; var stands_to_process = []; var saved_stands_changed = false; // Load the definition file and store to our global data structures 'targets'. // the properties are loaded during initialization of ABE from the CSV-file (agentDataFile) // the values of additional data columns are stored as stand-properties, which can be accessed by stand.flag('property-name') // stand properties can also be viewed in the iLand GUI. // In this project, the targets are derived from sample plots and contain: // * species share for spruce, fir, larch, pine, stone pine // * basal area function loadTargets() { stand_ids = fmengine.standIds; for (var i=0;i0) { var precut1 = ops['precut'](st.age - st.precut1); prog['precut1'] = precut1; console.log('extra cut (#1) for stand ' + st.id + ' at age ' + precut1.schedule); } if (st.precut2>0) { var precut2 = ops['precut'](st.age - st.precut2); prog['precut2'] = precut2; console.log('extra cut (#2) for stand ' + st.id + ' at age ' + precut2.schedule); } return prog; } /** The 'ops' object is a collection of management activities. Each function returns a javascript object that defines an ABE activity (http://iland.boku.ac.at/ABE+activities) */ var ops = { // create a default planting (2500 saplings = 100%, light-demanding species (lade, pisy) are planted in groups) stdplanting: function(target) { var my_items = []; // add a clear-item: my_items.push( { species: 'piab', fraction:0, height: 0.1, clear:true } ); for (s in target) { console.log(s + ": target:" + target[s] ); if (s != 'rest' && target[s]>0.01) { if (s in {'lade':'lade', 'pisy':'pisy', 'acps':'acps', 'ulgl':'ulgl', }) { // awesome feature: the planting could determine number of plots during runtime dynamically (via a JS function) // here we re-create the whole STP (and thus also the planting activity) at every rotation, so a constant value is fine // fancy JS: my_items.push( { species: s, pattern: 'rect10', random: true, n: function() { return Math.max(Math.min(100,100*stand.flag(s)),1) }, height: 0.1 } ); // Math.max(target[s]*100,1) my_items.push( { species: s, pattern: 'rect10', random: true, n: Math.max(Math.min(100,100*target[s]),1), height: 0.1 } ); // Math.max(target[s]*100,1) } else my_items.push( { species: s, fraction: target[s], height: 0.1 } ); } } // the planting is executed in the first year, in addition the 'plantingUpdated' flag is reset var x = { type: 'planting', schedule: 1, items: my_items, onExecuted: function() {stand.setFlag('plantingUpdated', false);} }; return x; }, // default harvest: a minimum rotation is 80 years (even if the forest state is compared and processed earlier) stdfinalcut: function(age) { var a_clearcut = { type: "scheduled", schedule: Math.max(age + 10, 80), onEvaluate: function(){ // fmengine.log("finalHarvest:" + activity.finalHarvest); stand.simulate=false; trees.loadAll(); trees.harvest("dbh>0"); trees.killSaplings('height>2'); return true; }, onExecuted: function() { stands_to_process.push( stand.id ); }, // now update the program after the final cut onCreate: function() { activity.finalHarvest=true; }, }; return a_clearcut; }, // extra removals: currently deactivated (return) pioneer: function() { var my_op = { type: 'general', schedule: 10, action: function() { return; // deactivate trees.loadAll(); var nk=0; for (s in stp.options.extra) { nk+=trees.killSaplings('species=' + s + ' and rnd(0,1)<0.9'); } nk+=trees.killSaplings('in(species,bepe,potr,tico) and rnd(0,1)<0.9'); // kill also pioneers... fmengine.log('pioneer:killed saplings #' + nk); } }; return my_op; }, // default tending operation stdtending: function() { var my_op = { type: 'thinning', schedule: 25, //constraint: ["stand.topHeight>12"], thinning: 'custom', onEvaluate: function(){ var t= calculateSpeciesProbabilities(stp.options.plan, this.targetValue, 1); if (stand.trace) { console.log("plan"); printObj(t);} // code below not used, but shows that quite specific actions are possible //t['bepe'] = 0.95; t['potr']=0.95; // remove 90% of all trees that are not in the target-list /* trees.simulate = false; // we really want to remove the trees! var fil="in(species"; for (s in stp.options.plan) if (s!='rest') fil += ',' + s; fil+=')=0'; trees.loadAll(); var nk=0; nk+=trees.kill(fil, 0.95); for (s in stp.options.extra) { nk+=trees.kill('species=' + s + ' and rnd(0,1)<0.9'); } fmengine.log('tending. extra-kill: #' + nk); */ return t; }, targetValue: 30, targetVariable: 'volume', targetRelative: true, minDbh: 10, classes: [10, 30, 25, 30, 5] }; return my_op; }, // default thinning: stdthinning: function() { var a_thinning2 = { type: 'thinning', schedule: 60, //constraint: ["stand.topHeight>21"], thinning: 'custom', onEvaluate: function(){ var t= calculateSpeciesProbabilities(stp.options.plan, this.targetValue, 5); if (stand.trace) { console.log("plan"); printObj(t);} trees.simulate = false; // we really want to remove the trees! /* var fil="in(species"; for (s in stp.options.plan) if (s!='rest') fil += ',' + s; fil+=')=0'; trees.loadAll(); var nk=trees.kill(fil, 0.9); for (s in stp.options.extra) { nk+=trees.kill('species=' + s + ' and rnd(0,1)<0.9'); } fmengine.log('thinning. extra-kill: #' + nk); */ return t; }, targetValue: 30, targetVariable: 'volume', targetRelative: true, minDbh: 10, classes: [10, 30, 25, 30, 5] }; return a_thinning2; }, // not used here; example for creating gaps precut: function(exec_year) { var a_precut = { type: 'general', schedule: exec_year, action: function() { // remove trees on 20% of the 10m pixels in groups trees.loadAll(); var rx=Math.random()*100, ry=Math.random()*100; var fil='mod(x+' + rx + ',60)<25 and mod(y+'+ry+',60)<25'; var nk=trees.kill(fil, 1.0); fmengine.log('extracut, killed: #' + nk); } }; return a_precut; } } // Add operators here, e.g. to add new fancy ways of planting ops.myFancyPlanting= function(target) { // build and return.... return; } /** runEvaluator() is called automatically from the 'evaluator' activity when a stand reaches its target age. The function compares the current state with the target, calculates the 'similarity index', and triggers an update of the STPs; see also updateStands() */ function runEvaluator(id) { // compare stand characteristics as they are now with what is supposed to be var t = targets[id]; // targets for the stand if (typeof t === "undefined") return; stand.reload(); var v = stand.basalArea; var state_abs = {}; var state_rel = {}; var total = 0; for (var i=0;i0) v_rel_diff = v/t.basalArea - 1; // -0.1 -> 10% below target stand.setFlag('relBasalArea', v_rel_diff); // check bray-curtis index (see spinuplib.js) var bcindex = similarity(t.target, state_rel); console.log("dissimilarity: " + bcindex); stand.setFlag('bcindex', bcindex); // calculate total similarity score, larger weight for bcindex var total_score = bcindex + Math.abs(v_rel_diff)*0.25; stand.setFlag('score', total_score); fmengine.log("evaluation stand:" + stand.id + ", age: " + stand.age); lg.log("score: " + total_score + " " + bcindex + " " +v_diff+ " " +v_rel_diff + " (total dissimilarity delta_v delta_v_rel)"); if (stand.flag('plantingUpdated')==="undefined" || stand.flag('plantingUpdated')===false) { lg.log("share: " + speciesShareString()); // once per rotation if (bcindex > 0.2) { // modify planting fractions var upt = updatePlantingTarget(t.target, t.plant_target, state_rel); t.plant_target = upt.current; t.extra = upt.extra; lg.log("action: update_planting " + toFlatString(t.plant_target) ); lg.log("action: extra_removal " + toFlatString(t.extra) ); stand.setFlag('plantingUpdated', true); } } t.score = total_score; // save the stand if this iteration yields the best score so far if (t.score < t.best_score) { t.best_score = t.score; snapshot.save(stand.id); DBHdist.add(stand.id); saved_stands_changed = true; lg.log("save:" + t.score + " " + t.basalArea + " " + v + " (score basalarea_plan basalarea_current)"); } targets[id] = t; // write back } // Update of STPs // this is triggered by the onYearEnd() event (can't modify a program when a STP is currently executing) function updateStands() { while (stands_to_process.length>0) { var stand_id = stands_to_process.pop(); console.log("*** processing stand" + stand_id ); updateStand( stand_id ); } console.log('**** update stands finished *****'); } /** update an individual stand 'stand_id'. */ function updateStand(stand_id) { var t = targets[stand_id]; // targets for the stand // build new program fmengine.standId = stand_id; var prog = new buildProgram( t ); fmengine.updateManagement(prog, "mgmt_"+stand_id); // overwrite the existing STP with the new management program fmengine.log('new-prog: ' + "mgmt_"+stand_id); lg.log('update: mgmt_'+stand_id); stand.absoluteAge = 0; stand.reset(); // start rotation new } // Agent definition: the agent does not used the scheduler of ABE, and // creates an STP dynamically in the onSelect-handler. var base_agent = { scheduler: { enabled: false }, stp: { 'default': 'spinup', 'spinup': 'spinup' }, newAgent: function() { return { scheduler: this.scheduler }; }, run: function() { }, onSelect: function() { // create mgmt on the fly if (Object.keys(targets).length == 0) { loadTargets(); } var t = targets[stand.id]; // targets for the stand if (typeof t === "undefined") { console.log('no valid target-definition for stand ' + stand.id + '! Falling back to default stp.'); return 'default'; } var prog = new buildProgram( t ); var stpname = "mgmt_" + stand.id; console.log('creating mangement program for stand ' + stand.id); // register the newly created STP in iLand (using an unique name) fmengine.addManagement( prog, stpname ); fmengine.addManagementToAgentType( stpname, 'agent_factory'); return stpname; // tell ABE to use the new STP for this stand } //run: function() { console.log('base-agent run called'); } }; var base_stp = { U: [100,110,120], // short/normal/long rotation age options: {} // no options }; // register the stand treatment program stp with the name 'spinup' fmengine.addManagement(base_stp, 'spinup'); // register the agent-factory object base_agent under the name agent_factory fmengine.addAgentType(base_agent, 'agent_factory'); // use the 'agent_factory' agent-factory and create an agent named 'agent' fmengine.addAgent('agent_factory', 'agent'); // set up DBH distribution DBHdist.setup(); Globals.setUIshortcuts({ 'Globals.reloadABE()': 'fully reload ABE' }) ; // Global onYearEnd event handler of iLand: called automatically every year by iLand function onYearEnd() { console.log("GlobalEvent: on year end: " + Globals.year); // invoke updates if (Globals.year>1) { updateStands(); lg.save(); if (saved_stands_changed) { saved_stands_changed=false; DBHdist.save(); } } if (Globals.year>50 && Globals.year % 50 == 0) { // save DBH distribution every 50 years DBHdist.saveState(); } }