Formula.java
/*
* Copyright (C) 2021 INRAE AgroClim
*
* 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 <http://www.gnu.org/licenses/>.
*/
package fr.inrae.agroclim.indicators.model.indicator;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.DoubleStream;
import fr.inrae.agroclim.indicators.exception.IndicatorsException;
import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
import fr.inrae.agroclim.indicators.model.ExpressionParameter;
import fr.inrae.agroclim.indicators.model.JEXLFormula;
import fr.inrae.agroclim.indicators.model.Knowledge;
import fr.inrae.agroclim.indicators.model.Parameter;
import fr.inrae.agroclim.indicators.model.data.DailyData;
import fr.inrae.agroclim.indicators.model.data.Resource;
import fr.inrae.agroclim.indicators.model.data.Variable;
import fr.inrae.agroclim.indicators.util.Doublet;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
/**
* Compute values using JEXL expressions.
*
* Last changed : $Date$
*
* @author $Author$
* @version $Revision$
*/
@XmlType(propOrder = {"aggregation", "expression", "expressionParameters"})
@EqualsAndHashCode(
callSuper = false,
of = {"aggregation", "expression", "expressionParameters"}
)
public final class Formula extends SimpleIndicator implements Detailable {
/**
* Aggregation to apply to computed values.
*/
@RequiredArgsConstructor
public enum Aggregation {
/**
* Average.
*/
AVERAGE(s -> s.average().orElse(0)),
/**
* Count.
*/
COUNT(s -> Double.valueOf(s.count())),
/**
* Get max value.
*/
MAX(s -> s.max().orElse(0)),
/**
* Get min value.
*/
MIN(s -> Double.valueOf(s.count())),
/**
* Get sum of values.
*/
SUM(s -> s.sum());
/**
* Function to compute the aggregated value.
*/
@Getter
private final Function<DoubleStream, Double> function;
}
/**
* UUID for Serializable.
*/
private static final long serialVersionUID = 6030595237342422020L;
/**
* Aggregation to apply to computed values.
*/
@Getter
@Setter
private Aggregation aggregation;
/**
* JEXL expression.
*/
@Getter
@Setter
private String expression;
/**
* Parameters for JEXLFormula.
*/
@Getter
@Setter
@XmlElement(name = "expressionParameter")
private List<ExpressionParameter> expressionParameters;
/**
* org.apache.commons.jexl2.JexlEngine handler.
*/
private transient JEXLFormula formula = new JEXLFormula();
/**
* Values id of parameter ⮕ value of parameter.
*/
@Setter
@Getter
private transient Map<String, Double> parametersValues = new HashMap<>();
@Override
public double computeSingleValue(final Resource<? extends DailyData> res) throws IndicatorsException {
if (aggregation == null) {
throw new IndicatorsException(ComputationErrorType.FORMULA_AGGREGATION_NULL);
}
Objects.requireNonNull(res, "Resource must not be null!");
if (res.getData() == null) {
throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
}
final Set<Variable> dataVariables = getVariables();
final List<Double> computedValues = new ArrayList<>();
for (final DailyData data : res.getData()) {
final Map<String, Double> values = new HashMap<>();
dataVariables.forEach(variable -> values.put(variable.name(), data.getValue(variable)));
if (parametersValues != null) {
values.putAll(parametersValues);
}
if (expressionParameters != null) {
expressionParameters.forEach(p -> values.put(p.getName(), p.getValue()));
}
computedValues.add(formula.evaluate(values, Double.class));
}
final DoubleStream stream = computedValues.stream().mapToDouble(Double::doubleValue);
return aggregation.getFunction().apply(stream);
}
@Override
public List<Doublet<Parameter, Number>> getParameterDefaults() {
// no substitution
return List.of();
}
@Override
public List<Parameter> getParameters() {
final List<Parameter> params = new ArrayList<>();
if (super.getParameters() != null) {
params.addAll(super.getParameters());
}
if (parametersValues != null) {
parametersValues.forEach((paramId, paramValue) -> {
final Parameter param = new Parameter();
param.setId(paramId);
param.setAttribute(paramId);
params.add(param);
});
}
return params;
}
@Override
public Set<Variable> getVariables() {
Objects.requireNonNull(expression, "expression must not be null!");
formula.setExpression(expression);
return formula.getDataVariables();
}
@Override
public boolean isComputable(final Resource<? extends DailyData> res) {
if (res == null) {
throw new IllegalArgumentException("resource must no be null!");
}
formula.setExpression(expression);
return formula.isValid();
}
/**
* Context: Deserialization does not initialize formula.
*
* A final field must be initialized either by direct assignment of an initial value or in the constructor. During
* deserialization, neither of these are invoked, so initial values for transients must be set in the
* 'readObject()' private method that's invoked during deserialization. And for that to work, the transients must
* be non-final.
*
* @param ois input stream from deserialization
*/
private void readObject(final ObjectInputStream ois) throws ClassNotFoundException, IOException {
// perform the default de-serialization first
ois.defaultReadObject();
formula = new JEXLFormula();
}
@Override
public void setParametersFromKnowledge(final Knowledge knowledge) {
final Indicator indicator = knowledge.getIndicator(getId());
setParameters(indicator.getParameters());
}
@Override
public String toStringTree(final String indent) {
final StringBuilder sb = new StringBuilder();
sb.append(toStringTreeBase(indent));
sb.append(indent).append(" expression: ").append(expression).append("\n");
return sb.toString();
}
@Override
public void removeParameter(final Parameter param) {
if (parametersValues != null) {
parametersValues.remove(param.getId());
}
}
}