View Javadoc
1   /*
2    * Copyright (C) 2021 INRAE AgroClim
3    *
4    * This file is part of Indicators.
5    *
6    * Indicators is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU General Public License as published by
8    * the Free Software Foundation, either version 3 of the License, or
9    * (at your option) any later version.
10   *
11   * Indicators is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14   * GNU General Public License for more details.
15   *
16   * You should have received a copy of the GNU General Public License
17   * along with Indicators. If not, see <http://www.gnu.org/licenses/>.
18   */
19  package fr.inrae.agroclim.indicators.model.indicator;
20  
21  import java.io.IOException;
22  import java.io.ObjectInputStream;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.Set;
29  import java.util.function.Function;
30  import java.util.stream.DoubleStream;
31  
32  import fr.inrae.agroclim.indicators.exception.IndicatorsException;
33  import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
34  import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
35  import fr.inrae.agroclim.indicators.model.ExpressionParameter;
36  import fr.inrae.agroclim.indicators.model.JEXLFormula;
37  import fr.inrae.agroclim.indicators.model.Knowledge;
38  import fr.inrae.agroclim.indicators.model.Parameter;
39  import fr.inrae.agroclim.indicators.model.data.DailyData;
40  import fr.inrae.agroclim.indicators.model.data.Resource;
41  import fr.inrae.agroclim.indicators.model.data.Variable;
42  import fr.inrae.agroclim.indicators.util.Doublet;
43  import jakarta.xml.bind.annotation.XmlElement;
44  import jakarta.xml.bind.annotation.XmlType;
45  import lombok.EqualsAndHashCode;
46  import lombok.Getter;
47  import lombok.RequiredArgsConstructor;
48  import lombok.Setter;
49  
50  /**
51   * Compute values using JEXL expressions.
52   *
53   * Last changed : $Date$
54   *
55   * @author $Author$
56   * @version $Revision$
57   */
58  @XmlType(propOrder = {"aggregation", "expression", "expressionParameters"})
59  @EqualsAndHashCode(
60          callSuper = false,
61          of = {"aggregation", "expression", "expressionParameters"}
62          )
63  public final class Formula extends SimpleIndicator implements Detailable {
64  
65      /**
66       * Aggregation to apply to computed values.
67       */
68      @RequiredArgsConstructor
69      public enum Aggregation {
70          /**
71           * Average.
72           */
73          AVERAGE(s -> s.average().orElse(0)),
74          /**
75           * Count.
76           */
77          COUNT(s -> Double.valueOf(s.count())),
78          /**
79           * Get max value.
80           */
81          MAX(s -> s.max().orElse(0)),
82          /**
83           * Get min value.
84           */
85          MIN(s -> Double.valueOf(s.count())),
86          /**
87           * Get sum of values.
88           */
89          SUM(s -> s.sum());
90          /**
91           * Function to compute the aggregated value.
92           */
93          @Getter
94          private final Function<DoubleStream, Double> function;
95      }
96  
97      /**
98       * UUID for Serializable.
99       */
100     private static final long serialVersionUID = 6030595237342422020L;
101 
102     /**
103      * Aggregation to apply to computed values.
104      */
105     @Getter
106     @Setter
107     private Aggregation aggregation;
108 
109     /**
110      * JEXL expression.
111      */
112     @Getter
113     @Setter
114     private String expression;
115 
116     /**
117      * Parameters for JEXLFormula.
118      */
119     @Getter
120     @Setter
121     @XmlElement(name = "expressionParameter")
122     private List<ExpressionParameter> expressionParameters;
123 
124     /**
125      * org.apache.commons.jexl2.JexlEngine handler.
126      */
127     private transient JEXLFormula formula = new JEXLFormula();
128 
129     /**
130      * Values id of parameter ⮕ value of parameter.
131      */
132     @Setter
133     @Getter
134     private transient Map<String, Double> parametersValues = new HashMap<>();
135 
136     @Override
137     public double computeSingleValue(final Resource<? extends DailyData> res) throws IndicatorsException {
138         if (aggregation == null) {
139             throw new IndicatorsException(ComputationErrorType.FORMULA_AGGREGATION_NULL);
140         }
141         Objects.requireNonNull(res, "Resource must not be null!");
142         if (res.getData() == null) {
143             throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
144         }
145         final Set<Variable> dataVariables = getVariables();
146         final List<Double> computedValues = new ArrayList<>();
147         for (final DailyData data : res.getData()) {
148             final Map<String, Double> values = new HashMap<>();
149             dataVariables.forEach(variable -> values.put(variable.name(), data.getValue(variable)));
150             if (parametersValues != null) {
151                 values.putAll(parametersValues);
152             }
153             if (expressionParameters != null) {
154                 expressionParameters.forEach(p -> values.put(p.getName(), p.getValue()));
155             }
156             computedValues.add(formula.evaluate(values, Double.class));
157         }
158         final DoubleStream stream = computedValues.stream().mapToDouble(Double::doubleValue);
159         return aggregation.getFunction().apply(stream);
160     }
161 
162     @Override
163     public List<Doublet<Parameter, Number>> getParameterDefaults() {
164         // no substitution
165         return List.of();
166     }
167 
168     @Override
169     public List<Parameter> getParameters() {
170         final List<Parameter> params = new ArrayList<>();
171         if (super.getParameters() != null) {
172             params.addAll(super.getParameters());
173         }
174         if (parametersValues != null) {
175             parametersValues.forEach((paramId, paramValue) -> {
176                 final Parameter param = new Parameter();
177                 param.setId(paramId);
178                 param.setAttribute(paramId);
179                 params.add(param);
180             });
181         }
182         return params;
183     }
184 
185     @Override
186     public Set<Variable> getVariables() {
187         Objects.requireNonNull(expression, "expression must not be null!");
188         formula.setExpression(expression);
189         return formula.getDataVariables();
190     }
191 
192     @Override
193     public boolean isComputable(final Resource<? extends DailyData> res) {
194         if (res == null) {
195             throw new IllegalArgumentException("resource must no be null!");
196         }
197         formula.setExpression(expression);
198         return formula.isValid();
199     }
200 
201     /**
202      * Context: Deserialization does not initialize formula.
203      *
204      * A final field must be initialized either by direct assignment of an initial value or in the constructor. During
205      * deserialization, neither of these are invoked, so initial values for transients must be set in the
206      * 'readObject()' private method that's invoked during deserialization. And for that to work, the transients must
207      * be non-final.
208      *
209      * @param ois input stream from deserialization
210      */
211     private void readObject(final ObjectInputStream ois) throws ClassNotFoundException, IOException {
212         // perform the default de-serialization first
213         ois.defaultReadObject();
214         formula = new JEXLFormula();
215     }
216 
217     @Override
218     public void setParametersFromKnowledge(final Knowledge knowledge) {
219         final Indicator indicator = knowledge.getIndicator(getId());
220         setParameters(indicator.getParameters());
221     }
222 
223     @Override
224     public String toStringTree(final String indent) {
225         final StringBuilder sb = new StringBuilder();
226         sb.append(toStringTreeBase(indent));
227         sb.append(indent).append("  expression: ").append(expression).append("\n");
228         return sb.toString();
229     }
230 
231     @Override
232     public void removeParameter(final Parameter param) {
233         if (parametersValues != null) {
234             parametersValues.remove(param.getId());
235         }
236     }
237 
238 }