From a4a56b9e0b8c1c49fb7e010ba14111d4072ca4bc Mon Sep 17 00:00:00 2001 From: Bart Arendarczyk <s1924442@ed.ac.uk> Date: Fri, 13 Jan 2023 16:46:33 +0000 Subject: [PATCH] Improved price adjustment and stock level control. --- src/ac/ed/lurg/InternationalMarket.java | 8 +-- src/ac/ed/lurg/ModelConfig.java | 5 +- src/ac/ed/lurg/country/CountryAgent.java | 27 +++++++--- src/ac/ed/lurg/country/GlobalPrice.java | 62 ++++++++++++++++++----- src/ac/ed/lurg/landuse/CropUsageData.java | 4 ++ src/ac/ed/lurg/types/CropType.java | 20 ++++---- 6 files changed, 91 insertions(+), 35 deletions(-) diff --git a/src/ac/ed/lurg/InternationalMarket.java b/src/ac/ed/lurg/InternationalMarket.java index cb8be1b4..797c8eb6 100644 --- a/src/ac/ed/lurg/InternationalMarket.java +++ b/src/ac/ed/lurg/InternationalMarket.java @@ -141,7 +141,8 @@ public class InternationalMarket { double imports = totalImportCommodities.containsKey(crop) ? totalImportCommodities.get(crop) : 0.0; double exportsBeforeTransportLosses = totalExportCommodities.containsKey(crop) ? totalExportCommodities.get(crop) : 0.0; LogWriter.println(timestep.getYear() + " Updating " + crop.getGamsName() + " prices", 2); - GlobalPrice adjustedPrice = prevPrice.createWithUpdatedMarketPrices(imports, exportsBeforeTransportLosses, timestep, totalProduction.get(crop), true); + GlobalPrice adjustedPrice = prevPrice.createWithUpdatedMarketPrices(imports, exportsBeforeTransportLosses, + timestep, totalProduction.get(crop), true, crop.getStockToUseRatio()); LogWriter.println( String.format("Price for %s updated from %s to %s \n", crop.getGamsName(), prevPrice, adjustedPrice), 2); if (adjustedPrice.getStockLevel() < 0) LogWriter.println("Global stocks are below zero" + crop.getGamsName() + ", " + timestep.getYear(), 2); @@ -167,7 +168,8 @@ public class InternationalMarket { totalCarbonSequestered = Math.max(totalCarbonSequestered, 0.0000001); // avoid division by 0 GlobalPrice prevCPrice = carbonPrice; LogWriter.println(timestep.getYear() + " Updating carbon price", 2); - GlobalPrice adjustedCPrice = prevCPrice.createWithUpdatedMarketPrices(totalCarbonImport, totalCarbonExport, timestep, totalCarbonSequestered, false); + GlobalPrice adjustedCPrice = prevCPrice.createWithUpdatedMarketPrices(totalCarbonImport, totalCarbonExport, timestep, + totalCarbonSequestered, false, 0.2); LogWriter.println( String.format("Price for carbon updated from %s to %s \n", prevCPrice, adjustedCPrice), 2); if (adjustedCPrice.getStockLevel() < 0) LogWriter.println("Global stocks are below zero carbon, " + timestep.getYear(), 2); @@ -193,7 +195,7 @@ public class InternationalMarket { GlobalPrice prevTPrice = woodPrices.get(woodType); LogWriter.println(timestep.getYear() + " Updating " + woodType.getName() + " price", 2); GlobalPrice adjustedTPrice = prevTPrice.createWithUpdatedMarketPrices(totalWoodImport, totalWoodExport, - timestep, totalWoodProduction, true); + timestep, totalWoodProduction, true, 0.2); LogWriter.println( String.format("Price for wood updated from %s to %s \n", prevTPrice, adjustedTPrice), 2); if (adjustedTPrice.getStockLevel() < 0) LogWriter.println("Global stocks are below zero wood, " + timestep.getYear(), 2); diff --git a/src/ac/ed/lurg/ModelConfig.java b/src/ac/ed/lurg/ModelConfig.java index 61b8b573..92f5e57c 100755 --- a/src/ac/ed/lurg/ModelConfig.java +++ b/src/ac/ed/lurg/ModelConfig.java @@ -417,8 +417,9 @@ 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_LAMBA = getDoubleProperty("MARKET_LAMBA", 0.4); // controls international market price adjustment rate - public static final boolean PRICE_UPDATE_BY_MARKET_IMBALANCE = getBooleanProperty("PRICE_UPDATE_BY_MARKET_IMBALANCE", false);; + public static final double MARKET_LAMBDA = getDoubleProperty("MARKET_LAMBA", 0.4); // controls international market price adjustment rate + public static final boolean PRICE_UPDATE_BY_MARKET_IMBALANCE = getBooleanProperty("PRICE_UPDATE_BY_MARKET_IMBALANCE", false); + public static final boolean PRICE_UPDATE_BY_STOCK_USE_RATIO = getBooleanProperty("PRICE_UPDATE_BY_STOCK_USE_RATIO", true);; public static final double MAX_PRICE_INCREASE = getDoubleProperty("MAX_PRICE_INCREASE", 1.5); public static final double MAX_PRICE_DECREASE = getDoubleProperty("MAX_PRICE_DECREASE", .75); public static final int DEMAND_RECALC_MAX_ITERATIONS = IS_CALIBRATION_RUN ? 0 : getIntProperty("DEMAND_RECALC_MAX_ITERATIONS", 1); // 0 is original behaviour diff --git a/src/ac/ed/lurg/country/CountryAgent.java b/src/ac/ed/lurg/country/CountryAgent.java index 81d7aca6..625e3398 100644 --- a/src/ac/ed/lurg/country/CountryAgent.java +++ b/src/ac/ed/lurg/country/CountryAgent.java @@ -164,7 +164,7 @@ public class CountryAgent extends AbstractCountryAgent { RasterSet<WoodYieldItem> woodYieldData, RasterSet<CarbonFluxItem> carbonFluxData, Map<LccKey, Double> conversionCosts) { Map<CropType, TradeConstraint> importConstraints = new HashMap<CropType, TradeConstraint>(); - Map<CropType, Double> currentExportRestictions = (exportRestrictions == null) ? null : exportRestrictions.get(currentTimestep.getYear()); + Map<CropType, Double> currentExportRestrictions = (exportRestrictions == null) ? null : exportRestrictions.get(currentTimestep.getYear()); for (Map.Entry<CropType, CropUsageData> entry : previousGamsRasterOutput.getCropUsageData().entrySet()) { CropUsageData cropUsage = entry.getValue(); @@ -175,13 +175,28 @@ public class CountryAgent extends AbstractCountryAgent { // max of supply overall, needed for when imports are supplying feed and change is zero to allow for production to replace imports double maxOfProdOrSupply = cropUsage.getProductionExpected() + Math.max(baseTrade, 0); //max of supply for food - + double production = cropUsage.getProductionExpected(); + double netSupply = getCropUsageData().get(crop).getNetSupply(); + GlobalPrice cropPrice = currentWorldPrices.get(crop); + if (ModelConfig.IS_CALIBRATION_RUN && currentTimestep.getTimestep() <= ModelConfig.END_FIRST_STAGE_CALIBRATION) { changeUp = changeDown = 0; } else { if (crop.isImportedCrop()) { - changeUp = maxOfProdOrSupply * currentWorldPrices.get(crop).getMaxImportChange(); - changeDown = maxOfProdOrSupply * currentWorldPrices.get(crop).getMaxExportChange(); + // exporter or not fully reliant on exports. Avoid forcing import reliant countries to reduce imports + if (baseTrade < 0 || production > baseTrade) { + double tradeImbalanceFract = cropPrice.getTradeImbalanceFraction(); + // If in danger of stocks becoming negative, shift towards more exports + if (cropPrice.isStockCritical()) { + baseTrade = baseTrade - Math.abs(baseTrade) * tradeImbalanceFract * 1.1; + changeUp = 0; + } else { + changeUp = maxOfProdOrSupply * ModelConfig.MAX_IMPORT_CHANGE; + } + } else { + changeUp = maxOfProdOrSupply * ModelConfig.MAX_IMPORT_CHANGE; + } + changeDown = maxOfProdOrSupply * ModelConfig.MAX_IMPORT_CHANGE; } } @@ -190,8 +205,8 @@ public class CountryAgent extends AbstractCountryAgent { } double restiction = 0; - if (currentExportRestictions != null) { - Double r = currentExportRestictions.get(crop); + if (currentExportRestrictions != null) { + Double r = currentExportRestrictions.get(crop); if (r != null) restiction = r; } diff --git a/src/ac/ed/lurg/country/GlobalPrice.java b/src/ac/ed/lurg/country/GlobalPrice.java index 8f9ee722..8d0d9067 100644 --- a/src/ac/ed/lurg/country/GlobalPrice.java +++ b/src/ac/ed/lurg/country/GlobalPrice.java @@ -16,12 +16,15 @@ public class GlobalPrice implements Serializable { private double exportAmountBeforeLoss; private double transportLosses; private double stockLevel; + private double stockUseRatio; private double referencePrice; - private GlobalPrice(Timestep timestep, double exportPrice, double stockLevel, double importAmount, double exportAmountBeforeLoss, double transportLosses, double referencePrice) { + private GlobalPrice(Timestep timestep, double exportPrice, double stockLevel, double importAmount, double exportAmountBeforeLoss, + double transportLosses, double referencePrice, double stockUseRatio) { this.timestep = timestep; this.exportPrice = exportPrice; this.stockLevel = stockLevel; + this.stockUseRatio = stockUseRatio; this.importAmount = importAmount; this.exportAmountBeforeLoss = exportAmountBeforeLoss; this.transportLosses = transportLosses; @@ -30,7 +33,8 @@ public class GlobalPrice implements Serializable { } public static GlobalPrice createInitial(double exportPrice, double intitalStock) { - return new GlobalPrice(new Timestep(ModelConfig.START_TIMESTEP-1), exportPrice, intitalStock, Double.NaN, Double.NaN, Double.NaN, Double.NaN); + return new GlobalPrice(new Timestep(ModelConfig.START_TIMESTEP-1), exportPrice, intitalStock, Double.NaN, + Double.NaN, Double.NaN, Double.NaN, Double.NaN); } public double getExportPrice(){ @@ -68,7 +72,8 @@ public class GlobalPrice implements Serializable { return transportLosses; } - public GlobalPrice createWithUpdatedMarketPrices(double newImports, double newExportAmountBeforeLoss, Timestep thisTimeStep, double production, boolean hasTransportLosses) { + public GlobalPrice createWithUpdatedMarketPrices(double newImports, double newExportAmountBeforeLoss, Timestep thisTimeStep, + double production, boolean hasTransportLosses, double targetStockUseRatio) { if (newImports > 0 || newExportAmountBeforeLoss > 0) { double oldDiff = timestep.equals(thisTimeStep) ? exportAmountBeforeLoss - transportLosses - importAmount : 0.0; // if recomputing for same year need to back our previous adjustment @@ -81,6 +86,9 @@ public class GlobalPrice implements Serializable { updatedStock = stockLevel; // don't update stock in inital stage of calibration else updatedStock = stockLevel + stockChange; + + double targetStock = (production + newImports - newExportAmountBeforeLoss) * targetStockUseRatio; + double updatedStockUseRatio = production > 0 ? updatedStock / production : 0; LogWriter.println(String.format(" imports %.2f, exports %.2f", newImports, newExportAmountBeforeLoss - newTransportLosses), 2); LogWriter.println(String.format(" updatedStock %.2f, previous %.2f (last timestep %.2f), stockChange %.2f, oldDiff %.2f", updatedStock, stockLevel, stockLevel-oldDiff, stockChange, oldDiff), 2); @@ -92,16 +100,27 @@ public class GlobalPrice implements Serializable { } else if (ModelConfig.PRICE_UPDATE_BY_MARKET_IMBALANCE) { double ratio = stockChange/(production-stockChange); - adjustment = Math.exp(-ratio * ModelConfig.MARKET_LAMBA); + adjustment = Math.exp(-ratio * ModelConfig.MARKET_LAMBDA); LogWriter.println(String.format(" initally imbalance ratio= %.4f", ratio), 2); } + else if (ModelConfig.PRICE_UPDATE_BY_STOCK_USE_RATIO) { + // If stock increasing and current stock above target stock : decrease price + // If stock decreasing and current stock below target stock : increase price + // Otherwise keep price constant to refill/deplete stock to reach target level + if ((stockChange > 0 && updatedStock > targetStock) || (stockChange < 0 && updatedStock < targetStock)) { + adjustment = 1 - ModelConfig.MARKET_LAMBDA * stockChange / targetStock; + } else { + adjustment = 1; + } + LogWriter.print(String.format(" initally adjustment= %.4f", adjustment), 2); + } else { if (stockChange == 0 && production == 0) adjustment=1; else if (stockChange >= 0) // stock increasing, so decrease price. stockChange is positive so adjustment < 1 - adjustment = 1 - ModelConfig.MARKET_LAMBA * stockChange/production; + adjustment = 1 - ModelConfig.MARKET_LAMBDA * stockChange/production; else // stock decreasing, so increase price. stockChange is negative so adjustment > 1 - adjustment = 1 - ModelConfig.MARKET_LAMBA * stockChange/Math.max(0, stockLevel); + adjustment = 1 - ModelConfig.MARKET_LAMBDA * stockChange/Math.max(0, stockLevel); LogWriter.print(String.format(" initally adjustment= %.4f", adjustment), 2); } @@ -117,7 +136,8 @@ public class GlobalPrice implements Serializable { double newPrice = exportPrice * adjustment; double refPrice = ModelConfig.IS_CALIBRATION_RUN ? exportPrice : referencePrice; // during calibration reference price isn't fixed, but it is after that - return new GlobalPrice(thisTimeStep, newPrice, updatedStock, newImports, newExportAmountBeforeLoss, newTransportLosses, refPrice); + return new GlobalPrice(thisTimeStep, newPrice, updatedStock, newImports, newExportAmountBeforeLoss, + newTransportLosses, refPrice, updatedStockUseRatio); } else { LogWriter.printlnError(String.format("Price for not updated (still %s), as no imports and no exports for it", exportPrice)); @@ -140,7 +160,8 @@ public class GlobalPrice implements Serializable { } public GlobalPrice createPriceAdjustedByFactor(double factor) { - return new GlobalPrice(timestep, exportPrice * factor, stockLevel, importAmount, exportAmountBeforeLoss, transportLosses, referencePrice); + return new GlobalPrice(timestep, exportPrice * factor, stockLevel, importAmount, exportAmountBeforeLoss, + transportLosses, referencePrice, stockUseRatio); } public void resetStock(Double stock) { @@ -158,14 +179,27 @@ public class GlobalPrice implements Serializable { } public double getMaxImportChange() { - if (stockLevel <= 0) { - return 0; - } else { - return calcTradeMaxChange(importAmount / exportAmountBeforeLoss); - } + //return calcTradeMaxChange(importAmount / exportAmountBeforeLoss); + return stockLevel > importAmount ? Math.min(stockLevel / importAmount - 1, ModelConfig.MAX_IMPORT_CHANGE) : 0; } public double getMaxExportChange() { - return calcTradeMaxChange(exportAmountBeforeLoss/ importAmount); + //return calcTradeMaxChange(exportAmountBeforeLoss/ importAmount); + return (exportAmountBeforeLoss > 1.2 * importAmount) ? 0 : ModelConfig.MAX_IMPORT_CHANGE; + } + + public boolean isStockCritical() { + return stockLevel < importAmount && exportAmountBeforeLoss < importAmount * 1.2; + } + + public double getTradeImbalanceFraction() { + double netImports = importAmount - exportAmountBeforeLoss; + double tradeVolume = importAmount + exportAmountBeforeLoss; + return netImports / tradeVolume; } + + public double getImportToExportRatio() { + return exportAmountBeforeLoss > 0 ? importAmount / exportAmountBeforeLoss : 0; + } + } \ No newline at end of file diff --git a/src/ac/ed/lurg/landuse/CropUsageData.java b/src/ac/ed/lurg/landuse/CropUsageData.java index 2f42b7ee..9b371b6d 100644 --- a/src/ac/ed/lurg/landuse/CropUsageData.java +++ b/src/ac/ed/lurg/landuse/CropUsageData.java @@ -83,4 +83,8 @@ public class CropUsageData implements Serializable { public double getArea(){ return area; } + + public double getNetSupply() { + return prod + netImportsExpected; + } } \ No newline at end of file diff --git a/src/ac/ed/lurg/types/CropType.java b/src/ac/ed/lurg/types/CropType.java index 3595e090..4441e9f7 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.43), - MAIZE("MaizeMilletSorghum", "maize", true, false, 5.1, ModelConfig.INITAL_PRICE_MAIZE, 0.34), - RICE("Rice (Paddy Equivalent)", "rice", true, false, 8.3, ModelConfig.INITAL_PRICE_RICE, 0.46), - OILCROPS("Oilcrops", "oilcrops", true, false, 4.4, ModelConfig.INITAL_PRICE_OILCROPS, 0.24), - PULSES("Pulses", "pulses", true, false, 10.8, ModelConfig.INITAL_PRICE_PULSES, 0.37), - STARCHY_ROOTS("Starchy Roots", "starchyRoots", true, false, 14.3, ModelConfig.INITAL_PRICE_STARCHYROOTS, 0.13), + 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), 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.20), - RUMINANTS("Ruminants", "ruminants", true, true, 2.2, ModelConfig.INITAL_PRICE_RUMINANTS, 0.062), - FRUITVEG("FruitVeg", "fruitveg", true, false, 8.9, ModelConfig.INITAL_PRICE_FRUITVEG, 0.032), - SUGAR("Sugar", "sugar", true, false, 0.1, ModelConfig.INITAL_PRICE_SUGAR, 0.41), + 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), 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; -- GitLab