1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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
96
97 private static final long serialVersionUID = -5403460610691492829L;
98
99
100
101
102 private static final int DAYS_IN_YEAR = 365;
103
104
105
106
107 private static final int DEFAULT_S3_TO_S4_CURVE_GRAPEVINE = 35;
108
109
110
111
112
113
114
115
116
117
118
119
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
140
141
142
143
144
145
146
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
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
209
210
211
212
213
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
233
234
235
236 @Getter
237 @Setter
238 private boolean allYears = false;
239
240
241
242
243 @Getter
244 @Setter
245 private Double minTemperature;
246
247
248
249
250
251 @Getter
252 @Setter
253 private Double minTemperatureDeb;
254
255
256
257
258
259 @Getter
260 @Setter
261 private Double minTemperatureVer;
262
263
264
265
266 @Getter
267 @Setter
268 private Double maxTemperature;
269
270
271
272
273
274 @Getter
275 @Setter
276 private Double maxTemperatureDeb;
277
278
279
280
281
282 @Getter
283 @Setter
284 private Double maxTemperatureVer;
285
286
287
288
289 @Getter
290 @Setter
291 private Double optimalTemperature;
292
293
294
295
296
297 @Getter
298 @Setter
299 private Double optimalTemperatureDeb;
300
301
302
303
304
305 @Getter
306 @Setter
307 private Double optimalTemperatureVer;
308
309
310
311
312 @Getter
313 @Setter
314 private Double baseTemperature;
315
316
317
318
319 @Getter
320 @Setter
321 private Double chuineA;
322
323
324
325
326 @Getter
327 @Setter
328 private Double chuineB;
329
330
331
332
333 @Getter
334 @Setter
335 private Double chuineC;
336
337
338
339
340 @XmlTransient
341 @Getter
342 @Setter
343 private List<ClimaticDailyData> climaticDailyData;
344
345
346
347
348 @Getter
349 @Setter
350 private String crop;
351
352
353
354
355
356
357 @Getter
358 @Setter
359 private boolean isPhotoperiodToCompute;
360
361
362
363
364
365
366 @Getter
367 @Setter
368 private boolean isVernalizationToCompute;
369
370
371
372
373 @Getter
374 @Setter
375 private int minNbOfVernalizationDays;
376
377
378
379
380 @Getter
381 @Setter
382 private PhenologicalModelType model;
383
384
385
386
387 @Getter
388 @Setter
389 private int nbOfVernalizationDays;
390
391
392
393
394
395
396
397
398 @Getter
399 @Setter
400 private int nbOfYears;
401
402
403
404
405
406 @Getter
407 @Setter
408 private Double optimalVernalizationAmplitude;
409
410
411
412
413 @Getter
414 @Setter
415 private Double optimalVernalizationTemperature;
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438 @XmlTransient
439 private Double[] phaseSums;
440
441
442
443
444 @Getter
445 @Setter
446 private double photoperiodBase;
447
448
449
450
451 @Getter
452 @Setter
453 private double photoperiodSaturating;
454
455
456
457
458 @Getter
459 @Setter
460 private double photoperiodSensitivity;
461
462
463
464
465
466
467 @Getter
468 @Setter
469 private int s3ToS4 = DEFAULT_S3_TO_S4_CURVE_GRAPEVINE;
470
471
472
473
474 @Getter
475 @Setter
476 private double siteLatitude;
477
478
479
480
481 @Getter
482 @Setter
483 private int sowingDate;
484
485
486
487
488 @Getter
489 @Setter
490 private String variety;
491
492
493
494
495 public PhenologyCalculator() {
496
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
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
576
577 public int getNbOfStages() {
578 return phaseSums.length;
579 }
580
581
582
583
584
585 public Double getPhase1Sum() {
586 final int phase = 1;
587 return getPhaseSumOrNull(phase);
588 }
589
590
591
592
593 public Double getPhase2Sum() {
594 final int phase = 2;
595 return getPhaseSumOrNull(phase);
596 }
597
598
599
600
601 public Double getPhase3Sum() {
602 final int phase = 3;
603 return getPhaseSumOrNull(phase);
604 }
605
606
607
608
609 public Double getPhase4Sum() {
610 final int phase = 4;
611 return getPhaseSumOrNull(phase);
612 }
613
614
615
616
617 public Double getPhase5Sum() {
618 final int phase = 5;
619 return getPhaseSumOrNull(phase);
620 }
621
622
623
624
625 public Double getPhase6Sum() {
626 final int phase = 6;
627 return getPhaseSumOrNull(phase);
628 }
629
630
631
632
633
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
645
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
663
664
665
666
667
668
669
670
671
672
673
674
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
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
726 tmeans.addAll(dataForNextYear);
727 }
728 final List<Stage> stages = new ArrayList<>();
729
730 final Stage s0 = new Stage(stageNamePrefix + "0", sowingDate);
731 stages.add(s0);
732
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
751
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
775
776
777 @XmlElement
778 public void setNbOfStages(final int nb) {
779 phaseSums = new Double[nb];
780 }
781
782
783
784
785
786
787 @XmlElement
788 public void setPhase1Sum(final Double value) {
789 setPhaseSum(1, value);
790 }
791
792
793
794
795
796
797
798
799 @XmlElement
800 public void setPhase2Sum(final Double value) {
801 setPhaseSum(2, value);
802 }
803
804
805
806
807
808
809
810
811 @XmlElement
812 public void setPhase3Sum(final Double value) {
813 final int phase = 3;
814 setPhaseSum(phase, value);
815 }
816
817
818
819
820
821
822
823 @XmlElement
824 public void setPhase4Sum(final Double value) {
825 final int phase = 4;
826 setPhaseSum(phase, value);
827 }
828
829
830
831
832
833
834 @XmlElement
835 public void setPhase5Sum(final Double value) {
836 final int phase = 5;
837 setPhaseSum(phase, value);
838 }
839
840
841
842
843
844
845 @XmlElement
846 public void setPhase6Sum(final Double value) {
847 final int phase = 6;
848 setPhaseSum(phase, value);
849 }
850
851
852
853
854
855
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
875 }
876 }