diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index e9b46ec..5477bb2 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -75,7 +75,6 @@ jobs: if: github.event_name != 'pull_request' run: | echo "PUSH_TO_BENCHMARKS=true" >> $GITHUB_ENV - echo "COMMENT_ALWAYS=false" >> $GITHUB_ENV - name: Store Benchmark Result uses: benchmark-action/github-action-benchmark@v1 diff --git a/examples/lot_sizing/README.md b/examples/lot_sizing/README.md new file mode 100644 index 0000000..e87f35c --- /dev/null +++ b/examples/lot_sizing/README.md @@ -0,0 +1,28 @@ +# Lot Sizing Example + +You can find the source for the example +[here](https://github.com/cls-python/cls-luigi/tree/main/examples/lot_sizing/): + +Here we utilized CLS-Luigi to construct demand prediction pipelines for +lot sizing. The example was built to present CLS-Luigi at the +[LION17](https://lion17.org/) conference. + +# Requirements + +The example contains a +[requirements.txt](https://github.com/cls-python/cls-luigi/tree/main/examples/ny_taxi/requirements.txt) +file. To experiment with the example, you can set up your environment by +executing the following command: + +``` bash +# cd into the lot sizing example folder +pip install -r requirements.txt +``` + +# Static Visualization + +![image](images/static.png) + +# Dynamic Visualization + +![image](images/dynamic.png) diff --git a/examples/lot_sizing/images/dynamic.png b/examples/lot_sizing/images/dynamic.png new file mode 100644 index 0000000..ff7cdd6 Binary files /dev/null and b/examples/lot_sizing/images/dynamic.png differ diff --git a/examples/lot_sizing/images/static.png b/examples/lot_sizing/images/static.png new file mode 100644 index 0000000..f4aecbc Binary files /dev/null and b/examples/lot_sizing/images/static.png differ diff --git a/examples/lot_sizing/lot_optimizers/groff_heuristic.py b/examples/lot_sizing/lot_optimizers/groff_heuristic.py new file mode 100644 index 0000000..1eef328 --- /dev/null +++ b/examples/lot_sizing/lot_optimizers/groff_heuristic.py @@ -0,0 +1,81 @@ +import numpy as np + + +class GroffHeuristic: + def __init__(self): + pass + + def run(self, dict_in, demand): + # Füllen der Variablen aus Start_Dictionary + dem = demand + kf = dict_in["fixedCost"] + kv = dict_in["varCost"] + pp = len(dem) + orders = [0 for i in range(pp)] + cost_v = 0 + j = 0 + p = 0 + criterion = (2 * kf) / kv + + # testen ob "Null-Perioden am Anfang vorliegen + while dem[p] == 0: + orders[p] = 0 + p = p + 1 + if p == pp: + p = p - 1 + break + i = p + + while p < pp: + for i in range(p, pp): + crit_met = False + if dem[i] * j * ((i - p) + 1) <= criterion: + orders[p] += dem[i] + crit_met = True + cost = dem[i] * kv * (i - p) + j = j + 1 + cost_v = cost + cost_v + else: + break + if (p == (pp - 1)) or (crit_met and i == (pp - 1)): + break + j = 0 + p += i - p + x = orders.count(0) + fix = (pp - x) * kf + print(fix) + total_c = cost_v + fix + print("total cost: " + str(total_c)) + print(sum(orders)) + return np.array(orders) + + +if __name__ == "__main__": + # statisches Test dictionary + dict_in = { + "planningPeriod": 8, # Anzahl der Perioden + "fixedCost": 40, # Bestellkosten + "varCost": 1, # Lagerhaltungssatz + "roll": 4, + } + + demand = [40, 50, 10, 20, 30, 40, 20, 25] + # demand = [0, 0, 0, 10, 0, 1] + # demand = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0] + # demand = [20, 50, 10, 50, 50, 10, 20, 40, 20, 30] + # demand = [854, 1021, 984, 1030, 1240, 1178, 905, 1005, 958, 905, 966, 965, 1030, 1111, 1285, 1089, 959, 920] + # demand = [300, 5001, 6022, 6103, 3533, 4046, 3044, 3023, 4064, 9552, 3960, 5077, 4000, 4000, 3430, 300, 3400, 3440, + # 200, 3400, 9400, 4340, 3400, 300, 3040, 4000, 5000, 6500, 45454, 4443, 3244, 2334, 344, 3223, 2999, 4000, 4000, + # 4000, 3400, 0, 0, 0, 4500, 400, 4500, 3400, 5400, 3000, 600, 0, 4500, 0, 0, 555, 4540, 500, 800, + # 4555, 3000, 4555, 455, 3444, 4333, 2344, 4454, 4555, 3444] + # demand = [0, 0, 6022, 6103, 3533, 4046, 3044, 3023, 4064, 9552, 3960, + # 5077, 4000, 4000, 3430, 300, 3400, 3440, 200, + # 3400, 9400, 4340, 3400, 300, 3040, 4000, 5000, 6500, 45454, + # 4443, 3244, 2334, 344, 3223, 2999, 4000, 4000, + # 4000, 3400, 3400, 3444, 5006, 4500, 400, 4500, 3400, 5400, 0, + # 0, 0, 4500, 4500, 4400, 555, 4540, 0, 0, + # 4555, 3000, 4555, 455, 3444, 4333, 2344, 4454, 4555, 3444] + + groff = GroffHeuristic() + output = groff.run(dict_in, demand) + print(output) diff --git a/examples/lot_sizing/lot_optimizers/least_unit_cost_method.py b/examples/lot_sizing/lot_optimizers/least_unit_cost_method.py new file mode 100644 index 0000000..5eda0e7 --- /dev/null +++ b/examples/lot_sizing/lot_optimizers/least_unit_cost_method.py @@ -0,0 +1,144 @@ +import numpy as np + + +class LeastUnitCostMethod: + def __init__(self): + pass + + def run(self, dict_in, demand): + # Füllen der Variablen aus Start_Dictionary + dem = demand + kf = dict_in["fixedCost"] + kv = dict_in["varCost"] + pp = len(dem) + + p = 0 + kpj = 0 + kppj = 0 + orders = [0 for i in range(pp)] + total_c = 0 + + # testen ob "Null-Perioden am Anfang vorliegen + while dem[p] == 0: + orders[p] = 0 + p = p + 1 + if p == pp: + p = p - 1 + break + + while p < pp: + j = p + lg1 = 0 + lg2 = 0 + kppj = (kf + (kv * dem[j] * (j - p))) / dem[j] + while j < pp: + last_c = kpj * lg2 + lg1 += dem[j] * (j - p) # Aufsummierung der Nachfrage, in Abhängigkeit der Verzinsung + lg2 += dem[j] # Aufsummierung der Nachfrage + kpj = (kf + kv * lg1) / lg2 + if kpj > kppj: + orders[p] = lg2 - dem[j] + total_c += last_c + break + j += 1 + kppj = kpj + temp = p + p = j + total_c += kpj * lg2 + orders[temp] = lg2 + # print("Total Cost: " + str(total_c)) + # print(sum(orders)) + return np.array(orders) + + +if __name__ == "__main__": + # statisches Test dictionary + dict_in = { + "planningPeriod": 14, # Anzahl der Perioden + "fixedCost": 10000, # Bestellkosten + "varCost": 1, # Lagerhaltungssatz + "roll": 4, + } + + # demand = [40, 50, 10, 20, 30, 40, 20, 25] + # demand = [0, 0, 0, 10, 0, 1] + # demand = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0] + # demand = [20, 50, 10, 50, 50, 10, 20, 40, 20, 30] + # demand = [854, 1021, 984, 1030, 1240, 1178, 905, 1005, 958, 905, 966, 965, 1030, 1111, 1285, 1089, 959, 920] + # demand = [300, 5001, 6022, 6103, 3533, 4046, 3044, 3023, 4064, 9552, 3960, 5077, 4000, 4000, 3430, 300, 3400, 3440, + # 200, 3400, 9400, 4340, 3400, 300, 3040, 4000, 5000, 6500, 45454, 4443, 3244, 2334, 344, 3223, 2999, 4000, 4000, + # 4000, 3400, 0, 0, 0, 4500, 400, 4500, 3400, 5400, 3000, 600, 0, 4500, 0, 0, 555, 4540, 500, 800, + # 4555, 3000, 4555, 455, 3444, 4333, 2344, 4454, 4555, 3444] + demand = [ + 0, + 0, + 6022, + 6103, + 3533, + 4046, + 3044, + 3023, + 4064, + 9552, + 3960, + 5077, + 4000, + 4000, + 3430, + 300, + 3400, + 3440, + 200, + 3400, + 9400, + 4340, + 3400, + 300, + 3040, + 4000, + 5000, + 6500, + 45454, + 4443, + 3244, + 2334, + 344, + 3223, + 2999, + 4000, + 4000, + 4000, + 3400, + 3400, + 3444, + 5006, + 4500, + 400, + 4500, + 3400, + 5400, + 0, + 0, + 0, + 4500, + 4500, + 4400, + 555, + 4540, + 0, + 0, + 4555, + 3000, + 4555, + 455, + 3444, + 4333, + 2344, + 4454, + 4555, + 3444, + ] + + leastunitcostmethod = LeastUnitCostMethod() + output = leastunitcostmethod.run(dict_in, demand) + print(output) diff --git a/examples/lot_sizing/lot_optimizers/part_period_heuristic.py b/examples/lot_sizing/lot_optimizers/part_period_heuristic.py new file mode 100644 index 0000000..31cc71f --- /dev/null +++ b/examples/lot_sizing/lot_optimizers/part_period_heuristic.py @@ -0,0 +1,148 @@ +import numpy as np + + +class PartPeriod: + def __init__(self): + pass + + def run(self, dict_in, demand): + # Füllen der Variablen aus Start_Dictionary + dem = demand + kf = dict_in["fixedCost"] + kv = dict_in["varCost"] + pp = len(dem) + p = 0 # aktuelle Periode + i = 0 + orders = [0 for i in range(pp)] + orders1 = [0 for i in range(pp)] + # Hilfsvariablen Kosten summieren + cost = 0 + cost_i = 0 + cost_v = 0 + + # testen ob "Null-Perioden am Anfang vorliegen + while dem[p] == 0: + orders1[p] = 0 + p = p + 1 + if p == pp: + p = p - 1 + break + i = p + + while p < pp: # solange wie aktuelle Periode kleiner Anzahl PlanungsPerioden + cost = cost + demand[p] * kv * (p - i) # Formel Stückausgleichkostenverfahren + if cost < kf: + orders[p] += demand[p] + lot = sum(orders) - sum(orders1) + cost_v = cost + else: + cost_i = cost_v + cost_i + orders1[i] = lot + i = p + p = p - 1 + cost = 0 + cost_v = 0 + p = p + 1 + orders1[i] = lot + x = orders1.count(0) + fix = (pp - x) * kf + # print(cost_i) + # print(fix) + cost_i + fix + cost_v + # print("total cost: " + str(total_c)) + # print(x) + # print(sum(orders1)) + return np.array(orders1) + + +if __name__ == "__main__": + # statisches Test dictionary + dict_in = { + "planningPeriod": 8, # Anzahl der Perioden + "fixedCost": 10000, # Bestellkosten + "varCost": 1, # Lagerhaltungssatz + "roll": 4, + } + + # demand = [40, 50, 10, 20, 30, 40, 20, 25] + # demand = [0, 0, 0, 10, 0, 1] + # demand = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0] + # demand = [20, 50, 10, 50, 50, 10, 20, 40, 20, 30] + # demand = [854, 1021, 984, 1030, 1240, 1178, 905, 1005, 958, 905, 966, 965, 1030, 1111, 1285, 1089, 959, 920] + # demand = [300, 5001, 6022, 6103, 3533, 4046, 3044, 3023, 4064, 9552, 3960, 5077, 4000, 4000, 3430, 300, 3400, 3440, + # 200, 3400, 9400, 4340, 3400, 300, 3040, 4000, 5000, 6500, 45454, 4443, 3244, 2334, 344, 3223, 2999, 4000, 4000, + # 4000, 3400, 0, 0, 0, 4500, 400, 4500, 3400, 5400, 3000, 600, 0, 4500, 0, 0, 555, 4540, 500, 800, + # 4555, 3000, 4555, 455, 3444, 4333, 2344, 4454, 4555, 3444 + demand = [ + 0, + 0, + 6022, + 6103, + 3533, + 4046, + 3044, + 3023, + 4064, + 9552, + 3960, + 5077, + 4000, + 4000, + 3430, + 300, + 3400, + 3440, + 200, + 3400, + 9400, + 4340, + 3400, + 300, + 3040, + 4000, + 5000, + 6500, + 45454, + 4443, + 3244, + 2334, + 344, + 3223, + 2999, + 4000, + 4000, + 4000, + 3400, + 3400, + 3444, + 5006, + 4500, + 400, + 4500, + 3400, + 5400, + 0, + 0, + 0, + 4500, + 4500, + 4400, + 555, + 4540, + 0, + 0, + 4555, + 3000, + 4555, + 455, + 3444, + 4333, + 2344, + 4454, + 4555, + 3444, + ] + + pp = PartPeriod() + output = pp.run(dict_in, demand) + print(output) diff --git a/examples/lot_sizing/lot_optimizers/silver_meal_heuristic.py b/examples/lot_sizing/lot_optimizers/silver_meal_heuristic.py new file mode 100644 index 0000000..4eab59e --- /dev/null +++ b/examples/lot_sizing/lot_optimizers/silver_meal_heuristic.py @@ -0,0 +1,159 @@ +import numpy as np + + +class SilverMeal: + def __init__(self): + pass + + def run(self, dict_in, demand): + # Füllen der Variablen aus Start_Dictionary + dem = demand + kf = dict_in["fixedCost"] + kv = dict_in["varCost"] + pp = len(dem) + # rh = dict_in["roll"] + # print(pp) + orders = [0 for i in range(pp)] + orders1 = [0 for i in range(pp)] + + c = kf + d = kf + 1 + p = 0 + y = 1 + cost_i = 0 + + # testen ob "Null-Perioden am Anfang vorliegen + while dem[p] == 0: + orders1[p] = 0 + p = p + 1 + if p == pp: + p = p - 1 + break + i = p + + while p < pp: + c = c + kv * dem[p] * (p - i) + + bed = c / y + + if d > bed: + orders[p] += dem[p] + lot = sum(orders) - sum(orders1) + cost_i = kv * dem[p] * (p - i) + cost_i + d = bed + y = y + 1 + + else: + orders1[i] = lot + i = p + p = p - 1 + c = kf + bed = 0 + d = kf + 1 + y = 1 + + p = p + 1 + + lot = sum(orders) - sum(orders1) + orders1[i] = lot + + x = orders1.count(0) + fix = (pp - x) * kf + # print(cost_i) + cost_i + fix + # print("Total Cost: " + str(total_c)) + # print(sum(orders1)) + return np.array(orders1) + + +if __name__ == "__main__": + # static test dictionary + dict_in = { + "planningPeriod": 14, # Anzahl der Perioden + "fixedCost": 10000, # Bestellkosten + "varCost": 1, # Lagerhaltungssatz + "roll": 4, + } + + # demand = [40, 50, 10, 20, 30, 40, 20, 25] + # demand = [0, 0, 0, 10, 0, 1] + # demand = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0] + # demand = [20, 50, 10, 50, 50, 10, 20, 40, 20, 30] + # demand = [854, 1021, 984, 1030, 1240, 1178, 905, 1005, 958, 905, 966, 965, 1030, 1111, 1285, 1089, 959, 920] + # demand = [300, 5001, 6022, 6103, 3533, 4046, 3044, 3023, 4064, 9552, 3960, 5077, 4000, 4000, 3430, 300, 3400, 3440, + # 200, 3400, 9400, 4340, 3400, 300, 3040, 4000, 5000, 6500, 45454, 4443, 3244, 2334, 344, 3223, 2999, 4000, 4000, + # 4000, 3400, 0, 0, 0, 4500, 400, 4500, 3400, 5400, 3000, 600, 0, 4500, 0, 0, 555, 4540, 500, 800, + # 4555, 3000, 4555, 455, 3444, 4333, 2344, 4454, 4555, 3444] + demand = [ + 0, + 0, + 6022, + 6103, + 3533, + 4046, + 3044, + 3023, + 4064, + 9552, + 3960, + 5077, + 4000, + 4000, + 3430, + 300, + 3400, + 3440, + 200, + 3400, + 9400, + 4340, + 3400, + 300, + 3040, + 4000, + 5000, + 6500, + 45454, + 4443, + 3244, + 2334, + 344, + 3223, + 2999, + 4000, + 4000, + 4000, + 3400, + 3400, + 3444, + 5006, + 4500, + 400, + 4500, + 3400, + 5400, + 0, + 0, + 0, + 4500, + 4500, + 4400, + 555, + 4540, + 0, + 0, + 4555, + 3000, + 4555, + 455, + 3444, + 4333, + 2344, + 4454, + 4555, + 3444, + ] + + silver_m = SilverMeal() + output = silver_m.run(dict_in, demand) + print(output) diff --git a/examples/lot_sizing/lot_optimizers/wagner_whitin.py b/examples/lot_sizing/lot_optimizers/wagner_whitin.py new file mode 100644 index 0000000..1c2580d --- /dev/null +++ b/examples/lot_sizing/lot_optimizers/wagner_whitin.py @@ -0,0 +1,482 @@ +import numpy as np + + +class WagnerWhitin: + def __init__(self): + pass + + def run(self, dict_in, demand): + # Variablen definieren + var_kosten = dict_in["varCost"] + fix_kosten = dict_in["fixedCost"] + bestellung = np.array(demand) # Liste von den Bestellungen + anz_periode = len(demand) # Anzahl der Periode + min_kosten = 0 # Minimale Kosten + kosten = [[0 for i in range(anz_periode)] for j in range(anz_periode)] # Kostenmatrix + min_liste = [0 for i in range(anz_periode)] # liste von den minimalen Kosten je Periode + periode_liste = [] # liste von den Bestellperioden + bestellte_menge = [0 for i in range(anz_periode)] # Liste von den bestellten Mengen je Bestellperiode + i = 0 + + # aussuchen der Null Bestellungen ganz am Anfang + # beginnen mit der ersten Nicht-Null Bestellung + while bestellung[i] == 0: + bestellte_menge[i] = 0 + i = i + 1 + if i == anz_periode: + i = i - 1 + break + + # Beginn der Schleife von Kostenmatrix + for i in range(anz_periode): + if bestellung[i] > 0 or i == ( + anz_periode - 1 + ): # Bedingung erfüllt, wenn die Bestellmenge nicht null ist oder ganz am Ende der Bestellperiode + for j in range(i, anz_periode): + kosten[j][i] += fix_kosten + min_kosten + for k in range(i + 1, j + 1): + kosten[j][i] += var_kosten * (k - i) * bestellung[k] + min_kosten = min(filter(None, kosten[i])) + min_liste[i] = min_kosten + # Surely this must have been a mistake? + # else: + # i += 1 + + # Rückwarts Schleife, Aussuchen der Bestellperiode, + # mit der Beachtung, dass es Null Bestellungen dazwischen geben + periode = anz_periode + while periode != 0: + if ( + bestellung[periode - 1] != 0 + ): # Wenn die Bestellung von vorherigen Periode gleich null ist, dann geht es zu vor vorherigen Periode + index = kosten[periode - 1].index(min_liste[periode - 1]) + periode_liste.insert(0, index) + periode = index + if periode == 0: + break + else: + periode = periode - 1 + + # Berechnung der bestellten Mengen je Bestellperiode + anz_bestellperiode = len(periode_liste) + for i in range(anz_bestellperiode): + start = periode_liste[i] + if i < (anz_bestellperiode - 1): + for j in range(start, periode_liste[i + 1]): + bestellte_menge[periode_liste[i]] += bestellung[j] + else: + for j in range(start, anz_periode): + bestellte_menge[periode_liste[i]] += bestellung[j] + + # Definieren der Output Array + return np.array([0]) if len(demand) == 0 or bestellung.max() == 0 else np.array(bestellte_menge) + + +if __name__ == "__main__": + # static test dictionary + dict_in = { + "planningPeriod": 30, # Anzahl der Perioden + "fixedCost": 490, # Bestellkosten + "varCost": 0.01, # Lagerhaltungssatz + "roll": 4, + } + demand = [ + 3057, + 2860, + 3033, + 3182, + 3179, + 3118, + 3159, + 2930, + 2857, + 2962, + 2874, + 3134, + 3104, + 2938, + 2746, + 3073, + 2909, + 2789, + 2826, + 2999, + 2848, + 2719, + 2851, + 2733, + 3074, + 2773, + 2902, + 2998, + 2959, + 2842, + 2889, + 2718, + 2940, + 2855, + 2799, + 2896, + 2805, + 2878, + 2770, + 2788, + 2947, + 2982, + 2687, + 2750, + 2823, + 2647, + 2722, + 2918, + 2722, + 2839, + 2914, + 2702, + 2763, + 2795, + 2792, + 2573, + 2870, + 2847, + 2733, + 2746, + 2525, + 2710, + 2528, + 2510, + 2651, + 2620, + 2790, + 2621, + 2745, + 2553, + 2807, + 2427, + 2712, + 2436, + 2499, + 2748, + 2785, + 2543, + 2604, + 2436, + 2374, + 2443, + 2632, + 2708, + 2407, + 2578, + 2595, + 2358, + 2659, + 2693, + 2654, + 2328, + 2637, + 2302, + 2658, + 2532, + 2554, + 2322, + 2357, + 2330, + 2479, + 2387, + 2543, + 2331, + 2322, + 2342, + 2569, + 2498, + 2357, + 2236, + 2383, + 2201, + 2214, + 2555, + 2310, + 2371, + 2243, + 2414, + 2511, + 2231, + 2360, + 2527, + 2492, + 2356, + 2400, + 2314, + 2426, + 2201, + 2363, + 2461, + 2206, + 2248, + 2320, + 2096, + 2162, + 2370, + 2284, + 2131, + 2326, + 2073, + 2314, + 2233, + 2146, + 2232, + 2368, + 2384, + 2181, + 2293, + 2249, + 2032, + 2099, + 2023, + 2059, + 2199, + 2322, + 2302, + 2067, + 2116, + 2066, + 1955, + 1936, + 2279, + 2315, + 2032, + 2147, + 2088, + 2147, + 2057, + 2277, + 1964, + 2036, + 2161, + 1877, + 1897, + 2023, + 1962, + 2081, + 1986, + 2166, + 1852, + 1898, + 1868, + 2206, + 2003, + 1914, + 1988, + 1816, + 2047, + 1871, + 2144, + 1929, + 1898, + 1865, + 1871, + 1741, + 1801, + 1920, + 1750, + 1785, + 2077, + 2016, + 1722, + 1846, + 1975, + 2026, + 1868, + 1909, + 1915, + 1819, + 2052, + 1701, + 1958, + 1800, + 1829, + 2030, + 1816, + 1663, + 1743, + 1672, + 1698, + 1673, + 1705, + 1601, + 1938, + 1971, + 1881, + 1827, + 1726, + 1732, + 1690, + 1621, + 1651, + 1824, + 1562, + 1856, + 1699, + 1662, + 1634, + 1882, + 1626, + 1505, + 1489, + 1722, + 1729, + 1693, + 1722, + 1651, + 1587, + 1621, + 1518, + 1719, + 1443, + 1493, + 1742, + 1732, + 1680, + 1428, + 1447, + 1674, + 1553, + 1718, + 1662, + 1503, + 1707, + 1408, + 1647, + 1549, + 1719, + 1667, + 1590, + 1377, + 1654, + 1319, + 1462, + 1672, + 1623, + 1461, + 1286, + 1597, + 1359, + 1460, + 1432, + 1407, + 1557, + 1321, + 1626, + 1522, + 1535, + 1346, + 1305, + 1506, + 1221, + 1256, + 1583, + 1455, + 1536, + 1361, + 1300, + 1450, + 1535, + 1526, + 1446, + 1393, + 1259, + 1226, + 1228, + 1420, + 1377, + 1309, + 1471, + 1289, + 1171, + 1151, + 1212, + 1157, + 1208, + 1376, + 1323, + 1234, + 1352, + 1437, + 1200, + 1073, + 1258, + 1162, + 1087, + 1094, + 1321, + 1183, + 1022, + 1005, + 1031, + 993, + 1237, + 1167, + 1272, + 1116, + 1299, + 1278, + 1139, + 1306, + 1210, + 1087, + 972, + 1014, + 1028, + 963, + 1035, + 896, + 971, + 1028, + 984, + 1249, + 1098, + 1114, + 1101, + 928, + 926, + 1147, + 1073, + 1130, + 986, + 889, + 811, + 970, + 1000, + 983, + 976, + 965, + 994, + ] + # demand = [40, 50, 10, 20, 30, 40, 20, 25] + # demand = [0, 0, 0, 10, 0, 1 ] + # demand = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0] + # demand = [20, 50, 10, 50, 50, 10, 20, 40, 20, 30] + # demand = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + # demand = [0, 0, 1] + # demand = [854, 1021, 984, 1030, 1240, 1178, 905, 1005, 958, 905, 966, 965, 1030, 1111, 1285, 1089, 959, 920] + # demand = [300, 5001, 6022, 6103, 3533, 4046, 3044, 3023, 4064, 9552, 3960, 5077, 4000, 4000, 3430, 300, 3400, 3440, 200, + # 3400, 9400, 4340, 3400, 300, 3040, 4000, 5000, 6500, 45454, 4443, 3244, 2334, 344, 3223, 2999, 4000, 4000, + # 4000, 3400, 0, 0, 0, 4500, 400, 4500, 3400, 5400, 3000, 600, 0, 4500, 0, 0, 555, 4540, 500, 800, + # 4555, 3000, 4555, 455, 3444, 4333, 2344, 4454, 4555, 3444] + # demand = [0, 0, 6022, 6103, 3533, 4046, 3044, 3023, 4064, 9552, 3960, 5077, 4000, 4000, 3430, 300, 3400, 3440, 200, + # 3400, 9400, 4340, 3400, 300, 3040, 4000, 5000, 6500, 45454, 4443, 3244, 2334, 344, 3223, 2999, 4000, 4000, + # 4000, 3400, 3400, 3444, 5006, 4500, 400, 4500, 3400, 5400, 0, 0, 0, 4500, 4500, 4400, 555, 4540, 0, 0, + # 4555, 3000, 4555, 455, 3444, 4333, 2344, 4454, 4555, 3444] + # demand = [300, 5001, 6022, 6103, 3533, 4046, 3044, 3023, 4064, 9552, 3960, 5077, 4000, 4000, 3430, 300, 3400, 3440, + # 200, 3400, 9400, 4340, 3400, 300, 3040, 4000, 5000, 6500, 45454, 4443, 3244, 2334, 344, 3223, 2999, 4000, 4000, + # 4000, 3400, 0, 0, 0, 4500, 400, 4500, 3400, 5400, 3000, 600, 0, 4500, 0, 0, 555, 4540, 500, 800, + # 4555, 3000, 4555, 455, 3444, 4333, 2344, 4454, 4555, 3444] + + # demand = [0, 0, 0, 6103, 3533, 4046, 3044, 3023, 4064, 9552, 3960, 5077, 4000, 4000, 3430, 300, 3400, 3440, + # 200, 3400, 9400, 4340, 3400, 300, 3040, 4000, 5000, 6500, 45454, 4443, 3244, 2334, 344, 3223, 2999, 4000, 4000, + # 4000, 3400, 0, 0, 0, 4500, 400, 4500, 3400, 5400, 3000, 600, 0, 4500, 0, 0, 555, 4540, 500, 800, + # 4555, 3000, 4555, 455, 3444, 4333, 2344, 4454, 4555, 3444] + + ww = WagnerWhitin() + output = ww.run(dict_in, demand) + print(output) diff --git a/examples/lot_sizing/lot_sizing_pipeline.py b/examples/lot_sizing/lot_sizing_pipeline.py new file mode 100644 index 0000000..5173380 --- /dev/null +++ b/examples/lot_sizing/lot_sizing_pipeline.py @@ -0,0 +1,169 @@ +import json +import os +from abc import ABC, abstractmethod +from pathlib import Path + +import luigi +import pandas as pd +from cosy.maestro import Maestro +from lot_optimizers.groff_heuristic import GroffHeuristic +from lot_optimizers.least_unit_cost_method import LeastUnitCostMethod +from lot_optimizers.part_period_heuristic import PartPeriod +from lot_optimizers.silver_meal_heuristic import SilverMeal +from lot_optimizers.wagner_whitin import WagnerWhitin + +from cosy_luigi.combinatorics import CoSyLuigiRepo, CoSyLuigiTask, CoSyLuigiTaskParameter + + +class GetCosts(CoSyLuigiTask): + def output(self): + return {"costs": luigi.LocalTarget("data/costs.json")} + + def run(self): + d = { + "fixedCost": 400, # Bestellkosten + "varCost": 1, # Lagerhaltungssatz + } + os.makedirs("data", exist_ok=True) + with open(self.output()["costs"].path, "w") as f: + json.dump(d, f, indent=4) + + +class GetHistoricDemand(CoSyLuigiTask): + def output(self): + return {"historic_demand": luigi.LocalTarget("data/historic_demand.csv")} + + def run(self): + with self.output()["historic_demand"].open("w") as f: + f.write("1, 5, 7, 8, 9, 10, 14, 16, 19, 21, 19, 23, 24, 26, 26, 26, 28, 26, 28, 30") + + +class PredictDemand(CoSyLuigiTask): + get_historic_demand = CoSyLuigiTaskParameter(GetHistoricDemand) + prediction_horizon = 8 + output_filename: str = "" + + def output(self): + return {"predicted_demand": luigi.LocalTarget(self.output_filename)} + + +class PredictDemandByAverage(PredictDemand): + output_filename = "data/predicted_demand_by_average.json" + + def run(self): + with self.input()["get_historic_demand"]["historic_demand"].open() as infile: + text = infile.read() + historic_demand = [int(t) for t in text.split(",")] + avg = int(sum(historic_demand) / len(historic_demand) + 0.5) + predicted = [avg for _ in range(self.prediction_horizon)] + data = {"predicted_demand": predicted} + df_predicted = pd.DataFrame(data) + + df_predicted.to_json(self.output()["predicted_demand"].path) + + +class PredictDemandByLinearRegression(PredictDemand): + output_filename = "data/predicted_demand_by_linear_regression.json" + + def run(self): + with self.input()["get_historic_demand"]["historic_demand"].open(): + # Mocked for sake of example + predicted = [10 + i for i in range(self.prediction_horizon)] + data = {"predicted_demand": predicted} + df_predicted = pd.DataFrame(data) + + df_predicted.to_json(self.output()["predicted_demand"].path) + + +class OptimizeLots(CoSyLuigiTask, ABC): + predict_demand = CoSyLuigiTaskParameter(PredictDemand) + get_costs = CoSyLuigiTaskParameter(GetCosts) + output_filename: str = "" + + def _get_cost(self): + with open(self.input()["get_costs"]["costs"].path, "rb") as f: + return json.load(f) + + def _get_demand(self): + demand_df = pd.read_json(self.input()["predict_demand"]["predicted_demand"].path) + return list(demand_df["predicted_demand"]) + + def output(self): + return {"optimized_lots": luigi.LocalTarget("data/" + self._get_variant_label() + "-" + self.output_filename)} + + def run(self): + cost = self._get_cost() + demand = self._get_demand() + + orders = self.run_optimizer(cost, demand) + + with self.output()["optimized_lots"].open("w") as f: + f.write(str(list(orders))) + + @abstractmethod + def run_optimizer(self, cost, demand): + return NotImplementedError() + + def _get_variant_label(self): + if isinstance(self.input()["predict_demand"]["predicted_demand"], luigi.LocalTarget): + label = self.input()["predict_demand"]["predicted_demand"].path + return Path(label).stem + return None + + +class OptimizeLotsByGroff(OptimizeLots): + output_filename = "optimize_lots_by_groff.txt" + + def run_optimizer(self, cost, demand): + optimizer = GroffHeuristic() + return optimizer.run(cost, demand) + + +class OptimizeLotsByWagnerWhitin(OptimizeLots): + output_filename = "optimize_lots_by_wagner_within.txt" + + def run_optimizer(self, cost, demand): + optimizer = WagnerWhitin() + return optimizer.run(cost, demand) + + +class OptimizeLotsBySilverMeal(OptimizeLots): + output_filename = "optimize_lots_by_silver_meal.txt" + + def run_optimizer(self, cost, demand): + optimizer = SilverMeal() + return optimizer.run(cost, demand) + + +class OptimizeLotsByLeastUnitCost(OptimizeLots): + output_filename = "optimize_lots_by_least_unit_cost.txt" + + def run_optimizer(self, cost, demand): + optimizer = LeastUnitCostMethod() + return optimizer.run(cost, demand) + + +class OptimizeLotsByPartPeriod(OptimizeLots): + output_filename = "optimize_lots_by_part_period.txt" + + def run_optimizer(self, cost, demand): + optimizer = PartPeriod() + return optimizer.run(cost, demand) + + +if __name__ == "__main__": + repo = CoSyLuigiRepo( + GetCosts, + GetHistoricDemand, + PredictDemandByAverage, + PredictDemandByLinearRegression, + OptimizeLotsByLeastUnitCost, + OptimizeLotsByGroff, + OptimizeLotsByPartPeriod, + OptimizeLotsBySilverMeal, + OptimizeLotsByWagnerWhitin, + ) + maestro = Maestro(repo.cls_repo, repo.taxonomy) + for result in maestro.query(OptimizeLots.target()): + # print(deps_tree.print_tree(result)) + luigi.build([result], local_scheduler=True, detailed_summary=True) diff --git a/examples/lot_sizing/requirements.txt b/examples/lot_sizing/requirements.txt new file mode 100644 index 0000000..ab61093 --- /dev/null +++ b/examples/lot_sizing/requirements.txt @@ -0,0 +1,4 @@ +pandas +numpy +--pre +cosy-luigi diff --git a/examples/ml_blood_sugar_level/README.md b/examples/ml_blood_sugar_level/README.md new file mode 100644 index 0000000..a37e650 --- /dev/null +++ b/examples/ml_blood_sugar_level/README.md @@ -0,0 +1,38 @@ +# ML-Blood Sugar Level Example + +You can find the source for the example +[here](https://github.com/cls-python/cosy-luigi/tree/main/examples/ml_blood_sugar_level/): + +Here we used CLS-Luigi to create a ML Pipeline to predict the blood +sugar level of some patients. The Steps are very easy to understand. We +start by loading the dataset from Scikit-Learn, then we split it into 2 +subsets for training and testing. + +The first variation point is the scaling method. We introduce 2 concrete +implementation, namely `RobustScaler` & `MinMaxScaler`. After scaling we +have our second variation point which is the regression model. Here we +have also 2 concrete implementation, namely `LinearRegression` & +`LassoLars`. + +Lastly we evaluate each regression model by predicting the testing +target and calculating the root mean squared error. + +# Requirements + +The example contains a +[requirements.txt](https://github.com/cls-python/cls-luigi/tree/main/examples/ml_blood_sugar_level/requirements.txt) +file. To experiment with the example, you can set up your environment by +executing the following command: + +``` bash +# cd into the ml blood sugar level example folder +pip install -r requirements.txt +``` + +# Static Visualization + +![image](images/static.png) + +# Dynamic Visualization + +![image](images/dynamic.png) diff --git a/examples/ml_blood_sugar_level/images/dynamic.png b/examples/ml_blood_sugar_level/images/dynamic.png new file mode 100644 index 0000000..c9ce345 Binary files /dev/null and b/examples/ml_blood_sugar_level/images/dynamic.png differ diff --git a/examples/ml_blood_sugar_level/images/static.png b/examples/ml_blood_sugar_level/images/static.png new file mode 100644 index 0000000..fcc8fc6 Binary files /dev/null and b/examples/ml_blood_sugar_level/images/static.png differ diff --git a/examples/ml_blood_sugar/ml_blood_sugar.py b/examples/ml_blood_sugar_level/ml_blood_sugar_level.py similarity index 100% rename from examples/ml_blood_sugar/ml_blood_sugar.py rename to examples/ml_blood_sugar_level/ml_blood_sugar_level.py diff --git a/examples/ml_blood_sugar/requirements.txt b/examples/ml_blood_sugar_level/requirements.txt similarity index 100% rename from examples/ml_blood_sugar/requirements.txt rename to examples/ml_blood_sugar_level/requirements.txt diff --git a/properdocs.yml b/properdocs.yml index 85ed8f4..a4c42d5 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -42,7 +42,6 @@ theme: - content.code.annotate - content.tabs.link - content.tooltips - - navigation.expand - navigation.footer - navigation.instant - navigation.sections diff --git a/scripts/gen_example_pages.py b/scripts/gen_example_pages.py index 0d7cc38..318b0b5 100644 --- a/scripts/gen_example_pages.py +++ b/scripts/gen_example_pages.py @@ -1,5 +1,6 @@ """Generate the examples pages and navigation.""" +import os import re from pathlib import Path @@ -12,35 +13,64 @@ src = root / "examples" -for path in sorted(src.rglob("*.py")): - module_path = path.relative_to(src).with_suffix("") - doc_path = path.relative_to(src).with_suffix(".md") - full_doc_path = Path("examples", doc_path) +for directory in [Path(d.path) for d in os.scandir(src)]: + example_name = None - parts = tuple(module_path.parts) - with open(path) as f: - override_name = re.findall(r"##(.*?)##", f.readline()) + for path in sorted(directory.rglob("*.md"), key=lambda s: "aaa" if s.name == "README.md" else s.name): + module_path = path.relative_to(src).with_suffix("") + doc_path = path.relative_to(src) + full_doc_path = Path("examples", doc_path) - if parts[-1] == "__init__": - parts = parts[:-1] - elif parts[-1] == "__main__": - continue + parts = tuple(module_path.parts) - if override_name: - nav[override_name] = doc_path.as_posix() - else: - nav[parts] = doc_path.as_posix() + with open(path) as f: + override_name = re.findall(r"^#\s*([^#].*)\s*$", f.readline()) - with mkdocs_gen_files.open(full_doc_path, "w") as fd: - ident = ".".join(parts) - fd.write( - f"""::: {ident} - options: - members_order: source - """ - ) + if parts[-1] == "README": + example_name = tuple(override_name) + override_name = ("Overview",) + if example_name: + parts = example_name + parts[1:] - mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) + if override_name: + nav[parts[:-1] + override_name] = doc_path.as_posix() + else: + nav[parts] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + fd.write(path.read_text()) + + mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) + + for path in sorted(directory.rglob("*.png")): + module_path = path.relative_to(src).with_suffix("") + doc_path = path.relative_to(src) + full_doc_path = Path("examples", doc_path) + + with mkdocs_gen_files.open(full_doc_path, "wb") as fd: + fd.write(path.read_bytes()) + + for path in sorted(directory.rglob("*.py")): + module_path = path.relative_to(src).with_suffix("") + doc_path = path.relative_to(src).with_suffix(".md") + full_doc_path = Path("examples", doc_path) + + parts = tuple(module_path.parts) + if example_name: + nav[(*example_name, "Source Files", *parts[1:-1], f"{parts[-1]}.py")] = doc_path.as_posix() + else: + nav[parts] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + ident = ".".join(parts) + fd.write( + f"""::: {ident} + options: + members_order: source + """ + ) + + mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) with mkdocs_gen_files.open("examples/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav())