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