View Javadoc
1   /**
2    * This file is part of Indicators.
3    *
4    * Indicators is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * Indicators is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with Indicators. If not, see <https://www.gnu.org/licenses/>.
16   */
17  package fr.inrae.agroclim.indicators.model.data.phenology;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.Set;
29  import java.util.StringJoiner;
30  
31  import fr.inrae.agroclim.indicators.model.TimeScale;
32  import fr.inrae.agroclim.indicators.model.data.DataLoadingListenerHandler;
33  import fr.inrae.agroclim.indicators.model.data.ResourcesLoader;
34  import fr.inrae.agroclim.indicators.model.data.Variable;
35  import fr.inrae.agroclim.indicators.model.data.climate.ClimaticDailyData;
36  import jakarta.xml.bind.annotation.XmlAccessType;
37  import jakarta.xml.bind.annotation.XmlAccessorType;
38  import jakarta.xml.bind.annotation.XmlElement;
39  import jakarta.xml.bind.annotation.XmlTransient;
40  import jakarta.xml.bind.annotation.XmlType;
41  import lombok.EqualsAndHashCode;
42  import lombok.Getter;
43  import lombok.NonNull;
44  import lombok.Setter;
45  import lombok.extern.log4j.Log4j2;
46  
47  /**
48   * Calculator to provide phenological stages.
49   *
50   * Definitions : Vernalization is the induction of a plant's flowering process
51   * by exposure to the prolonged cold of winter, or by an artificial equivalent.
52   * After vernalization, plants have acquired the ability to flower, but they may
53   * require additional seasonal cues or weeks of growth before they will actually
54   * flower. Vernalization is sometimes used to refer to herbal (non-woody) plants
55   * requiring a cold dormancy to produce new shoots and leaves[1] but this usage
56   * is discouraged
57   *
58   * Phenology is related to the harvesting year.
59   *
60   * Soil water balance needs 4-stages phenological model.
61   *
62   * Curvilinear model uses Tmin, Topt, Tmax. Linear model uses Tbase.
63   *
64   * @author Olivier Maury
65   */
66  @XmlAccessorType(XmlAccessType.FIELD)
67  @XmlType(propOrder = {"crop", "variety", "allYears", "chuineA", "chuineB",
68          "chuineC", "baseTemperature", "isPhotoperiodToCompute",
69          "isVernalizationToCompute", "maxTemperature", "maxTemperatureDeb",
70          "maxTemperatureVer", "minNbOfVernalizationDays", "minTemperature",
71          "minTemperatureDeb", "minTemperatureVer", "model", "nbOfStages",
72          "nbOfVernalizationDays", "nbOfYears", "optimalTemperature",
73          "optimalTemperatureDeb", "optimalTemperatureVer",
74          "optimalVernalizationAmplitude", "optimalVernalizationTemperature",
75          "phase1Sum", "phase2Sum", "phase3Sum", "phase4Sum", "phase5Sum",
76          "phase6Sum", "photoperiodBase", "photoperiodSaturating",
77          "photoperiodSensitivity", "s3ToS4", "siteLatitude", "sowingDate"})
78  @EqualsAndHashCode(
79          callSuper = false,
80          of = {"crop", "variety", "allYears", "chuineA", "chuineB",
81                  "chuineC", "baseTemperature", "isPhotoperiodToCompute",
82                  "isVernalizationToCompute", "maxTemperature", "maxTemperatureDeb",
83                  "maxTemperatureVer", "minNbOfVernalizationDays", "minTemperature",
84                  "minTemperatureDeb", "minTemperatureVer", "model",
85                  "nbOfVernalizationDays", "nbOfYears", "optimalTemperature",
86                  "optimalTemperatureDeb", "optimalTemperatureVer",
87                  "optimalVernalizationAmplitude", "optimalVernalizationTemperature",
88                  "photoperiodBase", "photoperiodSaturating",
89                  "photoperiodSensitivity", "s3ToS4", "siteLatitude", "sowingDate"}
90          )
91  @Log4j2
92  public final class PhenologyCalculator extends DataLoadingListenerHandler
93  implements ResourcesLoader<List<AnnualStageData>> {
94      /**
95       * UID for serialization.
96       */
97      private static final long serialVersionUID = -5403460610691492829L;
98  
99      /**
100      * All years have the same number of days.
101      */
102     private static final int DAYS_IN_YEAR = 365;
103 
104     /**
105      * Default number of days between S3 and S4 for curve_grapevine.
106      */
107     private static final int DEFAULT_S3_TO_S4_CURVE_GRAPEVINE = 35;
108 
109     /**
110      * Compute the cumulative sum of JVI.
111      *
112      * @param optiVernAmp Thermal half-amplitude around the optimal temperature
113      * for vernalization (°C) (ver_ampfroid).
114      * @param optiVernTemp Optimal temperature for vernalization (°C)
115      * (Ver_tfroid).
116      * @param tmeans average temperatures
117      * @param startDate start of phase (day of year)
118      * @param initialCumjvi value of vernalization effect before phase
119      * @return list for all days of cumulative sum of JVI
120      */
121     public static double[] cumjvi(final double optiVernAmp,
122             final double optiVernTemp, @NonNull final List<Double> tmeans,
123             final int startDate, final double initialCumjvi) {
124         double cumjvi = initialCumjvi;
125         final double[] results = new double[tmeans.size() - startDate + 1];
126         for (int i = startDate; i < tmeans.size(); i++) {
127             final Double tmean = tmeans.get(i - 1);
128             final double d = 1
129                     - Math.pow((optiVernTemp - tmean) / optiVernAmp, 2);
130             if (d > 0) {
131                 cumjvi += d;
132             }
133             results[i - startDate] = cumjvi;
134         }
135         return results;
136     }
137 
138     /**
139      * Compute the photoperiod number of hours according to a given latitude on
140      * a specific julian day.
141      *
142      * @param day
143      *            julian day
144      * @param latitude
145      *            given latitude
146      * @return number of hours
147      */
148     public static double photoperiod(final int day, final double latitude) {
149         final int hoursInDay = 24;
150         final int daysInYear = 365;
151         final double alat = latitude / 57.296;
152         int doy;
153         if (day > daysInYear) {
154             doy = day - daysInYear;
155         } else {
156             doy = day;
157         }
158 
159         final double theta1 = 2 * Math.PI * (doy - 80) / daysInYear;
160         final double theta2 = 0.034 * (Math.sin(2 * Math.PI * doy / daysInYear)
161                 - Math.sin(2 * Math.PI * 80 / daysInYear));
162         final double theta = theta1 + theta2;
163         final double dec = Math.asin(0.3978 * Math.sin(theta));
164         final double d = -1.0 * 0.10453 / (Math.cos(alat) * Math.cos(dec));
165         double p = d - Math.tan(alat) * Math.tan(dec);
166         p = Math.acos(p);
167         final double photoperiod = hoursInDay * p / Math.PI;
168 
169         if (photoperiod > hoursInDay) {
170             return hoursInDay;
171         } else {
172             return photoperiod;
173         }
174     }
175 
176     /**
177      * Compute the RFPI (Effect of the photoperiod).
178      *
179      * RFPI = "effet de la photopériode, coefficient compris entre 0 et 1 qui
180      * tient compte des paramètres plante et de la latitude." ⮕ tiré du code R
181      *
182      * @param day
183      *            julian day
184      * @param latitude
185      *            given latitude
186      * @param sensitivity
187      *            photoperiod sensitivity (1=insensitive)
188      * @param saturating
189      *            photoperiod saturating (hours)
190      * @param base
191      *            photoperiod base (hours)
192      * @return RFPI
193      */
194     public static double rfpi(final int day, final double latitude,
195             final double sensitivity, final double saturating,
196             final double base) {
197         final double photoperiod = photoperiod(day, latitude);
198 
199         double cRFPI = (1.0 - sensitivity) / (saturating - base)
200                 * (photoperiod - saturating) + 1;
201 
202         cRFPI = Math.max(Math.min(cRFPI, 1), sensitivity);
203 
204         return cRFPI;
205     }
206 
207     /**
208      * Effect of vernalization for a day.
209      *
210      * @param nbOfVernalizationDays number of vernalization days
211      * @param minNbOfVernalizationDays minimal number of vernalization days
212      * @param cumjvi cumulative sum of JVI for the day
213      * @return effect of vernalization
214      */
215     public static double rfvi(final int nbOfVernalizationDays,
216             final int minNbOfVernalizationDays, final double cumjvi) {
217         if (cumjvi < nbOfVernalizationDays) {
218             double rfvi = (cumjvi - minNbOfVernalizationDays)
219                     / (nbOfVernalizationDays - minNbOfVernalizationDays);
220             if (rfvi < 0) {
221                 rfvi = 0;
222             }
223             if (rfvi > 1) {
224                 rfvi = 1;
225             }
226             return rfvi;
227         }
228         return 1.;
229     }
230 
231     /**
232      * Compute stages for all years, whatever is nbYears.
233      *
234      * True for SoilCalculator.
235      */
236     @Getter
237     @Setter
238     private boolean allYears = false;
239 
240     /**
241      * Minimal temperature for development (°C) used in the curvilinear model.
242      */
243     @Getter
244     @Setter
245     private Double minTemperature;
246 
247     /**
248      * Minimal temperature for development (°C) for DEB stage, used in the
249      * curvilinear grapevine model.
250      */
251     @Getter
252     @Setter
253     private Double minTemperatureDeb;
254 
255     /**
256      * Minimal temperature for development (°C) for VER stage, used in the
257      * curvilinear grapevine model.
258      */
259     @Getter
260     @Setter
261     private Double minTemperatureVer;
262 
263     /**
264      * Maximal temperature for development (°C) used in the curvilinear model.
265      */
266     @Getter
267     @Setter
268     private Double maxTemperature;
269 
270     /**
271      * Maximal temperatures for development (°C) for DEB stage, used in the
272      * curvilinear grapevine model.
273      */
274     @Getter
275     @Setter
276     private Double maxTemperatureDeb;
277 
278     /**
279      * Maximal temperatures for development (°C) for VER stage, used in the
280      * curvilinear grapevine model.
281      */
282     @Getter
283     @Setter
284     private Double maxTemperatureVer;
285 
286     /**
287      * Optimal temperature for development (°C) used in the curvilinear model.
288      */
289     @Getter
290     @Setter
291     private Double optimalTemperature;
292 
293     /**
294      * Optimal temperatures for development (°C) for DEB stage, used in the
295      * curvilinear grapevine model.
296      */
297     @Getter
298     @Setter
299     private Double optimalTemperatureDeb;
300 
301     /**
302      * Optimal temperatures for development (°C) for VER stage, used in the
303      * curvilinear grapevine model.
304      */
305     @Getter
306     @Setter
307     private Double optimalTemperatureVer;
308 
309     /**
310      * Base temperature for development (°C) used in linear model.
311      */
312     @Getter
313     @Setter
314     private Double baseTemperature;
315 
316     /**
317      * Parameter for Chuine function, used in curve_grapevine* models.
318      */
319     @Getter
320     @Setter
321     private Double chuineA;
322 
323     /**
324      * Parameter for Chuine function, used in curve_grapevine* models.
325      */
326     @Getter
327     @Setter
328     private Double chuineB;
329 
330     /**
331      * Parameter for Chuine function, used in curve_grapevine* models.
332      */
333     @Getter
334     @Setter
335     private Double chuineC;
336 
337     /**
338      * Climatic daily data used to compute phenology.
339      */
340     @XmlTransient
341     @Getter
342     @Setter
343     private List<ClimaticDailyData> climaticDailyData;
344 
345     /**
346      * Given crop.
347      */
348     @Getter
349     @Setter
350     private String crop;
351 
352     /**
353      * Compute photoperiod in phenology model ?
354      *
355      * Photoperiod = day duration.
356      */
357     @Getter
358     @Setter
359     private boolean isPhotoperiodToCompute;
360 
361     /**
362      * Compute vernalization in phenology model ?
363      *
364      * Vernalization = need cold.
365      */
366     @Getter
367     @Setter
368     private boolean isVernalizationToCompute;
369 
370     /**
371      * Minimum number of vernalization days.
372      */
373     @Getter
374     @Setter
375     private int minNbOfVernalizationDays;
376 
377     /**
378      * Given model.
379      */
380     @Getter
381     @Setter
382     private PhenologicalModelType model;
383 
384     /**
385      * Number of vernalization days.
386      */
387     @Getter
388     @Setter
389     private int nbOfVernalizationDays;
390 
391     /**
392      * Type of crop.
393      *
394      * nbOfYears = 1 (sowing and harvest the same year)
395      *
396      * nbOfYears = 2 (harvest the year following the year of sowing)
397      */
398     @Getter
399     @Setter
400     private int nbOfYears;
401 
402     /**
403      * Thermal half-amplitude around the optimal temperature for vernalization
404      * (°C).
405      */
406     @Getter
407     @Setter
408     private Double optimalVernalizationAmplitude;
409 
410     /**
411      * Optimal temperature for vernalization (°C).
412      */
413     @Getter
414     @Setter
415     private Double optimalVernalizationTemperature;
416 
417     /**
418      * Sum of effective temperature from
419      *
420      * - sowing to emergence (°C), aka ST_Phase1_4 OR ST_Phase1_6.
421      *
422      * - emergence to maximum acceleration of leaf growth (°C), aka ST_Phase2_4,
423      *
424      * - emergence to début tallage (°C), aka ST_Phase2_6.
425      *
426      * - maximum acceleration of leaf growth to flowering (°C), aka ST_Phase3_4,
427      *
428      * - début tallage to epi 1 cm (°C), aka ST_Phase3_6
429      *
430      * - flowering to physiological maturity (°C), aka ST_Phase4_4,
431      *
432      * - epi 1 cm to meiose (°C), aka ST_Phase4_6.
433      *
434      * - meiose to floraison (°C), aka ST_Phase5_6,
435      *
436      * - floraison to physiological maturity (°C), aka ST_Phase6_6.
437      */
438     @XmlTransient
439     private Double[] phaseSums;
440 
441     /**
442      * Base photoperiod (number of hours).
443      */
444     @Getter
445     @Setter
446     private double photoperiodBase;
447 
448     /**
449      * Saturation photoperiod (number of hours).
450      */
451     @Getter
452     @Setter
453     private double photoperiodSaturating;
454 
455     /**
456      * Sensibility to the photoperiod (from 0 to 1 with 1 = insensitive).
457      */
458     @Getter
459     @Setter
460     private double photoperiodSensitivity;
461 
462     /**
463      * Number of days between S3 and S4 for curve_grapevine.
464      *
465      * By default: 35 days.
466      */
467     @Getter
468     @Setter
469     private int s3ToS4 = DEFAULT_S3_TO_S4_CURVE_GRAPEVINE;
470 
471     /**
472      * Site latitude (°N) to compute photoperiod.
473      */
474     @Getter
475     @Setter
476     private double siteLatitude;
477 
478     /**
479      * Sowing date (julian day).
480      */
481     @Getter
482     @Setter
483     private int sowingDate;
484 
485     /**
486      * Given crop variety.
487      */
488     @Getter
489     @Setter
490     private String variety;
491 
492     /**
493      * Constructor.
494      */
495     public PhenologyCalculator() {
496         // Do nothing
497     }
498 
499     @Override
500     public PhenologyCalculator clone() {
501         final PhenologyCalculator clone = new PhenologyCalculator();
502         clone.baseTemperature = baseTemperature;
503         if (climaticDailyData != null) {
504             clone.climaticDailyData = new ArrayList<>();
505             clone.climaticDailyData.addAll(climaticDailyData);
506         }
507         clone.crop = crop;
508         clone.isPhotoperiodToCompute = isPhotoperiodToCompute;
509         clone.isVernalizationToCompute = isVernalizationToCompute;
510         clone.maxTemperature = maxTemperature;
511         clone.minNbOfVernalizationDays = minNbOfVernalizationDays;
512         clone.minTemperature = minTemperature;
513         clone.model = model;
514         clone.nbOfVernalizationDays = nbOfVernalizationDays;
515         clone.nbOfYears = nbOfYears;
516         clone.optimalTemperature = optimalTemperature;
517         clone.optimalVernalizationAmplitude = optimalVernalizationAmplitude;
518         clone.optimalVernalizationTemperature = optimalVernalizationTemperature;
519         clone.phaseSums = Arrays.copyOf(phaseSums, phaseSums.length);
520         clone.photoperiodBase = photoperiodBase;
521         clone.photoperiodSaturating = photoperiodSaturating;
522         clone.photoperiodSensitivity = photoperiodSensitivity;
523         clone.sowingDate = sowingDate;
524         clone.variety = variety;
525         return clone;
526     }
527 
528     /**
529      * @return year ⮕  Tmean
530      */
531     private Map<Integer, List<Double>> getClimaticDataByYear() {
532         if (climaticDailyData == null) {
533             throw new RuntimeException("climaticDailyData is not defined!");
534         }
535         if (climaticDailyData.isEmpty()) {
536             throw new RuntimeException("climaticDailyData is empty!");
537         }
538         final Map<Integer, List<Double>> dataByYear = new HashMap<>();
539 
540         for (final ClimaticDailyData climaticData : climaticDailyData) {
541             final int year = climaticData.getYear();
542             dataByYear.computeIfAbsent(year, y -> new ArrayList<>());
543             if (climaticData.getTmean() == null) {
544                 throw new RuntimeException("Null value for TMEAN at "
545                         + climaticData.getYear() + "/"
546                         + climaticData.getMonth() + "/"
547                         + climaticData.getDay());
548             }
549             dataByYear.get(year).add(climaticData.getTmean());
550         }
551 
552         return dataByYear;
553     }
554 
555     @Override
556     public Map<String, String> getConfigurationErrors() {
557         final Map<String, String> errors = new HashMap<>();
558         if (climaticDailyData == null) {
559             errors.put("phenology.climaticDailyData", "error.evaluation.phenology.climaticDailydata.missing");
560         } else if (climaticDailyData.isEmpty()) {
561             errors.put("phenology.climaticDailyData", "error.evaluation.phenology.climaticDailydata.empty");
562         }
563         if (errors.isEmpty()) {
564             return null;
565         }
566         return errors;
567     }
568 
569     @Override
570     public Collection<String> getMissingVariables() {
571         throw new UnsupportedOperationException("Not implemented for phenology!");
572     }
573 
574     /**
575      * @return number of stages to compute.
576      */
577     public int getNbOfStages() {
578         return phaseSums.length;
579     }
580 
581 
582     /**
583      * @return ST_Phase1_4, ST_Phase1_6
584      */
585     public Double getPhase1Sum() {
586         final int phase = 1;
587         return getPhaseSumOrNull(phase);
588     }
589 
590     /**
591      * @return ST_Phase2_4, ST_Phase2_6
592      */
593     public Double getPhase2Sum() {
594         final int phase = 2;
595         return getPhaseSumOrNull(phase);
596     }
597 
598     /**
599      * @return ST_Phase3_4, ST_Phase3_6
600      */
601     public Double getPhase3Sum() {
602         final int phase = 3;
603         return getPhaseSumOrNull(phase);
604     }
605 
606     /**
607      * @return ST_Phase4_4, ST_Phase4_6
608      */
609     public Double getPhase4Sum() {
610         final int phase = 4;
611         return getPhaseSumOrNull(phase);
612     }
613 
614     /**
615      * @return ST_Phase5_6
616      */
617     public Double getPhase5Sum() {
618         final int phase = 5;
619         return getPhaseSumOrNull(phase);
620     }
621 
622     /**
623      * @return ST_Phase6_6
624      */
625     public Double getPhase6Sum() {
626         final int phase = 6;
627         return getPhaseSumOrNull(phase);
628     }
629 
630     /**
631      * @param phase
632      *            phase number
633      * @return Sum of effective temperature for the phase
634      */
635     public double getPhaseSum(final int phase) {
636         if (phase < 1 || phase > phaseSums.length) {
637             throw new IllegalArgumentException("Phase must be between 1 and "
638                     + phaseSums.length);
639         }
640         return phaseSums[phase - 1];
641     }
642 
643     /**
644      * @param phase phase number
645      * @return Sum of effective temperature for the phase
646      */
647     private Double getPhaseSumOrNull(final int phase) {
648         if (phase < 1 || phase > phaseSums.length) {
649             return null;
650         }
651         return phaseSums[phase - 1];
652     }
653 
654     @Override
655     public Set<Variable> getVariables() {
656         final Set<Variable> variables = new HashSet<>();
657         variables.add(Variable.TMEAN);
658         return variables;
659     }
660 
661     /**
662      * This is a translation of the R code from the R "indicateurs" project.
663      *
664      * General idea: 1/ get average temperatures for 1 or 2 years according to
665      * crop type, 2/ Compute Phase 1 to N: sum of difference between base
666      * temperature (* photoperiod effect * vernalization effect) and average
667      * until threshold to get end date of phase
668      *
669      * Phases for linear 4 stages: 1 : sowing - emergence, 2 : emergence - LAI
670      * inflexion, 3 : inflexion - flowering, 4 : flowering - maturity
671      *
672      * The original algorithms are : pheno_curve_model_4stades.r,
673      * pheno_linear_model_4stades.r, pheno_linear_model_5stades.r,
674      * pheno_linear_model_6stades.r.
675      */
676     @Override
677     public List<AnnualStageData> load() {
678         LOGGER.traceEntry();
679         if (model == null) {
680             throw new RuntimeException("model is not defined!");
681         }
682         if (phaseSums == null) {
683             throw new RuntimeException("phaseSums is not defined!");
684         }
685         for (int i = 0; i < phaseSums.length; i++) {
686             if (phaseSums[i] == null) {
687                 final StringJoiner sj = new StringJoiner("\n");
688                 sj.add("Missing PhaseSums[" + i + "] for variety " + variety);
689                 for (int j = 0; j < phaseSums.length; j++) {
690                     sj.add(String.format(
691                             "PhaseSums[%d]=%s", j, phaseSums[j]));
692                 }
693                 LOGGER.error(sj.toString());
694                 throw new RuntimeException("phaseSum is not defined for stage "
695                         + (i + 1));
696             }
697         }
698         final List<AnnualStageData> phenologicalData = new ArrayList<>();
699 
700         Map<Integer, List<Double>> climaticDataByYear;
701         climaticDataByYear = getClimaticDataByYear();
702         final List<Integer> years;
703         years = new ArrayList<>(climaticDataByYear.keySet());
704         Collections.sort(years);
705         final Integer lastYear = years.get(years.size() - 1);
706 
707         final String stageNamePrefix = "s";
708         for (final Integer year : years) {
709             final boolean isLastYear = Objects.equals(year, lastYear);
710             if (!allYears && nbOfYears != 1 && isLastYear) {
711                 LOGGER.trace("Ignoring last year {}", year);
712                 continue;
713             }
714             // The calculation is made for each climatic year
715             final List<Double> dataForCurrentYear;
716             dataForCurrentYear = climaticDataByYear.get(year);
717             final List<Double> tmeans = new ArrayList<>(dataForCurrentYear);
718             if (nbOfYears == 2 && !(allYears && isLastYear)) {
719                 final List<Double> dataForNextYear;
720                 dataForNextYear = climaticDataByYear.get(year + 1);
721                 if (dataForNextYear == null) {
722                     throw new RuntimeException("No climatic data for year "
723                             + (year + 1));
724                 }
725                 // If the crop grows between 2 years
726                 tmeans.addAll(dataForNextYear);
727             }
728             final List<Stage> stages = new ArrayList<>();
729             // Stage 0
730             final Stage s0 = new Stage(stageNamePrefix + "0", sowingDate);
731             stages.add(s0);
732             // Stages 1 - nbOfStages
733             int phaseStart = sowingDate;
734             double cumjvi = 0.;
735             for (int stage = 1; stage <= phaseSums.length; stage++) {
736                 final String stageName = stageNamePrefix + stage;
737                 PhaseEnd phaseEnd;
738                 phaseEnd = model.getEndPhase(this, tmeans, phaseStart, cumjvi, stage);
739                 cumjvi = phaseEnd.getCumjvi();
740                 if (phaseEnd.getDay() == null) {
741                     LOGGER.warn("No end date for phase {}/{} in {}!", stage, phaseSums.length, year);
742                     final Stage s = new Stage();
743                     s.setName(stageName);
744                     stages.add(s);
745                     break;
746                 }
747                 phaseStart = phaseEnd.getDay() + 1;
748                 Stage s;
749                 if (model == PhenologicalModelType.curve_grapevine_sw) {
750                     // for this model, dates are computed for year N and N+1,
751                     // but used on 1 year only
752                     s = new Stage(stageName, phaseEnd.getDay() - DAYS_IN_YEAR);
753                 } else {
754                     s = new Stage(stageName, phaseEnd.getDay());
755                 }
756                 stages.add(s);
757             }
758             //
759             final AnnualStageData data = new AnnualStageData();
760             if (nbOfYears == 1 || allYears) {
761                 data.setYear(year);
762             } else {
763                 data.setYear(year + 1);
764             }
765             data.setStages(stages);
766             data.setComplete(stages.size() == phaseSums.length + 1);
767             phenologicalData.add(data);
768         }
769         LOGGER.traceExit();
770         return phenologicalData;
771     }
772 
773     /**
774      * @param nb
775      *            number of stages to compute.
776      */
777     @XmlElement
778     public void setNbOfStages(final int nb) {
779         phaseSums = new Double[nb];
780     }
781 
782     /**
783      * @param value
784      *            Sum of effective temperature from sowing to emergence (°C),
785      *            aka ST_Phase1_4 OR ST_Phase1_6.
786      */
787     @XmlElement
788     public void setPhase1Sum(final Double value) {
789         setPhaseSum(1, value);
790     }
791 
792     /**
793      * @param value
794      *            Sum of effective temperature from emergence to maximum
795      *            acceleration of leaf growth (°C), aka ST_Phase2_4, OR Sum of
796      *            effective temperature from emergence to début tallage (°C),
797      *            aka ST_Phase2_6.
798      */
799     @XmlElement
800     public void setPhase2Sum(final Double value) {
801         setPhaseSum(2, value);
802     }
803 
804     /**
805      * @param value
806      *            Sum of effective temperature from maximum acceleration of leaf
807      *            growth to flowering (°C), aka ST_Phase3_4, OR Sum of effective
808      *            temperature from début tallage to epi 1 cm (°C), aka
809      *            ST_Phase3_6.
810      */
811     @XmlElement
812     public void setPhase3Sum(final Double value) {
813         final int phase = 3;
814         setPhaseSum(phase, value);
815     }
816 
817     /**
818      * @param value
819      *            Sum of effective temperature from flowering to physiological
820      *            maturity (°C), aka ST_Phase4_4, OR Sum of effective
821      *            temperature from epi 1 cm to meiose (°C), aka ST_Phase4_6.
822      */
823     @XmlElement
824     public void setPhase4Sum(final Double value) {
825         final int phase = 4;
826         setPhaseSum(phase, value);
827     }
828 
829     /**
830      * @param value
831      *            Sum of effective temperature from meiose to floraison (°C),
832      *            aka ST_Phase5_6.
833      */
834     @XmlElement
835     public void setPhase5Sum(final Double value) {
836         final int phase = 5;
837         setPhaseSum(phase, value);
838     }
839 
840     /**
841      * @param value
842      *            Sum of effective temperature from floraison to physiological
843      *            maturity (°C), aka ST_Phase6_6.
844      */
845     @XmlElement
846     public void setPhase6Sum(final Double value) {
847         final int phase = 6;
848         setPhaseSum(phase, value);
849     }
850 
851     /**
852      * @param phase
853      *            phase number
854      * @param value
855      *            Sum of effective temperature (°C)
856      */
857     private void setPhaseSum(final int phase, final Double value) {
858         if (value == null) {
859             throw new IllegalArgumentException("phase" + phase
860                     + "Sum must not be null!");
861         }
862         if (phase < 1) {
863             throw new IllegalArgumentException("phase number must be >= 1");
864         }
865         if (phase > this.phaseSums.length) {
866             throw new IllegalArgumentException("phase number must be <= "
867                     + this.phaseSums.length);
868         }
869         this.phaseSums[phase - 1] = value;
870     }
871 
872     @Override
873     public void setTimeScale(final TimeScale timeScale) {
874         // do nothing
875     }
876 }