2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2016-2018 Ericsson. All rights reserved.
4 * Modifications Copyright (C) 2020 Nordix Foundation.
5 * ================================================================================
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
18 * SPDX-License-Identifier: Apache-2.0
19 * ============LICENSE_END=========================================================
22 package org.onap.policy.apex.examples.adaptive.model.java;
24 import java.util.Arrays;
25 import java.util.LinkedHashMap;
26 import java.util.LinkedList;
27 import java.util.List;
29 import org.apache.commons.math3.distribution.TDistribution;
30 import org.apache.commons.math3.util.FastMath;
31 import org.onap.policy.apex.core.engine.executor.context.TaskSelectionExecutionContext;
32 import org.onap.policy.apex.examples.adaptive.concepts.AnomalyDetection;
33 import org.onap.policy.apex.model.basicmodel.concepts.ApexException;
36 * The Class AnomalyDetectionPolicyDecideTaskSelectionLogic.
38 public class AnomalyDetectionPolicyDecideTaskSelectionLogic {
40 // Recurring string constants
41 private static final String ANOMALY_DETECTION_ALBUM = "AnomalyDetectionAlbum";
42 private static final String ANOMALY_DETECTION = "AnomalyDetection";
45 private static final double ANOMALY_SENSITIVITY = 0.05;
46 private static final int FREQUENCY = 360;
49 * Some utility methods
51 // exponential = 2(n+1)
52 private static final double EMA_EXPONENT = 2.0 / (7.0 + 1.0);
53 private static final double EMA_EXPONENT_1 = (1.0 - EMA_EXPONENT);
56 * A map to hold the Anomaly degree/levels/probabilities required for each task.<br>
57 * If there is no task defined for a calculated anomaly-degree, then the default task is
59 * The map use (LinkedHashMap) is an insertion-ordered map, so the first interval matching a
62 // CHECKSTYLE:OFF: checkstyle:magicNumber
63 private static final Map<double[], String> TASK_INTERVALS = new LinkedHashMap<>();
66 TASK_INTERVALS.put(new double[] {0.0, 0.1}, null); // null will mean default task
67 TASK_INTERVALS.put(new double[] {0.25, 0.5}, "AnomalyDetectionDecideTask1");
68 TASK_INTERVALS.put(new double[] {0.5, 1.01}, "AnomalyDetectionDecideTask2");
70 // CHECKSTYLE:ON: checkstyle:magicNumber
75 * @param executor the executor
78 public boolean getTask(final TaskSelectionExecutionContext executor) {
79 String id = executor.subject.getId();
80 executor.logger.debug(id);
81 String inFields = executor.inFields.toString();
82 executor.logger.debug(inFields);
83 final double now = (Double) (executor.inFields.get("MonitoredValue"));
84 final Integer iteration = (Integer) (executor.inFields.get("Iteration"));
85 // get the double[forecastedValue, AnomalyScore, AnomalyProbability]
86 final double[] vals = forecastingAndAnomaly(executor, now);
87 final double anomalyness = vals[2];
89 for (final Map.Entry<double[], String> i : TASK_INTERVALS.entrySet()) {
90 if (checkInterval(anomalyness, i.getKey())) {
96 executor.subject.getDefaultTaskKey().copyTo(executor.selectedTask);
98 executor.subject.getTaskKey(task).copyTo(executor.selectedTask);
100 executor.logger.debug(
101 "TestAnomalyDetectionTSLPolicy0000DecideStateTaskSelectionLogic.getTask():\t************\t\t\t\t"
102 + "Iteration:\t{}\tValue:\t{}\tForecast:\t{}\tAnomalyScore:\t{}\tAnomalyProbability:\t{}\t"
103 + "Invoking Task:\t{}", iteration, now, vals[0], vals[1], vals[2], executor.selectedTask);
108 * Anomaly detection and forecast.
110 * @param value The current value
111 * @return Null if the function can not be executed correctly, otherwise double[forecastedValue,
112 * AnomalyScore, AnomalyProbability]
114 private double[] forecastingAndAnomaly(final TaskSelectionExecutionContext executor, final double value) {
116 executor.getContextAlbum(ANOMALY_DETECTION_ALBUM).lockForWriting(ANOMALY_DETECTION);
117 } catch (final ApexException e) {
118 executor.logger.error("Failed to acquire write lock on \"AnomalyDetection\" context", e);
119 return new double[0];
122 // Get the context object
123 AnomalyDetection anomalyDetection =
124 (AnomalyDetection) executor.getContextAlbum(ANOMALY_DETECTION_ALBUM).get(ANOMALY_DETECTION);
125 if (anomalyDetection == null) {
126 anomalyDetection = new AnomalyDetection();
127 executor.getContextAlbum(ANOMALY_DETECTION_ALBUM).put(ANOMALY_DETECTION, anomalyDetection);
130 // Check the lists are initialized
131 if (!anomalyDetection.isInitialized()) {
132 anomalyDetection.init(FREQUENCY);
135 boolean unsetfirstround = false;
137 int frequency = anomalyDetection.getFrequency();
138 frequency = frequency + 1;
140 // reset frequency counter
141 if (frequency >= FREQUENCY) {
142 unsetfirstround = true;
145 anomalyDetection.setFrequency(frequency);
147 if (unsetfirstround && anomalyDetection.getFirstRound()) {
148 anomalyDetection.setFirstRound(false);
151 // --------- calculate the forecasted value - simple version
152 final Double lastForecast = anomalyDetection.getFrequencyForecasted().get(frequency);
154 // get forecast for current value
155 final double forecastedValue = lastForecast == null ? value : expMovingAverage(value, lastForecast);
157 // --------- calculate the anomalyScore
158 final double anomalyScore = lastForecast == null ? 0.0 : FastMath.abs(lastForecast - value);
160 anomalyDetection.getFrequencyForecasted().set(frequency, forecastedValue);
162 // anomaly score is ignored in the first frequency period
163 if (!anomalyDetection.getFirstRound()) {
164 ((LinkedList<Double>) anomalyDetection.getAnomalyScores()).addLast(anomalyScore);
167 // CHECKSTYLE:OFF: checkstyle:magicNumber
168 // max FREQUENCY*4 anomaly scores history
169 listSizeControl(anomalyDetection.getAnomalyScores(), FREQUENCY * 4);
171 // ---------- calculate the anomaly probability
172 double anomalyProbability = 0.0;
173 if (anomalyDetection.getAnomalyScores().size() > 30) {
175 anomalyProbability = getStatsTest(anomalyDetection.getAnomalyScores(), ANOMALY_SENSITIVITY);
177 // CHECKSTYLE:ON: checkstyle:magicNumber
180 executor.getContextAlbum(ANOMALY_DETECTION_ALBUM).unlockForWriting(ANOMALY_DETECTION);
181 } catch (final ApexException e) {
182 executor.logger.error("Failed to release write lock on \"AnomalyDetection\" context", e);
183 return new double[0];
186 return new double[] {forecastedValue, anomalyScore, anomalyProbability};
190 * Is the passed value inside the interval, i.e. (value < interval[1] && value>=interval[0]).
192 * @param value The value to check
193 * @param interval A 2 element double array describing an interval
194 * @return true if the value is between interval[0] (inclusive) and interval[1] (exclusive),
195 * i.e. (value < interval[1] && value>=interval[0]). Otherwise false;
197 private static boolean checkInterval(final double value, final double[] interval) {
198 if (interval == null || interval.length != 2) {
199 throw new IllegalArgumentException("something other than an interval passed to checkInterval");
201 final double min = interval[0];
202 final double max = interval[1];
203 return (value < max && value >= min);
207 * calculate the anomaly probability using statistical test.
209 * @param values the values
210 * @param significanceLevel the significance level
211 * @return the anomaly probability
213 private static double getStatsTest(final List<Double> values, final double significanceLevel) {
214 if (isAllEqual(values)) {
217 // the targeted value or the last value
218 final double currentV = values.get(values.size() - 1);
219 Double[] lvaluesCopy = values.toArray(new Double[values.size()]);
220 Arrays.sort(lvaluesCopy); // takes ~40% of method time
222 double mean = getMean(lvaluesCopy);
223 // get the test value: val
224 double val = getV(lvaluesCopy, mean, true);
225 // get the p value for the test value
226 double pvalue = getPValue(lvaluesCopy, val, mean); // takes approx 25% of method time
228 // check the critical level
229 while (pvalue < significanceLevel) { // takes approx 20% of method time
230 // the score value as the anomaly probability
231 final double score = (significanceLevel - pvalue) / significanceLevel;
232 if (Double.compare(val, currentV) == 0) {
235 // do the critical check again for the left values
236 lvaluesCopy = removevalue(lvaluesCopy, val);
237 if (isAllEqual(lvaluesCopy)) {
241 mean = getMean(lvaluesCopy);
242 val = getV(lvaluesCopy, mean, true);
243 pvalue = getPValue(lvaluesCopy, val, mean);
249 * Get the test value based on mean from sorted values.
251 * @param lvalues the l values
252 * @param mean the mean
253 * @param maxValueOnly : only the max extreme value will be tested
254 * @return the value to be tested
256 private static double getV(final Double[] lvalues, final double mean, final boolean maxValueOnly) {
257 double val = lvalues[lvalues.length - 1];
258 // max value as the extreme value
262 // check the extreme side
263 if ((val - mean) < (mean - lvalues[0])) {
270 * calculate the P value for the t distribution.
272 * @param lvalues the l values
273 * @param val the value
274 * @param mean the mean
275 * @return the p value
277 private static double getPValue(final Double[] lvalues, final double val, final double mean) {
279 final double z = FastMath.abs(val - mean) / getStdDev(lvalues, mean);
281 final double n = lvalues.length;
282 final double s = (z * z * n * (2.0 - n)) / (z * z * n - (n - 1.0) * (n - 1.0));
283 final double t = FastMath.sqrt(s);
284 // default p value = 0
286 if (!Double.isNaN(t)) {
287 // t distribution with n-2 degrees of freedom
288 final TDistribution tDist = new TDistribution(n - 2);
289 pvalue = n * (1.0 - tDist.cumulativeProbability(t));
290 // set max pvalue = 1
291 pvalue = pvalue > 1.0 ? 1.0 : pvalue;
297 * exponential moving average.
299 * @param value the value
300 * @param lastForecast the last forecast
303 private static double expMovingAverage(final double value, final double lastForecast) {
304 return (value * EMA_EXPONENT) + (lastForecast * EMA_EXPONENT_1);
308 * Remove the first occurrence of the value val from the array.
310 * @param lvalues the l values
311 * @param val the value
312 * @return the double[]
314 private static Double[] removevalue(final Double[] lvalues, final double val) {
315 for (int i = 0; i < lvalues.length; i++) {
316 if (Double.compare(lvalues[i], val) == 0) {
317 final Double[] ret = new Double[lvalues.length - 1];
318 System.arraycopy(lvalues, 0, ret, 0, i);
319 System.arraycopy(lvalues, i + 1, ret, i, lvalues.length - i - 1);
327 * get mean value of double list.
329 * @param lvalues the l values
332 private static double getMean(final Double[] lvalues) {
334 for (final double d : lvalues) {
338 return sum / lvalues.length;
342 * get standard deviation of double list.
344 * @param lvalues the l values
345 * @param mean the mean
348 private static double getStdDev(final Double[] lvalues, final double mean) {
350 for (final double d : lvalues) {
351 temp += (mean - d) * (mean - d);
353 return FastMath.sqrt(temp / lvalues.length);
357 * Chop head off list to make it length max .
359 * @param list the list to chop
360 * @param max the max size
362 private static void listSizeControl(final List<?> list, final int max) {
363 final int k = list.size();
365 // Chop the head off the list.
366 list.subList(0, k - max).clear();
371 * return true if all values are equal.
373 * @param lvalues the l values
374 * @return true, if checks if is all equal
376 private static boolean isAllEqual(final List<Double> lvalues) {
377 final double first = lvalues.get(0);
378 for (final Double d : lvalues) {
379 if (Double.compare(d, first) != 0) {
387 * return true if all values are equal.
389 * @param lvalues the l values
390 * @return true, if checks if is all equal
392 private static boolean isAllEqual(final Double[] lvalues) {
393 final double first = lvalues[0];
394 for (final Double d : lvalues) {
395 if (Double.compare(d, first) != 0) {