blob: 5277b0cd8f635f313533482300e3da1a41a0d851 [file] [log] [blame]
/**
********************************************************************************
* Copyright (c) 2020 Robert Bosch GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Robert Bosch GmbH - initial API and implementation
********************************************************************************
*/
package org.eclipse.app4mc.amalthea.model.util;
import org.apache.commons.math3.distribution.WeibullDistribution;
import org.apache.commons.math3.special.Gamma;
/**
* @author mlh2sh
*
*/
public class WeibullUtil {
private static final String NO_VALID_WEIBULL_DISTRIBUTION_FOUND = "invalid argument: there is no valid weibull distribution for the used parameter";
// Suppress default constructor
private WeibullUtil() {
throw new IllegalStateException("Utility class");
}
public static class Parameters {
public double given_lowerBound = 0.0;
public double given_upperBound = 0.0;
public double requested_mean = 0.0;
public double requested_pRemainPromille = 0.0;
public double shape = 1.0;
public double scale = 1.0;
public String error = null;
@Override
public String toString() {
String output = "WeibullParam"
+ "\n shape = " + shape
+ "\n scale = " + scale
+ "\n lowerBound = " + given_lowerBound
+ "\n upperBound = " + given_upperBound
+ "\n mean = " + requested_mean
+ "\n pRemainPromille = " + requested_pRemainPromille;
if (error != null) {
output = output + "\n *** error *** : " + error;
}
return output;
}
public String qualityReport() {
// check with Apache Commons Math
final WeibullDistribution mathFunction = new WeibullDistribution(null, shape, scale);
double cdfAvg = mathFunction.cumulativeProbability(requested_mean - given_lowerBound);
double cdfMax = mathFunction.cumulativeProbability(given_upperBound - given_lowerBound);
double remPromilleC = (1.0 - cdfMax) * 1000.0;
double averageC = WeibullUtil.computeAverage(shape, scale, given_lowerBound, given_upperBound);
double medianC = WeibullUtil.computeMedian(shape, scale, given_lowerBound, given_upperBound);
return "Weibull Parameter Quality"
+ "\nInput: [" + given_lowerBound + ", " + requested_mean + ", " + given_upperBound + "], " + requested_pRemainPromille
+ "\nEstimated parameters: shape = " + shape + "\tscale = " + scale
+ "\n computed average: " + averageC
+ "\n computed median: " + medianC
+ "\n computed pRemainPromille: " + remPromilleC
+ "\n computed cdf(lower interval) = " + cdfAvg
+ "\n computed cdf(upper interval) = " + (cdfMax - cdfAvg);
}
}
/**
* See Crénin, François, Truncated Weibull Distribution Functions and Moments (October 30, 2015).
* Available at SSRN: https://ssrn.com/abstract=2690255 or http://dx.doi.org/10.2139/ssrn.2690255
*
* @param shape
* @param scale
* @param location
* @param upperBound
* @return
*/
public static double computeAverage(double shape, double scale, double location, Double upperBound) {
if (shape <= 0.0 || scale <= 0.0) return Double.NaN;
if (upperBound == null || upperBound.isNaN() || upperBound.isInfinite()) {
// Standard Weibull distribution
return location + scale * Gamma.gamma(1 + 1 / shape);
} else if (location < upperBound) {
// Right truncated Weibull distribution
final double range = upperBound - location;
final double power = Math.pow(range / scale, shape);
double factor1 = scale / (1 - Math.exp(-1.0 * power));
double factor2 = lowerIncompleteGammaFunction(1 + 1 / shape, power);
return location + factor1 * factor2;
}
return Double.NaN;
}
/**
* See https://en.wikipedia.org/wiki/Incomplete_gamma_function
*
* @param s
* @param x
* @return
*/
private static double lowerIncompleteGammaFunction(double s, double x) {
return Gamma.regularizedGammaP(s, x) * Gamma.gamma(s);
}
public static double computeMedian(double shape, double scale, double location, Double upperBound) {
double pRemainPromille;
if (upperBound == null || upperBound.isNaN() || upperBound.isInfinite()) {
pRemainPromille = 0.0;
} else {
pRemainPromille = computePRemainPromille(shape, scale, location, upperBound);
}
return computeMedianWithPRemainPromille(shape, scale, location, pRemainPromille);
}
public static double computeMedianWithPRemainPromille(double shape, double scale, double location, double pRemainPromille) {
if (shape <= 0.0 || scale <= 0.0 || pRemainPromille < 0 || pRemainPromille >= 1000) return Double.NaN;
double x = scale * Math.pow(-1.0 * Math.log(0.5 + pRemainPromille / 2000.0), 1 / shape);
return location + x;
}
public static double computePRemainPromille(double shape, double scale, double location, double upperBound) {
if (shape <= 0.0 || scale <= 0.0) return Double.NaN;
if (location < upperBound) {
// Compute CDF at upper bound; pRemainPromille = (1 - CDF) * 1000
final double range = upperBound - location;
final double power = Math.pow(range / scale, shape);
return Math.exp(-1.0 * power) * 1000.0;
}
return Double.NaN;
}
public static double computeUpperBound(double shape, double scale, double location, double pRemainPromille) {
if (shape <= 0.0 || scale <= 0.0) return Double.NaN;
double pRemain = pRemainPromille / 1000.0;
if (pRemain > 0 && pRemain < 1) {
// Compute CDF at upper bound; pRemain = (1 - CDF)
return location + Math.pow(-1.0 * Math.log(pRemain), 1.0 / shape) * scale;
}
return Double.NaN;
}
/**
* This is a simple attempt to get a better parameter estimation for big pRemainPromille values.
*
* As an alternative to the regular estimation another estimation based on the median value is calculated.
* The better result is selected according to the relative error of both requested values.
*
* Better solutions are welcome !
*
* @param lowerBound
* @param average
* @param upperBound
* @param pRemainPromille
* @return
*/
public static Parameters findParameters(double lowerBound, double average, double upperBound, double pRemainPromille) {
Parameters p1 = findParametersForAverage(lowerBound, average, upperBound, pRemainPromille);
Parameters p2 = findParametersForMedian(lowerBound, average, upperBound, pRemainPromille);
if (p2.error != null) return p1;
// compute resulting values for estimated parameters
double p1_average = computeAverage(p1.shape, p1.scale, lowerBound, upperBound);
double p1_pRemainPromille = computePRemainPromille(p1.shape, p1.scale, lowerBound, upperBound);
double p2_average = computeAverage(p2.shape, p2.scale, lowerBound, upperBound);
double p2_pRemainPromille = computePRemainPromille(p2.shape, p2.scale, lowerBound, upperBound);
// find the better one
double error1 = Math.abs(1.0 - p1_average / average) + Math.abs(1.0 - p1_pRemainPromille / pRemainPromille);
double error2 = Math.abs(1.0 - p2_average / average) + Math.abs(1.0 - p2_pRemainPromille / pRemainPromille);
if (error2 < error1) return p2;
return p1;
}
/**
* The approximation from the given parameters boil down to the following problem (latex formulas):
* <p>
* CDF_{weibull}(upperBound) = 1 - pRemainPromille/1000
* <p>
* Using the Weibull CDF (1) and the Expectation Value (average) (2)
* we can derive a formula for the scale dependent on shape and known values from (2)
* <p>
* (1) CDF_{weibull}(x) = 1 - e^{- (\frac{x}{scale})^{shape}}
* <p>
* (2) avg = scale \cdot \Gamma(1 + \frac{1}{shape})
* <p>
* (3) scale = \frac{avg}{\Gamma(1 + (1 / shape))}
* <p>
* with (1), (3) and pRemain = pRemainPromille/1000 follows
* <p>
* (4) pRemain = e^{-(\frac{upper limit}{E_{avg}} \cdot \Gamma(1 + \frac{1}{shape}))^{shape}}
* <p>
* from this we form a zero finding problem
* <p>
* (5) 0 = e^{-(\frac{upper limit}{E_{avg}}\cdot \Gamma(1 + \frac{1}{shape}))^{shape}} - pRemain
* <p>
* The work "Robust Scheduling of Real-Time Applications on Efficient Embedded Multicore Systems"
* (https://mediatum.ub.tum.de/download/1063381/1063381.pdf) proposes an algorithm from (4) which
* is basically the bisection method.
*/
public static Parameters findParametersForAverage(double lowerBound, double average, double upperBound, double pRemainPromille) {
Parameters result = new Parameters();
result.shape = 1;
result.scale = 1;
result.requested_pRemainPromille = pRemainPromille;
result.given_lowerBound = lowerBound;
result.requested_mean = average;
result.given_upperBound = upperBound;
// shifted values for standard 2 parameter Weibull distribution
double avg = average - lowerBound;
double max = upperBound - lowerBound;
double pRemain = pRemainPromille / 1000;
if ((max / avg) > 10000) {
result.error = "finding: upper bound and mean have no valid value for parameter";
return result;
}
if (pRemain > 1) {
result.error = "invalid_argument: pRemainPromille needs to be smaller than 1";
return result;
}
/* find zero interval */
double right = 0.2;
double left = 0.1;
double right_f = paramZeroFunctionforAverage(right, avg, max, pRemain);
double left_f = paramZeroFunctionforAverage(left, avg, max, pRemain);
while ((right_f > 0 || left_f < 0) && right < 1000 && (pRemain + right_f) > 0) {
left = right;
right = right + Math.max((0.1 * right), 0.1);
right_f = paramZeroFunctionforAverage(right, avg, max, pRemain);
left_f = paramZeroFunctionforAverage(left, avg, max, pRemain);
}
if (right_f > 0 && left_f < 0) {
result.error = NO_VALID_WEIBULL_DISTRIBUTION_FOUND;
return result;
}
// if we found the exact solution on one of the interval borders, return it
if (left_f == 0) {
right = left;
right_f = left_f;
}
if (right_f == 0) {
result.shape = right;
result.scale = computeScaleForAverage(result.shape, avg);
return result;
}
while (true) {
double half = left + (right - left) / 2;
double half_f = paramZeroFunctionforAverage(half, avg, max, pRemain);
// found solution
if (half_f == 0.0 || left == right || half == right || half == left) {
result.shape = half;
result.scale = computeScaleForAverage(result.shape, avg);
return result;
} else {
if (Math.signum(1.0) == Math.signum(half_f)) {
left = half;
} else {
right = half;
}
}
if (!Double.isFinite(left) || !Double.isFinite(right) || !Double.isFinite(half_f)) {
result.error = NO_VALID_WEIBULL_DISTRIBUTION_FOUND;
return result;
}
}
}
private static double paramZeroFunctionforAverage(double shape, double avg, double max, double pRemain) {
return 1 / Math.exp(Math.pow((max / avg) * Gamma.gamma(1 + 1 / shape), shape)) - pRemain;
}
private static double computeScaleForAverage(double shape, double avg) {
return avg / (Gamma.gamma(1 + (1 / shape)));
}
public static Parameters findParametersForMedian(double lowerBound, double median, double upperBound, double pRemainPromille) {
Parameters result = new Parameters();
result.shape = 1;
result.scale = 1;
result.requested_pRemainPromille = pRemainPromille;
result.given_lowerBound = lowerBound;
result.requested_mean = median;
result.given_upperBound = upperBound;
// shifted values for standard 2 parameter Weibull distribution
double med = median - lowerBound;
double max = upperBound - lowerBound;
double pRemain = pRemainPromille / 1000;
if ((max / med) > 10000) {
result.error = "finding: upper bound and mean have no valid value for parameter";
return result;
}
if (pRemain > 1) {
result.error = "invalid_argument: pRemainPromille needs to be smaller than 1";
return result;
}
/* find zero interval */
double right = 0.2;
double left = 0.1;
double right_f = paramZeroFunctionForMedian(right, med, max, pRemain);
double left_f = paramZeroFunctionForMedian(left, med, max, pRemain);
while ((right_f > 0 || left_f < 0) && right < 1000 && (pRemain + right_f) > 0) {
left = right;
right = right + Math.max((0.1 * right), 0.1);
right_f = paramZeroFunctionForMedian(right, med, max, pRemain);
left_f = paramZeroFunctionForMedian(left, med, max, pRemain);
}
if (right_f > 0 && left_f < 0) {
result.error = NO_VALID_WEIBULL_DISTRIBUTION_FOUND;
return result;
}
// if we found the exact solution on one of the interval borders, return it
if (left_f == 0) {
right = left;
right_f = left_f;
}
if (right_f == 0) {
result.shape = right;
result.scale = computeScaleForMedian(result.shape, med, pRemain);
return result;
}
while (true) {
double half = left + (right - left) / 2;
double half_f = paramZeroFunctionForMedian(half, med, max, pRemain);
// found solution
if (half_f == 0.0 || left == right || half == right || half == left) {
result.shape = half;
result.scale = computeScaleForMedian(result.shape, med, pRemain);
return result;
} else {
if (Math.signum(1.0) == Math.signum(half_f)) {
left = half;
} else {
right = half;
}
}
if (!Double.isFinite(left) || !Double.isFinite(right) || !Double.isFinite(half_f)) {
result.error = NO_VALID_WEIBULL_DISTRIBUTION_FOUND;
return result;
}
}
}
private static double paramZeroFunctionForMedian(double shape, double median, double max, double pRemain) {
double factor1 = max / median;
double factor2 = Math.pow(-1.0 * Math.log(0.5 + pRemain / 2.0), 1 / shape);
return 1.0 / Math.exp(Math.pow(factor1 * factor2, shape)) - pRemain;
}
private static double computeScaleForMedian(double shape, double median, double pRemain) {
return median / Math.pow(-1.0 * Math.log(0.5 + pRemain / 2.0), 1 / shape);
}
}