From eb96c77d6b0a10295a7a03c58c183d61942cd667 Mon Sep 17 00:00:00 2001 From: Bart Arendarczyk <s1924442@ed.ac.uk> Date: Wed, 13 Mar 2024 11:31:41 +0000 Subject: [PATCH] Photovoltaics and agrivoltaics. --- GAMS/IntExtOpt.gms | 68 +++++++++---- data/agrivoltaic_yield_params.csv | 12 +++ src/ac/ed/lurg/ModelConfig.java | 34 +++++++ src/ac/ed/lurg/ModelMain.java | 27 +++++- src/ac/ed/lurg/country/CountryAgent.java | 11 ++- .../ed/lurg/country/CountryAgentManager.java | 7 +- .../lurg/country/gams/GamsLocationInput.java | 10 +- .../country/gams/GamsLocationOptimiser.java | 71 ++++++++++++-- .../ed/lurg/country/gams/GamsRasterInput.java | 11 ++- .../country/gams/GamsRasterOptimiser.java | 29 +++++- .../ed/lurg/landuse/ConversionCostReader.java | 48 +++++---- src/ac/ed/lurg/landuse/LandUseItem.java | 9 ++ src/ac/ed/lurg/output/LandUseOutputer.java | 16 ++- .../lurg/solar/AgrivoltaicsYieldManager.java | 42 ++++++++ src/ac/ed/lurg/solar/SolarPotentialItem.java | 97 +++++++++++++++++++ .../ed/lurg/solar/SolarPotentialReader.java | 37 +++++++ src/ac/ed/lurg/types/FarmingType.java | 2 +- src/ac/ed/lurg/types/LandCoverType.java | 2 + 18 files changed, 468 insertions(+), 65 deletions(-) create mode 100644 data/agrivoltaic_yield_params.csv create mode 100644 src/ac/ed/lurg/solar/AgrivoltaicsYieldManager.java create mode 100644 src/ac/ed/lurg/solar/SolarPotentialItem.java create mode 100644 src/ac/ed/lurg/solar/SolarPotentialReader.java diff --git a/GAMS/IntExtOpt.gms b/GAMS/IntExtOpt.gms index 35cde672..f1e8bab1 100644 --- a/GAMS/IntExtOpt.gms +++ b/GAMS/IntExtOpt.gms @@ -1,5 +1,5 @@ - SET all_types / monogastrics, ruminants, cerealsStarchyRoots, wheat, maize, rice, oilcrops, oilcropsNFix, oilcropsOther, pulses, starchyRoots, fruitveg, sugar, energycrops, pasture, setaside, roundwood, fuelwood, carbonCredits/; + SET all_types / monogastrics, ruminants, cerealsStarchyRoots, wheat, maize, rice, oilcrops, oilcropsNFix, oilcropsOther, pulses, starchyRoots, fruitveg, sugar, energycrops, pasture, setaside, roundwood, fuelwood, carbonCredits, energy /; SET crop(all_types) / wheat, maize, rice, oilcropsNFix, oilcropsOther, pulses, starchyRoots, fruitveg, sugar, energycrops, pasture, setaside /; SET crop_less_pasture(crop) / wheat, maize, rice, oilcropsNFix, oilcropsOther, pulses, starchyRoots, fruitveg, sugar, energycrops , setaside /; @@ -9,19 +9,20 @@ SET import_types(all_types) / monogastrics, ruminants, wheat, maize, rice, oilcropsNFix, oilcropsOther, pulses, starchyRoots, fruitveg, sugar, energycrops, roundwood, fuelwood, carbonCredits/; SET animal(all_types) / monogastrics, ruminants /; - SET commodity(all_types) / ruminants, monogastrics, cerealsStarchyRoots, oilcrops, pulses, fruitveg, sugar, energycrops, roundwood, fuelwood, carbonCredits /; + SET commodity(all_types) / ruminants, monogastrics, cerealsStarchyRoots, oilcrops, pulses, fruitveg, sugar, roundwood, fuelwood, carbonCredits, energycrops, energy /; SET animal_commodity(commodity) / ruminants, monogastrics /; SET crop_commodity(commodity) / cerealsStarchyRoots, oilcrops, pulses, fruitveg, sugar, energycrops /; SET demand_map(commodity, all_types); - SET land_cover / cropland, pasture, timberForest, carbonForest, natural /; + SET land_cover / cropland, pasture, timberForest, carbonForest, natural, agrivoltaics, photovoltaics /; SET protection / unprotected, protected /; SET managed_forest(land_cover) / timberForest, carbonForest /; + SET solar_land(land_cover) / agrivoltaics, photovoltaics /; ALIAS (land_cover, land_cover_before); ALIAS (land_cover, land_cover_after); - SET farming_type / conventional, restricted /; + SET farming_type / conventional, restricted, agrivoltaics /; SET farming_map(protection, farming_type) / unprotected.conventional, protected.restricted /; SET wood_type(commodity) / roundwood, fuelwood /; @@ -66,6 +67,10 @@ PARAMETER forestBaseCost(managed_forest); PARAMETER previousRotationIntensity(location); PARAMETER maxLandCoverChange(land_cover_before, land_cover_after, protection, location); + + PARAMETER solarEnergyDensity(solar_land, location) MWh per ha; + PARAMETER solarCost(solar_land, location) 1000$ per ha; + PARAMETER agrivoltaicsYieldFactor(crop, farming_type, location); SCALAR meatEfficency efficiency of converting feed and pasture into animal products; SCALAR fertiliserUnitCost fert cost at max fert rate; @@ -85,10 +90,12 @@ SCALAR otherLimConventional; SCALAR forestManagementCost cost $1000 per ha; - SCALAR carbonForestMaxProportion maximum proportion of land cover as carbon forest; + + SCALAR energycropsEnergyDensity; + SCALAR bioenergyGenerationCost; -*$gdxin "./_gams_java_gdb1.gdx" +*$gdxin "/Users/bart/Documents/PLUM_Output/calib_forest/GamsTmp/loc4965753693958365911/_gams_java_gdb1.gdx" $gdxin %gdxincname% $load location, suitableLandArea, demand, demand_map $load previousCropArea, previousFertIntensity, previousIrrigIntensity, previousOtherIntensity, previousRuminantFeed, previousMonogastricFeed, previousImportAmount, previousExportAmount @@ -98,6 +105,7 @@ $load meatEfficency, otherICost, irrigCost, irrigMaxRate, irrigConstraint, ferti $load previousLandCoverArea, maxCroplandArea, carbonCreditRate, conversionCost, woodYieldMax, woodYieldParam, forestBaseCost, tradeAdjustmentCostRate $load forestManagementCost, carbonForestMaxProportion, maxFertChange, maxIrrigChange, previousRotationIntensity, maxLandCoverChange $load fertLimRestricted, irrigLimRestricted, otherLimRestricted, fertLimConventional, irrigLimConventional, otherLimConventional +$load solarEnergyDensity, agrivoltaicsYieldFactor, solarCost, energycropsEnergyDensity, bioenergyGenerationCost $gdxin SCALAR delta "use to smooth power function see 7.5 www.gams.com dd docs solversconopt.pdf" / 0.00000000001 /; @@ -160,6 +168,8 @@ $gdxin fuelwood 0.03 /; +demand('energy') = demand('energycrops') * energycropsEnergyDensity; +demand('energycrops') = 0; VARIABLES unitCost(crop, farming_type, location) cost per area for each crop - cost @@ -188,11 +198,16 @@ $gdxin forestryCost(managed_forest, location) total cost in 1000$ carbonCredits(location) + bioenergyFeedstock(crop) + + energyProdCost + + * A "artificial variable for debugging https://www.gams.com/blog/2017/07/misbehaving-model-infeasible/" total_cost total cost of domestic supply including net imports; - POSITIVE VARIABLE cropArea, fertI, irrigI, otherIntensity, netSupply, ruminantFeed, monogastricFeed, importAmount, exportAmount, totalFeedDM, - landCoverArea, landCoverChange, totalConversionCost, animalProd, woodYieldRota, rotationIntensity, woodSupply; + POSITIVE VARIABLE cropArea, fertI, irrigI, otherIntensity, netSupply, ruminantFeed, monogastricFeed, importAmount, exportAmount, totalFeedDM, bioenergyFeedstock, + landCoverArea, landCoverChange, totalConversionCost, animalProd, woodYieldRota, rotationIntensity, woodSupply, energyProd, energyProdCost; * POSITIVE VARIABLE A; @@ -201,8 +216,6 @@ $gdxin UNIT_COST_EQ(crop, farming_type, location) cost per area - $1000 per ha or $billion per Mha YIELD_EQ(crop, farming_type, location) yield given chosen intensity - tonnes per hectare IRRIGATION_CONSTRAINT(location) constraint on water usage -* FERT_RATE_INCREASE_CONSTRAINT -* IRRIG_RATE_INCREASE_CONSTRAINT NET_SUPPLY_EQ(crop) calc net supply for crops CROP_DEMAND_CONSTRAINT(crop_commodity, crop) satisfy demand for crop products @@ -220,6 +233,7 @@ $gdxin SETASIDE_AREA_CALC(farming_type, location) CROPLAND_LAND_COVER_CALC(protection, farming_type, location) cropland area equals sum of all crop areas PASTURE_LAND_COVER_CALC(protection, farming_type, location) pasture area (land cover) equals pasture area (land use) + AGRIVOLTAICS_LAND_COVER_CALC(location) LAND_COVER_CHANGE_CALC(land_cover, protection, location) "calc land cover change. landCoverChange(A, A, location) defined as unchanged land cover" LAND_COVER_CHANGE_CONSTRAINT(land_cover, protection, location) conservation of land area CONVERSION_COST_CALC(location) cost of land cover conversion @@ -230,6 +244,9 @@ $gdxin WOOD_DEMAND_CONSTRAINT(wood_type) ROUNDWOOD_DEMAND_CONSTRAINT satisfy roundwood demand (can only by obtained from older trees) TIMBER_FOREST_COST_CALC(location) + + ENERGY_COST_CALC + ENERGY_DEMAND_CONSTRAINT CARBON_CREDIT_CALC(location) CARBON_CREDIT_CONSTRAINT @@ -254,17 +271,15 @@ $gdxin (yieldIrrigOnly(crop, location) - yieldNone(crop, location)) * (1 - exp(-irrigParam(crop, location) * irrigI(crop, farming_type, location))) + (yieldBoth(crop, location) + yieldNone(crop, location) - yieldFertOnly(crop, location) - yieldIrrigOnly(crop, location)) * (1 - exp(-fertParam(crop, location) * fertI(crop, farming_type, location))) * (1 - exp(-irrigParam(crop, location) * irrigI(crop, farming_type, location))) - ) * (1.01 - exp(-otherIntensity(crop, farming_type, location) * otherIParam)); + ) * (1.01 - exp(-otherIntensity(crop, farming_type, location) * otherIParam)) * agrivoltaicsYieldFactor(crop, farming_type, location); IRRIGATION_CONSTRAINT(location) .. irrigConstraint(location) * suitableLandArea(location) =G= sum((crop, farming_type), irrigMaxRate(crop, location) * irrigI(crop, farming_type, location) * cropArea(crop, farming_type, location)); - -* FERT_RATE_INCREASE_CONSTRAINT .. sum((crop, location), fertI(crop, location) * cropArea(crop, location)) =L= -* sum((crop, location), previousFertIntensity(crop, location) * previousCropArea(crop, location)) * (1 + maxFertChange); - -* IRRIG_RATE_INCREASE_CONSTRAINT .. sum((crop, location), irrigI(crop, location) * irrigMaxRate(crop, location) * cropArea(crop, location)) =L= -* sum((crop, location), previousIrrigIntensity(crop, location) * irrigMaxRate(crop, location) * previousCropArea(crop, location)) * (1 + maxIrrigChange); * Intensities must be <= 1 + fertI.UP(crop, farming_type, location) = 1; + irrigI.UP(crop, farming_type, location) = 1; + otherIntensity.UP(crop, farming_type, location) = 1; + fertI.UP(crop, 'conventional', location) = fertLimConventional; irrigI.UP(crop, 'conventional', location) = irrigLimConventional; otherIntensity.UP(crop, 'conventional', location) = otherLimConventional; @@ -273,13 +288,17 @@ $gdxin irrigI.UP(crop, 'restricted', location) = irrigLimRestricted; otherIntensity.UP(crop, 'restricted', location) = otherLimRestricted; + otherIntensity.LO(crop, 'agrivoltaics', location) = 0.2; + * Fixing irrigI at 0 where no water to avoid optimisation problems irrigI.FX(crop, farming_type, location) $ (irrigConstraint(location) = 0) = 0; *************** Crop supply ************************* NET_SUPPLY_EQ(crop) .. netSupply(crop) =E= sum((location, farming_type), cropArea(crop, farming_type, location) * yield(crop, farming_type, location)) * - (1 - seedAndWasteRate(crop)) - ruminantFeed(crop) - monogastricFeed(crop) + importAmount(crop) - exportAmount(crop); + (1 - seedAndWasteRate(crop)) - ruminantFeed(crop) - monogastricFeed(crop) + importAmount(crop) - exportAmount(crop) - bioenergyFeedstock(crop); + + bioenergyFeedstock.FX(crop)$(not sameAs(crop, 'energycrops')) = 0; CROP_DEMAND_CONSTRAINT(crop_commodity, crop)$demand_map(crop_commodity, crop) .. netSupply(crop) =G= demand(crop_commodity) * minDemandFraction(crop_commodity, crop); @@ -301,6 +320,8 @@ $gdxin PASTURE_LAND_COVER_CALC(protection, farming_type, location)$farming_map(protection, farming_type) .. landCoverArea('pasture', protection, location) =E= cropArea('pasture', farming_type, location); + AGRIVOLTAICS_LAND_COVER_CALC(location) .. landCoverArea('agrivoltaics', 'unprotected', location) =E= sum(crop, cropArea(crop, 'agrivoltaics', location)); + LAND_COVER_CHANGE_CALC(land_cover, protection, location) .. landCoverArea(land_cover, protection, location) =E= previousLandCoverArea(land_cover, protection, location) + sum(land_cover_before, landCoverChange(land_cover_before, land_cover, protection, location)) - sum(land_cover_after, landCoverChange(land_cover, land_cover_after, protection, location)); @@ -312,7 +333,7 @@ $gdxin landCoverChange(land_cover_before, land_cover_after, protection, location) * conversionCost(land_cover_before, land_cover_after, location)); * Cropland area restriction due to slope - MAX_CROPLAND_CONSTRAINT(location) .. sum(protection, landCoverArea('cropland', protection, location)) =L= maxCroplandArea(location); + MAX_CROPLAND_CONSTRAINT(location) .. sum((crop_less_pasture, farming_type), cropArea(crop_less_pasture, farming_type, location)) =L= maxCroplandArea(location); landCoverChange.UP(land_cover_before, land_cover_after, protection, location) = maxLandCoverChange(land_cover_before, land_cover_after, protection, location); @@ -345,6 +366,13 @@ $gdxin CARBON_FOREST_COST_CALC(location) .. forestryCost('carbonForest', location) =E= sum(land_cover$[not sameAs(land_cover, 'carbonForest')], landCoverChange(land_cover, 'carbonForest', 'unprotected', location) * forestManagementCost) + landCoverArea('carbonForest', 'unprotected', location) * forestBaseCost('carbonForest'); + +*********** Energy ********************* + + ENERGY_COST_CALC .. energyProdCost =E= sum((solar_land, location), solarCost(solar_land, location) * landCoverArea(solar_land, 'unprotected', location)) + + bioenergyGenerationCost * energycropsEnergyDensity * bioenergyFeedstock('energycrops'); + + ENERGY_DEMAND_CONSTRAINT .. sum((solar_land, location), solarEnergyDensity(solar_land, location) * landCoverArea(solar_land, 'unprotected', location)) + energycropsEnergyDensity * bioenergyFeedstock('energycrops') =G= demand('energy'); *************** Trade ******************************* @@ -366,6 +394,7 @@ $gdxin sum(location, totalConversionCost(location)) + sum((managed_forest, location), forestryCost(managed_forest, location)) + sum(wood_type, woodSupply(wood_type) * loggingCost(wood_type)) + + energyProdCost + sum(import_types, importPrices(import_types) * importAmount(import_types)) - sum(import_types, exportPrices(import_types) * exportAmount(import_types)) + sum(import_types, tradeAdjustmentCost(import_types)); @@ -389,7 +418,6 @@ $gdxin woodYieldRota.L(location) = woodYieldMax(location) * (1 - exp(-woodYieldParam(location) / rotationIntensity.L(location))) * rotationIntensity.L(location); woodSupply.L(wood_type) = demand(wood_type); - * LAND_USE.OptFile = 1; * display landCoverChange.L diff --git a/data/agrivoltaic_yield_params.csv b/data/agrivoltaic_yield_params.csv new file mode 100644 index 00000000..ad890fe0 --- /dev/null +++ b/data/agrivoltaic_yield_params.csv @@ -0,0 +1,12 @@ +Crop,ParamA,ParamB +fruitveg,0.6773239338183652,-1.7756495663799552 +pasture,0.6069465888798169,-1.7756495663799552 +maize,-1.407993616487431,-1.7756495663799552 +oilcropsNFix,-0.9831919372331905,-1.7756495663799552 +wheat,-0.23457877882018402,-1.7756495663799552 +starchyRoots,-0.44668611705014233,-1.7756495663799552 +sugar,-0.5936845126152303,-1.7756495663799552 +pulses,-1.663306897928871,-1.7756495663799552 +rice,-0.23457877882018402,-1.7756495663799552 +energycrops,-1.407993616487431,-1.7756495663799552 +oilcropsOther,-0.9831919372331905,-1.7756495663799552 diff --git a/src/ac/ed/lurg/ModelConfig.java b/src/ac/ed/lurg/ModelConfig.java index b7f3e01f..d140a83c 100755 --- a/src/ac/ed/lurg/ModelConfig.java +++ b/src/ac/ed/lurg/ModelConfig.java @@ -439,6 +439,7 @@ public class ModelConfig { public static final double AGRI_LAND_EXPANSION_COST_FACTOR = getDoubleProperty("AGRI_LAND_EXPANSION_COST_FACTOR", 1.0); // for monte carlo public static final double CROPLAND_CONVERSION_COST = getDoubleProperty("CROPLAND_CONVERSION_COST", 0.3); public static final double PASTURE_CONVERSION_COST = getDoubleProperty("PASTURE_CONVERSION_COST", 0.2); + public static final double SOLAR_CONVERSION_COST = getDoubleProperty("SOLAR_CONVERSION_COST", 0.8); public static final double FOREST_CONVERSION_COST = getDoubleProperty("FOREST_CONVERSION_COST", 0.5); public static final double NATURAL_CONVERSION_COST = getDoubleProperty("NATURAL_CONVERSION_COST", 0.02); @@ -584,4 +585,37 @@ public class ModelConfig { // How demand is allocated. Options: global (countries can export carbon credits to market), country (global demand allocated by GDP), price (no demand, exogenous price) public static final String CARBON_DEMAND_METHOD = getProperty("CARBON_DEMAND_METHOD", "global"); + // Solar + public static final boolean IS_PHOTOVOLTAICS_ON = getBooleanProperty("IS_PHOTOVOLTAICS_ON", true); + public static final boolean IS_AGRIVOLTAICS_ON = getBooleanProperty("IS_AGRIVOLTAICS_ON", true); + public static final String AGRIVOLTAICS_YIELD_PARAM_FILE = getProperty("AGRIVOLTAICS_YIELD_PARAM_FILE", DATA_DIR + File.separator + "agrivoltaic_yield_params.csv"); + public static final String SOLAR_POTENTIAL_DIR_BASE = getProperty("SOLAR_POTENTIAL_DIR_BASE"); + public static final String SOLAR_POTENTIAL_DIR_TOP = getProperty("SOLAR_POTENTIAL_DIR_TOP"); + public static final String SOLAR_POTENTIAL_FILENAME = getProperty("SOLAR_POTENTIAL_FILENAME", "pv_yield.csv"); + public static final int SOLAR_POTENTIAL_DATA_STEP_SIZE = getIntProperty("SOLAR_POTENTIAL_DATA_STEP_SIZE", 10); + + // PV costs from IRENA, Renewable Power Generation Costs in 2022 + public static final double PV_INSTALLED_COST = getDoubleProperty("PV_INSTALLED_COST", 0.876); // $1000/kW + public static final double PV_MAINTENANCE_COST = getDoubleProperty("PV_MAINTENANCE_COST", 0.0132); // $1000/kW/yr + public static final double PV_LIFESPAN = getDoubleProperty("PV_LIFESPAN", 25.0); // years + public static final double PV_EFFICIENCY = getDoubleProperty("PV_EFFICIENCY", 0.2); + + // GSR and performance ratio from https://doi.org/10.1016/j.energy.2015.10.108 + public static final double PV_GSR = getDoubleProperty("PV_GSR", 0.75); // Generator to System Ratio (area of (PV + spacing) / total area) + public static final double PV_PERFORMANCE_RATIO = getDoubleProperty("PV_PERFORMANCE_RATIO", 0.65); // Losses e.g. power conversion, PV cell degradation + + // Typical GCR, cost factor and yield factor from https://doi.org/10.1007/s10457-023-00906-3 + public static final double AV_GCR = getDoubleProperty("AV_GCR", 0.5); // Ground Coverage Ratio of agrivoltaics + public static final double AV_COST_FACTOR = getDoubleProperty("AV_COST_FACTOR", 1.3); // Cost multiplier for AV compared to PV + public static final double AV_YIELD_FACTOR = getDoubleProperty("AV_YIELD_FACTOR", 0.9); // Yield reduction due to area taken by infrastructure + + // Bioenergy + + // Bioenergy assumptions: installed cost = 2162 $/kW, fixed O&M = 4% of installed cost per year, + // variable O&M = 0.005 $/kWh, project lifespan = 20 years. + // Source: IRENA, Renewable Power Generation Costs in 2022 + public static final double BIOENERGY_GENERATION_COST = getDoubleProperty("BIOENERGY_COST", 0.0272); // Cost of electricity generation from biomass $1000/MWh. + public static final double ENERGYCROPS_ENERGY_DENSITY = getDoubleProperty("ENERGYCROPS_ENERGY_DENSITY", 5.0); // MWh/t DM + public static final double BIOENERGY_GENERATION_EFFICIENCY = getDoubleProperty("BIOENERGY_GENERATION_EFFICIENCY", 0.8); // IRENA, CHP system efficiency + } diff --git a/src/ac/ed/lurg/ModelMain.java b/src/ac/ed/lurg/ModelMain.java index 8f3e706e..b439da89 100644 --- a/src/ac/ed/lurg/ModelMain.java +++ b/src/ac/ed/lurg/ModelMain.java @@ -13,6 +13,8 @@ import ac.ed.lurg.landuse.*; import ac.ed.lurg.output.LandCoverChangeOutputer; import ac.ed.lurg.output.LandUseOutputer; import ac.ed.lurg.output.LpjgOutputer; +import ac.ed.lurg.solar.SolarPotentialItem; +import ac.ed.lurg.solar.SolarPotentialReader; import ac.ed.lurg.types.CommodityType; import ac.ed.lurg.types.CropType; import ac.ed.lurg.types.LandCoverType; @@ -46,6 +48,7 @@ public class ModelMain { private WoodYieldRasterSet woodYieldData; private CarbonFluxReader carbonFluxReader; private CarbonFluxRasterSet carbonFluxData; + private RasterSet solarPotentialData; public static void main(String[] args) { @@ -125,6 +128,8 @@ public class ModelMain { getWoodYieldData(timestep); getCarbonFluxData(timestep); + + getSolarPotentialData(timestep); LogWriter.println("Memory usage 1: " + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024.0*1024.0*1024.0)); @@ -133,7 +138,7 @@ public class ModelMain { handleMissingData(); - countryAgents.determineProductionForAll(timestep, yieldSurfaces, currentIrrigationData, carbonFluxData, woodYieldData, conversionCosts); + countryAgents.determineProductionForAll(timestep, yieldSurfaces, currentIrrigationData, carbonFluxData, woodYieldData, solarPotentialData, conversionCosts); if (ModelConfig.RESET_STOCK_YEAR == timestep.getYear()) internationalMarket.resetStocks(); @@ -882,6 +887,20 @@ public class ModelMain { } } + /** Get solar potential data */ + private void getSolarPotentialData(Timestep timestep) { + solarPotentialData = new RasterSet<SolarPotentialItem>(desiredProjection) { + private static final long serialVersionUID = 2517662660687253540L; + protected SolarPotentialItem createRasterData() { + return new SolarPotentialItem(); + } + }; + + if (ModelConfig.IS_PHOTOVOLTAICS_ON || ModelConfig.IS_AGRIVOLTAICS_ON) { + new SolarPotentialReader(solarPotentialData).readData(timestep); + } + } + /** Ugly in situ update of currentIrrigationData, better if IrrigationRasterSets were handled more immutably */ private void getUpdateIrrigationData(Timestep timestep) { String rootDir = timestep.getYearSubDir(ModelConfig.YIELD_DIR); @@ -995,7 +1014,8 @@ public class ModelMain { boolean hasIrrig = currentIrrigationData.containsKey(key); boolean hasWoodYields = !ModelConfig.IS_FORESTRY_ON || woodYieldData.containsKey(key); boolean hasCarbonFluxes = !ModelConfig.IS_CARBON_ON || carbonFluxData.containsKey(key); - boolean isComplete = (hasCropYields && hasIrrig && hasWoodYields && hasCarbonFluxes); + boolean hasSolarPotential = !(ModelConfig.IS_AGRIVOLTAICS_ON || ModelConfig.IS_PHOTOVOLTAICS_ON) || solarPotentialData.containsKey(key); + boolean isComplete = (hasCropYields && hasIrrig && hasWoodYields && hasCarbonFluxes && hasSolarPotential); if (isComplete) { coverage++; } @@ -1010,6 +1030,9 @@ public class ModelMain { if (!hasCarbonFluxes) carbonFluxData.put(key, CarbonFluxItem.getDefault()); + + if(!hasSolarPotential) + solarPotentialData.put(key, SolarPotentialItem.getDefault()); } double coveragePc = (double) coverage / totalCount * 100; LogWriter.println(String.format("Data coverage: %.2f%%", coveragePc), 1); diff --git a/src/ac/ed/lurg/country/CountryAgent.java b/src/ac/ed/lurg/country/CountryAgent.java index 1d2a809c..b9b1cf05 100644 --- a/src/ac/ed/lurg/country/CountryAgent.java +++ b/src/ac/ed/lurg/country/CountryAgent.java @@ -16,6 +16,7 @@ import ac.ed.lurg.forestry.WoodYieldItem; import ac.ed.lurg.landuse.*; import ac.ed.lurg.shock.MinMaxNetImportManager; import ac.ed.lurg.shock.MinMaxNetImportManager.LimitType; +import ac.ed.lurg.solar.SolarPotentialItem; import ac.ed.lurg.types.CommodityType; import ac.ed.lurg.types.CropType; import ac.ed.lurg.types.LandCoverType; @@ -103,7 +104,8 @@ public class CountryAgent extends AbstractCountryAgent { public GamsRasterOutput determineProduction(YieldRaster countryYieldSurfaces, RasterSet<IrrigationItem> irrigData, Map<CropType, GlobalPrice> worldPrices, RasterSet<CarbonFluxItem> carbonFluxData, - RasterSet<WoodYieldItem> woodYieldData, Map<LandCoverChangeKey, Double> conversionCosts, + RasterSet<WoodYieldItem> woodYieldData, RasterSet<SolarPotentialItem> solarPotentialData, + Map<LandCoverChangeKey, Double> conversionCosts, GlobalPrice carbonPrice, Map<WoodType, GlobalPrice> globalWoodPrices) { // project demand @@ -129,7 +131,7 @@ public class CountryAgent extends AbstractCountryAgent { } // optimize areas and intensity - GamsRasterInput input = getGamsRasterInput(irrigData, countryYieldSurfaces, woodYieldData, carbonFluxData, conversionCosts); + GamsRasterInput input = getGamsRasterInput(irrigData, countryYieldSurfaces, woodYieldData, carbonFluxData, solarPotentialData, conversionCosts); GamsRasterOptimiser opti = new GamsRasterOptimiser(input, yieldClusters); LogWriter.println("Running " + country.getName() + ", currentTimestep " + currentTimestep, 3); @@ -151,7 +153,8 @@ public class CountryAgent extends AbstractCountryAgent { } private GamsRasterInput getGamsRasterInput(RasterSet<IrrigationItem> irrigData, YieldRaster countryYieldSurfaces, - RasterSet<WoodYieldItem> woodYieldData, RasterSet<CarbonFluxItem> carbonFluxData, Map<LandCoverChangeKey, Double> conversionCosts) { + RasterSet<WoodYieldItem> woodYieldData, RasterSet<CarbonFluxItem> carbonFluxData, + RasterSet<SolarPotentialItem> solarPotentialData, Map<LandCoverChangeKey, Double> conversionCosts) { Map<CropType, TradeConstraint> importConstraints = new HashMap<CropType, TradeConstraint>(); Map<CropType, Double> currentExportRestrictions = (exportRestrictions == null) ? null : exportRestrictions.get(currentTimestep.getYear()); @@ -271,7 +274,7 @@ public class CountryAgent extends AbstractCountryAgent { previousGamsRasterOutput.getCropUsageData(), getTotalDemandFraction(), subsidyRates, currentGen2EcDemand, currentCarbonDemand, currentCarbonPrice, carbonTradeConstraint, currentWoodDemand, currentWoodPrices, woodTradeConstraints, previousGamsRasterOutput.getWoodUsageData()); GamsRasterInput input = new GamsRasterInput(currentTimestep, countryYieldSurfaces, previousGamsRasterOutput.getLandUses(), irrigData, - woodYieldData, carbonFluxData, conversionCosts, countryLevelInputs); + woodYieldData, carbonFluxData, solarPotentialData, conversionCosts, countryLevelInputs); return input; } diff --git a/src/ac/ed/lurg/country/CountryAgentManager.java b/src/ac/ed/lurg/country/CountryAgentManager.java index 6fd55c28..015f88ea 100644 --- a/src/ac/ed/lurg/country/CountryAgentManager.java +++ b/src/ac/ed/lurg/country/CountryAgentManager.java @@ -26,6 +26,8 @@ import ac.ed.lurg.landuse.*; import ac.ed.lurg.shock.ExportRestrictionManager; import ac.ed.lurg.shock.MinMaxNetImportManager; import ac.ed.lurg.shock.MinMaxNetImportManager.LimitType; +import ac.ed.lurg.solar.SolarPotentialItem; +import ac.ed.lurg.solar.SolarPotentialReader; import ac.ed.lurg.types.CropType; import ac.ed.lurg.types.WoodType; import ac.ed.lurg.utils.LogWriter; @@ -121,7 +123,8 @@ public class CountryAgentManager { } public void determineProductionForAll(Timestep timestep, YieldRaster yieldSurfaces, IrrigationRasterSet currentIrrigationData, - CarbonFluxRasterSet currentCarbonFluxData, WoodYieldRasterSet currentWoodYieldData, Map<LandCoverChangeKey, Double> conversionCosts) { + CarbonFluxRasterSet currentCarbonFluxData, WoodYieldRasterSet currentWoodYieldData, + RasterSet<SolarPotentialItem> solarPotentialData, Map<LandCoverChangeKey, Double> conversionCosts) { for (AbstractCountryAgent aca : countryAgents) { aca.setCurrentTimestep(timestep); @@ -145,7 +148,7 @@ public class CountryAgentManager { try { ca.determineProduction(countryYieldSurfaces, irrigData, internationalMarket.getWorldPrices(), carbonFluxData, woodYieldData, - conversionCosts, internationalMarket.getCarbonPrice(), internationalMarket.getWoodPrices()); + solarPotentialData, conversionCosts, internationalMarket.getCarbonPrice(), internationalMarket.getWoodPrices()); // update global rasters globalLandUseRaster.putAll(ca.getLandUses()); diff --git a/src/ac/ed/lurg/country/gams/GamsLocationInput.java b/src/ac/ed/lurg/country/gams/GamsLocationInput.java index fc619fed..0b9f6627 100644 --- a/src/ac/ed/lurg/country/gams/GamsLocationInput.java +++ b/src/ac/ed/lurg/country/gams/GamsLocationInput.java @@ -9,6 +9,7 @@ import ac.ed.lurg.forestry.WoodYieldData; import ac.ed.lurg.landuse.IrrigationItem; import ac.ed.lurg.landuse.LandCoverChangeKey; import ac.ed.lurg.landuse.LandUseItem; +import ac.ed.lurg.solar.SolarPotentialItem; import ac.ed.lurg.yield.YieldResponsesItem; public class GamsLocationInput { @@ -20,6 +21,7 @@ public class GamsLocationInput { private Map<Integer, ? extends IrrigationItem> irrigationCosts; private Map<Integer, ? extends CarbonFluxItem> carbonFluxes; private Map<Integer, ? extends WoodYieldData> woodYields; + private Map<Integer, ? extends SolarPotentialItem> solarPotentials; private Map<LandCoverChangeKey, Double> conversionCosts; private GamsCountryInput countryInput; @@ -27,6 +29,7 @@ public class GamsLocationInput { Map<Integer, ? extends LandUseItem> previousLandUse, Map<Integer, ? extends IrrigationItem> irrigationCosts, Map<Integer, ? extends CarbonFluxItem> carbonFluxes, Map<Integer, ? extends WoodYieldData> woodYields, + Map<Integer, ? extends SolarPotentialItem> solarPotentials, Map<LandCoverChangeKey, Double> conversionCosts, GamsCountryInput countryInput) { super(); this.timestep = timestep; @@ -36,6 +39,7 @@ public class GamsLocationInput { this.irrigationCosts = irrigationCosts; this.carbonFluxes = carbonFluxes; this.woodYields = woodYields; + this.solarPotentials = solarPotentials; this.conversionCosts = conversionCosts; this.countryInput = countryInput; } @@ -59,7 +63,11 @@ public class GamsLocationInput { public Map<Integer, ? extends WoodYieldData> getWoodYields() { return woodYields; } - + + public Map<Integer, ? extends SolarPotentialItem> getSolarPotentials() { + return solarPotentials; + } + public Map<LandCoverChangeKey, Double> getConversionCosts() { return conversionCosts; } diff --git a/src/ac/ed/lurg/country/gams/GamsLocationOptimiser.java b/src/ac/ed/lurg/country/gams/GamsLocationOptimiser.java index 6ee50225..6ad2b85b 100644 --- a/src/ac/ed/lurg/country/gams/GamsLocationOptimiser.java +++ b/src/ac/ed/lurg/country/gams/GamsLocationOptimiser.java @@ -11,6 +11,8 @@ import java.util.*; import java.util.Map.Entry; import ac.ed.lurg.landuse.*; +import ac.ed.lurg.solar.AgrivoltaicsYieldManager; +import ac.ed.lurg.solar.SolarPotentialItem; import ac.ed.lurg.types.*; import com.gams.api.GAMSDatabase; import com.gams.api.GAMSException; @@ -147,8 +149,11 @@ public class GamsLocationOptimiser { Vector<String> v = new Vector<>(); v.add(crop.getGamsName()); LandProtectionType lpType = landKey.getLpType(); + LandCoverType lcType = landKey.getLcType(); - if (lpType.equals(LandProtectionType.UNPROTECTED)) { + if (lcType.equals(LandCoverType.AGRIVOLTAICS)) { + v.add(FarmingType.AGRIVOLTAICS.getName()); + } else if (lpType.equals(LandProtectionType.UNPROTECTED)) { v.add(FarmingType.CONVENTIONAL.getName()); } else if (lpType.equals(LandProtectionType.PROTECTED)) { v.add(FarmingType.RESTRICTED.getName()); @@ -488,6 +493,52 @@ public class GamsLocationOptimiser { } } } + + // Solar + GAMSParameter solarEnergyP = inDB.addParameter("solarEnergyDensity", 2); + GAMSParameter solarCostP = inDB.addParameter("solarCost", 2); + GAMSParameter avYieldFactorP = inDB.addParameter("agrivoltaicsYieldFactor", 3); + AgrivoltaicsYieldManager agrivoltParamManager = AgrivoltaicsYieldManager.getInstance(); + + for (Entry<Integer, ? extends SolarPotentialItem> entry : inputData.getSolarPotentials().entrySet()) { + Integer locationId = entry.getKey(); + String locString = Integer.toString(locationId); + SolarPotentialItem solarPot = entry.getValue(); + Vector<String> v = new Vector<>(); + + v.add(LandCoverType.PHOTOVOLTAICS.getName()); + v.add(locString); + setGamsParamValue(solarEnergyP.addRecord(v), solarPot.getEnergyDensityPV(), -1); + setGamsParamValue(solarCostP.addRecord(v), solarPot.getCostPV(), -1); + + v.clear(); + v.add(LandCoverType.AGRIVOLTAICS.getName()); + v.add(locString); + setGamsParamValue(solarEnergyP.addRecord(v), solarPot.getEnergyDensityAV(), -1); + setGamsParamValue(solarCostP.addRecord(v), solarPot.getCostAV(), -1); + + for (CropType crop : CropType.getNonMeatTypes()) { + if (crop.equals(CropType.SETASIDE)) { + continue; + } + for (FarmingType farmingType : FarmingType.values()) { + v.clear(); + v.add(crop.getGamsName()); + v.add(farmingType.getName()); + v.add(locString); + if (farmingType.equals(FarmingType.AGRIVOLTAICS)){ + setGamsParamValue(avYieldFactorP.addRecord(v), agrivoltParamManager.getYieldFactor(crop, solarPot.getAgrivoltaicsGCR()), -1); + } else { + // Not agrivoltaics so yields not affected + setGamsParamValue(avYieldFactorP.addRecord(v), 1.0, -1); + } + v.clear(); + } + } + } + + addScalar(inDB, "energycropsEnergyDensity", ModelConfig.ENERGYCROPS_ENERGY_DENSITY * ModelConfig.BIOENERGY_GENERATION_EFFICIENCY, -1); + addScalar(inDB, "bioenergyGenerationCost", ModelConfig.BIOENERGY_GENERATION_COST, -1); // Land cover conversion cost GAMSParameter conversionCostP = inDB.addParameter("conversionCost", 3); @@ -601,22 +652,24 @@ public class GamsLocationOptimiser { for (GAMSVariableRecord rec : varAreas) { String itemName = rec.getKey(0); - String farmingType = rec.getKey(1); + String farmingTypeName = rec.getKey(1); String locationName = rec.getKey(2); area = rec.getLevel(); if (area == 0) { continue; } - fertIntensity = varFertIntensities.findRecord(itemName, farmingType, locationName).getLevel(); - irrigIntensity = varIrrigIntensities.findRecord(itemName, farmingType, locationName).getLevel(); - otherIntensity = varOtherIntensities.findRecord(itemName, farmingType, locationName).getLevel(); - yield = varYields.findRecord(itemName, farmingType, locationName).getLevel(); - unitCost = varUnitEnergies.findRecord(itemName, farmingType, locationName).getLevel(); + fertIntensity = varFertIntensities.findRecord(itemName, farmingTypeName, locationName).getLevel(); + irrigIntensity = varIrrigIntensities.findRecord(itemName, farmingTypeName, locationName).getLevel(); + otherIntensity = varOtherIntensities.findRecord(itemName, farmingTypeName, locationName).getLevel(); + yield = varYields.findRecord(itemName, farmingTypeName, locationName).getLevel(); + unitCost = varUnitEnergies.findRecord(itemName, farmingTypeName, locationName).getLevel(); int locId = Integer.parseInt(locationName); CropType cropType = CropType.getForGamsName(itemName); - LandCoverType lcType = cropType.equals(CropType.PASTURE) ? LandCoverType.PASTURE : LandCoverType.CROPLAND; - LandProtectionType lpType = farmingType.equals("restricted") ? LandProtectionType.PROTECTED : LandProtectionType.UNPROTECTED; + FarmingType farmingType = FarmingType.getForName(farmingTypeName); + + LandCoverType lcType = farmingType.equals(FarmingType.AGRIVOLTAICS) ? LandCoverType.AGRIVOLTAICS : cropType.equals(CropType.PASTURE) ? LandCoverType.PASTURE : LandCoverType.CROPLAND; + LandProtectionType lpType = farmingType.equals(FarmingType.RESTRICTED) ? LandProtectionType.PROTECTED : LandProtectionType.UNPROTECTED; LandKey landKey = new LandKey(lcType, lpType); LandUseItem landUseItem = landUses.lazyGet(locId); // returns landUseItem for location. If does not exist, creates new one diff --git a/src/ac/ed/lurg/country/gams/GamsRasterInput.java b/src/ac/ed/lurg/country/gams/GamsRasterInput.java index f272fc8c..6919f4d5 100644 --- a/src/ac/ed/lurg/country/gams/GamsRasterInput.java +++ b/src/ac/ed/lurg/country/gams/GamsRasterInput.java @@ -8,6 +8,7 @@ import ac.ed.lurg.forestry.WoodYieldItem; import ac.ed.lurg.landuse.LandCoverChangeKey; import ac.ed.lurg.landuse.LandUseItem; import ac.ed.lurg.landuse.IrrigationItem; +import ac.ed.lurg.solar.SolarPotentialItem; import ac.ed.lurg.yield.YieldRaster; import ac.sac.raster.RasterSet; @@ -19,11 +20,12 @@ public class GamsRasterInput { private RasterSet<IrrigationItem> irrigationCost; private RasterSet<WoodYieldItem> woodYields; private RasterSet<CarbonFluxItem> carbonFluxes; + private RasterSet<SolarPotentialItem> solarPotentialData; private Map<LandCoverChangeKey, Double> conversionCosts; private GamsCountryInput countryInput; public GamsRasterInput(Timestep timestep, YieldRaster yields, RasterSet<LandUseItem> previousLandsUses, RasterSet<IrrigationItem> irrigationCost, - RasterSet<WoodYieldItem> woodYields, RasterSet<CarbonFluxItem> carbonFluxes, + RasterSet<WoodYieldItem> woodYields, RasterSet<CarbonFluxItem> carbonFluxes, RasterSet<SolarPotentialItem> solarPotentialData, Map<LandCoverChangeKey, Double> conversionCosts, GamsCountryInput countryInput) { super(); this.timestep = timestep; @@ -32,6 +34,7 @@ public class GamsRasterInput { this.irrigationCost = irrigationCost; this.woodYields = woodYields; this.carbonFluxes = carbonFluxes; + this.solarPotentialData = solarPotentialData; this.conversionCosts = conversionCosts; this.countryInput = countryInput; } @@ -55,7 +58,11 @@ public class GamsRasterInput { public RasterSet<CarbonFluxItem> getCarbonFluxes() { return carbonFluxes; } - + + public RasterSet<SolarPotentialItem> getSolarPotentialData() { + return solarPotentialData; + } + public Map<LandCoverChangeKey, Double> getConversionCosts() { return conversionCosts; } diff --git a/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java b/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java index 58ea51f2..0292d606 100644 --- a/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java +++ b/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java @@ -8,6 +8,7 @@ import ac.ed.lurg.carbon.CarbonFluxItem; import ac.ed.lurg.forestry.WoodYieldData; import ac.ed.lurg.forestry.WoodYieldItem; import ac.ed.lurg.landuse.*; +import ac.ed.lurg.solar.SolarPotentialItem; import ac.ed.lurg.types.*; import ac.ed.lurg.utils.LazyTreeMap; import ac.ed.lurg.utils.LogWriter; @@ -231,6 +232,8 @@ public class GamsRasterOptimiser { RasterSet<WoodYieldItem> woodYieldRaster = rasterInputData.getWoodYields(); + RasterSet<SolarPotentialItem> solarPotentialRaster = rasterInputData.getSolarPotentialData(); + // look for inconsistencies for (Entry<RasterKey, YieldResponsesItem> entry : yieldRaster.entrySet()) { YieldResponsesItem yresp = entry.getValue(); @@ -262,6 +265,11 @@ public class GamsRasterOptimiser { LazyTreeMap<Integer, WoodYieldData> aggregatedWoodYields = new LazyTreeMap<Integer, WoodYieldData>() { protected WoodYieldData createValue() { return new WoodYieldData(); } }; + LazyTreeMap<Integer, SolarPotentialItem> aggregatedSolarPotentials = new LazyTreeMap<Integer, SolarPotentialItem>() { + protected SolarPotentialItem createValue() { + return new SolarPotentialItem(); + } + }; for (RasterKey key : landUseRaster.keySet()) { int clusterId = mapping.get(key).getInt(); @@ -271,12 +279,14 @@ public class GamsRasterOptimiser { IrrigationItem irrigItem = irrigRaster.get(key); WoodYieldItem woodYieldItem = woodYieldRaster.get(key); CarbonFluxItem carbonFluxItem = carbonFluxRaster.get(key); + SolarPotentialItem solarPotentialItem = solarPotentialRaster.get(key); YieldResponsesItem aggYResp = aggregatedYields.lazyGet(clusterId); LandUseItem aggLandUse = aggregatedAreas.lazyGet(clusterId); IrrigationItem aggIrrig = aggregatedIrrigCosts.lazyGet(clusterId); WoodYieldData aggWYield = aggregatedWoodYields.lazyGet(clusterId); CarbonFluxItem aggCFlux = aggregatedCarbonFluxes.lazyGet(clusterId); + SolarPotentialItem aggSolarPot = aggregatedSolarPotentials.lazyGet(clusterId); double suitableAreaThisTime = landUseItem.getSuitableArea(); double suitableAreaSoFar = aggLandUse.getSuitableArea(); @@ -321,6 +331,23 @@ public class GamsRasterOptimiser { } } + if (ModelConfig.IS_PHOTOVOLTAICS_ON || ModelConfig.IS_AGRIVOLTAICS_ON) { + double areaThisTime = landUseItem.getTotalLandArea(); + double areaSoFar = landUseItem.getTotalLandArea(); + + double pvEnergyAgg = aggregateMean(aggSolarPot.getEnergyDensityPV(), areaSoFar, solarPotentialItem.getEnergyDensityPV(), areaThisTime); + double avEnergyAgg = aggregateMean(aggSolarPot.getEnergyDensityAV(), areaSoFar, solarPotentialItem.getEnergyDensityAV(), areaThisTime); + double pvCostAgg = aggregateMean(aggSolarPot.getCostPV(), areaSoFar, solarPotentialItem.getCostPV(), areaThisTime); + double avCostAgg = aggregateMean(aggSolarPot.getCostAV(), areaSoFar, solarPotentialItem.getCostAV(), areaThisTime); + double avGCRAgg = aggregateMean(aggSolarPot.getAgrivoltaicsGCR(), areaSoFar, solarPotentialItem.getAgrivoltaicsGCR(), areaThisTime); + + aggSolarPot.setEnergyDensityPV(pvEnergyAgg); + aggSolarPot.setEnergyDensityAV(avEnergyAgg); + aggSolarPot.setCostPV(pvCostAgg); + aggSolarPot.setCostAV(avCostAgg); + aggSolarPot.setAgrivoltaicsGCR(avGCRAgg); + } + // Crops yields and area fractions for (CropType crop : CropType.getNonMeatTypes()) { if (crop.equals(CropType.SETASIDE)) { @@ -390,7 +417,7 @@ public class GamsRasterOptimiser { } return new GamsLocationInput(rasterInputData.getTimestep(), locations, aggregatedYields, aggregatedAreas, aggregatedIrrigCosts, - aggregatedCarbonFluxes, aggregatedWoodYields, rasterInputData.getConversionCosts(), rasterInputData.getCountryInput()); + aggregatedCarbonFluxes, aggregatedWoodYields, aggregatedSolarPotentials, rasterInputData.getConversionCosts(), rasterInputData.getCountryInput()); } private void logWarningWithCoord(String message, RasterKey key, YieldRaster yieldRaster, CropType crop) { diff --git a/src/ac/ed/lurg/landuse/ConversionCostReader.java b/src/ac/ed/lurg/landuse/ConversionCostReader.java index 4d1de464..9681936b 100644 --- a/src/ac/ed/lurg/landuse/ConversionCostReader.java +++ b/src/ac/ed/lurg/landuse/ConversionCostReader.java @@ -70,31 +70,37 @@ public class ConversionCostReader { } LandCoverChangeKey key = new LandCoverChangeKey(fromLc, toLc); - switch(fromLc) { - case CROPLAND: - conversionCosts.put(key, ModelConfig.getAdjParam("CROPLAND_CONVERSION_COST") * costAdjFactor); - break; - case PASTURE: - conversionCosts.put(key, ModelConfig.getAdjParam("PASTURE_CONVERSION_COST") * costAdjFactor); - break; - case TIMBER_FOREST: - conversionCosts.put(key, ModelConfig.getAdjParam("FOREST_CONVERSION_COST") * costAdjFactor); - break; - case CARBON_FOREST: - conversionCosts.put(key, ModelConfig.getAdjParam("FOREST_CONVERSION_COST") * costAdjFactor - + ModelConfig.CARBON_FOREST_CONVERSION_PENALTY); - break; - case NATURAL: - conversionCosts.put(key, ModelConfig.getAdjParam("NATURAL_CONVERSION_COST") * costAdjFactor); - break; - default: - conversionCosts.put(key, 0.2 * costAdjFactor); - break; + switch (fromLc) { + case CROPLAND: + conversionCosts.put(key, ModelConfig.getAdjParam("CROPLAND_CONVERSION_COST") * costAdjFactor); + break; + case PASTURE: + conversionCosts.put(key, ModelConfig.getAdjParam("PASTURE_CONVERSION_COST") * costAdjFactor); + break; + case PHOTOVOLTAICS: + conversionCosts.put(key, ModelConfig.getAdjParam("SOLAR_CONVERSION_COST") * costAdjFactor); + break; + case AGRIVOLTAICS: + conversionCosts.put(key, ModelConfig.getAdjParam("SOLAR_CONVERSION_COST") * costAdjFactor); + break; + case TIMBER_FOREST: + conversionCosts.put(key, ModelConfig.getAdjParam("FOREST_CONVERSION_COST") * costAdjFactor); + break; + case CARBON_FOREST: + conversionCosts.put(key, ModelConfig.getAdjParam("FOREST_CONVERSION_COST") * costAdjFactor + + ModelConfig.CARBON_FOREST_CONVERSION_PENALTY); + break; + case NATURAL: + conversionCosts.put(key, ModelConfig.getAdjParam("NATURAL_CONVERSION_COST") * costAdjFactor); + break; + default: + conversionCosts.put(key, 0.2 * costAdjFactor); + break; } // Agricultural expansion cost factor - if (fromLc.equals(LandCoverType.NATURAL) && (toLc.equals(LandCoverType.CROPLAND) || toLc.equals(LandCoverType.PASTURE))) { + if (fromLc.equals(LandCoverType.NATURAL) && (toLc.equals(LandCoverType.CROPLAND) || toLc.equals(LandCoverType.PASTURE) || toLc.equals(LandCoverType.AGRIVOLTAICS))) { double baseCost = conversionCosts.get(key); conversionCosts.put(key, baseCost * ModelConfig.getAdjParam("AGRI_LAND_EXPANSION_COST_FACTOR")); } diff --git a/src/ac/ed/lurg/landuse/LandUseItem.java b/src/ac/ed/lurg/landuse/LandUseItem.java index a2a7f88f..7948ec3d 100644 --- a/src/ac/ed/lurg/landuse/LandUseItem.java +++ b/src/ac/ed/lurg/landuse/LandUseItem.java @@ -25,10 +25,14 @@ public class LandUseItem implements InterpolatingRasterItem<LandUseItem>, Serial setProtectedFraction(landCover.getProtectedFraction(landCoverAreas)); AgricultureDetail cropDetail = (AgricultureDetail) getLandCoverDetail(new LandKey(LandCoverType.CROPLAND, LandProtectionType.UNPROTECTED)); + AgricultureDetail avDetail = (AgricultureDetail) getLandCoverDetail(new LandKey(LandCoverType.AGRIVOLTAICS, LandProtectionType.UNPROTECTED)); for (CropType crop : CropType.getCropsLessPasture()) { cropDetail.setCropFraction(crop, landCover.getCropFraction(crop)); cropDetail.setIntensity(crop, new Intensity(0.5, 0.5, 0.5)); + + avDetail.setCropFraction(crop, 0); + avDetail.setIntensity(crop, new Intensity(0.5, 0.5, 0.5)); } AgricultureDetail pastureDetail = (AgricultureDetail) getLandCoverDetail(new LandKey(LandCoverType.PASTURE, LandProtectionType.UNPROTECTED)); @@ -71,6 +75,10 @@ public class LandUseItem implements InterpolatingRasterItem<LandUseItem>, Serial return new AgricultureDetail(0); case PASTURE: return new AgricultureDetail(0); + case AGRIVOLTAICS: + return new AgricultureDetail(0); + case PHOTOVOLTAICS: + return new LandCoverDetail(0); case TIMBER_FOREST: return new ForestDetail(0); case CARBON_FOREST: @@ -100,6 +108,7 @@ public class LandUseItem implements InterpolatingRasterItem<LandUseItem>, Serial Set<LandKey> keys = new HashSet<>(); keys.addAll(getLandKeysForLandCover(LandCoverType.CROPLAND)); keys.addAll(getLandKeysForLandCover(LandCoverType.PASTURE)); + keys.addAll(getLandKeysForLandCover(LandCoverType.AGRIVOLTAICS)); return keys; } diff --git a/src/ac/ed/lurg/output/LandUseOutputer.java b/src/ac/ed/lurg/output/LandUseOutputer.java index ff6ae8c1..2613aac6 100644 --- a/src/ac/ed/lurg/output/LandUseOutputer.java +++ b/src/ac/ed/lurg/output/LandUseOutputer.java @@ -9,6 +9,7 @@ import java.util.Map.Entry; import ac.ed.lurg.landuse.*; import ac.ed.lurg.types.CropType; +import ac.ed.lurg.types.FarmingType; import ac.ed.lurg.types.LandCoverType; import ac.ed.lurg.types.LandProtectionType; import ac.ed.lurg.utils.LogWriter; @@ -58,8 +59,17 @@ public class LandUseOutputer extends AbstractLandUseOutputer { cropData.add(intensity==null ? 0.0 : intensity.getOtherIntensity()); cropData.add(intensity==null ? 0.0 : intensity.getYield()); + String farmingType = "ERROR"; + if (landKey.getLcType().equals(LandCoverType.AGRIVOLTAICS)) { + farmingType = FarmingType.AGRIVOLTAICS.getName(); + } else if (landKey.getLpType().equals(LandProtectionType.PROTECTED)) { + farmingType = FarmingType.RESTRICTED.getName(); + } else if (landKey.getLpType().equals(LandProtectionType.UNPROTECTED)) { + farmingType = FarmingType.CONVENTIONAL.getName(); + } + StringBuilder sbData = new StringBuilder(String.format("%.2f %.2f %s %s", lon, lat, - landKey.getLpType().getGamsName(), crop.getGamsName())); + farmingType, crop.getGamsName())); for (Double d : cropData) { String formatStr = d > 0 ? " %.8f" : " %.0f"; @@ -95,7 +105,7 @@ public class LandUseOutputer extends AbstractLandUseOutputer { try { String fileName = outputDir.getPath() + File.separator + "LandCover.txt"; writer = new BufferedWriter(new FileWriter(fileName)); - writer.write("Lon Lat Protection TotalArea Cropland Pasture TimberForest CarbonForest UnmanagedForest OtherNatural Barren Urban"); + writer.write("Lon Lat Protection TotalArea Cropland Pasture TimberForest CarbonForest UnmanagedForest OtherNatural Photovoltaics Agrivoltaics Barren Urban"); writer.newLine(); for (Entry<RasterKey, LandUseItem> entry : landUseRaster.entrySet()) { @@ -116,6 +126,8 @@ public class LandUseOutputer extends AbstractLandUseOutputer { double unmanagedForestFraction = detail.getUnmanagedForestFraction(); appendData(sbData, totalNatural * unmanagedForestFraction); appendData(sbData, totalNatural * (1 - unmanagedForestFraction)); + appendData(sbData, item.getLandCoverArea(new LandKey(LandCoverType.PHOTOVOLTAICS, lpType))); + appendData(sbData, item.getLandCoverArea(new LandKey(LandCoverType.AGRIVOLTAICS, lpType))); appendData(sbData, item.getLandCoverArea(new LandKey(LandCoverType.BARREN, lpType))); appendData(sbData, item.getLandCoverArea(new LandKey(LandCoverType.URBAN, lpType))); diff --git a/src/ac/ed/lurg/solar/AgrivoltaicsYieldManager.java b/src/ac/ed/lurg/solar/AgrivoltaicsYieldManager.java new file mode 100644 index 00000000..7b42b039 --- /dev/null +++ b/src/ac/ed/lurg/solar/AgrivoltaicsYieldManager.java @@ -0,0 +1,42 @@ +package ac.ed.lurg.solar; + +import ac.ed.lurg.ModelConfig; +import ac.ed.lurg.types.CropType; +import ac.ed.lurg.utils.StringTabularReader; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AgrivoltaicsYieldManager { + + private final StringTabularReader reader; + + private static AgrivoltaicsYieldManager apm; + public static AgrivoltaicsYieldManager getInstance() { + if (apm == null) + apm = new AgrivoltaicsYieldManager(); + + return apm; + } + + private AgrivoltaicsYieldManager() { + reader = new StringTabularReader(",", new String[]{"Crop", "ParamA", "ParamB"}); + reader.read(ModelConfig.AGRIVOLTAICS_YIELD_PARAM_FILE); + + } + + private double getParam(CropType crop, String param) { + Map<String, String> query = new HashMap<>(); + query.put("Crop", crop.getGamsName()); + List<Map<String, String>> queryResult = reader.query(query); + String result = queryResult.get(0).get(param); + return Double.parseDouble(result); + } + + public double getYieldFactor(CropType crop, double avGCR) { + double shading = avGCR + 0.05; + return Math.exp(getParam(crop, "ParamA") * shading + getParam(crop, "ParamB") * Math.pow(shading, 2)) * ModelConfig.AV_YIELD_FACTOR; + } + +} diff --git a/src/ac/ed/lurg/solar/SolarPotentialItem.java b/src/ac/ed/lurg/solar/SolarPotentialItem.java new file mode 100644 index 00000000..60a3ba26 --- /dev/null +++ b/src/ac/ed/lurg/solar/SolarPotentialItem.java @@ -0,0 +1,97 @@ +package ac.ed.lurg.solar; + +import ac.ed.lurg.ModelConfig; +import ac.sac.raster.RasterItem; +import ac.sac.raster.RasterKey; + +public class SolarPotentialItem implements RasterItem { + private double pvPotential; // in MWh/ha + private double energyDensityPV = Double.NaN; // MWh/ha + private double energyDensityAV = Double.NaN; // MWh/ha + private double costPV = Double.NaN; // $1000/ha + private double costAV = Double.NaN; // $1000/ha + private double optimalGCR; // optimal Ground Coverage Ratio (PV area) / (land area) + private double agrivoltaicsGCR; + + public void setPvPotential(double pvPotential) { + this.pvPotential = pvPotential; + } + + public void calcOptimalGCR(double latitude) { + // https://doi.org/10.1016/j.solener.2023.04.038 + // Optimal panel spacing and therefore Ground Coverage ratio depends on latitude + this.optimalGCR = -0.55 / (1 + Math.exp(-0.138 * (latitude - 43.4))) + 0.71; + this.agrivoltaicsGCR = optimalGCR * ModelConfig.AV_GCR; + } + + public void calcEnergyDensity() { + // MWh per ha including spacing between panels (GCR) and additional infrastructure area (GSR) + this.energyDensityPV = pvPotential * ModelConfig.PV_EFFICIENCY * optimalGCR * ModelConfig.PV_GSR; + // Ground Coverage Ratio (GCR) for AV is fixed. + this.energyDensityAV = pvPotential * ModelConfig.PV_EFFICIENCY * agrivoltaicsGCR * ModelConfig.PV_GSR; + } + + public void calcCostPerHectarePV() { // photovoltaics cost $1000/ha + // Capacity in kW/ha of total land used. Factor 10000 to convert from kW per m2 to hectare. + double capacity = ModelConfig.PV_EFFICIENCY * optimalGCR * ModelConfig.PV_GSR * 10000; + // Cost + this.costPV = capacity * (ModelConfig.PV_INSTALLED_COST / ModelConfig.PV_LIFESPAN + ModelConfig.PV_MAINTENANCE_COST); + } + + public void getCostPerHectareAV() { // agrivoltaics cost $1000/ha + // Capacity in kW/ha of total land used. Factor 10000 to convert from kW per m2 to hectare. + double capacity = ModelConfig.PV_EFFICIENCY * agrivoltaicsGCR * ModelConfig.PV_GSR * 10000; + // Cost + this.costAV = capacity * (ModelConfig.PV_INSTALLED_COST / ModelConfig.PV_LIFESPAN + ModelConfig.PV_MAINTENANCE_COST) * ModelConfig.AV_COST_FACTOR; + } + + public double getEnergyDensityPV() { + return ModelConfig.IS_PHOTOVOLTAICS_ON ? energyDensityPV : 0; + } + + public void setEnergyDensityPV(double energyDensityPV) { + this.energyDensityPV = energyDensityPV; + } + + public double getEnergyDensityAV() { + return ModelConfig.IS_AGRIVOLTAICS_ON ? energyDensityAV : 0; + } + + public void setEnergyDensityAV(double energyDensityAV) { + this.energyDensityAV = energyDensityAV; + } + + public double getCostPV() { + return costPV; + } + + public void setCostPV(double costPV) { + this.costPV = costPV; + } + + public double getCostAV() { + return costAV; + } + + public void setCostAV(double costAV) { + this.costAV = costAV; + } + + public double getAgrivoltaicsGCR() { + return agrivoltaicsGCR; + } + + public void setAgrivoltaicsGCR(double agrivoltaicsGCR) { + this.agrivoltaicsGCR = agrivoltaicsGCR; + } + + public static SolarPotentialItem getDefault() { + SolarPotentialItem item = new SolarPotentialItem(); + item.setPvPotential(0); + item.setEnergyDensityPV(0); + item.setEnergyDensityAV(0); + item.setCostPV(0); + item.setCostAV(0); + return item; + } +} diff --git a/src/ac/ed/lurg/solar/SolarPotentialReader.java b/src/ac/ed/lurg/solar/SolarPotentialReader.java new file mode 100644 index 00000000..b8967ae6 --- /dev/null +++ b/src/ac/ed/lurg/solar/SolarPotentialReader.java @@ -0,0 +1,37 @@ +package ac.ed.lurg.solar; + +import ac.ed.lurg.ModelConfig; +import ac.ed.lurg.Timestep; +import ac.sac.raster.AbstractTabularRasterReader; +import ac.sac.raster.RasterKey; +import ac.sac.raster.RasterSet; + +import java.io.File; +import java.util.Map; + +public class SolarPotentialReader extends AbstractTabularRasterReader<SolarPotentialItem> { + + private static final int MIN_COL = 3; + private static final String DELIM = ","; + private static final double CONVERSION_FACTOR = 10; // convert from kWh/m2 to MWh/ha + + public SolarPotentialReader(RasterSet<SolarPotentialItem> dataset) { + super(DELIM, MIN_COL, dataset); + } + + public void readData(Timestep timestep) { + int dataYear = Math.floorDiv(timestep.getYear(), ModelConfig.SOLAR_POTENTIAL_DATA_STEP_SIZE) * ModelConfig.SOLAR_POTENTIAL_DATA_STEP_SIZE; + String filePath = ModelConfig.SOLAR_POTENTIAL_DIR_BASE + File.separator + ModelConfig.SOLAR_POTENTIAL_DIR_TOP + + File.separator + dataYear + File.separator + ModelConfig.SOLAR_POTENTIAL_FILENAME; + getRasterDataFromFile(filePath); + } + + @Override + protected void setData(RasterKey key, SolarPotentialItem item, Map<String, Double> rowValues) { + item.setPvPotential(getValueForCol(rowValues, "PVYield") * CONVERSION_FACTOR); + item.calcOptimalGCR(dataset.getYCoordin(key)); + item.calcEnergyDensity(); + item.calcCostPerHectarePV(); + item.getCostPerHectareAV(); + } +} diff --git a/src/ac/ed/lurg/types/FarmingType.java b/src/ac/ed/lurg/types/FarmingType.java index e6cc4ce6..8fd854c8 100644 --- a/src/ac/ed/lurg/types/FarmingType.java +++ b/src/ac/ed/lurg/types/FarmingType.java @@ -7,7 +7,7 @@ public enum FarmingType { CONVENTIONAL("conventional"), RESTRICTED("restricted"), - AGRIVOLTAIC("agrivoltaic"); + AGRIVOLTAICS("agrivoltaics"); private String name; diff --git a/src/ac/ed/lurg/types/LandCoverType.java b/src/ac/ed/lurg/types/LandCoverType.java index bd47294d..1120f9c4 100644 --- a/src/ac/ed/lurg/types/LandCoverType.java +++ b/src/ac/ed/lurg/types/LandCoverType.java @@ -14,6 +14,8 @@ public enum LandCoverType { NATURAL ("natural", true), CROPLAND ("cropland", true), PASTURE ("pasture", true), + PHOTOVOLTAICS("photovoltaics", true), + AGRIVOLTAICS("agrivoltaics", true), BARREN ("barren", false), URBAN("urban", false); -- GitLab