From 7aed1e459c3dd1e535532b795f796ca9f4c663cc Mon Sep 17 00:00:00 2001
From: Marcel Becker <stu117960@mail.uni-kiel.de>
Date: Tue, 8 Feb 2022 15:33:09 +0100
Subject: [PATCH] Finished capacity metric support for search strategies

---
 theodolite/crd/crd-execution.yaml             | 36 ++++-----
 .../strategies/searchstrategy/BinarySearch.kt | 63 ++++++++++++---
 .../InitialGuessSearchStrategy.kt             | 80 +++++++++++++++++--
 .../searchstrategy/PrevResourceMinGuess.kt    |  1 +
 .../searchstrategy/SearchStrategy.kt          |  1 -
 .../main/kotlin/theodolite/util/Results.kt    | 45 +++++++++--
 .../k8s-resource-files/test-execution.yaml    | 10 ++-
 7 files changed, 191 insertions(+), 45 deletions(-)

diff --git a/theodolite/crd/crd-execution.yaml b/theodolite/crd/crd-execution.yaml
index f2319269e..3e87df678 100644
--- a/theodolite/crd/crd-execution.yaml
+++ b/theodolite/crd/crd-execution.yaml
@@ -81,25 +81,25 @@ spec:
                 description: Defines the overall parameter for the execution.
                 type: object
                 required: ["strategy", "duration", "repetitions", "restrictions"]
-                metric:
-                  default: "demand"
-                  type: string
-                  oneOf:
-                    - "demand"
-                    - "capacity"
-                strategy:
-                  description: Defines the used strategy for the execution, either 'LinearSearch', 'BinarySearch' or 'InitialGuessSearch'
-                  type: object
-                  name: string
-                  properties:
-                    restrictions:
-                      description: List of restriction strategies used to delimit the search space.
-                      type: array
-                      items:
-                        type: string
-                    guessStrategy: string
-                    searchStrategy: string
                 properties:
+                  metric:
+                    default: "demand"
+                    type: string
+                    oneOf:
+                      - "demand"
+                      - "capacity"
+                  strategy:
+                    description: Defines the used strategy for the execution, either 'LinearSearch', 'BinarySearch' or 'InitialGuessSearch'
+                    type: object
+                    name: string
+                    properties:
+                      restrictions:
+                        description: List of restriction strategies used to delimit the search space.
+                        type: array
+                        items:
+                          type: string
+                      guessStrategy: string
+                      searchStrategy: string
                   duration:
                     description: Defines the duration of each experiment in seconds.
                     type: integer
diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/BinarySearch.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/BinarySearch.kt
index 8aaa893b0..08934bb5d 100644
--- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/BinarySearch.kt
+++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/BinarySearch.kt
@@ -12,7 +12,7 @@ private val logger = KotlinLogging.logger {}
  */
 class BinarySearch(benchmarkExecutor: BenchmarkExecutor) : SearchStrategy(benchmarkExecutor) {
     override fun findSuitableResource(load: Int, resources: List<Int>): Int? {
-        val result = binarySearch(load, resources, 0, resources.size - 1, true)
+        val result = binarySearchDemand(load, resources, 0, resources.size - 1)
         if (result == -1) {
             return null
         }
@@ -20,24 +20,26 @@ class BinarySearch(benchmarkExecutor: BenchmarkExecutor) : SearchStrategy(benchm
     }
 
     override fun findSuitableLoad(resource: Int, loads: List<Int>): Int? {
-        // TODO
-        return null
+        val result = binarySearchCapacity(resource, loads, 0, loads.size - 1)
+        if (result == -1) {
+            return null
+        }
+        return loads[result]
     }
 
     /**
-     * Apply binary search.
+     * Apply binary search for metric demand.
      *
      * @param load the load dimension to perform experiments for
      * @param resources the list in which binary search is performed
      * @param lower lower bound for binary search (inclusive)
      * @param upper upper bound for binary search (inclusive)
      */
-    private fun binarySearch(load: Int, resources: List<Int>, lower: Int, upper: Int,
-                             demand: Boolean): Int {
+    private fun binarySearchDemand(load: Int, resources: List<Int>, lower: Int, upper: Int): Int {
         if (lower > upper) {
             throw IllegalArgumentException()
         }
-        // special case:  length == 1 or 2
+        // special case:  length == 1, so lower and upper bounds are the same
         if (lower == upper) {
             val res = resources[lower]
             logger.info { "Running experiment with load '${load}' and resources '$res'" }
@@ -48,17 +50,58 @@ class BinarySearch(benchmarkExecutor: BenchmarkExecutor) : SearchStrategy(benchm
             }
         } else {
             // apply binary search for a list with
-            // length > 2 and adjust upper and lower depending on the result for `resources[mid]`
+            // length >= 2 and adjust upper and lower depending on the result for `resources[mid]`
             val mid = (upper + lower) / 2
             val res = resources[mid]
             logger.info { "Running experiment with load '${load}' and resources '$res'" }
             if (this.benchmarkExecutor.runExperiment(load, resources[mid])) {
+                // case length = 2
                 if (mid == lower) {
                     return lower
                 }
-                return binarySearch(load, resources, lower, mid - 1, demand)
+                return binarySearchDemand(load, resources, lower, mid - 1)
+            } else {
+                return binarySearchDemand(load, resources, mid + 1, upper)
+            }
+        }
+    }
+
+
+    /**
+     * Apply binary search for metric capacity.
+     *
+     * @param resource the load dimension to perform experiments for
+     * @param loads the list in which binary search is performed
+     * @param lower lower bound for binary search (inclusive)
+     * @param upper upper bound for binary search (inclusive)
+     */
+    private fun binarySearchCapacity(resource: Int, loads: List<Int>, lower: Int, upper: Int): Int {
+        if (lower > upper) {
+            throw IllegalArgumentException()
+        }
+        // length = 1, so lower and upper bounds are the same
+        if (lower == upper) {
+            val res = loads[lower]
+            logger.info { "Running experiment with load '$resource' and resources '$res'" }
+            if (this.benchmarkExecutor.runExperiment(resource, loads[lower])) return lower
+            else {
+                if (lower + 1 == loads.size) return -1
+                return lower - 1
+            }
+        } else {
+            // apply binary search for a list with
+            // length > 2 and adjust upper and lower depending on the result for `resources[mid]`
+            val mid = (upper + lower + 1) / 2 //round to next int
+            val res = loads[mid]
+            logger.info { "Running experiment with load '$resource' and resources '$res'" }
+            if (this.benchmarkExecutor.runExperiment(resource, loads[mid])) {
+                // length = 2, so since we round down mid is equal to lower
+                if (mid == upper) {
+                    return upper
+                }
+                return binarySearchCapacity(resource, loads, mid + 1, upper)
             } else {
-                return binarySearch(load, resources, mid + 1, upper, demand)
+                return binarySearchCapacity(resource, loads, lower, mid - 1)
             }
         }
     }
diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt
index 7df3f9013..d8c5c1c4e 100644
--- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt
+++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/InitialGuessSearchStrategy.kt
@@ -35,7 +35,6 @@ class InitialGuessSearchStrategy(benchmarkExecutor: BenchmarkExecutor, guessStra
             return null
         }
 
-
         var lastLowestResource : Int? = null
 
         // Getting the lastLowestResource from results and calling firstGuess() with it
@@ -50,7 +49,7 @@ class InitialGuessSearchStrategy(benchmarkExecutor: BenchmarkExecutor, guessStra
             val resourcesToCheck: List<Int>
             val startIndex: Int = resources.indexOf(lastLowestResource)
 
-            logger.info { "Running experiment with load '${load}' and resources '$lastLowestResource'" }
+            logger.info { "Running experiment with load '$load' and resources '$lastLowestResource'" }
 
             // If the first experiment passes, starting downward linear search
             // otherwise starting upward linear search
@@ -62,7 +61,7 @@ class InitialGuessSearchStrategy(benchmarkExecutor: BenchmarkExecutor, guessStra
                 var currentMin: Int = lastLowestResource
                 for (res in resourcesToCheck) {
 
-                    logger.info { "Running experiment with load '${load}' and resources '$res'" }
+                    logger.info { "Running experiment with load '$load' and resources '$res'" }
                     if (this.benchmarkExecutor.runExperiment(load, res)) {
                         currentMin = res
                     }
@@ -78,19 +77,86 @@ class InitialGuessSearchStrategy(benchmarkExecutor: BenchmarkExecutor, guessStra
 
                 for (res in resourcesToCheck) {
 
-                    logger.info { "Running experiment with load '${load}' and resources '$res'" }
+                    logger.info { "Running experiment with load '$load' and resources '$res'" }
                     if (this.benchmarkExecutor.runExperiment(load, res)) return res
                 }
             }
         }
         else {
-            logger.info { "InitialGuessSearchStrategy called without lastLowestResource value, which is needed as a " +
-                    "starting point!" }
+            logger.info { "lastLowestResource was null." }
         }
         return null
     }
 
     override fun findSuitableLoad(resource: Int, loads: List<Int>): Int? {
-        TODO("Not yet implemented")
+
+        if(loads.isEmpty()) {
+            logger.info { "You need to specify resources to be checked for the InitialGuessSearchStrategy to work." }
+            return null
+        }
+
+        if(guessStrategy == null){
+            logger.info { "Your InitialGuessSearchStrategy doesn't have a GuessStrategy. This is not supported." }
+            return null
+        }
+
+        if(results == null){
+            logger.info { "The results need to be initialized." }
+            return null
+        }
+
+        var lastMaxLoad : Int? = null
+
+        // Getting the lastLowestLoad from results and calling firstGuess() with it
+        if (!results.isEmpty()) {
+            val maxResource: Int? = this.results.getMaxBenchmarkedXDimensionValue(resource)
+            lastMaxLoad = this.results.getMaxRequiredYDimensionValue(maxResource)
+            if (lastMaxLoad == Int.MIN_VALUE) lastMaxLoad = null
+        }
+        lastMaxLoad = this.guessStrategy.firstGuess(loads, lastMaxLoad)
+
+        if (lastMaxLoad != null) {
+            val loadsToCheck: List<Int>
+            val startIndex: Int = loads.indexOf(lastMaxLoad)
+
+            logger.info { "Running experiment with resource '$resource' and load '$lastMaxLoad'" }
+
+            // If the first experiment passes, starting upwards linear search
+            // otherwise starting downward linear search
+            if (!this.benchmarkExecutor.runExperiment(resource, lastMaxLoad)) {
+                // downward search
+
+                loadsToCheck = loads.subList(0, startIndex).reversed()
+                if (loadsToCheck.isNotEmpty()) {
+                    for (load in loadsToCheck) {
+
+                        logger.info { "Running experiment with resource '$resource' and load '$load'" }
+                        if (this.benchmarkExecutor.runExperiment(resource, load)) {
+                            return load
+                        }
+                    }
+                }
+            }
+            else {
+                // upward search
+                if (loads.size <= startIndex + 1) {
+                    return lastMaxLoad
+                }
+                loadsToCheck = loads.subList(startIndex + 1, loads.size)
+
+                var currentMax: Int = lastMaxLoad
+                for (load in loadsToCheck) {
+                    logger.info { "Running experiment with resource '$resource' and load '$load'" }
+                    if (this.benchmarkExecutor.runExperiment(resource, load)) {
+                        currentMax = load
+                    }
+                }
+                return currentMax
+            }
+        }
+        else {
+            logger.info { "lastMaxLoad was null." }
+        }
+        return null
     }
 }
\ No newline at end of file
diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/PrevResourceMinGuess.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/PrevResourceMinGuess.kt
index bc18982f1..04d68c373 100644
--- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/PrevResourceMinGuess.kt
+++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/PrevResourceMinGuess.kt
@@ -14,6 +14,7 @@ class PrevResourceMinGuess() : GuessStrategy(){
      *
      * @return the value of lastLowestResource if given otherwise the first element of the resource list or null
      */
+    // TODO verallgemeinern  für loads
     override fun firstGuess(resources: List<Int>, lastLowestResource: Int?): Int? {
 
         if (lastLowestResource != null) return lastLowestResource
diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt
index d870d38ca..bbd0f0b51 100644
--- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt
+++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/SearchStrategy.kt
@@ -46,7 +46,6 @@ abstract class SearchStrategy(val benchmarkExecutor: BenchmarkExecutor, val gues
      */
     abstract fun findSuitableResource(load: Int, resources: List<Int>): Int?
 
-    // TODO findSuitableLoad und findSuitableResource zusammenfuehren?
     /**
      * Find biggest suitable load from the specified load list for the given resource amount.
      *
diff --git a/theodolite/src/main/kotlin/theodolite/util/Results.kt b/theodolite/src/main/kotlin/theodolite/util/Results.kt
index eca33eaab..9feee0643 100644
--- a/theodolite/src/main/kotlin/theodolite/util/Results.kt
+++ b/theodolite/src/main/kotlin/theodolite/util/Results.kt
@@ -14,6 +14,8 @@ class Results (val metric: Metric) {
     //TODO: enum statt Boolean
     private val results: MutableMap<Pair<Int, Int>, Boolean> = mutableMapOf()
 
+    //TODO: min instance (or max respectively) also as fields so we do not loop over results, speichert alle results für alle load/resource pairs
+
     /**
      * Set the result for an experiment.
      *
@@ -39,12 +41,12 @@ class Results (val metric: Metric) {
     /**
      * Get the smallest suitable number of instances for a specified LoadDimension.
      *
-     * @param load the LoadDimension
+     * @param xValue the Value of the x-dimension of the current metric
      *
-     * @return the smallest suitable number of resources. If the experiment was not executed yet,
-     * a @see Resource with the constant Int.MAX_VALUE as value is returned.
-     * If no experiments have been marked as either successful or unsuccessful
-     * yet, a Resource with the constant value Int.MIN_VALUE is returned.
+     * @return the smallest suitable number of resources/loads (depending on metric).
+     * If there is no experiment that has been executed yet, Int.MIN_VALUE is returned.
+     * If there is no experiment for the given [xValue] or there is none marked successful yet,
+     * Int.MAX_VALUE is returned.
      */
     fun getMinRequiredYDimensionValue(xValue: Int?): Int {
         if (this.results.isEmpty()) { //should add || xValue == null
@@ -53,7 +55,7 @@ class Results (val metric: Metric) {
 
         var minRequiredYValue = Int.MAX_VALUE
         for (experiment in results) {
-            // Get all successful experiments for requested load
+            // Get all successful experiments for requested xValue
             if (getXDimensionValue(experiment.key) == xValue && experiment.value) {
                 val experimentYValue = getYDimensionValue(experiment.key)
                 if (experimentYValue < minRequiredYValue) {
@@ -65,6 +67,37 @@ class Results (val metric: Metric) {
         return minRequiredYValue
     }
 
+
+    /**
+     * Get the largest y-Value for which the given x-Value has a positive experiment outcome.
+     * x- and y-values depend on the metric in use.
+     *
+     * @param xValue the Value of the x-dimension of the current metric
+     *
+     * @return the largest suitable number of resources/loads (depending on metric).
+     * If there wasn't any experiment executed yet, Int.MAX_VALUE is returned.
+     * If the experiments for the specified [xValue] wasn't executed yet or the experiments were not successful
+     * Int.MIN_VALUE is returned.
+     */
+    fun getMaxRequiredYDimensionValue(xValue: Int?): Int {
+        if (this.results.isEmpty()) { //should add || xValue == null
+            return Int.MAX_VALUE
+        }
+
+        var maxRequiredYValue = Int.MIN_VALUE
+        for (experiment in results) {
+            // Get all successful experiments for requested xValue
+            if (getXDimensionValue(experiment.key) == xValue && experiment.value) {
+                val experimentYValue = getYDimensionValue(experiment.key)
+                if (experimentYValue > maxRequiredYValue) {
+                    // Found new largest value
+                    maxRequiredYValue = experimentYValue
+                }
+            }
+        }
+        return maxRequiredYValue
+    }
+
     // TODO: SÖREN FRAGEN WARUM WIR DAS BRAUCHEN UND NICHT EINFACH PREV, WEIL NICHT DURCHGELAUFEN?
     // TODO Kommentar zu XDimension und YDimension
     /**
diff --git a/theodolite/src/test/resources/k8s-resource-files/test-execution.yaml b/theodolite/src/test/resources/k8s-resource-files/test-execution.yaml
index e12c851da..ee7a901c8 100644
--- a/theodolite/src/test/resources/k8s-resource-files/test-execution.yaml
+++ b/theodolite/src/test/resources/k8s-resource-files/test-execution.yaml
@@ -20,10 +20,14 @@ spec:
         externalSloUrl: "http://localhost:80/evaluate-slope"
         warmup: 60 # in seconds
   execution:
-    strategy: "LinearSearch"
+    metric: "demand"
+    strategy: "RestrictionSearch"
+      properties:
+        restrictions:
+          - "LowerBound"
+        searchStrategy: "LinearSearch"
     duration: 300 # in seconds
     repetitions: 1
     loadGenerationDelay: 30 # in seconds
-    restrictions:
-      - "LowerBound"
+
   configOverrides: []
-- 
GitLab