PhenologicalModelType.java

/*
 * This file is part of Indicators.
 *
 * Indicators is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Indicators is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indicators. If not, see <https://www.gnu.org/licenses/>.
 */
package fr.inrae.agroclim.indicators.model.data.phenology;

import java.util.List;
import java.util.Locale;
import java.util.Objects;

import fr.inrae.agroclim.indicators.resources.Resources;
import lombok.extern.log4j.Log4j2;

/**
 * Type of phenological model.
 *
 * @author Olivier Maury
 */
@Log4j2
public enum PhenologicalModelType {
    /**
     * Type for curve phenological model.
     */
    curve {

        @Override
        PhaseEnd getEndPhase(final PhenologyCalculator calc, final List<Double> tmeans, final int startDate,
                final double cumjvi, final int stageNb) {

            final Double tMax = calc.getMaxTemperature();
            final Double tMin = calc.getMinTemperature();
            final Double tOpt = calc.getOptimalTemperature();
            Objects.requireNonNull(tMax, "PhenologyCalculator.maxTemperature must not be null for curve model!");
            Objects.requireNonNull(tMin, "PhenologyCalculator.minTemperature must not be null for curve model!");
            Objects.requireNonNull(tOpt, "PhenologyCalculator.optimalTemperature must not be null for curve model!");
            return getCurveEndPhase(tMax, tMin, tOpt, calc, tmeans, startDate, cumjvi, stageNb);
        }
    },
    /**
     * Alternative for grapevine.
     *
     * - S0 : Aout
     *
     * - S1 : Dormance
     *
     * - S2 : Débourrement (BB)
     *
     * - S3 : Floraison (FLO)
     *
     * - S4 : Véraison (VER)
     *
     * - S5 : Véraison + 35 jours
     */
    curve_grapevine {
        @Override
        public PhaseEnd getEndPhase(final PhenologyCalculator calc, final List<Double> tmeans, final int startDate,
                final double cumjvi, final int stageNb) {
            LOGGER.traceEntry("curve_grapevine StageNb={}", stageNb);
            double tMax;
            double tMin;
            double tOpt;
            switch (stageNb) {
            case Stage.ONE:
                // Phase 1 -- Aout - Dormance
                // Chuine function
                final double chuineA = calc.getChuineA();
                final double chuineB = calc.getChuineB();
                final double chuineC = calc.getChuineC();
                final int nbOfDays = tmeans.size();
                final double threshold = calc.getPhaseSum(stageNb);
                Double cum = 0.;
                Integer phaseEnd = null;
                for (int day = startDate; day < nbOfDays; day++) {
                    final double tmean = tmeans.get(day - 1);
                    final double fa1 = chuineA * Math.pow(tmean - chuineC, 2);
                    final double fa2 = chuineB * (tmean - chuineC);
                    final double efficient = 1 / (1 + Math.exp(fa1 + fa2));
                    cum += efficient;
                    if (cum >= threshold) {
                        phaseEnd = day;
                        break;
                    }
                }
                return new PhaseEnd(phaseEnd, cum);
            case Stage.TWO:
                // Phase 2 -- DOR - BB
                // same as 'curve' model, with special temperatures
                // (no photoperiod effect, no vernalization effect)
                tMax = calc.getMaxTemperatureDeb();
                tMin = calc.getMinTemperatureDeb();
                tOpt = calc.getOptimalTemperatureDeb();
                return getCurveEndPhase(tMax, tMin, tOpt, calc, tmeans, startDate, cumjvi, stageNb);
            case Stage.FOUR:
                // Phase 4 -- Floraison - Véraison
                // same as 'curve' model, with special temperatures
                // (no photoperiod effect, no vernalization effect)
                tMax = calc.getMaxTemperatureVer();
                tMin = calc.getMinTemperatureVer();
                tOpt = calc.getOptimalTemperatureVer();
                return getCurveEndPhase(tMax, tMin, tOpt, calc, tmeans, startDate, cumjvi, stageNb);
            case Stage.THREE:
                // Phase 3 -- BB - Floraison
                // same as 'curve' model, with special temperatures
                // (no photoperiod effect, no vernalization effect)
                return curve.getEndPhase(calc, tmeans, startDate, cumjvi, stageNb);
            case Stage.FIVE:
                // simply adding 35 days
                final int end = calc.getS3ToS4() - 1;
                return new PhaseEnd(startDate + end, cumjvi);
            default:
                throw new RuntimeException("curve_grapevine not tested with more than 5 stages.");
            }
        }
    },
    /**
     * Alternative for grapevine to compute stages for water balance.
     *
     * - S1 = (BB-365), so == S2 in curve_grapevine
     *
     * - S2 = (FLO-365), so == S3 in curve_grapevine
     *
     * - S3 = (VER+35-365), so == S4 in curve_grapevine
     *
     * - S4 = max((S3+1),330)
     */
    curve_grapevine_sw {
        @Override
        PhaseEnd getEndPhase(final PhenologyCalculator calc, final List<Double> tmeans, final int startDate,
                final double cumjvi, final int stageNb) {
            LOGGER.traceEntry("curve_grapevine_sw StageNb={}", stageNb);
            final int end = 330 + 365;
            PhaseEnd pe;
            switch (stageNb) {
            case Stage.ONE:
                LOGGER.trace("DEB");
                pe = curve_grapevine.getEndPhase(calc, tmeans, startDate, cumjvi, 1);
                if (pe.getDay() == null) {
                    return new PhaseEnd(null, cumjvi);
                }
                return curve_grapevine.getEndPhase(calc, tmeans, pe.getDay() + 1, pe.getCumjvi(), Stage.TWO);
            case Stage.TWO, Stage.THREE:
                if (stageNb == Stage.TWO) {
                    LOGGER.trace("FLO");
                }
                if (stageNb == Stage.THREE) {
                    LOGGER.trace("VER");
                }
                pe = curve_grapevine.getEndPhase(calc, tmeans, startDate, cumjvi, stageNb + 1);
                if (pe.getDay() == null) {
                    return new PhaseEnd(null, cumjvi);
                }
                int day = pe.getDay();
                if (stageNb == Stage.THREE) {
                    day += calc.getS3ToS4();
                }
                if (day > end) {
                    day = end;
                }
                return new PhaseEnd(day, pe.getCumjvi());
            case Stage.FOUR:
                if (startDate > end) {
                    return new PhaseEnd(startDate, cumjvi);
                } else {
                    return new PhaseEnd(end, cumjvi);
                }
            default:
                break;
            }
            throw new UnsupportedOperationException("No stage > 4 for curve_grapevine_sw");
        }
    },
    /**
     * Type for linear phenological model.
     */
    linear {
        @Override
        public PhaseEnd getEndPhase(final PhenologyCalculator calc, final List<Double> tmeans, final int startDate,
                final double cumjvi, final int stageNb) {
            final double threshold = calc.getPhaseSum(stageNb);
            final boolean photoperiod;
            final boolean vernalization;
            final Double baseTemperature = calc.getBaseTemperature();
            if (baseTemperature == null) {
                throw new RuntimeException("calc.baseTemperature must not be null!");
            }
            // never use photoperiod and vernalisation
            // for first stage or last stage
            if (stageNb == 1 || stageNb == calc.getNbOfStages()) {
                photoperiod = false;
                vernalization = false;
            } else {
                photoperiod = calc.isPhotoperiodToCompute();
                vernalization = calc.isVernalizationToCompute();
            }
            Integer phaseEnd = null;
            double newCumjvi = 0;
            Double cum = 0.;
            final int nbOfDays = tmeans.size();
            double[] cumjvivalues = null;
            if (vernalization) {
                final double optiVernTemp = calc.getOptimalVernalizationTemperature();
                final double optiVernAmp = calc.getOptimalVernalizationAmplitude();
                cumjvivalues = PhenologyCalculator.cumjvi(optiVernAmp, optiVernTemp, tmeans, startDate, cumjvi);
            }
            for (int day = startDate; day < nbOfDays; day++) {
                final Double tmean = tmeans.get(day - 1);
                if (tmean == null) {
                    throw new RuntimeException("TMEAN of day " + (day - 1) + " must not be null!");
                }
                double efficient = tmean - baseTemperature;
                if (efficient <= 0) {
                    continue;
                }
                if (photoperiod) {
                    final double rfpi = PhenologyCalculator.rfpi(day, calc.getSiteLatitude(),
                            calc.getPhotoperiodSensitivity(), calc.getPhotoperiodSaturating(),
                            calc.getPhotoperiodBase());
                    efficient *= rfpi;
                }
                if (vernalization && cumjvivalues != null) {
                    newCumjvi = cumjvivalues[day - startDate];
                    final int nbOfVerDays = calc.getNbOfVernalizationDays();
                    final int minNbOfVerDays = calc.getMinNbOfVernalizationDays();
                    final double rfvi = PhenologyCalculator.rfvi(nbOfVerDays, minNbOfVerDays,
                            cumjvivalues[day - startDate]);
                    efficient *= rfvi;
                }
                cum += efficient;
                if (cum >= threshold) {
                    phaseEnd = day;
                    break;
                }
            }
            return new PhaseEnd(phaseEnd, newCumjvi);
        }
    },
    /**
     * Type for the grassland model.
     *
     * 1 year, no vernalization, no photoperiod.
     */
    richardson {
        @Override
        PhaseEnd getEndPhase(final PhenologyCalculator calc, final List<Double> tmeans, final int startDate,
                final double cumjvi, final int stageNb) {
            final Double baseTemperature = calc.getBaseTemperature();
            final double threshold = calc.getPhaseSum(stageNb);
            if (baseTemperature == null) {
                throw new RuntimeException("calc.baseTemperature must not be null!");
            }
            final Double maxTemperature = calc.getMaxTemperature();
            if (maxTemperature == null) {
                throw new RuntimeException("calc.maxTemperature must not be null!");
            }
            final int nbOfDays = tmeans.size();
            Double cum = 0.;
            Integer phaseEnd = null;
            for (int day = startDate; day < nbOfDays; day++) {
                final Double tmean = tmeans.get(day - 1);
                if (tmean == null) {
                    throw new RuntimeException("TMEAN of day " + (day - 1) + " must not be null!");
                }
                if (tmean < baseTemperature) {
                    continue;
                }
                if (tmean > maxTemperature) {
                    cum += maxTemperature;
                } else {
                    cum += tmean;
                }
                if (cum >= threshold) {
                    phaseEnd = day;
                    break;
                }
            }
            return new PhaseEnd(phaseEnd, 0);
        }
    };

    /**
     * Compute end of phase according to start of phase and sum of temperature.
     *
     * @param tMax      Maximal temperature for development (°C) used in the
     *                  curvilinear model.
     * @param tMin      Minimal temperature for development (°C) used in the
     *                  curvilinear model.
     * @param tOpt      Optimal temperature for development (°C) used in the
     *                  curvilinear model.
     * @param calc      phenological calculator for this type of model
     * @param tmeans    average temperatures for the whole year (or 2 years)
     * @param startDate start of phase (day of year)
     * @param cumjvi    Cumulative value of vernalization effects
     * @param stageNb   stage number
     * @return day of year and cumjvi
     */
    protected PhaseEnd getCurveEndPhase(final double tMax, final double tMin, final double tOpt,
            final PhenologyCalculator calc, final List<Double> tmeans, final int startDate, final double cumjvi,
            final int stageNb) {
        final boolean photoperiod;
        final boolean vernalization;
        if (calc.getModel() == curve && calc.getNbOfStages() != Stage.FOUR) {
            LOGGER.warn("Curve model with {} stages was not tested!", calc.getNbOfStages());
        }
        if (stageNb == Stage.ONE || stageNb == calc.getNbOfStages()) {
            photoperiod = false;
            vernalization = false;
        } else {
            photoperiod = calc.isPhotoperiodToCompute();
            vernalization = calc.isVernalizationToCompute();
        }
        final double threshold = calc.getPhaseSum(stageNb);

        Double cum = 0.;

        // calcul du coefficient alpha qui est fonction des trois paramètres
        // de la fonction (tmin, topt et tmax)
        final double alpha = Math.log(2) / Math.log((tMax - tMin) / (tOpt - tMin));

        Integer phaseEnd = null;
        double newCumjvi = 0;
        final int nbOfDays = tmeans.size();
        double[] cumjvivalues = null;
        if (vernalization) {
            final double optiVernTemp = calc.getOptimalVernalizationTemperature();
            final double optiVernAmp = calc.getOptimalVernalizationAmplitude();
            cumjvivalues = PhenologyCalculator.cumjvi(optiVernAmp, optiVernTemp, tmeans, startDate, cumjvi);
        }
        for (int day = startDate; day < nbOfDays; day++) {
            final double tmean = tmeans.get(day - 1);

            if (tmean - tMin <= 0 || tMax - tmean <= 0) {
                continue;
            }

            // calcul du numérateur de la fonction de Wang and Engel
            // (1998). On utilise les trois paramètres de la fonction et
            // la température moyenne journalière.
            final double numerator = 2 * Math.pow(tmean - tMin, alpha) * Math.pow(tOpt - tMin, alpha)
                    - Math.pow(tmean - tMin, 2 * alpha);
            // Calcul du dénominateur de la fonction qui utilise la
            // valeur d'alpha
            final double denominator = Math.pow(tOpt - tMin, 2 * alpha);
            // valeur journalière d'unités de développement calculés avec la
            // fonction de Wang and Engel
            final double ratio = numerator / denominator;
            double efficient = ratio;
            if (photoperiod) {
                efficient *= PhenologyCalculator.rfpi(day, calc.getSiteLatitude(), calc.getPhotoperiodSensitivity(),
                        calc.getPhotoperiodSaturating(), calc.getBaseTemperature());
            }
            if (vernalization && cumjvivalues != null) {
                newCumjvi = cumjvivalues[day - startDate];
                final int nbOfVerDays = calc.getNbOfVernalizationDays();
                final int minNbOfVerDays = calc.getMinNbOfVernalizationDays();
                final double rfvi = PhenologyCalculator.rfvi(nbOfVerDays, minNbOfVerDays, newCumjvi);
                efficient *= rfvi;
            }

            cum += efficient;
            if (cum >= threshold) {
                phaseEnd = day;
                break;
            }
        }
        return new PhaseEnd(phaseEnd, newCumjvi);
    }

    /**
     * Compute end of phase according to start of phase and sum of temperature.
     *
     * @param calc      phenological calculator for this type of model
     * @param tmeans    average temperatures for the whole year (or 2 years)
     * @param startDate start of phase (day of year)
     * @param stage     stage number
     * @param cumjvi    Cumulative value of vernalization effects
     * @return day of year and cumjvi
     */
    abstract PhaseEnd getEndPhase(PhenologyCalculator calc, List<Double> tmeans, int startDate, double cumjvi,
            int stage);

    /**
     * @param locale local for the localized name
     * @return localized name for the model type
     */
    public String getName(final Locale locale) {
        final Resources res = new Resources("fr.inrae.agroclim.indicators.resources.messages", locale);
        return res.getString("PhenologicalModelType." + name() + ".name");
    }

}