blob: c64247b9d93673b2a7071de38b10afbdb9b37050 [file] [log] [blame]
/*********************************************************************************
* Copyright (c) 2020-2021 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.visualizations.standard;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.annotation.PostConstruct;
import org.apache.commons.math3.distribution.BetaDistribution;
import org.apache.commons.math3.distribution.NormalDistribution;
import org.apache.commons.math3.distribution.WeibullDistribution;
import org.eclipse.app4mc.amalthea.model.AmaltheaServices;
import org.eclipse.app4mc.amalthea.model.ITimeDeviation;
import org.eclipse.app4mc.amalthea.model.Time;
import org.eclipse.app4mc.amalthea.model.TimeBetaDistribution;
import org.eclipse.app4mc.amalthea.model.TimeBoundaries;
import org.eclipse.app4mc.amalthea.model.TimeConstant;
import org.eclipse.app4mc.amalthea.model.TimeGaussDistribution;
import org.eclipse.app4mc.amalthea.model.TimeHistogram;
import org.eclipse.app4mc.amalthea.model.TimeHistogramEntry;
import org.eclipse.app4mc.amalthea.model.TimeInterval;
import org.eclipse.app4mc.amalthea.model.TimeStatistics;
import org.eclipse.app4mc.amalthea.model.TimeUniformDistribution;
import org.eclipse.app4mc.amalthea.model.TimeUnit;
import org.eclipse.app4mc.amalthea.model.TimeWeibullEstimatorsDistribution;
import org.eclipse.app4mc.amalthea.model.util.WeibullUtil;
import org.eclipse.app4mc.visualization.ui.registry.Visualization;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.osgi.service.component.annotations.Component;
import javafx.embed.swt.FXCanvas;
import javafx.scene.Scene;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ValueAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Label;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
@Component(property= {
"name=Probability Density Diagram (time values)",
"description=Visualize the Probability Density Function (PDF) of the deviation"
})
public class DeviationChartTime extends AbstractDeviationChart implements Visualization {
@PostConstruct
public void createVisualization(ITimeDeviation dev, Composite parent) {
parent.setLayout(new GridLayout());
FXCanvas canvas = new FXCanvas(parent, SWT.NONE);
GridDataFactory
.fillDefaults()
.grab(true, true)
.applyTo(canvas);
// create the root layout pane
BorderPane layout = new BorderPane();
layout.setBackground(new Background(new BackgroundFill(Color.WHITE, null, null)));
// create a Scene instance
// set the layout container as root
// set the background fill to the background color of the shell
Scene scene = new Scene(layout);
// set the Scene to the FXCanvas
canvas.setScene(scene);
// ***** display message 'invalid input' *****
if (!isValid(dev)) {
Label output = new Label();
output.setText("Invalid input");
layout.setCenter(output);
return;
}
// ***** display chart with single peek (includes ContinuousValueConstant) *****
TimeUnit commonUnit = getCommonUnit(dev);
if (commonUnit == null) commonUnit = TimeUnit.MS; // fallback
final Double lowerBound = (dev.getLowerBound() != null) ? scaleTo(dev.getLowerBound(), commonUnit) : null;
final Double upperBound = (dev.getUpperBound() != null) ? scaleTo(dev.getUpperBound(), commonUnit) : null;
if (isSinglePeek(lowerBound, upperBound)) {
AreaChart<Number, Number> chart = addNewChart(layout, dev, null);
setChartXBounds(chart, lowerBound - 5, upperBound + 5);
setChartYBounds(chart, 100);
addSinglePeek(chart, 80, lowerBound);
return;
}
// ***** display standard chart *****
AreaChart<Number, Number> chart = addNewChart(layout, dev, commonUnit.getName());
if (lowerBound != null && upperBound != null && lowerBound < upperBound) {
final double margin = 0.25 * (upperBound - lowerBound);
final double xMin = lowerBound - margin;
final double xMax = upperBound + margin;
setChartXBounds(chart, xMin, xMax);
}
String status = null;
if (dev instanceof TimeHistogram) {
fillChart(chart, (TimeHistogram) dev, commonUnit);
} else if (dev instanceof TimeGaussDistribution) {
status = fillChart(chart, (TimeGaussDistribution) dev, commonUnit);
} else if (dev instanceof TimeBoundaries) {
fillChart(chart, (TimeBoundaries) dev, commonUnit);
} else if (dev instanceof TimeStatistics) {
fillChart(chart, (TimeStatistics) dev, commonUnit);
} else if (dev instanceof TimeUniformDistribution) {
fillChart(chart, (TimeUniformDistribution) dev, commonUnit);
} else if (dev instanceof TimeBetaDistribution) {
fillChart(chart, (TimeBetaDistribution) dev, commonUnit);
} else if (dev instanceof TimeWeibullEstimatorsDistribution) {
status = fillChart(chart, (TimeWeibullEstimatorsDistribution) dev, commonUnit);
}
// ***** display status line *****
addNewStatus(layout, status);
}
private TimeUnit getCommonUnit(ITimeDeviation dev) {
if (dev instanceof TimeConstant) {
return ((TimeConstant) dev).getValue().getUnit();
}
if (dev instanceof TimeHistogram) {
List<Time> list = new ArrayList<>();
for (TimeHistogramEntry entry : ((TimeHistogram) dev).getEntries()) {
list.add(entry.getLowerBound());
list.add(entry.getUpperBound());
}
return getCommonUnit(list);
}
if (dev instanceof TimeInterval) {
final TimeInterval interval = (TimeInterval) dev;
List<Time> list = new ArrayList<>();
list.add(interval.getLowerBound());
list.add(interval.getUpperBound());
final Time average = getAverage(interval);
if (average != null) {
list.add(average);
}
return getCommonUnit(list);
}
if (dev instanceof TimeGaussDistribution) {
final TimeGaussDistribution gauss = (TimeGaussDistribution) dev;
List<Time> list = new ArrayList<>();
list.add(gauss.getMean());
list.add(gauss.getSd());
return getCommonUnit(list);
}
return null;
}
private TimeUnit getCommonUnit(List<Time> times) {
List<TimeUnit> units = AmaltheaServices.TIME_UNIT_LIST;
int minIndex = units.size() - 1;
for (Time time : times) {
int index = units.indexOf(time.getUnit());
if (index < minIndex) minIndex = index;
}
return units.get(minIndex);
}
private double scaleTo(Time time, TimeUnit unit) {
// Get the difference from the source TimeUnit to the destination TimeUnit as a factor
List<TimeUnit> units = AmaltheaServices.TIME_UNIT_LIST;
int power = units.indexOf(time.getUnit()) - units.indexOf(unit);
double factor = Math.pow(1000, power);
return time.getValue().doubleValue() * factor;
}
private void fillChart(AreaChart<Number, Number> chart, TimeHistogram histogram, TimeUnit commonUnit) {
long maxY = 0;
// add chart content
for (TimeHistogramEntry entry : histogram.getEntries()) {
double lower = scaleTo(entry.getLowerBound(), commonUnit);
double upper = scaleTo(entry.getUpperBound(), commonUnit);
long occur = entry.getOccurrences();
maxY = Math.max(occur, maxY);
final Series<Number, Number> series = new XYChart.Series<>();
series.getData().add(new Data<>(lower, 0L));
series.getData().add(new Data<>(lower, occur));
series.getData().add(new Data<>(upper, occur));
series.getData().add(new Data<>(upper, 0L));
// add and style series
addSeriesStandard(chart, series);
}
// adopt range and add markers (min, avg, max)
setChartYBounds(chart, maxY * 1.2);
addMarkers(chart, maxY * 1.1,
scaleTo(histogram.getLowerBound(), commonUnit),
scaleTo(histogram.getAverage(), commonUnit),
scaleTo(histogram.getUpperBound(), commonUnit)
);
}
private String fillChart(AreaChart<Number, Number> chart, TimeGaussDistribution dev, TimeUnit commonUnit) {
double maxY = 0.000001; // initialize with a lower bound > 0
final double mean = scaleTo(dev.getMean(), commonUnit);
final double sd = scaleTo(dev.getSd(), commonUnit);
// add chart content
final Double lowerBound = (dev.getLowerBound() != null) ? scaleTo(dev.getLowerBound(), commonUnit) : null;
final Double upperBound = (dev.getUpperBound() != null) ? scaleTo(dev.getUpperBound(), commonUnit) : null;
// standard display range for non truncated Gauss
double xMin = mean - 4 * sd;
double xMax = mean + 4 * sd;
if (lowerBound != null && upperBound != null && lowerBound < upperBound) {
// standard display range for truncated Gauss with upper and lower bounds
double margin = 0.25 * (upperBound - lowerBound);
xMin = lowerBound - margin;
xMax = upperBound + margin;
} else {
// only lower bound set
if (lowerBound != null && upperBound == null) {
xMin = Math.min(xMin, lowerBound - sd);
xMax = Math.max(xMax, lowerBound + sd);
}
// only upper bound set
if (lowerBound == null && upperBound != null) {
xMin = Math.min(xMin, upperBound - sd);
xMax = Math.max(xMax, upperBound + sd);
}
}
final double step = (xMax - xMin) / 200;
final NormalDistribution mathFunction = new NormalDistribution(null, mean, sd);
final Series<Number, Number> seriesMain = new XYChart.Series<>();
final Series<Number, Number> seriesLeft = new XYChart.Series<>();
final Series<Number, Number> seriesRight = new XYChart.Series<>();
for (double x = xMin; x <= xMax; x=x+step) {
double y = mathFunction.density(x);
maxY = Math.max(y, maxY);
if (lowerBound != null && x < lowerBound) {
seriesLeft.getData().add(new Data<>(x, y));
} else if (upperBound != null && x > upperBound) {
seriesRight.getData().add(new Data<>(x, y));
} else {
seriesMain.getData().add(new Data<>(x, y));
}
}
// add and style series
addSeriesOffLimit(chart, seriesLeft);
addSeriesOffLimit(chart, seriesRight);
addSeriesStandard(chart, seriesMain);
// adopt range
setChartXBounds(chart, xMin, xMax);
setChartYBounds(chart, maxY * 1.2);
// add markers (min, avg, max)
Double average = (dev.getAverage() == null) ? null : scaleTo(dev.getAverage(), commonUnit);
addMarkers(chart, maxY * 1.1, lowerBound, average, upperBound);
// add warning if probability is too low (< 10%)
final double cdfUpper = (upperBound == null) ? 1 : mathFunction.cumulativeProbability(upperBound);
final double cdfLower = (lowerBound == null) ? 0 : mathFunction.cumulativeProbability(lowerBound);
final double p = cdfUpper - cdfLower;
if (p < 0.1) {
DecimalFormat df = new DecimalFormat("0.0000", new DecimalFormatSymbols(Locale.ENGLISH));
return "Cumulative probability within specified bounds is " + df.format(p);
}
return null;
}
private void fillChart(AreaChart<Number, Number> chart, TimeBoundaries dev, TimeUnit commonUnit) {
final double lower = scaleTo(dev.getLowerBound(), commonUnit);
final double upper = scaleTo(dev.getUpperBound(), commonUnit);
final Series<Number, Number> series = new XYChart.Series<>();
series.getData().add(new Data<>(lower, 60));
series.getData().add(new Data<>(upper, 60));
addSeriesGradient(chart, series);
setChartYBounds(chart, 100);
addMarkers(chart, 80, lower, null, upper);
}
private void fillChart(AreaChart<Number, Number> chart, TimeStatistics dev, TimeUnit commonUnit) {
double maxY = 0;
final double lower = scaleTo(dev.getLowerBound(), commonUnit);
final double upper = scaleTo(dev.getUpperBound(), commonUnit);
final double average = scaleTo(dev.getAverage(), commonUnit);
if (lower < upper) {
double y1 = 100.0 / (average - lower);
double y2 = 100.0 / (upper - average);
maxY = Math.max(y1, y2);
final Series<Number, Number> series1 = new XYChart.Series<>();
series1.getData().add(new Data<>(lower, y1));
series1.getData().add(new Data<>(average, y1));
final Series<Number, Number> series2 = new XYChart.Series<>();
series2.getData().add(new Data<>(average, y2));
series2.getData().add(new Data<>(upper, y2));
// add and style series
addSeriesGradient(chart, series1);
addSeriesGradient(chart, series2);
} else {
maxY = 100; // single value
}
// adopt range and add markers (min, avg, max)
setChartYBounds(chart, maxY * 1.2);
addMarkers(chart, maxY * 1.1, lower, average, upper);
}
private void fillChart(AreaChart<Number, Number> chart, TimeUniformDistribution dev, TimeUnit commonUnit) {
final double lower = scaleTo(dev.getLowerBound(), commonUnit);
final double upper = scaleTo(dev.getUpperBound(), commonUnit);
final Series<Number, Number> series = new XYChart.Series<>();
series.getData().add(new Data<>(lower, 60));
series.getData().add(new Data<>(upper, 60));
addSeriesStandard(chart, series);
setChartYBounds(chart, 100);
addMarkers(chart, 80, lower, null, upper);
}
private void fillChart(AreaChart<Number, Number> chart, TimeBetaDistribution dev, TimeUnit commonUnit) {
double maxY = 0.0;
final double x1 = scaleTo(dev.getLowerBound(), commonUnit);
final double x2 = scaleTo(dev.getUpperBound(), commonUnit);
final double range = x2 - x1;
if (range > 0) {
final BetaDistribution mathFunction = new BetaDistribution(null, dev.getAlpha(), dev.getBeta());
final Series<Number, Number> series = new XYChart.Series<>();
for (double x = 0.005; x < 1; x=x+0.005) {
double y = mathFunction.density(x);
maxY = Math.max(y, maxY);
series.getData().add(new Data<>(x1 + (x * range), y));
}
// add and style series
addSeriesStandard(chart, series);
}
// adopt range and add markers (min, avg, max)
setChartYBounds(chart, maxY * 1.2);
addMarkers(chart, maxY * 1.1, x1, scaleTo(dev.getAverage(), commonUnit), x2);
}
private String fillChart(AreaChart<Number, Number> chart, TimeWeibullEstimatorsDistribution dev, TimeUnit commonUnit) {
double maxY = 0;
final double lower = scaleTo(dev.getLowerBound(), commonUnit);
final double upper = scaleTo(dev.getUpperBound(), commonUnit);
final double average = scaleTo(dev.getAverage(), commonUnit);
final double remain = dev.getPRemainPromille();
WeibullUtil.Parameters params = WeibullUtil.findParameters(lower, average, upper, remain);
double shape = params.shape;
double scale = params.scale;
if (lower < upper) {
if (params.error == null) {
final WeibullDistribution mathFunction = new WeibullDistribution(null, shape, scale);
final Series<Number, Number> seriesMain = new XYChart.Series<>();
final Series<Number, Number> seriesRight = new XYChart.Series<>();
final double xMax = (chart.getXAxis() instanceof NumberAxis) ?
((ValueAxis<Number>) chart.getXAxis()).getUpperBound() : upper;
final double step = (xMax - lower) / 200;
for (double x = lower; x <= xMax; x = x + step) {
double y = mathFunction.density(x - lower);
if (Double.isFinite(y)) {
maxY = Math.max(y, maxY);
if (x <= upper) {
seriesMain.getData().add(new Data<>(x, y));
} else {
seriesRight.getData().add(new Data<>(x, y));
}
}
}
// add and style series
addSeriesStandard(chart, seriesMain);
addSeriesOffLimit(chart, seriesRight);
} else {
maxY = 100;
}
// adopt range
setChartYBounds(chart, maxY * 1.2);
// add computed average
double computedAvg = WeibullUtil.computeAverage(shape, scale, lower, upper);
final Series<Number, Number> series2 = new XYChart.Series<>();
chart.getData().add(series2);
series2.setName("avg");
series2.getData().add(new XYChart.Data<>(computedAvg, 0));
series2.getData().add(new XYChart.Data<>(computedAvg, maxY / 2));
series2.getNode().lookup(".chart-series-area-line").setStyle("-fx-stroke: rgba(5, 150, 5, 1.0); -fx-stroke-dash-array: 3;");
series2.getData().get(1).getNode().setVisible(false);
// add markers (min, avg, max)
setChartYBounds(chart, maxY * 1.2);
addMarkers(chart, maxY * 1.1, lower, average, upper);
// add warning if difference between input and actual value of average is too big
if (Math.abs(average - computedAvg) / (upper - lower) > 0.01) {
return "Approximation is deviating. Average marker: red" + ARROW + "requested, green" + ARROW + "actual";
}
}
return null;
}
}