From 331f3aa424c60a93ee64577bb674b81057d4e227 Mon Sep 17 00:00:00 2001
From: Bart Arendarczyk <s1924442@ed.ac.uk>
Date: Wed, 12 Jun 2024 18:40:45 +0100
Subject: [PATCH] Clustering confined to FPUs.

---
 src/ac/ed/lurg/ModelConfig.java               |  8 +-
 src/ac/ed/lurg/country/CountryAgent.java      | 73 +++++++++++--------
 .../ed/lurg/country/CountryAgentManager.java  |  3 +-
 src/ac/ed/lurg/landuse/FPUManager.java        |  9 +++
 src/ac/ed/lurg/landuse/Fpu.java               |  4 +-
 .../ed/lurg/landuse/IrrigationRasterSet.java  |  4 +
 src/ac/ed/lurg/yield/YieldClusterPoint.java   | 29 ++++----
 7 files changed, 79 insertions(+), 51 deletions(-)

diff --git a/src/ac/ed/lurg/ModelConfig.java b/src/ac/ed/lurg/ModelConfig.java
index c65c609f..ce1e7c6c 100755
--- a/src/ac/ed/lurg/ModelConfig.java
+++ b/src/ac/ed/lurg/ModelConfig.java
@@ -284,7 +284,7 @@ public class ModelConfig {
 	public static final double PROD_COST_MAIZE = getDoubleProperty("PROD_COST_MAIZE", 0.3);
 	public static final double PROD_COST_RICE = getDoubleProperty("PROD_COST_RICE", 0.8);
 	public static final double PROD_COST_OILCROPS_NFIX = getDoubleProperty("PROD_COST_OILCROPS_NFIX", 0.4);
-	public static final double PROD_COST_OILCROPS_OTHER = getDoubleProperty("PROD_COST_OILCROPS_OTHER", 0.8);
+	public static final double PROD_COST_OILCROPS_OTHER = getDoubleProperty("PROD_COST_OILCROPS_OTHER", 0.25);
 	public static final double PROD_COST_PULSES = getDoubleProperty("PROD_COST_PULSES", 0.6);
 	public static final double PROD_COST_STARCHYROOTS = getDoubleProperty("PROD_COST_STARCHYROOTS", 3.0);
 	public static final double PROD_COST_MONOGASTRICS = getDoubleProperty("PROD_COST_MONOGASTRICS", 0.05);
@@ -515,7 +515,7 @@ public class ModelConfig {
 	public static final int LPJG_TIMESTEP_SIZE = getIntProperty("LPJG_TIMESTEP_SIZE", 5);
 	public static final int LPJ_YEAR_OFFSET = getIntProperty("LPJ_YEAR_OFFSET", -1);
 	
-	public static final int NUM_YIELD_CLUSTERS = getIntProperty("NUM_YIELD_CLUSTERS", 2000);
+	public static final int NUM_YIELD_CLUSTERS = getIntProperty("NUM_YIELD_CLUSTERS", 50);
 	public static final long RANDOM_SEED = getIntProperty("RANDOM_SEED", 1974329);  // any number will do
 	
 	public static final String CHECKPOINT_YEARS = getProperty("CHECKPOINT_YEARS", null); // 2020,2050
@@ -566,8 +566,8 @@ public class ModelConfig {
 	public static final double INIT_WOOD_STOCK = getDoubleProperty("INIT_WOOD_STOCK", 1200.0); // million m3
 	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 FOREST_MANAGEMENT_COST = IS_FORESTRY_ON ? getDoubleProperty("FOREST_MANAGEMENT_COST", 2.0) : 0.0; // establishment, management etc. $1000/ha
-	public static final double WOOD_TRADE_BARRIER = getDoubleProperty("WOOD_TRADE_BARRIER", 0.1); //$1000/m3
-	public static final double INIT_WOOD_PRICE = IS_FORESTRY_ON ? getDoubleProperty("INIT_WOOD_PRICE", 0.04) : 0.0; // $1000/m3
+	public static final double WOOD_TRADE_BARRIER = getDoubleProperty("WOOD_TRADE_BARRIER", 0.2); //$1000/m3
+	public static final double INIT_WOOD_PRICE = IS_FORESTRY_ON ? getDoubleProperty("INIT_WOOD_PRICE", 0.03) : 0.0; // $1000/m3
 	public static final int CARBON_WOOD_MAX_TIME = getIntProperty("CARBON_WOOD_MAX_TIME", 160); // upper data limit, years
 	
 	// Carbon
diff --git a/src/ac/ed/lurg/country/CountryAgent.java b/src/ac/ed/lurg/country/CountryAgent.java
index 8df574a9..1fc62f44 100644
--- a/src/ac/ed/lurg/country/CountryAgent.java
+++ b/src/ac/ed/lurg/country/CountryAgent.java
@@ -62,63 +62,76 @@ public class CountryAgent extends AbstractCountryAgent {
 	}
 
 	private RasterSet<IntegerRasterItem> calcYieldClusters(RasterSet<IrrigationItem> irrigData, YieldRaster countryYieldSurfaces,
-														   RasterSet<WoodYieldItem> woodYieldData, RasterSet<SolarPotentialItem> solarData) {
+														   RasterSet<WoodYieldItem> woodYieldData, RasterSet<SolarPotentialItem> solarData,
+														   RasterSet<Fpu> fpuRasterSet) {
 
-		LogWriter.println("calcYieldClusters: for " + ModelConfig.NUM_YIELD_CLUSTERS + " clusters", 3);	
-		
-		// create collection of ClusteringPoints from countryYieldSurfaces, these have the RasterKey and data for yield (or access to them)
-		HashSet<YieldClusterPoint> clusteringPoints = new HashSet<YieldClusterPoint>();
-		for (Entry<RasterKey, YieldResponsesItem> entry : countryYieldSurfaces.entrySet()) {
-			YieldResponsesItem yieldresp = entry.getValue();
-			if (!yieldresp.isMissing()) {
-				RasterKey key = entry.getKey();
+		LogWriter.println("calcYieldClusters: for " + ModelConfig.NUM_YIELD_CLUSTERS + " clusters", 3);
+
+		Map<Fpu, Set<RasterKey>> fpuMap = new HashMap<>();
+		for (Map.Entry<RasterKey, Fpu> entry : fpuRasterSet.entrySet()) {
+			Set<RasterKey> keySet = fpuMap.computeIfAbsent(entry.getValue(), k -> new HashSet<>());
+			keySet.add(entry.getKey());
+		}
+
+		RasterSet<IntegerRasterItem> mapping = new RasterSet<IntegerRasterItem>(countryYieldSurfaces.getHeaderDetails());
+
+		int clusterId = 1;
+		// Calculate clusters within each FPU
+		for (Fpu fpu : fpuMap.keySet()) {
+			Set<RasterKey> keySet = fpuMap.get(fpu);
+			HashSet<YieldClusterPoint> clusteringPoints = new HashSet<YieldClusterPoint>();
+
+			// create clustering points
+			for (RasterKey key : keySet) {
+				if (countryYieldSurfaces.get(key).isMissing()) {
+					continue; // missing yields, skipping. Later we put all cells with missing yield into one cluster
+				}
+
+				YieldResponsesItem yieldresp = countryYieldSurfaces.get(key);
 				IrrigationItem irrigItem = irrigData.get(key);
-				
 				LandUseItem luItem = previousGamsRasterOutput.getLandUses().get(key);
 				double totalLand = luItem.getTotalLandArea()-luItem.getLandCoverArea(LandCoverType.URBAN);
 				double protectedAreaFrac = (totalLand <= 0) ? 0.0 : luItem.getLandCoverArea(LandProtectionType.PROTECTED) / totalLand;
-				
 				WoodYieldItem wyItem = woodYieldData.get(key);
 				SolarPotentialItem solarItem = solarData.get(key);
-				
+
 				clusteringPoints.add(new YieldClusterPoint(key, yieldresp, irrigItem, protectedAreaFrac, wyItem, solarItem));
 			}
-		}
 
-		// do the clustering
-		KMeans<String, YieldClusterPoint> kmeans = new KMeans<String, YieldClusterPoint>(clusteringPoints, ModelConfig.NUM_YIELD_CLUSTERS);
-		kmeans.calculateClusters(100, 0.1);
-		kmeans.printClusters();
+			// do the clustering
+			KMeans<String, YieldClusterPoint> kmeans = new KMeans<String, YieldClusterPoint>(clusteringPoints, ModelConfig.NUM_YIELD_CLUSTERS);
+			kmeans.calculateClusters(100, 0.1);
 
-		// reformat output
-		List<Cluster<String, YieldClusterPoint>> yieldClusters = kmeans.getClusters();
-		RasterSet<IntegerRasterItem> mapping = new RasterSet<IntegerRasterItem>(countryYieldSurfaces.getHeaderDetails());
-		
-		int id = 1;
-		for (Cluster<String, YieldClusterPoint> c : yieldClusters) {
-			if (!c.getPoints().isEmpty()) {
-				for (YieldClusterPoint p : c.getPoints()) {
-					mapping.put(p.getRasterKey(), new IntegerRasterItem(id));
+			// reformat output
+			List<Cluster<String, YieldClusterPoint>> yieldClusters = kmeans.getClusters();
+
+			for (Cluster<String, YieldClusterPoint> c : yieldClusters) {
+				if (!c.getPoints().isEmpty()) {
+					for (YieldClusterPoint p : c.getPoints()) {
+						mapping.put(p.getRasterKey(), new IntegerRasterItem(clusterId));
+					}
+					clusterId++;
 				}
-				id++;
 			}
 		}
 
 		// Add cells with missing yields to one cluster
 		for (RasterKey key : countryYieldSurfaces.keySet()) {
 			if (countryYieldSurfaces.get(key).isMissing()) {
-				IntegerRasterItem item = new IntegerRasterItem(id);
+				IntegerRasterItem item = new IntegerRasterItem(clusterId);
 				mapping.put(key, item);
 			}
 		}
 
+		LogWriter.println(String.format("%s calculated %d clusters", country, clusterId));
+
 		return mapping;
 	}
 	
 	public GamsRasterOutput determineProduction(YieldRaster countryYieldSurfaces, RasterSet<IrrigationItem> irrigData, 
 			Map<CropType, GlobalPrice> worldPrices, RasterSet<CarbonFluxItem> carbonFluxData,
 			RasterSet<WoodYieldItem> woodYieldData, RasterSet<SolarPotentialItem> solarPotentialData,
-			GlobalPrice carbonPrice, GlobalPrice woodPrice) {
+			GlobalPrice carbonPrice, GlobalPrice woodPrice, RasterSet<Fpu> fpuRasterSet) {
 
 		// project demand
 		calculateCountryPricesAndDemand(worldPrices, carbonPrice, woodPrice, true);
@@ -140,7 +153,7 @@ public class CountryAgent extends AbstractCountryAgent {
 		}
 		else {			
 			if (yieldClusters==null) {
-				yieldClusters = calcYieldClusters(irrigData, countryYieldSurfaces, woodYieldData, solarPotentialData);  // this should only be on the first timestep
+				yieldClusters = calcYieldClusters(irrigData, countryYieldSurfaces, woodYieldData, solarPotentialData, fpuRasterSet);  // this should only be on the first timestep
 			}
 			// optimize areas and intensity 
 			GamsRasterInput input = getGamsRasterInput(irrigData, countryYieldSurfaces, woodYieldData, carbonFluxData, solarPotentialData);
diff --git a/src/ac/ed/lurg/country/CountryAgentManager.java b/src/ac/ed/lurg/country/CountryAgentManager.java
index 2aa9af4a..47848214 100644
--- a/src/ac/ed/lurg/country/CountryAgentManager.java
+++ b/src/ac/ed/lurg/country/CountryAgentManager.java
@@ -145,6 +145,7 @@ public class CountryAgentManager {
 			RasterSet<IrrigationItem> irrigData = currentIrrigationData.createSubsetForKeys(countryKeys);
 			RasterSet<CarbonFluxItem> carbonFluxData = currentCarbonFluxData.createSubsetForKeys(countryKeys);
 			RasterSet<WoodYieldItem> woodYieldData = currentWoodYieldData.createSubsetForKeys(countryKeys);
+			RasterSet<Fpu> fpuRasterSet = currentIrrigationData.getFpuRasterSet().createSubsetForKeys(countryKeys);
 			
 			// Need to instantiate Runnable object for multithreading
 			execService.execute(new Runnable() {
@@ -153,7 +154,7 @@ public class CountryAgentManager {
 
 					try {
 						ca.determineProduction(countryYieldSurfaces, irrigData, internationalMarket.getWorldPrices(), carbonFluxData, woodYieldData, 
-								solarPotentialData, internationalMarket.getCarbonPrice(), internationalMarket.getWoodPrice());
+								solarPotentialData, internationalMarket.getCarbonPrice(), internationalMarket.getWoodPrice(), fpuRasterSet);
 
 						// update global rasters
 						globalLandUseRaster.putAll(ca.getLandUses());
diff --git a/src/ac/ed/lurg/landuse/FPUManager.java b/src/ac/ed/lurg/landuse/FPUManager.java
index 12620562..3838c027 100644
--- a/src/ac/ed/lurg/landuse/FPUManager.java
+++ b/src/ac/ed/lurg/landuse/FPUManager.java
@@ -25,11 +25,14 @@ public class FPUManager {
 	private final StringTabularReader fpuGroups;
 	private final Map<WaterBasin, List<RasterKey>> basinToRasterKeysMap = new HashMap<>();
 	private final Map<WaterBasin, Map<Integer, Double>> basinToOtherUses = new HashMap<>();
+	private final RasterSet<Fpu> fpuMap; // for clustering
 
 	public FPUManager(RasterHeaderDetails desiredProjection) {		
 		fpuGroups = new StringTabularReader(",", new String[]{"fpu", "basin"});
 		fpuGroups.read(ModelConfig.FPU_GROUPING_FILE);
 
+		fpuMap = new RasterSet<>(desiredProjection);
+
 		// Handle rasterkeys
 		Map<Fpu, List<RasterKey>> fpuToRasterKeys = getMapFpuToKeys(desiredProjection);		
 		for (Map.Entry<Fpu, List<RasterKey>> entry : fpuToRasterKeys.entrySet()) {
@@ -90,6 +93,8 @@ public class FPUManager {
 			List<RasterKey> rasterKeys = invertedBoundaries.computeIfAbsent(fpu, k -> new ArrayList<>());
 
 			rasterKeys.add(entry.getKey());
+
+			fpuMap.put(entry.getKey(), fpu);
  		}
 
 		return invertedBoundaries;
@@ -147,4 +152,8 @@ public class FPUManager {
 	public Collection<WaterBasin> getWaterBasins() {
 		return basinToRasterKeysMap.keySet();
 	}
+
+	public RasterSet<Fpu> getFpuRasterSet() {
+		return fpuMap;
+	}
 }
\ No newline at end of file
diff --git a/src/ac/ed/lurg/landuse/Fpu.java b/src/ac/ed/lurg/landuse/Fpu.java
index 5164b04a..0c3b031a 100644
--- a/src/ac/ed/lurg/landuse/Fpu.java
+++ b/src/ac/ed/lurg/landuse/Fpu.java
@@ -1,8 +1,10 @@
 package ac.ed.lurg.landuse;
 
+import ac.sac.raster.RasterItem;
+
 /** This is currently really just a marker class to aid type safety, using Integer directly would have worked, but been less clear*/
 
-public class Fpu {
+public class Fpu implements RasterItem {
 	private Integer id;
 	
 	Fpu(Integer id) {
diff --git a/src/ac/ed/lurg/landuse/IrrigationRasterSet.java b/src/ac/ed/lurg/landuse/IrrigationRasterSet.java
index 621a579e..fe896bb4 100644
--- a/src/ac/ed/lurg/landuse/IrrigationRasterSet.java
+++ b/src/ac/ed/lurg/landuse/IrrigationRasterSet.java
@@ -88,4 +88,8 @@ public class IrrigationRasterSet extends RasterSet<IrrigationItem> {
 		}
 		return lpjIrrigConstraint;
 	}
+
+	public RasterSet<Fpu> getFpuRasterSet() {
+		return fpuManager.getFpuRasterSet();
+	}
 }
diff --git a/src/ac/ed/lurg/yield/YieldClusterPoint.java b/src/ac/ed/lurg/yield/YieldClusterPoint.java
index c3560663..083df7a4 100644
--- a/src/ac/ed/lurg/yield/YieldClusterPoint.java
+++ b/src/ac/ed/lurg/yield/YieldClusterPoint.java
@@ -1,17 +1,16 @@
 package ac.ed.lurg.yield;
 
-import java.util.Arrays;
-import java.util.Collection;
-
 import ac.ed.lurg.forestry.WoodYieldItem;
 import ac.ed.lurg.landuse.IrrigationItem;
 import ac.ed.lurg.solar.SolarPotentialItem;
 import ac.ed.lurg.types.CropType;
-import ac.ed.lurg.types.LandCoverType;
 import ac.ed.lurg.types.YieldType;
 import ac.ed.lurg.utils.cluster.ClusteringPoint;
 import ac.sac.raster.RasterKey;
 
+import java.util.Arrays;
+import java.util.Collection;
+
 public class YieldClusterPoint implements ClusteringPoint<String> {
 
 	private final static String PASTURE = "pas";
@@ -45,17 +44,17 @@ public class YieldClusterPoint implements ClusteringPoint<String> {
 		
 		// not sure if we be better to get a reference to the YieldResponsesItem, rather than caching these values?
 		// multiplying each variable by scaling factor so all are on a similar scale
-		this.wheatMin = yields.getYield(YieldType.NO_FERT_NO_IRRIG, CropType.WHEAT) * 0.1;
-		this.riceMax = yields.getYield(YieldType.FERT_MAX_IRRIG_MAX, CropType.RICE) * 0.1;
-		this.maizeMin = yields.getYield(YieldType.NO_FERT_NO_IRRIG, CropType.MAIZE) * 0.1;
-		this.pasture = yields.getYield(YieldType.NO_FERT_NO_IRRIG, CropType.PASTURE) * 0.1;
-		this.wheatMax = yields.getYield(YieldType.FERT_MAX_IRRIG_MAX, CropType.WHEAT) * 0.1;
-		this.maizeMax = yields.getYield(YieldType.FERT_MAX_IRRIG_MAX, CropType.MAIZE) * 0.1;
-		this.irrigWaterAval = (irrigItem==null) ? 0.0 : irrigItem.getIrrigConstraint() * 0.1;
-		this.protectedArea = protectedArea;
-		this.woodMax = (woodYields==null) ? 0.0 : woodYields.getYieldResponse().getMaxYield() * 0.01;
-		this.woodSlope = (woodYields==null) ? 0.0 : -woodYields.getYieldResponse().getSlope() * 10;
-		this.solarPotential = solarPotentialItem == null ? 0.0 : solarPotentialItem.getPvPotential() * 0.001;
+		this.wheatMin = yields.getYield(YieldType.NO_FERT_NO_IRRIG, CropType.WHEAT);
+		this.riceMax = yields.getYield(YieldType.FERT_MAX_IRRIG_MAX, CropType.RICE);
+		this.maizeMin = yields.getYield(YieldType.NO_FERT_NO_IRRIG, CropType.MAIZE);
+		this.pasture = yields.getYield(YieldType.NO_FERT_NO_IRRIG, CropType.PASTURE);
+		this.wheatMax = yields.getYield(YieldType.FERT_MAX_IRRIG_MAX, CropType.WHEAT);
+		this.maizeMax = yields.getYield(YieldType.FERT_MAX_IRRIG_MAX, CropType.MAIZE);
+		this.irrigWaterAval = irrigItem == null ? 0.0 : irrigItem.getIrrigConstraint();
+		this.protectedArea = protectedArea * 10;
+		this.woodMax = woodYields == null ? 0.0 : woodYields.getYieldResponse().getMaxYield() * 0.1;
+		this.woodSlope = woodYields == null ? 0.0 : -woodYields.getYieldResponse().getSlope();
+		this.solarPotential = solarPotentialItem == null ? 0.0 : solarPotentialItem.getPvPotential() * 0.01;
 	}
 
 	public RasterKey getRasterKey() {
-- 
GitLab