diff --git a/GAMS/IntExtOpt.gms b/GAMS/IntExtOpt.gms
index eccb3b92c69a92622cb724ca144eac08af6f7861..4dc891f2e971fea8f0fa771d576abff138bd8680 100644
--- a/GAMS/IntExtOpt.gms
+++ b/GAMS/IntExtOpt.gms
@@ -59,7 +59,7 @@
  PARAMETER woodYieldMaxParam(location);
  PARAMETER woodYieldSlopeParam(location);
  PARAMETER woodYieldShapeParam(location);
- PARAMETER previousRotationIntensity(location);
+ PARAMETER previousRotationIntensity(protection, location);
  PARAMETER maxLandCoverChange(land_cover_before, land_cover_after, protection, location);
  PARAMETER maxLandCoverArea(land_cover, protection, location);
  
@@ -85,6 +85,8 @@
  SCALAR fertLimConventional;
  SCALAR irrigLimConventional;
  SCALAR otherLimConventional;
+ SCALAR timberLimProtected;
+ SCALAR timberLimUnprotected;
 
  SCALAR forestManagementCost    cost $1000 per ha;
  SCALAR carbonForestMaxProportion maximum proportion of land cover as carbon forest;
@@ -103,7 +105,7 @@ $load fertParam, irrigParam, otherIParam, exportPrices, importPrices, maxNetImpo
 $load meatEfficency, otherICost, irrigCost, irrigConstraintLocation, irrigConstraintFarmingType, irrigMaxRate, fertiliserUnitCost, domesticPriceMarkup, seedAndWasteRate
 $load previousLandCoverArea, carbonCreditRate, conversionCost, woodYieldMaxParam, woodYieldSlopeParam, woodYieldShapeParam, tradeAdjustmentCostRate
 $load forestManagementCost, carbonForestMaxProportion, previousRotationIntensity, maxLandCoverChange, maxLandCoverArea
-$load fertLimRestricted, otherLimRestricted, fertLimConventional, otherLimConventional
+$load fertLimRestricted, otherLimRestricted, fertLimConventional, otherLimConventional, timberLimProtected, timberLimUnprotected
 $load solarEnergyDensity, agrivoltaicsYieldFactor, prodCost, animalProdCost, solarCostRate
 $load discountRate, energyPrice, monoculturePenalty, agrivoltaicsCropCostFactor, previousProduction, pastureIntensityPenalty
 $gdxin
@@ -162,8 +164,8 @@ $gdxin
        landCoverChange(land_cover_before, land_cover_after, protection, location) land cover change in Mha
        totalConversionCost(location)      land cover conversion cost - $1000 per ha
 
-       woodYieldRota(location)              wood yield m3 per ha
-       rotationIntensity(location)          equivalent to 1 divided by rotation period
+       woodYieldRota(protection, location)              wood yield m3 per ha
+       rotationIntensity(protection, location)          equivalent to 1 divided by rotation period
        woodSupply                           total wood supply - million m3
        
        carbonCredits(location)              carbon credits generated - Mt C
@@ -205,7 +207,7 @@ $gdxin
        CONVERSION_COST_CALC(location)                          cost of land cover conversion
        MAX_CROPLAND_CONSTRAINT(protection, location)                       limits on maximum cropland due to slope
 
-       WOOD_YIELD_CALC(location)              wood yield at rotation - tC per ha
+       WOOD_YIELD_CALC(protection, location)              wood yield at rotation - tC per ha
        WOOD_SUPPLY_CALC                       wood supply calculation
        WOOD_DEMAND_CONSTRAINT                 wood demand constraint
 
@@ -310,13 +312,15 @@ $gdxin
  
 ************* Forestry ***********************************
  
- WOOD_YIELD_CALC(location) .. woodYieldRota(location) =E= woodYieldMaxParam(location) * rpower(1 - exp(woodYieldSlopeParam(location) / rotationIntensity(location)), woodYieldShapeParam(location));
+ WOOD_YIELD_CALC(protection, location) .. woodYieldRota(protection, location) =E= woodYieldMaxParam(location) * rpower(1 - exp(woodYieldSlopeParam(location) / rotationIntensity(protection, location)), woodYieldShapeParam(location));
  
 * Upper and lower bounds on rotation intensity
- rotationIntensity.UP(location) = 1 / 10;
- rotationIntensity.LO(location) = 1 / 160;
- 
- WOOD_SUPPLY_CALC .. woodSupply =E= sum(location, woodYieldRota(location) * rotationIntensity(location) * landCoverArea('timberForest', 'unprotected', location));
+
+ rotationIntensity.LO(protection, location) = 1 / 160;
+ rotationIntensity.UP('unprotected', location) = timberLimUnprotected;
+ rotationIntensity.UP('protected', location) = timberLimProtected;
+
+ WOOD_SUPPLY_CALC .. woodSupply =E= sum((protection, location), woodYieldRota(protection, location) * rotationIntensity(protection, location) * landCoverArea('timberForest', protection, location));
 
  WOOD_DEMAND_CONSTRAINT .. woodSupply =E= demand('wood') + exportAmount('wood') - importAmount('wood');
  
@@ -366,7 +370,7 @@ $gdxin
 *                           Cost of imports
                             - sum(import_types, importPrices(import_types) * importAmount(import_types))
 *                           Cost of wood production                           
-                            - sum(location, forestManagementCost * rotationIntensity(location) * landCoverArea('timberForest', 'unprotected', location))
+                            - sum((protection, location), forestManagementCost * rotationIntensity(protection, location) * landCoverArea('timberForest', protection, location))
 *                           Cost of solar energy                           
                             - sum((solar_land, location), solarCostRate(solar_land, location) * landCoverArea(solar_land, 'unprotected', location))                     
 *                           Land cover conversion costs
@@ -400,9 +404,9 @@ $gdxin
  importAmount.L(all_types) = previousImportAmount(all_types);
  exportAmount.L(all_types) = previousExportAmount(all_types);
  netImportAmount.L(import_types) = previousImportAmount(import_types) - previousExportAmount(import_types);
- rotationIntensity.L(location) = previousRotationIntensity(location);
- rotationIntensity.L(location) $ (previousRotationIntensity(location) = 0) = 1 / 160;
- woodYieldRota.L(location) = woodYieldMaxParam(location) * rpower(1 - exp(woodYieldSlopeParam(location) / rotationIntensity.L(location)), woodYieldShapeParam(location));
+ rotationIntensity.L(protection, location) = previousRotationIntensity(protection, location);
+ rotationIntensity.L(protection, location) $ (previousRotationIntensity(protection, location) = 0) = 1 / 160;
+ woodYieldRota.L(protection, location) = woodYieldMaxParam(location) * rpower(1 - exp(woodYieldSlopeParam(location) / rotationIntensity.L(protection, location)), woodYieldShapeParam(location));
  totalProduction.L(agri_types) = previousProduction(agri_types);
  
  option nlp = conopt4;
@@ -447,7 +451,7 @@ $gdxin
                                      +  (1 - importFraction(feed_crop)) * prodCostRate(feed_crop)) + totalProduction.l(animal) * animalProdCost(animal);
 
  If(woodSupply.L > 0,
-    woodProdCost = sum(location, forestManagementCost * rotationIntensity.L(location) * landCoverArea.L('timberForest', 'unprotected', location));
+    woodProdCost = sum((protection, location), forestManagementCost * rotationIntensity.L(protection, location) * landCoverArea.L('timberForest', protection, location));
     );
  
  netCarbonCredits = SUM(location, carbonCredits.L(location));
@@ -463,7 +467,7 @@ $gdxin
 * Diagnostics
  parameter totalLandCoverChange(land_cover, land_cover_after);
  parameter landCoverDiff(land_cover);
- parameter rotationPeriod(location);
+ parameter rotationPeriod(protection, location);
  parameter irrigWaterWithdrawn(crop, location);
  parameter totalIrrig;
  parameter fertiliserUsed(crop);
@@ -474,7 +478,7 @@ $gdxin
  parameter woodProd(location);
  totalLandCoverChange(land_cover, land_cover_after) = sum((protection, location), landCoverChange.L(land_cover, land_cover_after, protection, location));
  landCoverDiff(land_cover) = sum((protection, location), landCoverArea.L(land_cover, protection, location)) - sum((protection, location), previousLandCoverArea(land_cover, protection, location));
- rotationPeriod(location) = 1 / rotationIntensity.L(location);
+ rotationPeriod(protection, location) = 1 / rotationIntensity.L(protection, location);
  irrigWaterWithdrawn(crop, location) = sum(farming_type, irrigMaxRate(crop, location) * irrigI.L(crop, farming_type, location) * cropArea.L(crop, farming_type, location) * 0.01);
  totalIrrig = sum((crop, location), irrigWaterWithdrawn(crop, location));
  fertiliserUsed(crop) = sum((farming_type, location), fertI.L(crop, farming_type, location) * cropArea.L(crop, farming_type, location));
@@ -482,4 +486,4 @@ $gdxin
  abandonedArea(crop) = sum((farming_type, location)$(otherIntensity.L(crop, farming_type, location) = 0), cropArea.L(crop, farming_type, location));
  otherIntUsed(crop, location) = sum(farming_type, otherIntensity.L(crop, farming_type, location) * cropArea.L(crop, farming_type, location));
  totalOtherI = sum((crop, location), otherIntUsed(crop, location));
- woodProd(location) = woodYieldRota.L(location) * landCoverArea.L('timberForest', 'unprotected', location) * rotationIntensity.L(location);
+ woodProd(location) = sum(protection, woodYieldRota.L(protection, location) * landCoverArea.L('timberForest', protection, location) * rotationIntensity.L(protection, location));
diff --git a/src/ac/ed/lurg/ModelConfig.java b/src/ac/ed/lurg/ModelConfig.java
index 7cdd8324aec2396210c20e9f718ac0ed3888d584..fb0edc718ed3f5b16bcde24b24f46704bf1f976e 100755
--- a/src/ac/ed/lurg/ModelConfig.java
+++ b/src/ac/ed/lurg/ModelConfig.java
@@ -573,6 +573,8 @@ public class ModelConfig {
 	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
+	public static final double PROTECTED_TIMBER_INTENSITY_LIMIT = getDoubleProperty("PROTECTED_TIMBER_INTENSITY_LIMIT", 0.02); // = 1/rotation_period
+	public static final double UNPROTECTED_TIMBER_INTENSITY_LIMIT = getDoubleProperty("UNPROTECTED_TIMBER_INTENSITY_LIMIT", 0.1);
 	
 	// Carbon
 	public static final boolean IS_CARBON_ON = !IS_CALIBRATION_RUN && getBooleanProperty("IS_CARBON_ON", false);
diff --git a/src/ac/ed/lurg/country/gams/GamsLocationOptimiser.java b/src/ac/ed/lurg/country/gams/GamsLocationOptimiser.java
index 3a321d8d60d795fc83e912938fad9acffac1d65f..4ec7fa77b190db4ca30dd611a50aca7f96d57fe2 100644
--- a/src/ac/ed/lurg/country/gams/GamsLocationOptimiser.java
+++ b/src/ac/ed/lurg/country/gams/GamsLocationOptimiser.java
@@ -120,7 +120,7 @@ public class GamsLocationOptimiser {
 		GAMSParameter prevLandCoverP = inDB.addParameter("previousLandCoverArea", 3);
 		GAMSParameter maxLandCoverAreaP = inDB.addParameter("maxLandCoverArea", 3);
 
-		GAMSParameter prevRotaIntP = inDB.addParameter("previousRotationIntensity", 1);
+		GAMSParameter prevRotaIntP = inDB.addParameter("previousRotationIntensity", 2);
 
 		@SuppressWarnings("unused")
 		double totalAgriLand = 0;
@@ -207,8 +207,15 @@ public class GamsLocationOptimiser {
 			}
 
 			// Forestry
-			ForestDetail forestDetail = (ForestDetail) landUseItem.getLandCoverDetail(new LandKey(LandCoverType.TIMBER_FOREST, LandProtectionType.UNPROTECTED));
-			setGamsParamValue(prevRotaIntP.addRecord(locString), forestDetail != null ? forestDetail.getRotationIntensity() : 0.02, -1);
+			for (LandKey landKey : landUseItem.getLandKeysForLandCover(LandCoverType.TIMBER_FOREST)) {
+				ForestDetail forestDetail = (ForestDetail) landUseItem.getLandCoverDetail(landKey);
+				Vector<String> v = new Vector<>();
+				v.add(landKey.getLpType().getGamsName());
+				v.add(locString);
+
+				setGamsParamValue(prevRotaIntP.addRecord(v), forestDetail != null ? forestDetail.getRotationIntensity() : 0.02, -1);
+			}
+
 		}
 		
 		if (DEBUG) LogWriter.println(String.format("  Total agricultural %.1f,\t suitable %.1f", totalAgriLand, totalSuitable));
@@ -465,6 +472,8 @@ public class GamsLocationOptimiser {
 		setGamsParamValue(importPriceP.addRecord("wood"), countryInput.getWoodPrice().getImportPrice() , -1);
 		setGamsParamValue(exportPriceP.addRecord("wood"), countryInput.getWoodPrice().getExportPrice(), -1);
 
+		addScalar(inDB, "timberLimProtected", ModelConfig.PROTECTED_TIMBER_INTENSITY_LIMIT, -1);
+		addScalar(inDB, "timberLimUnprotected", ModelConfig.UNPROTECTED_TIMBER_INTENSITY_LIMIT, -1);
 
 		addScalar(inDB, "forestManagementCost",
 				ModelConfig.getAdjParam("FOREST_MANAGEMENT_COST") * calibrationItem.getForestryCostFactor(), -1);
@@ -835,12 +844,14 @@ public class GamsLocationOptimiser {
 		}
 
 		// Rotation intensity
-		Map<Integer, Double> rotationIntensities = new HashMap<>();
+		Map<LandProtectionType, Map<Integer, Double>> rotationIntensities = new HashMap<>();
 		GAMSVariable varRota = outDB.getVariable("rotationIntensity");
 		for (GAMSVariableRecord rec : varRota) {
-			int loc = Integer.parseInt(rec.getKey(0));
+			LandProtectionType lpType = LandProtectionType.getForGamsName(rec.getKey(0));
+			int loc = Integer.parseInt(rec.getKey(1));
 			double intensity = rec.getLevel();
-			rotationIntensities.put(loc, intensity);
+			Map<Integer, Double> locMap = rotationIntensities.computeIfAbsent(lpType, k -> new HashMap<>());
+			locMap.put(loc, intensity);
 		}
 		
 		// Carbon
diff --git a/src/ac/ed/lurg/country/gams/GamsLocationOutput.java b/src/ac/ed/lurg/country/gams/GamsLocationOutput.java
index 8de888c98784cb0df2deebc00308f1850858d0e3..46c51c92dd205f539fec84d1b4866ad9ac2da587 100644
--- a/src/ac/ed/lurg/country/gams/GamsLocationOutput.java
+++ b/src/ac/ed/lurg/country/gams/GamsLocationOutput.java
@@ -4,6 +4,7 @@ import java.util.List;
 import java.util.Map;
 
 import ac.ed.lurg.landuse.*;
+import ac.ed.lurg.types.LandProtectionType;
 import com.gams.api.GAMSGlobals.ModelStat;
 
 import ac.ed.lurg.types.CropType;
@@ -17,14 +18,14 @@ public class GamsLocationOutput {
 	Map<Integer, List<LandCoverChangeItem>> landCoverChanges;
 	private CarbonUsageData carbonUsageData;
 	private WoodUsageData woodUsageData;
-	private Map<Integer, Double> rotationIntensities;
+	private Map<LandProtectionType, Map<Integer, Double>> rotationIntensities;
 	
 	public GamsLocationOutput(ModelStat status,
                               Map<Integer, LandUseItem> landUses,
                               Map<CropType, CropUsageData> cropUsageData,
                               Map<Integer, List<LandCoverChangeItem>> landCoverChanges,
                               CarbonUsageData carbonUsageData, WoodUsageData woodUsageData,
-                              Map<Integer, Double> rotationIntensities) {
+							  Map<LandProtectionType, Map<Integer, Double>> rotationIntensities) {
 		super();
 		this.status = status;
 		this.landUses = landUses;
@@ -58,7 +59,7 @@ public class GamsLocationOutput {
 		return woodUsageData;
 	}
 
-	public Map<Integer, Double> getRotationIntensities() {
+	public Map<LandProtectionType, Map<Integer, Double>> getRotationIntensities() {
 		return rotationIntensities;
 	}
 }
diff --git a/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java b/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java
index 4d7495fb6c79c0c55ef65f15af2786076cc44e96..cbfb7b77a111eac708bd6b4d9eece42a57e29311 100644
--- a/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java
+++ b/src/ac/ed/lurg/country/gams/GamsRasterOptimiser.java
@@ -89,13 +89,15 @@ public class GamsRasterOptimiser {
 		}
 
 		// Forest rotation intensity and yields
-		Map<Integer, Double> rotaIntensities = gamsOutput.getRotationIntensities();
+		Map<LandProtectionType, Map<Integer, Double>> rotaIntensities = gamsOutput.getRotationIntensities();
 		for (RasterKey key : newLandUseRaster.keySet()) {
 			LandUseItem luItem = newLandUseRaster.get(key);
 			Integer locId = mapping.get(key).getInt();
-			double rotaInt = rotaIntensities.getOrDefault(locId, 0.0);
-			ForestDetail detail = (ForestDetail) luItem.getLandCoverDetail(new LandKey(LandCoverType.TIMBER_FOREST, LandProtectionType.UNPROTECTED));
-			detail.setRotationIntensity(rotaInt);
+			for (LandProtectionType lpType : LandProtectionType.values()) {
+				double rotaInt = rotaIntensities.containsKey(lpType) ? rotaIntensities.get(lpType).getOrDefault(locId, 0.0) : 0.0;
+				ForestDetail detail = (ForestDetail) luItem.getLandCoverDetail(new LandKey(LandCoverType.TIMBER_FOREST, lpType));
+				detail.setRotationIntensity(rotaInt);
+			}
 		}
 
 		// Crop areas and intensity
diff --git a/src/ac/ed/lurg/forestry/ForestryDataOutputer.java b/src/ac/ed/lurg/forestry/ForestryDataOutputer.java
index 74a0337a12cafb2f6636b6927e96221612e1f4cc..2a248ff6eddbce7f17a22b2e9af019c808959bd6 100644
--- a/src/ac/ed/lurg/forestry/ForestryDataOutputer.java
+++ b/src/ac/ed/lurg/forestry/ForestryDataOutputer.java
@@ -32,7 +32,7 @@ public class ForestryDataOutputer extends AbstractLandUseOutputer {
 		BufferedWriter outputWriter = null;
 		try {	
 			outputWriter = new BufferedWriter(new FileWriter(outputFileName, false));
-			outputWriter.write("Lon Lat RotationIntensity TimberForestArea Yield");
+			outputWriter.write("Lon Lat Protection RotationIntensity TimberForestArea Yield");
 			outputWriter.newLine();
 	
 			for (Map.Entry<RasterKey, LandUseItem> entry : landUseRaster.entrySet()) {
@@ -46,18 +46,19 @@ public class ForestryDataOutputer extends AbstractLandUseOutputer {
 
 				double lat = landUseRaster.getXCoordin(key);
 				double lon = landUseRaster.getYCoordin(key);
-				LandKey landKey = new LandKey(LandCoverType.TIMBER_FOREST, LandProtectionType.UNPROTECTED);
-				ForestDetail forestDetail = (ForestDetail) luItem.getLandCoverDetail(landKey);
-				double rotaInt = forestDetail.getRotationIntensity();
-				double area = luItem.getLandCoverArea(landKey);
-				if (area == 0) { // skip if no forest
-					continue;
-				}
-				double yield = wyItem.getYield(rotaInt);
 
-				outputWriter.write(String.format("%.2f %.2f %.14f %.14f %.14f", lat, lon, rotaInt, area, yield));
-				outputWriter.newLine();
-				
+				for (LandKey landKey : luItem.getLandKeysForLandCover(LandCoverType.TIMBER_FOREST)) {
+					ForestDetail forestDetail = (ForestDetail) luItem.getLandCoverDetail(landKey);
+					double rotaInt = forestDetail.getRotationIntensity();
+					double area = luItem.getLandCoverArea(landKey);
+					if (area == 0) { // skip if no forest
+						continue;
+					}
+					double yield = wyItem.getYield(rotaInt);
+
+					outputWriter.write(String.format("%.2f %.2f %s %.14f %.14f %.14f", lat, lon, landKey.getLpType().getGamsName(), rotaInt, area, yield));
+					outputWriter.newLine();
+				}
 			}
 		}
 		catch (IOException e) {