From 5bd310ee80465b3886a64128fb083b5e9a2f6ffe Mon Sep 17 00:00:00 2001
From: Tor-Einar Skog <tor-einar.skog@nibio.no>
Date: Mon, 20 Dec 2021 12:29:36 +0100
Subject: [PATCH] First working version with separate cutoffs per phase

---
 .../yellowstemborertempmodel/DataMatrix.java  |   8 +-
 .../YellowStemborerTempModel.java             | 130 +++++++++++++-----
 .../YellowStemborerTempModelTest.java         |  40 ++++--
 3 files changed, 130 insertions(+), 48 deletions(-)

diff --git a/src/main/java/no/nibio/vips/model/yellowstemborertempmodel/DataMatrix.java b/src/main/java/no/nibio/vips/model/yellowstemborertempmodel/DataMatrix.java
index f4dcd06..17d2e76 100644
--- a/src/main/java/no/nibio/vips/model/yellowstemborertempmodel/DataMatrix.java
+++ b/src/main/java/no/nibio/vips/model/yellowstemborertempmodel/DataMatrix.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016 NIBIO <http://www.nibio.no/>. 
+ * Copyright (c) 2021 NIBIO <http://www.nibio.no/>. 
  * 
  * This file is part of PsilaRosaeTempModel.
  * PsilaRosaeTempModel is free software: you can redistribute it and/or modify
@@ -22,7 +22,7 @@ package no.nibio.vips.model.yellowstemborertempmodel;
 import no.nibio.vips.util.DateMap;
 
 /**
- * @copyright 2016 <a href="http://www.nibio.no/">NIBIO</a>
+ * @copyright 2021 <a href="http://www.nibio.no/">NIBIO</a>
  * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
  */
 public class DataMatrix extends DateMap{
@@ -31,4 +31,8 @@ public class DataMatrix extends DateMap{
     public final static String DAILY_CONTRIB = "DC";
     public final static String HEAT_SUM = "HEAT_SUM";
     public final static String PHASE = "PHASE";
+    public final static String PHASE_EGG ="EGG";
+    public final static String PHASE_LARVAE ="LARVAE";
+    public final static String PHASE_PUPA ="PUPA";
+    public final static String PHASE_ADULT ="ADULT";
 }
diff --git a/src/main/java/no/nibio/vips/model/yellowstemborertempmodel/YellowStemborerTempModel.java b/src/main/java/no/nibio/vips/model/yellowstemborertempmodel/YellowStemborerTempModel.java
index 7244505..4180168 100644
--- a/src/main/java/no/nibio/vips/model/yellowstemborertempmodel/YellowStemborerTempModel.java
+++ b/src/main/java/no/nibio/vips/model/yellowstemborertempmodel/YellowStemborerTempModel.java
@@ -26,6 +26,7 @@ import java.text.DecimalFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
@@ -58,17 +59,21 @@ public class YellowStemborerTempModel extends I18nImpl implements Model {
     private TimeZone timeZone;
     private DataMatrix dataMatrix;
     private Date biofixDate;
-    private String[] phases = {"EGG","LARVAE","PUPA","ADULT"};
+    private final String[] phases = {
+        DataMatrix.PHASE_EGG,
+        DataMatrix.PHASE_LARVAE,
+        DataMatrix.PHASE_PUPA,
+        DataMatrix.PHASE_ADULT
+    };
     
+    // DD aggregation cutoff values for each phase
+    private Double[] dd_lower = {13.0,12.0,15.0,13.0};
+    private Double[] dd_upper = {36.0, 34.0,36.0,36.0};
     
-    // Threshold values
-    // TODO: ADJUST with actual biologically sensible defaults
-    private Double dd_lower = 0.0;
-    private Double dd_upper = 40.0;
+    // Heat sum thresholds for each phase
+    private Double[] heatRequirements = {113.0, 370.0, 188.0, 25.0};
     
-    private Double THRESHOLD_1 = 260.0; // Egg to larvae
-    private Double THRESHOLD_2 = 360.0; // Larvae to pupa
-    private Double THRESHOLD_3 = 560.0; // Pupa to adult
+    private String observedPhase = DataMatrix.PHASE_EGG;
     
     public YellowStemborerTempModel()
     {
@@ -79,7 +84,7 @@ public class YellowStemborerTempModel extends I18nImpl implements Model {
     @Override
     public List<Result> getResult() throws ModelExcecutionException {
        this.calculateTemperatureSum();
-       
+       //System.out.println(this.dataMatrix.toCSV());
        List<Result> retVal = new ArrayList<>();
        Date currentDate = this.dataMatrix.getFirstDateWithParameterValue(DataMatrix.HEAT_SUM);
        Calendar cal = Calendar.getInstance(this.timeZone);
@@ -94,35 +99,35 @@ public class YellowStemborerTempModel extends I18nImpl implements Model {
            
            Double DAILY_CONTRIB = this.dataMatrix.getParamDoubleValueForDate(currentDate, DataMatrix.DAILY_CONTRIB);
            Double HEAT_SUM = this.dataMatrix.getParamDoubleValueForDate(currentDate, DataMatrix.HEAT_SUM);
+           String PHASE = this.dataMatrix.getParamStringValueForDate(currentDate, DataMatrix.PHASE);
            
            result.setValue(CommonNamespaces.NS_WEATHER, DataMatrix.TND, dFormat.format(TNDCurrentDate));
            result.setValue(CommonNamespaces.NS_WEATHER, DataMatrix.TXD, dFormat.format(TXDCurrentDate));
            result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), DataMatrix.DAILY_CONTRIB, dFormat.format(DAILY_CONTRIB));
            result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), DataMatrix.HEAT_SUM, dFormat.format(HEAT_SUM));
-           result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), "THRESHOLD_1", dFormat.format(this.THRESHOLD_1));
-           result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), "THRESHOLD_2", dFormat.format(this.THRESHOLD_2));
-           result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), "THRESHOLD_3", dFormat.format(this.THRESHOLD_3));
+           result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), DataMatrix.PHASE, PHASE);
+           result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), "thresholdLarvae", dFormat.format(this.heatRequirements[0]));
+           result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), "thresholdPupa", dFormat.format(this.heatRequirements[0] + this.heatRequirements[1]));
+           result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), "thresholdAdult", dFormat.format(this.heatRequirements[0] + this.heatRequirements[1] + this.heatRequirements[2]));
+           result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), DataMatrix.PHASE, PHASE);
            
+           Integer warningStatus = Result.WARNING_STATUS_NO_WARNING;
            
-           Integer warningStatus = Result.WARNING_STATUS_NO_RISK;
-           if(HEAT_SUM < this.THRESHOLD_1)
+           if(PHASE.equals(DataMatrix.PHASE_EGG))
            {
-               result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), DataMatrix.PHASE, this.phases[0]);
+               warningStatus = Result.WARNING_STATUS_NO_RISK;
            }
-           if(HEAT_SUM >= this.THRESHOLD_1)
+           if(PHASE.equals(DataMatrix.PHASE_LARVAE))
            {
-               result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), DataMatrix.PHASE, this.phases[1]);
                warningStatus = Result.WARNING_STATUS_MINOR_RISK;
            }
-           if(HEAT_SUM >= this.THRESHOLD_2)
+           if(PHASE.equals(DataMatrix.PHASE_PUPA))
            {
-               result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), DataMatrix.PHASE, this.phases[2]);
-               warningStatus = Result.WARNING_STATUS_HIGH_RISK;
+               warningStatus = Result.WARNING_STATUS_MINOR_RISK;
            }
-           if(HEAT_SUM >= this.THRESHOLD_3)
+           if(PHASE.equals(DataMatrix.PHASE_ADULT))
            {
-               result.setValue(YellowStemborerTempModel.MODEL_ID.toString(), DataMatrix.PHASE, this.phases[3]);
-               warningStatus = Result.WARNING_STATUS_NO_WARNING;
+               warningStatus = Result.WARNING_STATUS_HIGH_RISK;
            }
            result.setWarningStatus(warningStatus);
            retVal.add(result);
@@ -236,6 +241,8 @@ public class YellowStemborerTempModel extends I18nImpl implements Model {
                 "\t\t}\n" +
                 "]}\n";
     }
+    
+    
 
     @Override
     public void setConfiguration(ModelConfiguration config) throws ConfigValidationException {
@@ -243,26 +250,47 @@ public class YellowStemborerTempModel extends I18nImpl implements Model {
     	// Adjusting defaults
     	try
     	{
-	        if(config.getConfigParameter("THRESHOLD_1") != null)
+	        if(config.getConfigParameter("heatRequirements") != null)
 	        {
-	        	this.THRESHOLD_1 = this.modelUtil.getDouble(config.getConfigParameter("THRESHOLD_1"));
-	        }
-	        if(config.getConfigParameter("THRESHOLD_2") != null)
-	        {
-	        	this.THRESHOLD_2 = this.modelUtil.getDouble(config.getConfigParameter("THRESHOLD_2"));
-	        }
-	        if(config.getConfigParameter("THRESHOLD_3") != null)
-	        {
-	        	this.THRESHOLD_3 = this.modelUtil.getDouble(config.getConfigParameter("THRESHOLD_3"));
+                    this.heatRequirements = this.modelUtil.getDoubleArray(config.getConfigParameter("heatRequirements"));
+                    if(this.heatRequirements.length != 4)
+                    {
+                        throw new ConfigValidationException("ERROR: Array of heat requirements has " + this.dd_lower.length + " elements, should be 4");
+                    }
 	        }
 	        if(config.getConfigParameter("dd_lower") != null)
 	        {
-	        	this.dd_lower = this.modelUtil.getDouble(config.getConfigParameter("dd_lower"));
+                    this.dd_lower = this.modelUtil.getDoubleArray(config.getConfigParameter("dd_lower"));
+                    if(this.dd_lower.length != 4)
+                    {
+                        throw new ConfigValidationException("ERROR: Array of lower cutoffs has " + this.dd_lower.length + " elements, should be 4");
+                    }
 	        }
 	        if(config.getConfigParameter("dd_upper") != null)
 	        {
-	        	this.dd_upper = this.modelUtil.getDouble(config.getConfigParameter("dd_upper"));
+                    this.dd_upper = this.modelUtil.getDoubleArray(config.getConfigParameter("dd_upper"));
+                    if(this.dd_upper.length != 4)
+                    {
+                        throw new ConfigValidationException("ERROR: Array of upper cutoffs has " + this.dd_upper.length + " elements, should be 4");
+                    }
 	        }
+                if(config.getConfigParameter("observedPhase") != null)
+                {
+                    if(!(config.getConfigParameter("observedPhase") instanceof String))
+                    {
+                        throw new ConfigValidationException("ERROR: Input parameter \"configParameter\" is " + config.getConfigParameter("observedPhase").getClass().getName() + ", must be a String");
+                    }
+                    String inputObservedPhase = (String) config.getConfigParameter("observedPhase");
+                    if(Arrays.asList(phases).contains(inputObservedPhase))
+                    {
+                        this.observedPhase = inputObservedPhase;
+                    }
+                    else
+                    {
+                        throw new ConfigValidationException("ERROR: observedPhase \"" + inputObservedPhase + "\" is not among the valid phases (" + String.join(",", phases) + ")");
+                    }
+                   
+                }
     	}
     	catch(ClassCastException ex)
     	{
@@ -338,7 +366,15 @@ public class YellowStemborerTempModel extends I18nImpl implements Model {
         Date lastDate = this.dataMatrix.getLastDateWithParameterValue(DataMatrix.TND);
         Calendar cal = Calendar.getInstance(this.timeZone);
         
-        Double sum = 0.0;
+        String currentPhase = this.observedPhase;
+        
+        Double sum = currentPhase.equals(DataMatrix.PHASE_EGG) ? 0.0
+                : currentPhase.equals(DataMatrix.PHASE_LARVAE) ? this.heatRequirements[0]
+                : currentPhase.equals(DataMatrix.PHASE_PUPA) ? this.heatRequirements[0] + this.heatRequirements[1]
+                : this.heatRequirements[0] + this.heatRequirements[1] + this.heatRequirements[2];
+        
+        List<String> phasesList = Arrays.asList(this.phases);
+        
         while(today.compareTo(lastDate) <= 0)
         {
             WeatherObservation todayMinTemp = (WeatherObservation)this.dataMatrix.getParamValueForDate(today, DataMatrix.TND);
@@ -348,10 +384,30 @@ public class YellowStemborerTempModel extends I18nImpl implements Model {
                 throw new ModelExcecutionException("Missing weather data at " + today + ": " + (todayMinTemp == null ? "TND ": "") + (todayMaxTemp == null ? "TXD ": ""));
             }
             //System.out.println("today=" + today + ",todayTemp=" + todayTemp);
-            Double dailyContribution = nlc.calculateSingleSineWaveWithCutoff(todayMinTemp.getValue(), todayMaxTemp.getValue(), this.dd_lower, this.dd_upper);
+            Double current_dd_lower = this.dd_lower[phasesList.indexOf(currentPhase)];
+            Double current_dd_upper = this.dd_upper[phasesList.indexOf(currentPhase)];
+            Double dailyContribution = nlc.calculateSingleSineWaveWithCutoff(todayMinTemp.getValue(), todayMaxTemp.getValue(), current_dd_lower, current_dd_upper);
             this.dataMatrix.setParamDoubleValueForDate(today, DataMatrix.DAILY_CONTRIB, dailyContribution);
             sum += dailyContribution;
             this.dataMatrix.setParamDoubleValueForDate(today, DataMatrix.HEAT_SUM, sum);
+            // Check phase transition
+            if(sum >= this.heatRequirements[0] + this.heatRequirements[1] + this.heatRequirements[2])
+            {
+                currentPhase = DataMatrix.PHASE_ADULT;
+            }
+            else if(sum >= this.heatRequirements[0] + this.heatRequirements[1])
+            {
+                currentPhase = DataMatrix.PHASE_PUPA;
+            }
+            else if(sum >= this.heatRequirements[0])
+            {
+                currentPhase = DataMatrix.PHASE_LARVAE;
+            }
+            else
+            {
+                currentPhase = DataMatrix.PHASE_EGG;
+            }
+            this.dataMatrix.setParamStringValueForDate(today, DataMatrix.PHASE, currentPhase);
             cal.setTime(today);
             cal.add(Calendar.DATE, 1);
             today = cal.getTime();
diff --git a/src/test/java/no/nibio/vips/model/yellowstemborertempmodel/YellowStemborerTempModelTest.java b/src/test/java/no/nibio/vips/model/yellowstemborertempmodel/YellowStemborerTempModelTest.java
index 1d85d40..a37f366 100644
--- a/src/test/java/no/nibio/vips/model/yellowstemborertempmodel/YellowStemborerTempModelTest.java
+++ b/src/test/java/no/nibio/vips/model/yellowstemborertempmodel/YellowStemborerTempModelTest.java
@@ -50,26 +50,48 @@ public class YellowStemborerTempModelTest {
     @Test
     public void testGetResult() throws Exception {
         System.out.println("getResult");
+        
+        // REQUIRED
         WeatherDataFileReader wfr = new WeatherDataFileReader();
+        // Weather data files can be placed in ("src/test/resources")
         ModelConfiguration config = wfr.getModelConfigurationWithWeatherData("/test_data_2019.json", YellowStemborerTempModel.MODEL_ID.toString());
+        // The timezone is used to set daily temperatures and biofix date correctly
         config.setConfigParameter("timeZone", "GMT+05:30");
+        // The date for when to start calculating heat sums
         config.setConfigParameter("biofixDate", "2019-01-05");
-        config.setConfigParameter("THRESHOLD_1", 290.0);
-        config.setConfigParameter("THRESHOLD_2", 390.0);
-        config.setConfigParameter("THRESHOLD_3", 590);
-        config.setConfigParameter("dd_lower", 15);
-        config.setConfigParameter("dd_upper", 31.1);
+        
+        // OPTIONAL
+        // The observed phase at biofix date. Default is EGG
+        config.setConfigParameter("observedPhase", DataMatrix.PHASE_EGG);
+        
+        // Heat requirement for transitioning out of [EGG,LARVAE,PUPA,ADULT] phases
+        Double[] heatRequirements = {113.0, 370.0, 188.0, 25.0};
+        // Lower and upper cutoffs for [EGG,LARVAE,PUPA,ADULT] phases
+        Double[] dd_lower = {13.0,12.0,15.0,13.0};
+        Double[] dd_upper = {36.0, 34.0,36.0,36.0};
+        config.setConfigParameter("heatRequirements", heatRequirements);
+        config.setConfigParameter("dd_lower", dd_lower);
+        config.setConfigParameter("dd_upper", dd_upper);
+        
+        
+        
+        
+        //config.setConfigParameter("observedPhase", "Donald Duck");
+        
         // Print the config
-        System.out.println(config.toJSON());
+        //System.out.println(config.toJSON());
+        
+        // Run the test
         YellowStemborerTempModel instance = new YellowStemborerTempModel();
         instance.setConfiguration(config);
         
         List<Result> result = instance.getResult();
         assertNotNull(result);
+        
         // Print the result
-        //ObjectMapper mapper = new ObjectMapper();
-        //System.out.println(mapper.writeValueAsString(result));
-        //result.forEach(r->System.out.println(r));
+        /*ObjectMapper mapper = new ObjectMapper();
+        System.out.println(mapper.writeValueAsString(result));
+        result.forEach(r->System.out.println(r));*/
     }
 
     
-- 
GitLab