| /********************************************************************************* |
| * 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; |
| } |
| |
| } |