From 22095f91ea830dcfdcf14da08110e377d839185a Mon Sep 17 00:00:00 2001 From: Bart Arendarczyk <s1924442@ed.ac.uk> Date: Sat, 4 Mar 2023 15:26:57 +0000 Subject: [PATCH] Forestry and carbon fixes & some parameter adjustments. --- src/ac/ed/lurg/ModelConfig.java | 34 ++-- src/ac/ed/lurg/ModelMain.java | 2 +- src/ac/ed/lurg/carbon/CarbonFluxItem.java | 17 +- src/ac/ed/lurg/country/CountryAgent.java | 3 +- .../country/gams/GamsRasterOptimiser.java | 44 ++++- src/ac/ed/lurg/demand/WoodDemandManager.java | 9 +- src/ac/ed/lurg/forestry/ForestManager.java | 180 +++++++++--------- src/ac/ed/lurg/forestry/WoodHarvestItem.java | 34 ++++ src/ac/ed/lurg/landuse/LandCoverTile.java | 3 - src/ac/ed/lurg/landuse/LandUseItem.java | 53 +++++- src/ac/ed/lurg/landuse/WoodUsageReader.java | 4 +- src/ac/ed/lurg/types/CropType.java | 20 +- src/ac/ed/lurg/types/WoodType.java | 2 +- 13 files changed, 258 insertions(+), 147 deletions(-) create mode 100644 src/ac/ed/lurg/forestry/WoodHarvestItem.java diff --git a/src/ac/ed/lurg/ModelConfig.java b/src/ac/ed/lurg/ModelConfig.java index 6fe441a2..b53c18e5 100755 --- a/src/ac/ed/lurg/ModelConfig.java +++ b/src/ac/ed/lurg/ModelConfig.java @@ -200,14 +200,14 @@ public class ModelConfig { public static final String YIELD_FILENAME = getProperty("YIELD_FILENAME", "yield.out"); public static final boolean PASTURE_FERT_RESPONSE_FROM_LPJ = getBooleanProperty("PASTURE_FERT_RESPONSE_FROM_LPJ", false);; - public static final double CALIB_FACTOR_CEREAL_C3 = getDoubleProperty("CALIB_FACTOR_CEREAL_C3", 0.904); - public static final double CALIB_FACTOR_CEREAL_C4 = getDoubleProperty("CALIB_FACTOR_CEREAL_C4", 0.504); - public static final double CALIB_FACTOR_MISCANTHUS = getDoubleProperty("CALIB_FACTOR_MISCANTHUS", 2.047); - public static final double CALIB_FACTOR_RICE = getDoubleProperty("CALIB_FACTOR_RICE", 0.978); - public static final double CALIB_FACTOR_OILCROPS = getDoubleProperty("CALIB_FACTOR_OILCROPS", 1.0); - public static final double CALIB_FACTOR_PULSES = getDoubleProperty("CALIB_FACTOR_PULSES", 0.347); - public static final double CALIB_FACTOR_STARCHY_ROOTS = getDoubleProperty("CALIB_FACTOR_STARCHY_ROOTS",4.679); - public static final double CALIB_FACTOR_FRUITVEG = getDoubleProperty("CALIB_FACTOR_FRUITVEG",3.377); + public static final double CALIB_FACTOR_CEREAL_C3 = getDoubleProperty("CALIB_FACTOR_CEREAL_C3", 1.07); + public static final double CALIB_FACTOR_CEREAL_C4 = getDoubleProperty("CALIB_FACTOR_CEREAL_C4", 0.707); + public static final double CALIB_FACTOR_MISCANTHUS = getDoubleProperty("CALIB_FACTOR_MISCANTHUS", 2.481); + public static final double CALIB_FACTOR_RICE = getDoubleProperty("CALIB_FACTOR_RICE", 1.464); + public static final double CALIB_FACTOR_OILCROPS = getDoubleProperty("CALIB_FACTOR_OILCROPS", 1.47); + public static final double CALIB_FACTOR_PULSES = getDoubleProperty("CALIB_FACTOR_PULSES", 0.446); + public static final double CALIB_FACTOR_STARCHY_ROOTS = getDoubleProperty("CALIB_FACTOR_STARCHY_ROOTS",5.656); + public static final double CALIB_FACTOR_FRUITVEG = getDoubleProperty("CALIB_FACTOR_FRUITVEG",4.812); public static final double CALIB_FACTOR_SUGAR = getDoubleProperty("CALIB_FACTOR_SUGAR", 1.0); public static final double CALIB_FACTOR_PASTURE = getDoubleProperty("CALIB_FACTOR_PASTURE", 1.0); @@ -363,7 +363,7 @@ public class ModelConfig { public static final int BASE_YEAR = getIntProperty("BASE_YEAR", 2019); // Import export limits - public static final double ANNUAL_MAX_IMPORT_CHANGE = getDoubleProperty("ANNUAL_MAX_IMPORT_CHANGE", 0.02); + public static final double ANNUAL_MAX_IMPORT_CHANGE = getDoubleProperty("ANNUAL_MAX_IMPORT_CHANGE", 0.05); public static final double MAX_IMPORT_CHANGE = getDoubleProperty("MAX_IMPORT_CHANGE", ANNUAL_MAX_IMPORT_CHANGE*TIMESTEP_SIZE); public static final double IMPORT_CHANGE_ELASTICITY = getDoubleProperty("IMPORT_CHANGE_ELASTICITY", 3.0); // higher value = steeper trade imbalance adjustment @@ -394,7 +394,7 @@ public class ModelConfig { public static final double PASTURE_HARVEST_FRACTION = getDoubleProperty("PASTURE_HARVEST_FRACTION", 0.5); public static final double MEAT_EFFICIENCY = getDoubleProperty("MEAT_EFFICIENCY", 1.0); // 'meat' is includes feed conversion ratio already, this is tech. change or similar - public static final double IRRIGIATION_EFFICIENCY = getDoubleProperty("IRRIGIATION_EFFICIENCY", 0.6); + public static final double IRRIGIATION_EFFICIENCY = getDoubleProperty("IRRIGIATION_EFFICIENCY", 0.33); public static final int ELLIOTT_BASEYEAR = 2010; public static final double ENVIRONMENTAL_WATER_CONSTRAINT = getDoubleProperty("ENVIRONMENTAL_WATER_CONSTRAINT", 0.5); // change with care, as due to normalisation it might not have the impact you first imagine public static final double OTHER_WATER_USE_FACTOR = getDoubleProperty("OTHER_WATER_USE_FACTOR", 1.0); @@ -425,8 +425,8 @@ public class ModelConfig { public static final boolean RESET_ENERGYCROP_PRICE = getBooleanProperty("RESET_ENERGYCROP_PRICE", true); // Resets price after calibration to avoid problems due to low initial demand // public static final double BIOENERGY_HEATING_VALUE_GJ_PER_T = getDoubleProperty("BIOENERGY_HEATING_VALUE_GJ_PER_T", 17.5); // GJ per t DM - public static final double MARKET_LAMBDA = getDoubleProperty("MARKET_LAMBDA", 0.2); // controls international market price adjustment rate - public static final double MARKET_DAMPING = getDoubleProperty("MARKET_DAMPING", 2.0); // controls international market price adjustment rate + public static final double MARKET_LAMBDA = getDoubleProperty("MARKET_LAMBDA", 0.04); // controls international market price adjustment rate + public static final double MARKET_DAMPING = getDoubleProperty("MARKET_DAMPING", 0.4); // controls international market price adjustment rate public static final String PRICE_UPDATE_METHOD = getProperty("PRICE_UPDATE_METHOD", "StockUseRatio"); // Options: StockUseRatio, MarketImbalance, StockChange public static final double MAX_PRICE_INCREASE = getDoubleProperty("MAX_PRICE_INCREASE", 1.5); @@ -445,10 +445,10 @@ public class ModelConfig { public static final double UNHANDLED_CROP_RATE = getDoubleProperty("UNHANDLED_CROP_RATE", 0.0498); // mostly forage crops public static final double SETASIDE_RATE = getDoubleProperty("SETASIDE_RATE", 0.103); // includes aside, fallow and failed cropland areas - public static final double OTHER_INTENSITY_COST = getDoubleProperty("OTHER_INTENSITY_COST", 0.6); + public static final double OTHER_INTENSITY_COST = getDoubleProperty("OTHER_INTENSITY_COST", 0.8); public static final double OTHER_INTENSITY_PARAM = getDoubleProperty("OTHER_INTENSITY_PARAM", 3.22); - public static final double IRRIG_COST_SCALE_FACTOR = getDoubleProperty("IRRIG_COST_SCALE_FACTOR", 0.0002); + public static final double IRRIG_COST_SCALE_FACTOR = getDoubleProperty("IRRIG_COST_SCALE_FACTOR", 0.00035); public static final double IRRIG_COST_MULTIPLIER = getDoubleProperty("IRRIG_COST_MULTIPLIER", 1.0); public static final double FERTILISER_COST_PER_T = getDoubleProperty("FERTILISER_COST_PER_T", 1.6); // $500/t, 18% N/t public static final double FERTILISER_MAX_COST = FERTILISER_COST_PER_T * MAX_FERT_AMOUNT/1000; @@ -522,11 +522,11 @@ public class ModelConfig { public static final double FUELWOOD_DEMAND_ELASTICITY = getDoubleProperty("FUELDWOOD_DEMAND_ELASTICITY", -0.3598551); public static final double WOOD_BIOMASS_CONVERSION_FACTOR = getDoubleProperty("WOOD_BIOMASS_CONVERSION_FACTOR", 0.3); // m3 to tC-eq p.16 [https://doi.org/10.5194/gmd-13-5425-2020] public static final double VEGETATION_CLEARING_COST = getDoubleProperty("VEGETATION_CLEARING_COST", 0.001); //$1000/tC - public static final double FOREST_MANAGEMENT_COST = IS_FORESTRY_ON ? getDoubleProperty("FOREST_ESTABLISHMENT_COST", 3.0) : 0.0; // establishment, management etc. $1000/ha + public static final double FOREST_MANAGEMENT_COST = IS_FORESTRY_ON ? getDoubleProperty("FOREST_ESTABLISHMENT_COST", 5.0) : 0.0; // establishment, management etc. $1000/ha public static final double FOREST_BASE_COST = getDoubleProperty("FOREST_BASE_COST", 0.05); public static final double WOOD_TRADE_BARRIER = getDoubleProperty("WOOD_TRADE_BARRIER", 0.2); //$1000/tC - public static final double INIT_ROUNDWOOD_PRICE = IS_FORESTRY_ON ? getDoubleProperty("INIT_ROUNDWOOD_PRICE", 0.36) : 0.0; // $1000/tC-eq - public static final double INIT_FUELWOOD_PRICE = IS_FORESTRY_ON ? getDoubleProperty("INIT_FUELWOOD_PRICE", 0.24) : 0.0; // $1000/tC-eq + public static final double INIT_ROUNDWOOD_PRICE = IS_FORESTRY_ON ? getDoubleProperty("INIT_ROUNDWOOD_PRICE", 0.1) : 0.0; // $1000/tC-eq + public static final double INIT_FUELWOOD_PRICE = IS_FORESTRY_ON ? getDoubleProperty("INIT_FUELWOOD_PRICE", 0.05) : 0.0; // $1000/tC-eq public static final int MAX_FOREST_ROTATION_PERIOD = getIntProperty("CARBON_WOOD_MAX_TIME", 160); // upper data limit, years public static final int LAND_AGE_GROUP_SIZE = getIntProperty("LAND_AGE_GROUP_SIZE", 5); // grouping size for land cover age diff --git a/src/ac/ed/lurg/ModelMain.java b/src/ac/ed/lurg/ModelMain.java index 4b725b71..b17122b7 100644 --- a/src/ac/ed/lurg/ModelMain.java +++ b/src/ac/ed/lurg/ModelMain.java @@ -370,7 +370,7 @@ public class ModelMain { continue; double prod = woodUsage.getHarvest(); - double netImports = woodUsage.getNetImport() - woodUsage.getLucHarvest(); // does not include transport losses + double netImports = woodUsage.getNetImport(); CountryPrice px = country.getCurrentCountryWoodPrices().get(woodType); double importPrice = px.getImportPrice(); diff --git a/src/ac/ed/lurg/carbon/CarbonFluxItem.java b/src/ac/ed/lurg/carbon/CarbonFluxItem.java index d020dde9..cc164a0e 100644 --- a/src/ac/ed/lurg/carbon/CarbonFluxItem.java +++ b/src/ac/ed/lurg/carbon/CarbonFluxItem.java @@ -53,18 +53,17 @@ public class CarbonFluxItem implements RasterItem, Serializable { } double meanFlux = totalFlux / totalArea; carbonFluxRate.put(key, meanFlux); - - // Carbon credit rate for unchanged land cover - - } - + + // Potential carbon flux from this existing land cover double fromLcCumulFlux = 0; for (int age : landTile.getAgeKeys()) { for (int i = age; i <= age + ModelConfig.CARBON_HORIZON; i++) { fromLcCumulFlux += cFluxes[i]; } } + + // Potential carbon flux if converting to this land cover double toLcCumulFlux = 0; for (int age = 0; age <= ModelConfig.CARBON_HORIZON; age++) { toLcCumulFlux += cFluxes[age]; @@ -77,8 +76,12 @@ public class CarbonFluxItem implements RasterItem, Serializable { for (LandCoverType lc : LandCoverType.getConvertibleTypes()) { LccKey fromKey = new LccKey(lcType, lc); LccKey toKey = new LccKey(lc, lcType); - carbonCreditRate.merge(fromKey, fromLcCumulFlux, (a, b) -> a - b); - carbonCreditRate.merge(toKey, toLcCumulFlux, (a, b) -> a + b); + if (lcType.equals(lc)) { // no net difference if land cover stays same + carbonCreditRate.put(fromKey, 0.0); + } else { + carbonCreditRate.merge(fromKey, -fromLcCumulFlux, Double::sum); + carbonCreditRate.merge(toKey, toLcCumulFlux, Double::sum); + } } } diff --git a/src/ac/ed/lurg/country/CountryAgent.java b/src/ac/ed/lurg/country/CountryAgent.java index ee700c56..ded725a0 100644 --- a/src/ac/ed/lurg/country/CountryAgent.java +++ b/src/ac/ed/lurg/country/CountryAgent.java @@ -261,8 +261,7 @@ public class CountryAgent extends AbstractCountryAgent { if (ModelConfig.IS_CALIBRATION_RUN && currentTimestep.getTimestep() <= ModelConfig.END_FIRST_STAGE_CALIBRATION) { changeUp = changeDown = 0.0; } else { - changeUp = maxOfProdOrSupply * globalWoodPrices.get(woodType).getMaxImportChange(); - changeDown = maxOfProdOrSupply * globalWoodPrices.get(woodType).getMaxExportChange(); + changeUp = changeDown = maxOfProdOrSupply * ModelConfig.MAX_IMPORT_CHANGE; } woodTradeConstraints.put(woodType, new TradeConstraint(baseTrade - changeDown, baseTrade + changeUp)); diff --git a/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java b/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java index 235ab9c0..fa726076 100644 --- a/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java +++ b/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java @@ -7,6 +7,7 @@ import ac.ed.lurg.ModelConfig; import ac.ed.lurg.carbon.CarbonFluxItem; import ac.ed.lurg.country.LandCoverChangeItem; import ac.ed.lurg.forestry.ForestManager; +import ac.ed.lurg.forestry.WoodHarvestItem; import ac.ed.lurg.forestry.WoodYieldData; import ac.ed.lurg.forestry.WoodYieldItem; import ac.ed.lurg.landuse.*; @@ -97,7 +98,7 @@ public class GamsRasterOptimiser { private RasterSet<LandUseItem> allocAreas(Map<Integer, ? extends LandUseItem> prevAreasAgg, GamsLocationOutput gamsOutput, int year) { RasterSet<LandUseItem> newLandUseRaster = createWithSameLandCovers(rasterInputData.getPreviousLandUses()); - // Order is important here + // *** Order is important here *** // Disaggregate cluster land cover changes to raster grid Map<RasterKey, List<LandCoverChangeItem>> lcChanges = disaggregateLandCoverChanges(gamsOutput.getLandCoverChanges(), @@ -110,11 +111,46 @@ public class GamsRasterOptimiser { // Allocate land cover changes allocLandCoverChanges(newLandUseRaster, lcChanges); - // Calculate land cover changes for timber forest rotation and clear-cut - Map<RasterKey, List<LandCoverChangeItem>> lcChangesRota = ForestManager.handleRotaWood( - newLandUseRaster, rasterInputData.getCountryInput().getWoodDemand(), + Map<LandCoverType, Double> foo = new HashMap<>(); + for (LandUseItem i : newLandUseRaster.values()) { + for (LandCoverType c : LandCoverType.getConvertibleTypes()) { + foo.merge(c, i.getLandCoverArea(c, LandProtectionType.CONVERTIBLE), Double::sum); + } + } + + // Set wood type forest fractions + Map<Integer, Map<WoodType, Double>> woodAreas = gamsOutput.getForestAreas(); + for (int locId : woodAreas.keySet()) { + Set<RasterKey> keys = new HashSet<RasterKey>(); + for (Entry<RasterKey, IntegerRasterItem> mapEntry : mapping.entrySet()) { + IntegerRasterItem iri = mapEntry.getValue(); + if (locId == iri.getInt()) + keys.add(mapEntry.getKey()); + } + Map<WoodType, Double> areaMap = woodAreas.get(locId); + double totalArea = areaMap.values().stream().reduce(0.0, Double::sum); + for (WoodType wType : WoodType.values()) { + double fraction = totalArea > 0 ? areaMap.get(wType) / totalArea : 0; + for (RasterKey key : keys) { + LandUseItem luItem = newLandUseRaster.get(key); + luItem.setForestFraction(wType, fraction); + } + } + } + + // Calculate land cover changes for timber forest rotation and clear-cut. Resets harvested stands to age 0 + Map<RasterKey, WoodHarvestItem> rotaHarvestMap = ForestManager.handleRotaWood(newLandUseRaster, rasterInputData.getWoodYields(), gamsOutput, mapping); + // Set forest rotations + for (RasterKey key : rotaHarvestMap.keySet()) { + WoodHarvestItem whItem = rotaHarvestMap.get(key); + LandUseItem luItem = newLandUseRaster.get(key); + luItem.setWoodHarvests(whItem); + } + + + // Add wood produced from LCC to output Map<WoodType, WoodUsageData> woodUsageMap = gamsOutput.getWoodUsageData(); for (WoodType wType : lccWoodAmount.keySet()) { diff --git a/src/ac/ed/lurg/demand/WoodDemandManager.java b/src/ac/ed/lurg/demand/WoodDemandManager.java index d9fcac3f..2ef60933 100644 --- a/src/ac/ed/lurg/demand/WoodDemandManager.java +++ b/src/ac/ed/lurg/demand/WoodDemandManager.java @@ -1,5 +1,7 @@ package ac.ed.lurg.demand; +import java.io.Serial; +import java.io.Serializable; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -11,7 +13,9 @@ import ac.ed.lurg.types.WoodType; import ac.ed.lurg.utils.LogWriter; import ac.ed.lurg.utils.StringTabularReader; -public class WoodDemandManager { +public class WoodDemandManager implements Serializable { + @Serial + private static final long serialVersionUID = 171124405614624397L; private Map<SingleCountry, Map<WoodType, Double>> baseWoodDemand = new HashMap<SingleCountry, Map<WoodType, Double>>(); public WoodDemandManager() { @@ -26,8 +30,7 @@ public class WoodDemandManager { for (Map<String, String> row : rowList) { String countryCode = row.get("Iso3"); String woodTypeStr = row.get("Type"); - double demand = Double.parseDouble(row.get("Demand")); - demand *= ModelConfig.WOOD_BIOMASS_CONVERSION_FACTOR; // converting from m3 to MtC-eq + double demand = Double.parseDouble(row.get("Demand")) * ModelConfig.WOOD_BIOMASS_CONVERSION_FACTOR; SingleCountry sc = CountryManager.getInstance().getForCode(countryCode); WoodType woodType = WoodType.getForName(woodTypeStr); Map<WoodType, Double> wMap = baseWoodDemand.computeIfAbsent(sc, k -> new HashMap<WoodType, Double>()); diff --git a/src/ac/ed/lurg/forestry/ForestManager.java b/src/ac/ed/lurg/forestry/ForestManager.java index 44a4bbc5..4fbb77c4 100644 --- a/src/ac/ed/lurg/forestry/ForestManager.java +++ b/src/ac/ed/lurg/forestry/ForestManager.java @@ -23,11 +23,11 @@ public class ForestManager { RasterSet<LandUseItem> inputLandUseRaster) { Map<WoodType, Double> woodAmount = new HashMap<>(); + for (WoodType w : WoodType.values()) { + woodAmount.put(w, 0.0); + } if (!ModelConfig.IS_FORESTRY_ON) { - for (WoodType w : WoodType.values()) { - woodAmount.put(w, 0.0); - } return woodAmount; } @@ -41,9 +41,10 @@ public class ForestManager { LandCoverType toLc = lccItem.getToLandCover(); double changedArea = lccItem.getArea(); double totalArea = tiles.getTotalArea(fromLc, LandProtectionType.CONVERTIBLE); - if (changedArea > totalArea) { + if (changedArea > totalArea && Math.abs(changedArea - totalArea) > 1e-10) { LogWriter.printlnError("Error: ForestManager changed area exceeds total area."); } + changedArea = Math.min(changedArea, totalArea); // deal with floating point errors double fraction = changedArea / totalArea; // Assume stands are converted proportionally by area. // Type of wood harvested depends on stand age. @@ -65,114 +66,113 @@ public class ForestManager { } // Calculates area of timber forest cleared to meet demand - public static Map<RasterKey, List<LandCoverChangeItem>> handleRotaWood(RasterSet<LandUseItem> outputLandUseRaster, - Map<WoodType, Double> woodDemand, + public static Map<RasterKey, WoodHarvestItem> handleRotaWood(RasterSet<LandUseItem> outputLandUseRaster, RasterSet<WoodYieldItem> woodYieldRaster, - GamsLocationOutput locationOutput, - RasterSet<IntegerRasterItem> mapping) { + GamsLocationOutput locationOutput, + RasterSet<IntegerRasterItem> mapping) { // Since some trees could be used for either roundwood or fuelwood, we harvest the more valuable roundwood first final List<WoodType> harvestOrder = new ArrayList<>(Arrays.asList(WoodType.IND_ROUNDWOOD, WoodType.FUELWOOD)); - Map<RasterKey, List<LandCoverChangeItem>> allChanges = new HashMap<>(); + Map<RasterKey, WoodHarvestItem> harvestMap = new HashMap<>(); if (!ModelConfig.IS_FORESTRY_ON) { - return allChanges; + return harvestMap; } for (WoodType wType : harvestOrder) { - Map<RasterKey, List<LandCoverChangeItem>> changes = handleRotaWoodForType(outputLandUseRaster, woodDemand, - woodYieldRaster, locationOutput, mapping, wType); - for (RasterKey key : changes.keySet()) { - List<LandCoverChangeItem> changesList = changes.get(key); - List<LandCoverChangeItem> changesListAll = allChanges.computeIfAbsent(key, k -> new ArrayList<>()); - changesListAll.addAll(changesList); - } + handleRotaWoodForType(outputLandUseRaster, woodYieldRaster, locationOutput, mapping, wType, harvestMap); } - return allChanges; + return harvestMap; } - private static Map<RasterKey, List<LandCoverChangeItem>> handleRotaWoodForType(RasterSet<LandUseItem> outputLandUseRaster, - Map<WoodType, Double> woodDemand, - RasterSet<WoodYieldItem> woodYieldRaster, - GamsLocationOutput locationOutput, - RasterSet<IntegerRasterItem> mapping, - WoodType wType) { - Map<Integer, Map<WoodType, Double>> rotationPeriods = locationOutput.getRotationPeriods(); - Map<RasterKey, List<LandCoverChangeItem>> lcChanges = new HashMap<>(); - - double demand = woodDemand.get(wType); - if (demand == 0) - return lcChanges; - - // First calculate how much wood is available - Map<RasterKey, Double> stockMap = new HashMap<>(); - for (RasterKey key : outputLandUseRaster.keySet()) { - LandUseItem luItem = outputLandUseRaster.get(key); - LandCoverTile tiles = luItem.getLandCoverTiles(); - WoodYieldItem wyItem = woodYieldRaster.get(key); - Integer locId = mapping.get(key).getInt(); - double stock = 0; - double rotationAge = rotationPeriods.get(locId).get(wType); - for (int age : tiles.getAgeKeys()) { - if (age >= rotationAge) { - double area = tiles.getArea(LandCoverType.TIMBER_FOREST, LandProtectionType.CONVERTIBLE, age); - double yield = wyItem.getYieldRota(LandCoverType.TIMBER_FOREST, age); - stock += yield * area; - } - } - stockMap.put(key, stock); - } + private static void handleRotaWoodForType(RasterSet<LandUseItem> outputLandUseRaster, + RasterSet<WoodYieldItem> woodYieldRaster, + GamsLocationOutput locationOutput, + RasterSet<IntegerRasterItem> mapping, + WoodType wType, + Map<RasterKey, WoodHarvestItem> harvestMap) { + + double demand = locationOutput.getWoodUsageData().get(wType).getHarvest() + - locationOutput.getWoodUsageData().get(wType).getLucHarvest(); + if (demand <= 0) { + return; + } - double totalStock = stockMap.values().stream().reduce(0.0, Double::sum); - if (demand > totalStock) { - LogWriter.printlnWarning("Insufficient " + wType.getName() + " to meet demand. Shortfall: " + (demand - totalStock)); - } - double harvestFraction = Math.min(demand / totalStock, 1); - // Now harvest wood to meet demand. - // Side effect! Resets land cover age to 0 for harvested stands - for (RasterKey key : outputLandUseRaster.keySet()) { - LandUseItem luItem = outputLandUseRaster.get(key); - LandCoverTile tiles = luItem.getLandCoverTiles(); - WoodYieldItem wyItem = woodYieldRaster.get(key); - Integer locId = mapping.get(key).getInt(); - double rotationAge = rotationPeriods.get(locId).get(wType); - double totalAmountToRemove = stockMap.get(key) * harvestFraction; - List<Integer> ageList = new ArrayList<>(tiles.getAgeKeys()); - List<Integer> rotationAgeList = new ArrayList<>(); - for (int i : ageList) { - if (i >= rotationAge) { - rotationAgeList.add(i); - } - } - // Harvest in descending age order - rotationAgeList.sort(Collections.reverseOrder()); - for (int age : rotationAgeList) { - if (totalAmountToRemove == 0) - break; + // First calculate how much wood is available + Map<RasterKey, Double> stockMap = new HashMap<>(); + for (RasterKey key : outputLandUseRaster.keySet()) { + LandUseItem luItem = outputLandUseRaster.get(key); + LandCoverTile tiles = luItem.getLandCoverTiles(); + WoodYieldItem wyItem = woodYieldRaster.get(key); + Integer locId = mapping.get(key).getInt(); + double stock = 0; + double rotationAge = wType.getMinRotation(); + for (int age : tiles.getAgeKeys()) { + if (age >= rotationAge) { double area = tiles.getArea(LandCoverType.TIMBER_FOREST, LandProtectionType.CONVERTIBLE, age); - double yield = wyItem.getYieldRota(LandCoverType.TIMBER_FOREST, age); - if (area == 0 || yield == 0) + if (area == 0) continue; - double standStock = area * yield; - double amountToRemove = Math.min(totalAmountToRemove, standStock); - double convertedArea = Math.min(area, amountToRemove / yield); - LandCoverChangeItem lccItem = new LandCoverChangeItem(LandCoverType.TIMBER_FOREST, - LandCoverType.TIMBER_FOREST, age, convertedArea); - List<LandCoverChangeItem> mapForKey = lcChanges.computeIfAbsent(key, k -> new ArrayList<>()); - mapForKey.add(lccItem); - if (!ModelConfig.IS_CALIBRATION_RUN) - luItem.doForestRotation(age, convertedArea); // Side effect! - totalAmountToRemove -= amountToRemove; + double yield = wyItem.getYieldRota(LandCoverType.TIMBER_FOREST, age); + stock += yield * area; } + } + stockMap.put(key, stock); + } - if (totalAmountToRemove > 0) - LogWriter.printlnWarning("ForestManager: Could not meet wood demand."); + double totalStock = stockMap.values().stream().reduce(0.0, Double::sum); + LogWriter.println("Total stock for " + wType + " = " + totalStock); + if (demand > totalStock) { + LogWriter.printlnWarning("Insufficient " + wType.getName() + " to meet demand. Shortfall: " + (demand - totalStock)); + } + if (totalStock < 1e-10) { + return; + } + double harvestFraction = Math.min(demand / totalStock, 1); + // Now harvest wood to meet demand. + for (RasterKey key : outputLandUseRaster.keySet()) { + LandUseItem luItem = outputLandUseRaster.get(key); + LandCoverTile tiles = luItem.getLandCoverTiles(); + WoodYieldItem wyItem = woodYieldRaster.get(key); + Integer locId = mapping.get(key).getInt(); + double rotationAge = wType.getMinRotation(); + double totalAmountToRemove = stockMap.get(key) * harvestFraction; + if (totalAmountToRemove < 1e-10) { + continue; + } + // Assuming that oldest trees get harvested first + List<Integer> ageList = new ArrayList<>(tiles.getAgeKeys()); + List<Integer> rotationAgeList = new ArrayList<>(); + for (int i : ageList) { + if (i >= rotationAge) { + rotationAgeList.add(i); + } + } + // Harvest in descending age order + rotationAgeList.sort(Collections.reverseOrder()); + + for (int age : rotationAgeList) { + double area = tiles.getArea(LandCoverType.TIMBER_FOREST, LandProtectionType.CONVERTIBLE, age); + double yield = wyItem.getYieldRota(LandCoverType.TIMBER_FOREST, age); + if (area == 0 || yield == 0) + continue; + double standStock = area * yield; + double amountToRemove = Math.min(totalAmountToRemove, standStock); + double convertedArea = Math.min(area, amountToRemove / yield); + LandCoverChangeItem lccItem = new LandCoverChangeItem(LandCoverType.TIMBER_FOREST, + LandCoverType.TIMBER_FOREST, age, convertedArea); + WoodHarvestItem whItem = harvestMap.computeIfAbsent(key, k -> new WoodHarvestItem()); + whItem.addAreasHarvested(lccItem); + whItem.addWoodAmount(wType, amountToRemove); + luItem.doForestRotation(age, convertedArea); // WARNING! Side effect + totalAmountToRemove -= amountToRemove; } - return lcChanges; + if (totalAmountToRemove > 0) + LogWriter.printlnWarning("ForestManager: Could not meet wood demand."); + } } } diff --git a/src/ac/ed/lurg/forestry/WoodHarvestItem.java b/src/ac/ed/lurg/forestry/WoodHarvestItem.java new file mode 100644 index 00000000..228c118a --- /dev/null +++ b/src/ac/ed/lurg/forestry/WoodHarvestItem.java @@ -0,0 +1,34 @@ +package ac.ed.lurg.forestry; + +import ac.ed.lurg.country.LandCoverChangeItem; +import ac.ed.lurg.types.WoodType; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class WoodHarvestItem implements Serializable { + @Serial + private static final long serialVersionUID = -79494635617251642L; + private List<LandCoverChangeItem> areasHarvested = new ArrayList<>(); + private Map<WoodType, Double> woodAmount = new HashMap<>(); + + public List<LandCoverChangeItem> getAreasHarvested() { + return areasHarvested; + } + + public void addAreasHarvested(LandCoverChangeItem lccItem) { + areasHarvested.add(lccItem); + } + + public Map<WoodType, Double> getWoodAmount() { + return woodAmount; + } + + public void addWoodAmount(WoodType woodType, double amount) { + woodAmount.merge(woodType, amount, Double::sum); + } +} diff --git a/src/ac/ed/lurg/landuse/LandCoverTile.java b/src/ac/ed/lurg/landuse/LandCoverTile.java index 246adc27..3fb40566 100644 --- a/src/ac/ed/lurg/landuse/LandCoverTile.java +++ b/src/ac/ed/lurg/landuse/LandCoverTile.java @@ -39,9 +39,6 @@ public class LandCoverTile implements Serializable, InterpolatingRasterItem<Land public void addArea(LandCoverType lcType, LandProtectionType lpType, int age, double a) { LandKey key = new LandKey(lcType, lpType, age); - if (Double.isNaN(a)) { - int foo = 1; - } addArea(key, a); } diff --git a/src/ac/ed/lurg/landuse/LandUseItem.java b/src/ac/ed/lurg/landuse/LandUseItem.java index cdb21b97..d343e173 100644 --- a/src/ac/ed/lurg/landuse/LandUseItem.java +++ b/src/ac/ed/lurg/landuse/LandUseItem.java @@ -5,6 +5,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; import ac.ed.lurg.ModelConfig; +import ac.ed.lurg.forestry.WoodHarvestItem; import ac.ed.lurg.types.*; import ac.ed.lurg.utils.Interpolator; import ac.sac.raster.InterpolatingRasterItem; @@ -16,7 +17,8 @@ public class LandUseItem implements InterpolatingRasterItem<LandUseItem>, Serial private Map<CropType, Double> cropFractions = new HashMap<CropType, Double>(); private LandCoverTile landCoverAreas = new LandCoverTile(); private Map<WoodType, Double> forestFractions = new HashMap<>(); - private Map<LccKey, Double> landCoverChange = new HashMap<LccKey, Double>(); // previous timestep LCC + private Map<LccKey, Double> landCoverChange = new HashMap<LccKey, Double>(); // land cover change for previous timestep + private WoodHarvestItem woodHarvests; // forest rotations private double protectedFraction; // protected fraction of total area public LandUseItem() {} @@ -112,21 +114,40 @@ public class LandUseItem implements InterpolatingRasterItem<LandUseItem>, Serial if (area == 0) return 0; - double prevFrom = getLandCoverArea(fromType, LandProtectionType.CONVERTIBLE); + double prevFromArea = getLandCoverArea(fromType, LandProtectionType.CONVERTIBLE); double areaMoved; - areaMoved = Math.min(area, prevFrom); + areaMoved = Math.min(area, prevFromArea); if (Math.abs(area - areaMoved) / area > 1e-6) { throw new RuntimeException("Move unprotected area shortfall. Should not happen due to GAMS constraint."); } - - // Note if toType == fromType, area will be removed from fromType and added back in with age 0 - landCoverAreas.removeArea(fromType, LandProtectionType.CONVERTIBLE, areaMoved); if (ModelConfig.IS_CALIBRATION_RUN) { - landCoverAreas.addArea(toType, LandProtectionType.CONVERTIBLE, areaMoved); // preserve age distribution + // Preserve age distribution of source land cover. + // This is mainly so that forest <> non-forest transitions don't lose trees. + if (fromType.equals(LandCoverType.NATURAL) && toType.equals(LandCoverType.TIMBER_FOREST)) { + double moveFraction = areaMoved / prevFromArea; + for (int age : landCoverAreas.getAgeKeys()) { + double fromArea = landCoverAreas.getArea(fromType, LandProtectionType.CONVERTIBLE, age); + if (fromArea == 0) + continue; + double partialArea = fromArea * moveFraction; + landCoverAreas.removeArea(fromType, LandProtectionType.CONVERTIBLE, age, partialArea); + landCoverAreas.addArea(toType, LandProtectionType.CONVERTIBLE, age, partialArea); + } + } else { + + double areaMovedCalib = areaMoved; + if (fromType.isNatural()) { + areaMovedCalib *= 0.95; // keep some area during calibration to preserve age distribution + } + landCoverAreas.removeArea(fromType, LandProtectionType.CONVERTIBLE, areaMovedCalib); + landCoverAreas.addArea(toType, LandProtectionType.CONVERTIBLE, areaMovedCalib); + } + } else { + landCoverAreas.removeArea(fromType, LandProtectionType.CONVERTIBLE, areaMoved); landCoverAreas.addArea(toType, LandProtectionType.CONVERTIBLE, 0, areaMoved); // new area starts at age=0 } @@ -137,9 +158,27 @@ public class LandUseItem implements InterpolatingRasterItem<LandUseItem>, Serial } public void doForestRotation(int age, double area) { + if (ModelConfig.IS_CALIBRATION_RUN) + return; landCoverAreas.removeArea(LandCoverType.TIMBER_FOREST, LandProtectionType.CONVERTIBLE, age, area); landCoverAreas.addArea(LandCoverType.TIMBER_FOREST, LandProtectionType.CONVERTIBLE, 0, area); } + + public void setForestFraction(WoodType woodType, double fraction) { + forestFractions.put(woodType, fraction); + } + + public double getForestFraction(WoodType woodType) { + return forestFractions.get(woodType); + } + + public void setWoodHarvests(WoodHarvestItem woodHarvests) { + this.woodHarvests = woodHarvests; + } + + public WoodHarvestItem getWoodHarvests() { + return woodHarvests; + } public void resetLccAreas() { landCoverChange.clear(); diff --git a/src/ac/ed/lurg/landuse/WoodUsageReader.java b/src/ac/ed/lurg/landuse/WoodUsageReader.java index 5c95651d..a9d0862d 100644 --- a/src/ac/ed/lurg/landuse/WoodUsageReader.java +++ b/src/ac/ed/lurg/landuse/WoodUsageReader.java @@ -50,8 +50,8 @@ public class WoodUsageReader { for (Map<String, String> row : rowList) { String countryCode = row.get("Iso3"); String woodTypeName = row.get("Type"); - double production = Double.parseDouble(row.get("Production")); - double netImport = Double.parseDouble(row.get("NetImports")); + double production = Double.parseDouble(row.get("Production")) * ModelConfig.WOOD_BIOMASS_CONVERSION_FACTOR; + double netImport = Double.parseDouble(row.get("NetImports")) * ModelConfig.WOOD_BIOMASS_CONVERSION_FACTOR; SingleCountry country = CountryManager.getInstance().getForCode(countryCode); WoodType woodType = WoodType.getForName(woodTypeName); CompositeCountry cc = CountryManager.getInstance().getForSingleCountry(country); diff --git a/src/ac/ed/lurg/types/CropType.java b/src/ac/ed/lurg/types/CropType.java index 4441e9f7..06457535 100644 --- a/src/ac/ed/lurg/types/CropType.java +++ b/src/ac/ed/lurg/types/CropType.java @@ -11,18 +11,18 @@ import ac.ed.lurg.utils.LogWriter; public enum CropType { - WHEAT("WheatBarleyOats", "wheat", true, false, 9.5, ModelConfig.INITAL_PRICE_WHEAT, 0.35), - MAIZE("MaizeMilletSorghum", "maize", true, false, 5.1, ModelConfig.INITAL_PRICE_MAIZE, 0.35), - RICE("Rice (Paddy Equivalent)", "rice", true, false, 8.3, ModelConfig.INITAL_PRICE_RICE, 0.35), - OILCROPS("Oilcrops", "oilcrops", true, false, 4.4, ModelConfig.INITAL_PRICE_OILCROPS, 0.25), - PULSES("Pulses", "pulses", true, false, 10.8, ModelConfig.INITAL_PRICE_PULSES, 0.35), - STARCHY_ROOTS("Starchy Roots", "starchyRoots", true, false, 14.3, ModelConfig.INITAL_PRICE_STARCHYROOTS, 0.15), + WHEAT("WheatBarleyOats", "wheat", true, false, 9.5, ModelConfig.INITAL_PRICE_WHEAT, 0.4), + MAIZE("MaizeMilletSorghum", "maize", true, false, 5.1, ModelConfig.INITAL_PRICE_MAIZE, 0.4), + RICE("Rice (Paddy Equivalent)", "rice", true, false, 8.3, ModelConfig.INITAL_PRICE_RICE, 0.4), + OILCROPS("Oilcrops", "oilcrops", true, false, 4.4, ModelConfig.INITAL_PRICE_OILCROPS, 0.3), + PULSES("Pulses", "pulses", true, false, 10.8, ModelConfig.INITAL_PRICE_PULSES, 0.5), + STARCHY_ROOTS("Starchy Roots", "starchyRoots", true, false, 14.3, ModelConfig.INITAL_PRICE_STARCHYROOTS, 0.2), ENERGY_CROPS("Energy crops", "energycrops", true, false, 5, ModelConfig.INITAL_PRICE_ENERGYCROPS, 0.2), SETASIDE("setaside", "setaside", false, false, 0, 0, 0), - MONOGASTRICS("Monogastrics", "monogastrics", true, true, 3.1, ModelConfig.INITAL_PRICE_MONOGASTRICS, 0.1), - RUMINANTS("Ruminants", "ruminants", true, true, 2.2, ModelConfig.INITAL_PRICE_RUMINANTS, 0.1), - FRUITVEG("FruitVeg", "fruitveg", true, false, 8.9, ModelConfig.INITAL_PRICE_FRUITVEG, 0.1), - SUGAR("Sugar", "sugar", true, false, 0.1, ModelConfig.INITAL_PRICE_SUGAR, 0.4), + MONOGASTRICS("Monogastrics", "monogastrics", true, true, 3.1, ModelConfig.INITAL_PRICE_MONOGASTRICS, 0.2), + RUMINANTS("Ruminants", "ruminants", true, true, 2.2, ModelConfig.INITAL_PRICE_RUMINANTS, 0.2), + FRUITVEG("FruitVeg", "fruitveg", true, false, 8.9, ModelConfig.INITAL_PRICE_FRUITVEG, 0.2), + SUGAR("Sugar", "sugar", true, false, 0.1, ModelConfig.INITAL_PRICE_SUGAR, 0.5), PASTURE("pasture", "pasture", false, false, 0, 0, 0); // confusing having a land cover and a crop type. Needed here for yields (but not used for cropland area fractions). private String faoName; diff --git a/src/ac/ed/lurg/types/WoodType.java b/src/ac/ed/lurg/types/WoodType.java index 4e4e7c68..73c6f370 100644 --- a/src/ac/ed/lurg/types/WoodType.java +++ b/src/ac/ed/lurg/types/WoodType.java @@ -5,7 +5,7 @@ import java.util.Map; import ac.ed.lurg.ModelConfig; public enum WoodType { - IND_ROUNDWOOD("roundwood", ModelConfig.IND_ROUNDWOOD_DEMAND_ELASTICITY, 40, ModelConfig.INIT_ROUNDWOOD_PRICE), + IND_ROUNDWOOD("roundwood", ModelConfig.IND_ROUNDWOOD_DEMAND_ELASTICITY, 30, ModelConfig.INIT_ROUNDWOOD_PRICE), FUELWOOD("fuelwood", ModelConfig.FUELWOOD_DEMAND_ELASTICITY, 10, ModelConfig.INIT_FUELWOOD_PRICE); private String name; -- GitLab