From cf03eb8dde54621f763ca20e5510c03c57f473e5 Mon Sep 17 00:00:00 2001 From: Owen Braun Date: Tue, 29 Sep 2026 16:03:21 +0200 Subject: [PATCH 1/4] [MINOR] Exception handling for exponential smoothing algorithms Previously, the variables which created the exponential smoothing had no validation, so a mistake or malicious actor could easily break the algorithm by using an invalid alpha, beta or gamma. In order to fix this issue, I created a helper function to validate the range of the smoothing variables. This function is optimal, because it lends itself to reuse amongst simple, double and exponential smoothing. --- .../apache/sysds/runtime/util/EMAUtils.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java b/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java index f6fd1d14294..04effee1f62 100644 --- a/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java +++ b/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java @@ -145,7 +145,27 @@ public Container(Double[] vals, double error) { double rsme; } + static void validateRange(Double... values) { + if (values == null || values.length == 0){ + throw new IllegalArgumentException("No arguments provided"); + } + for (Double value : values) { + if (value > 1 || value < 0) { + throw new IllegalArgumentException( + String.format("The Input %f is out of range [0,1]", value) + ); + } + } + + } public static Container single_exponential_smoothing(Double[] data, Double alpha) { + + try { + validateRange(alpha); + } catch (IllegalArgumentException e) { + System.err.println("Alpha is out of bounds"); + } + int n = data.length; Double[] pred = new Double[n]; @@ -186,6 +206,12 @@ public static Container single_exponential_smoothing(Double[] data, Double alpha } public static Container double_exponential_smoothing(Double[] data, Double alpha, Double beta) { + try { + validateRange(alpha, beta); + } catch (IllegalArgumentException e) { + System.err.println("Alpha is out of bounds"); + } + int n = data.length; ArrayList pred = new ArrayList<>(n-1); @@ -236,6 +262,13 @@ public static Container double_exponential_smoothing(Double[] data, Double alpha public static Container triple_exponential_smoothing(Double[] data, Double alpha, Double beta, Double gamma, Integer freq) { + + try { + validateRange(alpha, beta, gamma); + } catch (IllegalArgumentException e) { + System.err.println("Alpha is out of bounds"); + } + double l = freq * 2; ArrayList start_data = new ArrayList<>(); From 7ca015ed4c1d4efe53107619fac836b6ae11f148 Mon Sep 17 00:00:00 2001 From: Owen Braun Date: Tue, 20 Oct 2026 13:30:35 +0200 Subject: [PATCH 2/4] Improved exception handling, commenting, as well as error correction Firstly, the exception handling, which was previously in the main function, was moved into the helper functions. This was done to fit with best practices in exception handling. Secondly, a new helper function was implemented, so that the frequencies being put into the triple exponential are not zero or smaller, as a lower bound, or larger than half of the size of the incoming data array (as the algorithm uses double the size of the frequency to initialize the variables). The first aspect of this test is trivially beneficial, as it makes sure that no invalid values can be input into the algorithm. The second one is also best practice for this specific instance of the algorithm, as it currently fits to the initialization process of the TES algorithm, but is not applicable to all instances of TES. Thirdly, an error was found in the triple exponential smoothing algorithm, where the application of the centered moving average filter would result in an indexOutOfBoundsException for frequencies with an odd number, as the for loop was incorrectly declared. The downside to this is the assumption that the current workflow for the initialization of the starting variables is either valid or optimal. Finally, I have added comments to the exponential smoothing algorithms, so that future contributors have a better understanding of what is happening in underneath the hood. --- .../apache/sysds/runtime/util/EMAUtils.java | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java b/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java index 04effee1f62..c447a6da797 100644 --- a/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java +++ b/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java @@ -145,27 +145,32 @@ public Container(Double[] vals, double error) { double rsme; } - static void validateRange(Double... values) { + public static void validateRange(Double... values) { if (values == null || values.length == 0){ throw new IllegalArgumentException("No arguments provided"); } for (Double value : values) { if (value > 1 || value < 0) { throw new IllegalArgumentException( - String.format("The Input %f is out of range [0,1]", value) + "Input is out of range [0,1]" ); } } } - public static Container single_exponential_smoothing(Double[] data, Double alpha) { - try { - validateRange(alpha); - } catch (IllegalArgumentException e) { - System.err.println("Alpha is out of bounds"); + public static void validateFrequency(int freq, Double[] data){ + if (freq <= 0){ + throw new IllegalArgumentException("Value too low"); + } else if (2*freq >= data.length) { + throw new ArrayIndexOutOfBoundsException("Frequency too high"); } + } + public static Container single_exponential_smoothing(Double[] data, Double alpha) { + + validateRange(alpha); + int n = data.length; Double[] pred = new Double[n]; @@ -206,18 +211,18 @@ public static Container single_exponential_smoothing(Double[] data, Double alpha } public static Container double_exponential_smoothing(Double[] data, Double alpha, Double beta) { - try { - validateRange(alpha, beta); - } catch (IllegalArgumentException e) { - System.err.println("Alpha is out of bounds"); - } + // Exception handling + validateRange(alpha, beta); + + // Initializing variables int n = data.length; ArrayList pred = new ArrayList<>(n-1); Double[] s = new Double[n-1]; Double[] b = new Double[n-1]; + // Setting up s_0, b_0, y_0 s[0] = data[1]; b[0] = data[1] - data[0]; pred.add(s[0] + b[0]); @@ -228,7 +233,9 @@ public static Container double_exponential_smoothing(Double[] data, Double alpha ArrayList not_missing_pred = new ArrayList<>(); int n_size = 0; + // Iterative updating of the model for (int i = 1; i < n-1; i++) { + // Imputes missing data if (Double.isNaN(data[i + 1])) { val = pred.get(i - 1); } else { @@ -242,6 +249,7 @@ public static Container double_exponential_smoothing(Double[] data, Double alpha pred.add(0, data[0]); + // Initializing the data for calculation of RMSE for (int i = 0; i < data.length; i++) { if (!Double.isNaN(data[i])) { not_missing.add(data[i]); @@ -250,6 +258,7 @@ public static Container double_exponential_smoothing(Double[] data, Double alpha } } + // Calculation of RMSE double sum = .0; for (int i = 0; i < not_missing.size(); i++) { sum += Math.pow(not_missing.get(i) - not_missing_pred.get(i), 2); @@ -263,12 +272,11 @@ public static Container double_exponential_smoothing(Double[] data, Double alpha public static Container triple_exponential_smoothing(Double[] data, Double alpha, Double beta, Double gamma, Integer freq) { - try { - validateRange(alpha, beta, gamma); - } catch (IllegalArgumentException e) { - System.err.println("Alpha is out of bounds"); - } + // Exception handling + validateRange(alpha, beta, gamma); + validateFrequency(freq, data); + //Initialization data double l = freq * 2; ArrayList start_data = new ArrayList<>(); @@ -276,11 +284,15 @@ public static Container triple_exponential_smoothing(Double[] data, Double alpha start_data.add(data[i]); } + // Building a centered moving average for the inititialization of the level variable ArrayList filt = new ArrayList<>(); ArrayList trend = new ArrayList<>(); double len = freq; + // To set up the centered moving average filter, there is two cases, odd and even frequency. + // For the odd frequency, there is a true center point, so the filter weights are the same. + // For the even frequency, there is no center point, therefore, the edge weights are halved. if (freq % 2 == 0) { len = freq - 1; } @@ -294,9 +306,11 @@ public static Container triple_exponential_smoothing(Double[] data, Double alpha filt.add(0.5/freq); } + // Applying the CMA filter to the trend double trend_len = l - filt.size() + 1; - for (int i = 0; i < l - trend_len; i++) { + // Goes through the start data, and sums the average trend over the first to seasons. + for (int i = 0; i < trend_len; i++) { double sum = 0; for (int j = i; j < i + filt.size(); j++) { sum += start_data.get(j) * filt.get(j - i); @@ -305,6 +319,7 @@ public static Container triple_exponential_smoothing(Double[] data, Double alpha trend.add(sum); } + // Removing the trend, to reveal the seasonal pattern int cut = (int) (l - trend.size()) / 2; ArrayList season_tmp = new ArrayList<>(); @@ -315,6 +330,7 @@ public static Container triple_exponential_smoothing(Double[] data, Double alpha Double[] season = new Double[freq]; + // Average by season position for (int i = 0; i < freq; i++) { double combined = 0; if (i + freq < trend.size()) { @@ -326,6 +342,7 @@ public static Container triple_exponential_smoothing(Double[] data, Double alpha season[(i + (freq / 2)) % freq] = combined; } + // Preparing the data for linear regression, in order to initialize level double sum = 0; for (int i = 0; i < season.length; i++) { sum += season[i]; @@ -349,6 +366,8 @@ public static Container triple_exponential_smoothing(Double[] data, Double alpha int n = data.length; + + // Initalize arrays and create first forecast double[] s = new double[n - freq]; s[0] = linreg.intercept(); @@ -371,6 +390,7 @@ public static Container triple_exponential_smoothing(Double[] data, Double alpha int n_size = 0; + // Update the model per data point for (int i = 1; i < n - freq; i++) { if (Double.isNaN(data[i + freq - 1] )) { val = pred.get(i - 1); @@ -389,6 +409,7 @@ public static Container triple_exponential_smoothing(Double[] data, Double alpha pred.add(i, data[i]); } + // Initialize data for the RMSE calculation for (int i = 0; i < data.length; i++) { if (!Double.isNaN(data[i])) { not_missing.add(data[i]); @@ -397,6 +418,7 @@ public static Container triple_exponential_smoothing(Double[] data, Double alpha } } + // Calculate the RMSE sum = .0; for (int i = 0; i < not_missing.size(); i++) { sum += Math.pow(not_missing.get(i) - not_missing_pred.get(i), 2); From 8892bb3e706a88c82022b499090e555167df5237 Mon Sep 17 00:00:00 2001 From: Owen Braun Date: Sat, 31 Oct 2026 14:10:32 +0100 Subject: [PATCH 3/4] Completion of correction and testing In order to ensure the full correctness of the Java double and triple exponential smoothing algorithms, the current Java file is being tested by a new EMAUtils.java file, which then proceeds to get reference results from R. The reason why this was the option taken, was that there had been many issues when running the original code, so testing was needed. This includes three main cases, the regular double exponential smoothing algorithm, as well as the odd and even frequency cases for triple exponential smoothing. The reason why I split the last two, is due to the centered moving average used for the initialization, there is two different workflows for the even and odd cases, and in previous iterations of the program, the odd case would crash due to an exception. The reasoning behind the JUnit/R set up comes from the fact that R has prebuilt DES/TES functions, so the correctness of the R reference results can be assumed. The downsides to this approach are two-fold. Firstly, the sample of test data is very small. This is so that the testing does not last a long amount of time, however some may say the length is insufficient. Secondly, the inbuilt reference functions seek to find the optimal alpha, beta and gamma to minimize the RMSE, but since this is irrelevant to what is being tested, it slows down the actual testing. Assisted by: AI AI contributions include the generation of test data, as well as debugging the connection between the JUnit tests and the R tests. The specific AI used was ChatGPT, by OpenAI, and the specific model was GPT-6.1 Sol. --- .../apache/sysds/runtime/util/EMAUtils.java | 33 ++-- .../sysds/runtime/util/EMAUtilsTest.java | 170 ++++++++++++++++++ .../functions/builtin/exponentialSmoothing.R | 99 ++++++++++ 3 files changed, 291 insertions(+), 11 deletions(-) create mode 100644 src/test/java/org/apache/sysds/runtime/util/EMAUtilsTest.java create mode 100644 src/test/scripts/functions/builtin/exponentialSmoothing.R diff --git a/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java b/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java index c447a6da797..2e544ee32c9 100644 --- a/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java +++ b/src/main/java/org/apache/sysds/runtime/util/EMAUtils.java @@ -72,7 +72,7 @@ public double coef() { public class EMAUtils { public static FrameBlock exponentialMovingAverageImputation(FrameBlock block, int search_iterations, String mode, - int freq, Double alpha, Double beta, Double gamma) { + int freq, Double alpha, Double beta, Double gamma) { int cols = block.getNumColumns(); int rows = block.getNumRows(); @@ -149,13 +149,13 @@ public static void validateRange(Double... values) { if (values == null || values.length == 0){ throw new IllegalArgumentException("No arguments provided"); } - for (Double value : values) { - if (value > 1 || value < 0) { - throw new IllegalArgumentException( - "Input is out of range [0,1]" - ); - } - } + for (Double value : values) { + if (value > 1 || value < 0) { + throw new IllegalArgumentException( + "Input is out of range [0,1]" + ); + } + } } @@ -215,6 +215,14 @@ public static Container double_exponential_smoothing(Double[] data, Double alpha // Exception handling validateRange(alpha, beta); + if (data == null || data.length < 3) { + throw new IllegalArgumentException("At least three observations are needed to calculate RMSE"); + } + + if (data[0] == null || data[1] == null || Double.isNaN(data[0]) || Double.isNaN(data[1])) { + throw new IllegalArgumentException("The first two observations must not be missing"); + } + // Initializing variables int n = data.length; @@ -244,9 +252,12 @@ public static Container double_exponential_smoothing(Double[] data, Double alpha s[i] = alpha * val + (1 - alpha) * (s[i-1] + b[i-1]); b[i] = beta * (s[i] - s[i-1]) + (1 - beta) * b[i-1]; - pred.add(s[i] + b[i]); - } + if (i < n - 2){ + pred.add(s[i] + b[i]); + } + } + pred.add(0, data[1]); pred.add(0, data[0]); // Initializing the data for calculation of RMSE @@ -270,7 +281,7 @@ public static Container double_exponential_smoothing(Double[] data, Double alpha } public static Container triple_exponential_smoothing(Double[] data, Double alpha, Double beta, - Double gamma, Integer freq) { + Double gamma, Integer freq) { // Exception handling validateRange(alpha, beta, gamma); diff --git a/src/test/java/org/apache/sysds/runtime/util/EMAUtilsTest.java b/src/test/java/org/apache/sysds/runtime/util/EMAUtilsTest.java new file mode 100644 index 00000000000..250f4038e6a --- /dev/null +++ b/src/test/java/org/apache/sysds/runtime/util/EMAUtilsTest.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sysds.runtime.util; + +import org.apache.sysds.runtime.matrix.data.MatrixValue; +import org.apache.sysds.test.AutomatedTestBase; +import org.apache.sysds.test.TestConfiguration; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; + +import static org.apache.sysds.test.TestUtils.readRMatrixFromFS; +import static org.apache.sysds.runtime.util.EMAUtils.*; + +public class EMAUtilsTest extends AutomatedTestBase { + private static final String TEST_NAME = "exponentialSmoothing"; + private static final String TEST_DIR = "functions/builtin/"; + private static final String TEST_CLASS_DIR = TEST_DIR + EMAUtilsTest.class.getSimpleName() + "/"; + + private static final double REFERENCE_EPS = 1e-4; + private static final Double[] TS_DATA = { + 112., 118., 132., 129., 121., 135., 148., 148., 136., 119., 104., 118., + 115., 126., 141., 135., 125., 149., 170., 170., 158., 133., 114., 140., + 145., 150., 178., 163., 172., 178., 199., 199., 184., 162., 146., 166., + 171., 180., 193., 181., 183., 218., 230., 242., 209., 191., 172., 194., + 196., 196., 236., 235., 229., 243., 264., 272., 237., 211., 180., 201. + }; + @Override + public void setUp() { + addTestConfiguration(TEST_NAME, + new TestConfiguration(TEST_CLASS_DIR, TEST_NAME, new String[] { "R_double", "R_triple", "R_triple_odd" })); + } + + @Test + public void testDoubleExponentialSmoothing() { + fullRScriptName = SCRIPT_DIR + TEST_DIR + TEST_NAME + ".R"; + String resultPath = output("R_double"); + + rCmd = getRCmd("double", resultPath); + runRScript(true); + + HashMap rOutput = readRMatrixFromFS(resultPath); + Assert.assertFalse("R output should contain values", rOutput.isEmpty()); + + // 1. Extract optimized parameters + double rAlpha = rOutput.getOrDefault(new MatrixValue.CellIndex(1, 1),0.0); + double rBeta = rOutput.getOrDefault(new MatrixValue.CellIndex(1, 2),0.0); + + // 2. Extract the entire sequence of fitted values + int numRows = (int) rOutput.keySet().stream().mapToLong(idx -> idx.row).max().orElse(0); + double[] rFittedValues = new double[numRows]; + for (int i = 1; i <= numRows; i++) { + rFittedValues[i - 1] = rOutput.get(new MatrixValue.CellIndex(i, 3)); + } + + + Container javaFittedValues = double_exponential_smoothing(TS_DATA, rAlpha, rBeta); + + Assert.assertEquals("Array length mismatch", rFittedValues.length, javaFittedValues.values.length-2); + for (int i = 0; i < rFittedValues.length; i++) { + Assert.assertEquals("Mismatch at index " + i, rFittedValues[i], javaFittedValues.values[i+2], REFERENCE_EPS); + } + } + + @Test + public void testTripleExponentialSmoothing(){ + int freq = 12; + fullRScriptName = SCRIPT_DIR + TEST_DIR + TEST_NAME + ".R"; + String resultPath = output("R_triple"); + + rCmd = getRCmd("triple", resultPath); + runRScript(true); + + HashMap rOutputTriple = readRMatrixFromFS(resultPath); + Assert.assertFalse( + "R output should contain values", + rOutputTriple.isEmpty() + ); + + double rAlpha = rOutputTriple.getOrDefault( + new MatrixValue.CellIndex(1, 1), 0.0); + double rBeta = rOutputTriple.getOrDefault( + new MatrixValue.CellIndex(1, 2), 0.0); + double rGamma = rOutputTriple.getOrDefault( + new MatrixValue.CellIndex(1, 3), 0.0); + + int numRows = (int) rOutputTriple.keySet().stream() + .mapToLong(idx -> idx.row) + .max() + .orElse(0); + + double[] rFittedValues = new double[numRows]; + + for (int i = 0; i < numRows; i++) { + rFittedValues[i] = rOutputTriple.getOrDefault( + new MatrixValue.CellIndex(i + 1, 4), 0.0); + } + + Container javaResult = triple_exponential_smoothing( + TS_DATA, rAlpha, rBeta, rGamma, freq); + + Assert.assertEquals("Array length mismatch", rFittedValues.length, javaResult.values.length - freq); + + for (int i = 0; i < rFittedValues.length; i++) { + Assert.assertEquals("Mismatch at index " + i, rFittedValues[i], javaResult.values[i + freq], REFERENCE_EPS); + } + } + + @Test + public void testOddTripleExponentialSmoothing(){ + int freq = 7; + fullRScriptName = SCRIPT_DIR + TEST_DIR + TEST_NAME + ".R"; + String resultPath = output("R_triple_odd"); + + rCmd = getRCmd("odd_triple", resultPath); + runRScript(true); + + HashMap rOutputTriple = readRMatrixFromFS(resultPath); + Assert.assertFalse( + "R output should contain values", + rOutputTriple.isEmpty() + ); + + double rAlpha = rOutputTriple.getOrDefault( + new MatrixValue.CellIndex(1, 1), 0.0); + double rBeta = rOutputTriple.getOrDefault( + new MatrixValue.CellIndex(1, 2), 0.0); + double rGamma = rOutputTriple.getOrDefault( + new MatrixValue.CellIndex(1, 3), 0.0); + + int numRows = (int) rOutputTriple.keySet().stream() + .mapToLong(idx -> idx.row) + .max() + .orElse(0); + + double[] rFittedValues = new double[numRows]; + + for (int i = 0; i < numRows; i++) { + rFittedValues[i] = rOutputTriple.getOrDefault( + new MatrixValue.CellIndex(i + 1, 4), 0.0); + } + + Container javaResult = triple_exponential_smoothing( + TS_DATA, rAlpha, rBeta, rGamma, freq); + + Assert.assertEquals("Array length mismatch", rFittedValues.length, javaResult.values.length - freq); + + for (int i = 0; i < rFittedValues.length; i++) { + Assert.assertEquals("Mismatch at index " + i, rFittedValues[i], javaResult.values[i + freq], REFERENCE_EPS); + } + } +} \ No newline at end of file diff --git a/src/test/scripts/functions/builtin/exponentialSmoothing.R b/src/test/scripts/functions/builtin/exponentialSmoothing.R new file mode 100644 index 00000000000..cc51eadae43 --- /dev/null +++ b/src/test/scripts/functions/builtin/exponentialSmoothing.R @@ -0,0 +1,99 @@ +#------------------------------------------------------------- +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +#------------------------------------------------------------- + +args <- commandArgs(trailingOnly = TRUE) + +if (length(args) != 2L) { + stop("Expected arguments: ") +} + +mode <- args[1] +output_path <- args[2] + +ts_data <- c( + 112, 118, 132, 129, 121, 135, 148, 148, 136, 119, 104, 118, + 115, 126, 141, 135, 125, 149, 170, 170, 158, 133, 114, 140, + 145, 150, 178, 163, 172, 178, 199, 199, 184, 162, 146, 166, + 171, 180, 193, 181, 183, 218, 230, 242, 209, 191, 172, 194, + 196, 196, 236, 235, 229, 243, 264, 272, 237, 211, 180, 201 +) + +if (mode == "double") { + hw_model <- stats::HoltWinters(ts_data, beta = NULL, gamma = FALSE) + fit_vals <- as.numeric(fitted(hw_model)[, "xhat"]) + + res_matrix <- matrix(0, nrow = length(fit_vals), ncol = 3) + res_matrix[, 1] <- hw_model$alpha + res_matrix[, 2] <- hw_model$beta + res_matrix[, 3] <- fit_vals + +} else if (mode == "triple") { + ts_obj <- ts(ts_data, frequency = 12) + + hw_model <- stats::HoltWinters( + ts_obj, + alpha = NULL, + beta = NULL, + gamma = NULL, + seasonal = "additive" + ) + + fit_vals <- as.numeric(fitted(hw_model)[, "xhat"]) + + res_matrix <- matrix(0, nrow = length(fit_vals), ncol = 4) + res_matrix[, 1] <- hw_model$alpha + res_matrix[, 2] <- hw_model$beta + res_matrix[, 3] <- hw_model$gamma + res_matrix[, 4] <- fit_vals + +} else if (mode == "odd_triple") { + ts_obj <- ts(ts_data, frequency = 7) + + hw_model <- stats::HoltWinters( + ts_obj, + alpha = NULL, + beta = NULL, + gamma = NULL, + seasonal = "additive" + ) + + fit_vals <- as.numeric(fitted(hw_model)[, "xhat"]) + + res_matrix <- matrix(0, nrow = length(fit_vals), ncol = 4) + res_matrix[, 1] <- hw_model$alpha + res_matrix[, 2] <- hw_model$beta + res_matrix[, 3] <- hw_model$gamma + res_matrix[, 4] <- fit_vals + + } else { + stop(paste("Unknown smoothing mode:", mode)) +} + +dir.create( + dirname(output_path), + recursive = TRUE, + showWarnings = FALSE +) + +Matrix::writeMM( + Matrix::Matrix(res_matrix, sparse = TRUE), + file = output_path +) \ No newline at end of file From 77b517fa266eaccbadaebc1f13bc8d24edc25270 Mon Sep 17 00:00:00 2001 From: Owen Braun Date: Sat, 31 Oct 2026 17:40:20 +0100 Subject: [PATCH 4/4] Fixing the input values for R functions Initially the default had been set to search for the optimal alpha, beta and gamma values, the testing was much slower than needed, now I have just added a fixed value for all values across the JUnit and R tests. --- .../sysds/runtime/util/EMAUtilsTest.java | 31 +++---------- .../functions/builtin/exponentialSmoothing.R | 44 +++++++++---------- 2 files changed, 26 insertions(+), 49 deletions(-) diff --git a/src/test/java/org/apache/sysds/runtime/util/EMAUtilsTest.java b/src/test/java/org/apache/sysds/runtime/util/EMAUtilsTest.java index 250f4038e6a..e0284ffe9f6 100644 --- a/src/test/java/org/apache/sysds/runtime/util/EMAUtilsTest.java +++ b/src/test/java/org/apache/sysds/runtime/util/EMAUtilsTest.java @@ -60,19 +60,14 @@ public void testDoubleExponentialSmoothing() { HashMap rOutput = readRMatrixFromFS(resultPath); Assert.assertFalse("R output should contain values", rOutput.isEmpty()); - // 1. Extract optimized parameters - double rAlpha = rOutput.getOrDefault(new MatrixValue.CellIndex(1, 1),0.0); - double rBeta = rOutput.getOrDefault(new MatrixValue.CellIndex(1, 2),0.0); - - // 2. Extract the entire sequence of fitted values int numRows = (int) rOutput.keySet().stream().mapToLong(idx -> idx.row).max().orElse(0); double[] rFittedValues = new double[numRows]; for (int i = 1; i <= numRows; i++) { - rFittedValues[i - 1] = rOutput.get(new MatrixValue.CellIndex(i, 3)); + rFittedValues[i - 1] = rOutput.get(new MatrixValue.CellIndex(i, 1)); } - Container javaFittedValues = double_exponential_smoothing(TS_DATA, rAlpha, rBeta); + Container javaFittedValues = double_exponential_smoothing(TS_DATA, 0.3, 0.1); Assert.assertEquals("Array length mismatch", rFittedValues.length, javaFittedValues.values.length-2); for (int i = 0; i < rFittedValues.length; i++) { @@ -95,13 +90,6 @@ public void testTripleExponentialSmoothing(){ rOutputTriple.isEmpty() ); - double rAlpha = rOutputTriple.getOrDefault( - new MatrixValue.CellIndex(1, 1), 0.0); - double rBeta = rOutputTriple.getOrDefault( - new MatrixValue.CellIndex(1, 2), 0.0); - double rGamma = rOutputTriple.getOrDefault( - new MatrixValue.CellIndex(1, 3), 0.0); - int numRows = (int) rOutputTriple.keySet().stream() .mapToLong(idx -> idx.row) .max() @@ -111,11 +99,11 @@ public void testTripleExponentialSmoothing(){ for (int i = 0; i < numRows; i++) { rFittedValues[i] = rOutputTriple.getOrDefault( - new MatrixValue.CellIndex(i + 1, 4), 0.0); + new MatrixValue.CellIndex(i + 1, 1), 0.0); } Container javaResult = triple_exponential_smoothing( - TS_DATA, rAlpha, rBeta, rGamma, freq); + TS_DATA, 0.3, 0.1, 0.2, freq); Assert.assertEquals("Array length mismatch", rFittedValues.length, javaResult.values.length - freq); @@ -139,13 +127,6 @@ public void testOddTripleExponentialSmoothing(){ rOutputTriple.isEmpty() ); - double rAlpha = rOutputTriple.getOrDefault( - new MatrixValue.CellIndex(1, 1), 0.0); - double rBeta = rOutputTriple.getOrDefault( - new MatrixValue.CellIndex(1, 2), 0.0); - double rGamma = rOutputTriple.getOrDefault( - new MatrixValue.CellIndex(1, 3), 0.0); - int numRows = (int) rOutputTriple.keySet().stream() .mapToLong(idx -> idx.row) .max() @@ -155,11 +136,11 @@ public void testOddTripleExponentialSmoothing(){ for (int i = 0; i < numRows; i++) { rFittedValues[i] = rOutputTriple.getOrDefault( - new MatrixValue.CellIndex(i + 1, 4), 0.0); + new MatrixValue.CellIndex(i + 1, 1), 0.0); } Container javaResult = triple_exponential_smoothing( - TS_DATA, rAlpha, rBeta, rGamma, freq); + TS_DATA, 0.3, 0.1, 0.2, freq); Assert.assertEquals("Array length mismatch", rFittedValues.length, javaResult.values.length - freq); diff --git a/src/test/scripts/functions/builtin/exponentialSmoothing.R b/src/test/scripts/functions/builtin/exponentialSmoothing.R index cc51eadae43..50c30e29121 100644 --- a/src/test/scripts/functions/builtin/exponentialSmoothing.R +++ b/src/test/scripts/functions/builtin/exponentialSmoothing.R @@ -37,51 +37,47 @@ ts_data <- c( ) if (mode == "double") { - hw_model <- stats::HoltWinters(ts_data, beta = NULL, gamma = FALSE) + hw_model <- stats::HoltWinters( + ts_data, + alpha=0.3, + beta = 0.1, + gamma = FALSE) fit_vals <- as.numeric(fitted(hw_model)[, "xhat"]) - res_matrix <- matrix(0, nrow = length(fit_vals), ncol = 3) - res_matrix[, 1] <- hw_model$alpha - res_matrix[, 2] <- hw_model$beta - res_matrix[, 3] <- fit_vals + res_matrix <- matrix(0, nrow = length(fit_vals), ncol = 1) + res_matrix[, 1] <- fit_vals } else if (mode == "triple") { ts_obj <- ts(ts_data, frequency = 12) hw_model <- stats::HoltWinters( ts_obj, - alpha = NULL, - beta = NULL, - gamma = NULL, + alpha = 0.3, + beta = 0.1, + gamma = 0.2, seasonal = "additive" ) fit_vals <- as.numeric(fitted(hw_model)[, "xhat"]) - res_matrix <- matrix(0, nrow = length(fit_vals), ncol = 4) - res_matrix[, 1] <- hw_model$alpha - res_matrix[, 2] <- hw_model$beta - res_matrix[, 3] <- hw_model$gamma - res_matrix[, 4] <- fit_vals + res_matrix <- matrix(0, nrow = length(fit_vals), ncol = 1) + res_matrix[, 1] <- fit_vals } else if (mode == "odd_triple") { ts_obj <- ts(ts_data, frequency = 7) hw_model <- stats::HoltWinters( - ts_obj, - alpha = NULL, - beta = NULL, - gamma = NULL, - seasonal = "additive" - ) + ts_obj, + alpha = 0.3, + beta = 0.1, + gamma = 0.2, + seasonal = "additive" + ) fit_vals <- as.numeric(fitted(hw_model)[, "xhat"]) - res_matrix <- matrix(0, nrow = length(fit_vals), ncol = 4) - res_matrix[, 1] <- hw_model$alpha - res_matrix[, 2] <- hw_model$beta - res_matrix[, 3] <- hw_model$gamma - res_matrix[, 4] <- fit_vals + res_matrix <- matrix(0, nrow = length(fit_vals), ncol = 1) + res_matrix[, 1] <- fit_vals } else { stop(paste("Unknown smoothing mode:", mode))