From 0235e308acac70af11c0e526ed4bf0944f04b863 Mon Sep 17 00:00:00 2001 From: Nicolai Date: Fri, 24 Apr 2026 18:38:10 +0200 Subject: [PATCH] bunkerfix --- .../__pycache__/model_builder.cpython-313.pyc | Bin 106921 -> 106562 bytes src/optimization/model_builder.py | 347 ++++++++++++------ webapp/backend/main.py | 2 +- webapp/frontend/src/App.jsx | 40 +- 4 files changed, 240 insertions(+), 149 deletions(-) diff --git a/src/optimization/__pycache__/model_builder.cpython-313.pyc b/src/optimization/__pycache__/model_builder.cpython-313.pyc index e3cdeb719df8a835d900ef60f02b5f5b478be6e9..e0dc93a058593c876178508924b203976974af6e 100644 GIT binary patch delta 18668 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 diff --git a/src/optimization/model_builder.py b/src/optimization/model_builder.py index b6b560c..7091272 100644 --- a/src/optimization/model_builder.py +++ b/src/optimization/model_builder.py @@ -120,6 +120,33 @@ def build_model( ) -> pyo.ConcreteModel: if step_size_tonnes <= 0: raise ValueError("step_size_tonnes must be positive") + + # ========================================================================= + # INHALTSVERZEICHNIS build_model() + # ========================================================================= + # 1 Datenvorbereitung Bedarf, Bounds, Wochenstruktur + # 2 Sets & Parameter I, J, W, D, S; Bedarfs-/Toleranzparameter + # 3 Entscheidungsvariablen k (ganzzahlig), x = step_size * k + # 4 Bunkerlogik Bunker-Vars und Bilanzen (optional) + # 5 Aggregationen y_delivery_shift, y_delivery, y_week, y_month + # 6 Liefertoleranzen KW täglich, wöchentlich, monatlich, gesamt + # 7 Veredlungstoleranzen täglich, wöchentlich, monatlich (NO/WZ/ALL) + # 8 Mischungsgrenzen alpha hard (mix_lower/upper), soft (mix_target) + # 9 Abweichungsverfolgung dev, z_max, no_three_in_a_row, devV + # 10 Schichtglättung tagesübergreifend: Spätschicht Mi–Di, Früh-/Nacht Mo–Do / Fr–So + # 11 Zielfunktion Strafterme und Lambda-Gewichte + # 12 Routenverbote Reichwalde->V, Welzow->B4 + # 13 Förder-/Verladekapazitäten monatlich, Schicht, Tag + # 14 Verfügbarkeiten dynamische Caps aus Verfuegbarkeiten.parquet + # 15 Zugdurchlass Streckencaps aus zugdurchlass.parquet + # ========================================================================= + + # ========================================================================= + # 1 DATENVORBEREITUNG + # Bedarf-DataFrame aus Excel aufbereiten, Bergbauwoche berechnen, + # Bounds für Kraftwerke und Veredlung aus den Parquet-Tabellen laden. + # ========================================================================= + bedarf = tables["rohkohlebedarf"][ [ "datum", @@ -153,6 +180,12 @@ def build_model( bounds_power_plants = tables["bounds_power_plants"] bounds_day = bounds_power_plants[bounds_power_plants["zeitraum"] == "pro Tag"].copy() + # ========================================================================= + # 2 SETS & PARAMETER + # Pyomo-Modell anlegen, Mengen I/J/W/D/S definieren, Bedarfs- und + # Tagestoleranzparameter initialisieren und aus den Tabellen befüllen. + # ========================================================================= + model = pyo.ConcreteModel() J_POWER = ["J", "SP", "B3", "B4"] @@ -244,6 +277,12 @@ def build_model( model.a_min_day[j, w, d] = a_min model.a_max_day[j, w, d] = a_max + # ========================================================================= + # 3 ENTSCHEIDUNGSVARIABLEN + # k[i,j,w,d,s] als ganzzahlige Lieferschritte, x = step_size * k + # als abgeleiteter kontinuierlicher Fluss in Tonnen. + # ========================================================================= + model.step_size_tonnes = pyo.Param(initialize=float(step_size_tonnes), mutable=False) model.k = pyo.Var(model.I, model.J, model.W, model.D, model.S, domain=pyo.NonNegativeIntegers) @@ -252,6 +291,12 @@ def build_model( model.x = pyo.Expression(model.I, model.J, model.W, model.D, model.S, rule=x_rule) + # ========================================================================= + # 4 BUNKERLOGIK + # Falls eine Bunkertabelle vorliegt: Bunker-Vars, Ausfluss-Expressions + # und tägliche Bilanzbedingungen anlegen; sonst out = x direkt. + # ========================================================================= + bunker_df = tables.get("bunker") if bunker_df is not None: j_bunker_map = { @@ -357,6 +402,21 @@ def build_model( + sum(model.x[i, j, w, d, s] for s in model.S) - sum(model.bunker_out[i, j, w, d, s] for s in model.S) ) + + # Nebenbedingung: Täglicher Bunkerabfluss entspricht der Kraftwerksnachfrage. + # Damit wird bunker_out physikalisch korrekt an den Verbrauch gebunden. + model.bunker_out_demand = pyo.ConstraintList() + for j in model.J_BUNKER: + for w, d in time_points_day: + demand_expr = ( + model.dV_N[w, d] + model.dV_W[w, d] + if j == "V" + else model.d[j, w, d] + ) + model.bunker_out_demand.add( + sum(model.bunker_out[i, j, w, d, s] for i in model.I for s in model.S) + == demand_expr + ) else: def out_rule(m, i, j, w, d, s): return m.x[i, j, w, d, s] @@ -373,6 +433,12 @@ def build_model( model.y = pyo.Expression(model.J, model.W, model.D, rule=y_rule) + # ========================================================================= + # 5 AGGREGATIONEN + # Expressions für Schicht-, Tages-, Wochen- und Monatsflüsse aufbauen, + # die in Constraints und Zielfunktion wiederverwendet werden. + # ========================================================================= + def y_delivery_shift_rule(model, j, w, d, s): return sum(model.x[i, j, w, d, s] for i in model.I) @@ -383,6 +449,12 @@ def build_model( model.y_delivery = pyo.Expression(model.J, model.W, model.D, rule=y_delivery_rule) + # ========================================================================= + # 6 LIEFERTOLERANZEN KRAFTWERKE + # Tägliche, wöchentliche und monatliche Toleranzbänder je Kraftwerk + # sowie Gesamtsystemgrenzen über alle Werke. + # ========================================================================= + J_delivery = [j for j in model.J if j != "V"] def delivery_tolerance_rule(model, j, w, d): @@ -392,24 +464,9 @@ def build_model( model.d[j, w, d] + model.a_max_day[j, w, d], ) - # Constraint: Daily delivery must stay within demand tolerance for power plants. + # Nebenbedingung: Tägliche Lieferung muss innerhalb des Toleranzbandes der Kraftwerke liegen. model.delivery_tolerance = pyo.Constraint(J_delivery, model.W, model.D, rule=delivery_tolerance_rule) - model.shift_balance_dev = pyo.Var(model.J, model.W, model.D, model.S, domain=pyo.NonNegativeReals) - model.shift_balance_abs = pyo.ConstraintList() - for j in model.J: - for w in model.W: - for d in model.D: - for s in model.S: - model.shift_balance_abs.add( - model.shift_balance_dev[j, w, d, s] - >= model.y_delivery_shift[j, w, d, s] - model.y_delivery[j, w, d] / 3 - ) - model.shift_balance_abs.add( - model.shift_balance_dev[j, w, d, s] - >= model.y_delivery[j, w, d] / 3 - model.y_delivery_shift[j, w, d, s] - ) - week_to_month = bedarf.groupby("week")["datum"].min().dt.month.to_dict() model.M = pyo.Set(initialize=sorted(set(week_to_month.values()))) J_power = [j for j in model.J if j != "V"] @@ -490,7 +547,7 @@ def build_model( model.d_week[j, w] + model.a_max_week[j, w], ) - # Constraint: Weekly deliveries per plant stay within weekly tolerance band. + # Nebenbedingung: Wöchentliche Lieferung je Kraftwerk innerhalb des Wochentoleranzbandes. model.week_tolerance = pyo.Constraint(J_power, model.W, rule=week_tolerance_rule) def month_tolerance_rule(model, j, m): @@ -500,7 +557,7 @@ def build_model( model.d_month[j, m] + model.a_max_month[j, m], ) - # Constraint: Monthly deliveries per plant stay within monthly tolerance band. + # Nebenbedingung: Monatliche Lieferung je Kraftwerk innerhalb des Monatstoleranzbandes. model.month_tolerance = pyo.Constraint(J_power, model.M, rule=month_tolerance_rule) def week_total_rule(model, w): @@ -510,7 +567,7 @@ def build_model( model.d_week_total[w] + model.a_max_week_total[w], ) - # Constraint: Total weekly deliveries across all plants stay within tolerance band. + # Nebenbedingung: Wöchentliche Gesamtlieferung über alle Kraftwerke innerhalb des Toleranzbandes. model.week_total_tolerance = pyo.Constraint(model.W, rule=week_total_rule) def month_total_rule(model, m): @@ -520,9 +577,15 @@ def build_model( model.d_month_total[m] + model.a_max_month_total[m], ) - # Constraint: Total monthly deliveries across all plants stay within tolerance band. + # Nebenbedingung: Monatliche Gesamtlieferung über alle Kraftwerke innerhalb des Toleranzbandes. model.month_total_tolerance = pyo.Constraint(model.M, rule=month_total_rule) + # ========================================================================= + # 7 VEREDLUNGSTOLERANZEN + # Tages-, Wochen- und Monatsgrenzen für Nochten-, Welzow- und + # Gesamtkohle an die Veredlung (ISP). + # ========================================================================= + model.a_min_V_day = pyo.Param(["N", "W", "ALL"], model.W, model.D, initialize=0.0, mutable=True) model.a_max_V_day = pyo.Param(["N", "W", "ALL"], model.W, model.D, initialize=0.0, mutable=True) model.a_min_V_week = pyo.Param(["N", "W", "ALL"], model.W, initialize=0.0, mutable=True) @@ -581,7 +644,7 @@ def build_model( m.dV_W[w, d] + m.a_max_V_day["W", w, d], ) - # Constraint: Veredlung day tolerance for Welzow coal. + # Nebenbedingung: Tagestoleranz Veredlung für Welzower Kohle. model.v_w_day_tol = pyo.Constraint(model.W, model.D, rule=v_w_day_rule) def v_n_day_rule(m, w, d): @@ -591,7 +654,7 @@ def build_model( m.dV_N[w, d] + m.a_max_V_day["N", w, d], ) - # Constraint: Veredlung day tolerance for Nochten coal. + # Nebenbedingung: Tagestoleranz Veredlung für Nochtener Kohle. model.v_n_day_tol = pyo.Constraint(model.W, model.D, rule=v_n_day_rule) def v_all_day_rule(m, w, d): @@ -601,7 +664,7 @@ def build_model( m.dV_W[w, d] + m.dV_N[w, d] + m.a_max_V_day["ALL", w, d], ) - # Constraint: Veredlung day tolerance for combined coal types. + # Nebenbedingung: Tagestoleranz Veredlung für kombinierten Bedarf (NO+WZ). model.v_all_day_tol = pyo.Constraint(model.W, model.D, rule=v_all_day_rule) def v_w_week_rule(m, w): @@ -611,7 +674,7 @@ def build_model( sum(m.dV_W[w, d] for d in m.D) + m.a_max_V_week["W", w], ) - # Constraint: Veredlung weekly tolerance for Welzow coal. + # Nebenbedingung: Wochentoleranz Veredlung für Welzower Kohle. model.v_w_week_tol = pyo.Constraint(model.W, rule=v_w_week_rule) def v_n_week_rule(m, w): @@ -621,7 +684,7 @@ def build_model( sum(m.dV_N[w, d] for d in m.D) + m.a_max_V_week["N", w], ) - # Constraint: Veredlung weekly tolerance for Nochten coal. + # Nebenbedingung: Wochentoleranz Veredlung für Nochtener Kohle. model.v_n_week_tol = pyo.Constraint(model.W, rule=v_n_week_rule) def v_all_week_rule(m, w): @@ -631,7 +694,7 @@ def build_model( sum(m.dV_W[w, d] + m.dV_N[w, d] for d in m.D) + m.a_max_V_week["ALL", w], ) - # Constraint: Veredlung weekly tolerance for combined coal types. + # Nebenbedingung: Wochentoleranz Veredlung für kombinierten Bedarf (NO+WZ). model.v_all_week_tol = pyo.Constraint(model.W, rule=v_all_week_rule) def v_w_month_rule(m, mm): @@ -642,7 +705,7 @@ def build_model( sum(m.dV_W[w, d] for w in weeks_in_m for d in m.D) + m.a_max_V_month["W", mm], ) - # Constraint: Veredlung monthly tolerance for Welzow coal. + # Nebenbedingung: Monatstoleranz Veredlung für Welzower Kohle. model.v_w_month_tol = pyo.Constraint(model.M, rule=v_w_month_rule) def v_n_month_rule(m, mm): @@ -653,7 +716,7 @@ def build_model( sum(m.dV_N[w, d] for w in weeks_in_m for d in m.D) + m.a_max_V_month["N", mm], ) - # Constraint: Veredlung monthly tolerance for Nochten coal. + # Nebenbedingung: Monatstoleranz Veredlung für Nochtener Kohle. model.v_n_month_tol = pyo.Constraint(model.M, rule=v_n_month_rule) def v_all_month_rule(m, mm): @@ -664,7 +727,7 @@ def build_model( sum(m.dV_W[w, d] + m.dV_N[w, d] for w in weeks_in_m for d in m.D) + m.a_max_V_month["ALL", mm], ) - # Constraint: Veredlung monthly tolerance for combined coal types. + # Nebenbedingung: Monatstoleranz Veredlung für kombinierten Bedarf (NO+WZ). model.v_all_month_tol = pyo.Constraint(model.M, rule=v_all_month_rule) j_map = { @@ -682,6 +745,12 @@ def build_model( mix_df = tables["kohle_mix"] + # ========================================================================= + # 8 MISCHUNGSGRENZEN + # Harte Mindest- und Maximalanteile je Quelle/Ziel (alpha_min/max), + # plus weiche Zielbandabweichungen (mix_target_low/high/mid). + # ========================================================================= + model.alpha_min = pyo.Param(model.I, model.J, initialize=0.0, mutable=True) model.alpha_max = pyo.Param(model.I, model.J, initialize=1.0, mutable=True) @@ -714,7 +783,7 @@ def build_model( return pyo.Constraint.Skip return m.x_day[i, j, w, d] <= m.alpha_max[i, j] * m.y_day[j, w, d] - # Constraint: Mix lower/upper bound per source and plant (hard), per day. + # Nebenbedingung: Harte Mischungsunter- und -obergrenzen je Quelle und Kraftwerk, täglich. model.mix_lower = pyo.Constraint(model.I, model.J, model.W, model.D, rule=mix_lower_rule) model.mix_upper = pyo.Constraint(model.I, model.J, model.W, model.D, rule=mix_upper_rule) @@ -746,7 +815,7 @@ def build_model( return pyo.Constraint.Skip return m.x_day[i, j, w, d] <= m.alpha_target_high[i, j] * m.y_day[j, w, d] + m.mix_target_high_dev[i, j, w, d] - # Constraint: Mix target lower/upper band (soft via deviation variable). + # Nebenbedingung: Weiches Mischungszielband je Quelle und Kraftwerk (Abweichungsvariable). model.mix_target_low = pyo.Constraint(model.I, model.J, model.W, model.D, rule=mix_target_low_rule) model.mix_target_high = pyo.Constraint(model.I, model.J, model.W, model.D, rule=mix_target_high_rule) @@ -835,6 +904,12 @@ def build_model( >= model.bunker_target[j] - model.bunker_total[j, w_prev, d_prev] ) + # ========================================================================= + # 9 ABWEICHUNGSVERFOLGUNG + # Tagesabweichung in pos/neg aufteilen, Binärvariable für Maximalabweichung + # setzen und die Drei-Tage-Regel in Folge begrenzen. + # ========================================================================= + def demand_rule(m, j, w, d): return m.dV_N[w, d] + m.dV_W[w, d] if j == "V" else m.d[j, w, d] @@ -850,7 +925,7 @@ def build_model( def dev_balance_rule(model, j, w, d): return model.dev_pos[j, w, d] - model.dev_neg[j, w, d] == model.dev[j, w, d] - # Constraint: Split demand deviation into positive/negative components. + # Nebenbedingung: Nachfrageabweichung in positiven und negativen Anteil aufteilen. model.dev_balance = pyo.Constraint(J_dev, model.W, model.D, rule=dev_balance_rule) model.z_max = pyo.Var(J_power, model.W, model.D, domain=pyo.Binary) @@ -858,7 +933,7 @@ def build_model( def max_reached_rule(m, j, w, d): return m.dev_pos[j, w, d] <= m.a_max_day[j, w, d] * m.z_max[j, w, d] - # Constraint: Flag when daily deviation hits the maximum tolerance. + # Nebenbedingung: Binärvariable setzen, wenn Tagesabweichung die Maximumtoleranz erreicht. model.max_reached = pyo.Constraint(J_power, model.W, model.D, rule=max_reached_rule) D_list = list(model.D.ordered_data()) @@ -867,7 +942,7 @@ def build_model( d1, d2, d3 = D_list[idx : idx + 3] return m.z_max[j, w, d1] + m.z_max[j, w, d2] + m.z_max[j, w, d3] <= 3 - # Constraint: Prevent three consecutive days with max deviation. + # Nebenbedingung: Mehr als drei aufeinanderfolgende Tage mit Maximalabweichung verhindern. model.no_three_in_a_row = pyo.Constraint(J_power, model.W, range(len(D_list) - 2), rule=no_three_in_a_row_rule) def y_V_nochten(m, w, d): @@ -884,52 +959,95 @@ def build_model( model.devV_W_pos = pyo.Var(model.W, model.D, domain=pyo.NonNegativeReals) model.devV_W_neg = pyo.Var(model.W, model.D, domain=pyo.NonNegativeReals) - # Constraint: Split Nochten Veredlung deviation into positive/negative parts. + # Nebenbedingung: Veredlungsabweichung Nochten in positiven und negativen Anteil aufteilen. model.devV_N_balance = pyo.Constraint( model.W, model.D, rule=lambda m, w, d: m.devV_N_pos[w, d] - m.devV_N_neg[w, d] == m.devV_N[w, d] ) - # Constraint: Split Welzow Veredlung deviation into positive/negative parts. + # Nebenbedingung: Veredlungsabweichung Welzow in positiven und negativen Anteil aufteilen. model.devV_W_balance = pyo.Constraint( model.W, model.D, rule=lambda m, w, d: m.devV_W_pos[w, d] - m.devV_W_neg[w, d] == m.devV_W[w, d] ) + # ========================================================================= + # 10 SCHICHTGLÄTTUNG + # Tagesübergreifende Gleichmäßigkeit je Schicht innerhalb fixer Zeitfenster: + # Spätschicht: Mi–Di (7 Tage, wochenübergreifend) + # Früh-/Nachtschicht: Mo–Do (innerhalb Bergbauwoche) und Fr–So (Wochenübergang) + # Soft-Constraint: Paarweise Tagesdifferenz wird ab SHIFT_SMOOTH_TOL bestraft. + # ========================================================================= - lambda_dev = 100_000 + SHIFT_SMOOTH_TOL = step_size_tonnes # tolerierte Tagesdifferenz je Schicht in Tonnen + + lambda_dev = 100_000_000 lambda_v = 100_000 - lambda_shift = 100_000_000 + lambda_shift_smooth = 100_000_000 lambda_mix = 4_000_000_000 lambda_mix_mid = 4_000_000 lambda_b3_bunker_mix = 100_000_000 - lambda_shift_balance = 5_000_000 lambda_bunker_target = 50_000_000 - SHIFT_TOL = step_size_tonnes - SHIFT_PAIRS = [("F", "S"), ("S", "N"), ("F", "N")] - model.SHIFT_PAIRS = pyo.Set(initialize=SHIFT_PAIRS, dimen=2) - model.shift_dev_soft = pyo.Var(model.I, model.J, model.W, model.D, model.SHIFT_PAIRS, domain=pyo.NonNegativeReals) - model.shift_excess = pyo.Var(model.I, model.J, model.W, model.D, model.SHIFT_PAIRS, domain=pyo.NonNegativeReals) + # Nachfolgewoche je Bergbauwoche (für wochenübergreifende Fenster) + nw_map = {weeks[i]: weeks[i + 1] for i in range(len(weeks) - 1)} + + # Alle paarweisen Tages-Kombinationen je Schicht-Fenster sammeln. + # Einträge: (schicht, woche_a, tag_a, woche_b, tag_b) + smooth_pairs: list[tuple] = [] + + for w in weeks: + nw = nw_map.get(w) + + # Spätschicht: Mittwoch bis Dienstag der Folgewoche (7 Tage) + sp = [(w, "Mi"), (w, "Do"), (w, "Fr")] + if nw is not None: + sp += [(nw, "Sa"), (nw, "So"), (nw, "Mo"), (nw, "Di")] + for a_idx, (wa, da) in enumerate(sp): + for wb, db in sp[a_idx + 1 :]: + smooth_pairs.append(("S", wa, da, wb, db)) + + for s in ("F", "N"): + # Gruppe 1: Montag bis Donnerstag (innerhalb einer Bergbauwoche) + g1 = [(w, "Mo"), (w, "Di"), (w, "Mi"), (w, "Do")] + for a_idx, (wa, da) in enumerate(g1): + for wb, db in g1[a_idx + 1 :]: + smooth_pairs.append((s, wa, da, wb, db)) + + # Gruppe 2: Freitag bis Sonntag (Wochenübergang) + g2 = [(w, "Fr")] + if nw is not None: + g2 += [(nw, "Sa"), (nw, "So")] + for a_idx, (wa, da) in enumerate(g2): + for wb, db in g2[a_idx + 1 :]: + smooth_pairs.append((s, wa, da, wb, db)) + + model.SMOOTH_PAIR_IDX = pyo.Set(initialize=range(len(smooth_pairs)), dimen=1) + model.shift_smooth_dev = pyo.Var(model.I, model.J, model.SMOOTH_PAIR_IDX, domain=pyo.NonNegativeReals) + model.shift_smooth_excess = pyo.Var(model.I, model.J, model.SMOOTH_PAIR_IDX, domain=pyo.NonNegativeReals) + + # Nebenbedingung: Absolute Tagesdifferenz je Schicht-Fenster-Paar und Toleranzüberschreitung. + model.shift_smooth_abs = pyo.ConstraintList() + model.shift_smooth_excess_def = pyo.ConstraintList() - # ConstraintList: Enforce absolute shift differences and excess over tolerance. - model.shift_dev_abs = pyo.ConstraintList() - model.shift_excess_def = pyo.ConstraintList() for i in model.I: for j in model.J: - for w in model.W: - for d in model.D: - for s1, s2 in SHIFT_PAIRS: - if (i, j, w, d, s1) in model.x and (i, j, w, d, s2) in model.x: - model.shift_dev_abs.add( - model.shift_dev_soft[i, j, w, d, s1, s2] - >= model.x[i, j, w, d, s1] - model.x[i, j, w, d, s2] - ) - model.shift_dev_abs.add( - model.shift_dev_soft[i, j, w, d, s1, s2] - >= model.x[i, j, w, d, s2] - model.x[i, j, w, d, s1] - ) - model.shift_excess_def.add( - model.shift_excess[i, j, w, d, s1, s2] - >= model.shift_dev_soft[i, j, w, d, s1, s2] - SHIFT_TOL - ) + for p_idx, (s, w_a, d_a, w_b, d_b) in enumerate(smooth_pairs): + model.shift_smooth_abs.add( + model.shift_smooth_dev[i, j, p_idx] + >= model.x[i, j, w_a, d_a, s] - model.x[i, j, w_b, d_b, s] + ) + model.shift_smooth_abs.add( + model.shift_smooth_dev[i, j, p_idx] + >= model.x[i, j, w_b, d_b, s] - model.x[i, j, w_a, d_a, s] + ) + model.shift_smooth_excess_def.add( + model.shift_smooth_excess[i, j, p_idx] + >= model.shift_smooth_dev[i, j, p_idx] - SHIFT_SMOOTH_TOL + ) + + # ========================================================================= + # 11 ZIELFUNKTION + # Alle Strafterme mit Lambda-Gewichten zusammenführen und als + # Minimierungsziel im Modell registrieren. + # ========================================================================= def objective_rule(model): deviation_penalty = ( @@ -943,18 +1061,11 @@ def build_model( ) ) - smoothness_penalty = lambda_shift * sum( - model.shift_excess[i, j, w, d, s1, s2] + shift_smooth_penalty = lambda_shift_smooth * sum( + model.shift_smooth_excess[i, j, p_idx] for i in model.I for j in model.J - for w in model.W - for d in model.D - for s1, s2 in model.SHIFT_PAIRS - if (i, j, w, d, s1) in model.x and (i, j, w, d, s2) in model.x - ) - - shift_balance_penalty = lambda_shift_balance * sum( - model.shift_balance_dev[j, w, d, s] for j in model.J for w in model.W for d in model.D for s in model.S + for p_idx in model.SMOOTH_PAIR_IDX ) mix_target_penalty = lambda_mix * sum( @@ -985,27 +1096,32 @@ def build_model( return ( deviation_penalty - + smoothness_penalty - + shift_balance_penalty + + shift_smooth_penalty + mix_target_penalty + mix_target_mid_penalty + bunker_penalty + b3_bunker_mix_penalty ) - # Objective: penalize deviations/smoothness, apply route bonuses and penalties. + # Zielfunktion: Abweichungen und Ungleichmäßigkeit bestrafen, Routenanreize anwenden. model.obj = pyo.Objective(rule=objective_rule, sense=pyo.minimize) + # ========================================================================= + # 12 ROUTENVERBOTE + # Fachlich unzulässige Routen hart auf null setzen: + # Reichwalde→Veredlung und Welzow→Boxberg Werk 4. + # ========================================================================= + def forbid_reichwalde_V_rule(m, w, d, s): return m.x["Reichwalde", "V", w, d, s] == 0 - # Constraint: Disallow Reichwalde coal to Veredlung. + # Nebenbedingung: Reichwalde-Kohle an Veredlung verboten. model.forbid_reichwalde_V = pyo.Constraint(model.W, model.D, model.S, rule=forbid_reichwalde_V_rule) def forbid_welzow_B4_rule(m, w, d, s): return m.x["Welzow", "B4", w, d, s] == 0 - # Constraint: Disallow Welzow coal to Boxberg Werk 4. + # Nebenbedingung: Welzow-Kohle an Boxberg Werk 4 verboten. model.forbid_welzow_B4 = pyo.Constraint(model.W, model.D, model.S, rule=forbid_welzow_B4_rule) cap_df = tables["foerderkapaz"] @@ -1017,6 +1133,12 @@ def build_model( "Welzow-Süd": "Welzow", } + # ========================================================================= + # 13 FÖRDER- UND VERLADEKAPAZITÄTEN + # Monatliche Förderobergrenzen je Tagebau, Verladecaps für Boxberg + # und Welzow-Süd auf Schicht- und Tagesebene. + # ========================================================================= + model.F_max_month = pyo.Param(model.I, initialize=1e12, mutable=True) for _, row in cap_month.iterrows(): i = i_map.get(row["tagebau"]) @@ -1029,10 +1151,10 @@ def build_model( model.F_month = pyo.Expression(model.I, model.M, rule=F_month_rule) - # Constraint: Monthly production cap per mine. + # Nebenbedingung: Monatliche Förderobergrenze je Tagebau. model.cap_month = pyo.Constraint(model.I, model.M, rule=lambda m, i, mm: m.F_month[i, mm] <= m.F_max_month[i]) - # Constraint: Joint monthly cap for Reichwalde + Nochten. + # Nebenbedingung: Gemeinsame monatliche Förderobergrenze für Reichwalde und Nochten. model.cap_month_RWNO = pyo.Constraint( model.M, rule=lambda m, mm: m.F_month["Reichwalde", mm] + m.F_month["Nochten", mm] <= 3_000_000.0 ) @@ -1060,7 +1182,7 @@ def build_model( def boxberg_shift_cap_rule(m, w, d, s): return sum(m.x[i, j, w, d, s] for i in BOXBERG_SOURCES if i in m.I for j in BOXBERG_DEST if j in m.J) <= BOXBERG_SHIFT_CAP - # Constraint: Boxberg loading capacity per shift. + # Nebenbedingung: Verladungskapazität Boxberg je Schicht. model.cap_boxberg_shift = pyo.Constraint(model.W, model.D, model.S, rule=boxberg_shift_cap_rule) def boxberg_day_cap_rule(m, w, d): @@ -1073,13 +1195,13 @@ def build_model( for s in m.S ) <= BOXBERG_DAY_CAP - # Constraint: Boxberg loading capacity per day. + # Nebenbedingung: Verladungskapazität Boxberg je Tag. model.cap_boxberg_day = pyo.Constraint(model.W, model.D, rule=boxberg_day_cap_rule) def welzow_shift_cap_rule(m, w, d, s): return sum(m.x[i, j, w, d, s] for i in WELZOW_SOURCES if i in m.I for j in WELZOW_DEST if j in m.J) <= WELZOW_SHIFT_CAP - # Constraint: Welzow loading capacity per shift. + # Nebenbedingung: Verladungskapazität Welzow-Süd je Schicht. model.cap_welzow_shift = pyo.Constraint(model.W, model.D, model.S, rule=welzow_shift_cap_rule) def welzow_day_cap_rule(m, w, d): @@ -1092,9 +1214,15 @@ def build_model( for s in m.S ) <= WELZOW_DAY_CAP - # Constraint: Welzow loading capacity per day. + # Nebenbedingung: Verladungskapazität Welzow-Süd je Tag. model.cap_welzow_day = pyo.Constraint(model.W, model.D, rule=welzow_day_cap_rule) + # ========================================================================= + # 14 VERFÜGBARKEITEN + # Schichtweise dynamische Obergrenzen aus Verfuegbarkeiten.parquet + # für Welzow-Süd und den Boxberg-Verbund (RW + NO). + # ========================================================================= + avail = tables.get("Verfuegbarkeiten") if avail is not None: day_map = {"Sat": "Sa", "Sun": "So", "Mon": "Mo", "Tue": "Di", "Wed": "Mi", "Thu": "Do", "Fri": "Fr"} @@ -1142,7 +1270,7 @@ def build_model( <= v ) - # Constraint: Dynamic availability cap for Welzow (per shift). + # Nebenbedingung: Dynamische Verfügbarkeitsgrenze Welzow-Süd je Schicht. model.cap_welzow_con = pyo.Constraint(model.W, model.D, model.S, rule=cap_welzow_rule) def cap_rw_n_rule(m, w, d, s): @@ -1159,9 +1287,15 @@ def build_model( <= v ) - # Constraint: Dynamic availability cap for Reichwalde + Nochten (per shift). + # Nebenbedingung: Dynamische Verfügbarkeitsgrenze Reichwalde + Nochten je Schicht. model.cap_rw_n_con = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_rule) + # ========================================================================= + # 15 ZUGDURCHLASS + # Streckenbezogene Kapazitätsgrenzen aus zugdurchlass.parquet für + # KLP- und KUP-Routen, inkl. kombinierter Mengenbeschränkungen. + # ========================================================================= + zug = tables.get("zugdurchlass") if zug is not None: df = zug.copy() @@ -1200,31 +1334,31 @@ def build_model( def cap_rw_n_to_j_rule(m, w, d, s): return m.x["Reichwalde", "J", w, d, s] + m.x["Nochten", "J", w, d, s] <= CAP_RW_N_TO_J - # Constraint: KLP capacity from RW+NO to J. + # Nebenbedingung: KLP-Kapazität Reichwalde/Nochten nach Jänschwalde. model.cap_rw_n_to_j = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_to_j_rule) def cap_rw_n_to_sp_rule(m, w, d, s): return m.x["Reichwalde", "SP", w, d, s] + m.x["Nochten", "SP", w, d, s] <= CAP_RW_N_TO_SP - # Constraint: KLP capacity from RW+NO to SP. + # Nebenbedingung: KLP-Kapazität Reichwalde/Nochten nach Schwarze Pumpe. model.cap_rw_n_to_sp = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_to_sp_rule) def cap_rw_n_to_v_rule(m, w, d, s): return m.x["Reichwalde", "V", w, d, s] + m.x["Nochten", "V", w, d, s] <= CAP_RW_N_TO_V - # Constraint: KLP capacity from RW+NO to Veredlung. + # Nebenbedingung: KLP-Kapazität Reichwalde/Nochten zur Veredlung. model.cap_rw_n_to_v = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_to_v_rule) def cap_rw_n_to_bw3_rule(m, w, d, s): return m.x["Reichwalde", "B3", w, d, s] + m.x["Nochten", "B3", w, d, s] <= CAP_RW_N_TO_B3 - # Constraint: KLP capacity from RW+NO to B3. + # Nebenbedingung: KLP-Kapazität Reichwalde/Nochten nach Boxberg Werk 3. model.cap_rw_n_to_bw3 = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_to_bw3_rule) def cap_w_to_j_rule(m, w, d, s): return m.x["Welzow", "J", w, d, s] <= CAP_W_TO_J - # Constraint: KUP capacity from Welzow to J. + # Nebenbedingung: KUP-Kapazität Welzow nach Jänschwalde. model.cap_w_to_j = pyo.Constraint(model.W, model.D, model.S, rule=cap_w_to_j_rule) model.k_w_to_j_steps = pyo.Var(model.W, model.D, model.S, domain=pyo.NonNegativeIntegers) @@ -1232,25 +1366,25 @@ def build_model( def welzow_j_multiple_rule(m, w, d, s): return m.x["Welzow", "J", w, d, s] == (2 * m.step_size_tonnes) * m.k_w_to_j_steps[w, d, s] - # Constraint: Enforce 2kt multiples for Welzow->J flows. + # Nebenbedingung: Welzow→Jänschwalde-Fluss auf 2.000-t-Vielfache beschränken. model.welzow_j_multiple_2kt = pyo.Constraint(model.W, model.D, model.S, rule=welzow_j_multiple_rule) def cap_w_to_sp_rule(m, w, d, s): return m.x["Welzow", "SP", w, d, s] <= CAP_W_TO_SP - # Constraint: KUP capacity from Welzow to SP. + # Nebenbedingung: KUP-Kapazität Welzow nach Schwarze Pumpe. model.cap_w_to_sp = pyo.Constraint(model.W, model.D, model.S, rule=cap_w_to_sp_rule) def cap_w_to_v_rule(m, w, d, s): return m.x["Welzow", "V", w, d, s] <= CAP_W_TO_V - # Constraint: KUP capacity from Welzow to Veredlung. + # Nebenbedingung: KUP-Kapazität Welzow zur Veredlung. model.cap_w_to_v = pyo.Constraint(model.W, model.D, model.S, rule=cap_w_to_v_rule) def cap_w_to_bw3_rule(m, w, d, s): return m.x["Welzow", "B3", w, d, s] <= CAP_W_TO_B3 - # Constraint: KUP capacity from Welzow to B3. + # Nebenbedingung: KUP-Kapazität Welzow nach Boxberg Werk 3. model.cap_w_to_bw3 = pyo.Constraint(model.W, model.D, model.S, rule=cap_w_to_bw3_rule) def cap_rw_n_to_sp_v_rule(m, w, d, s): @@ -1262,7 +1396,7 @@ def build_model( <= CAP_RW_N_TO_SP_V ) - # Constraint: KLP combined capacity to SP + Veredlung. + # Nebenbedingung: KLP-Gesamtkapazität nach Schwarze Pumpe und Veredlung. model.cap_rw_n_to_sp_v = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_to_sp_v_rule) def cap_rw_n_to_j_sp_v_b3_rule(m, w, d, s): @@ -1278,13 +1412,13 @@ def build_model( <= CAP_RW_N_TO_ALL ) - # Constraint: KLP combined capacity to J + SP + V + B3. + # Nebenbedingung: KLP-Gesamtkapazität nach Jänschwalde, SP, Veredlung und B3. model.cap_rw_n_to_j_sp_v_b3 = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_to_j_sp_v_b3_rule) def cap_w_to_sp_v_rule(m, w, d, s): return m.x["Welzow", "SP", w, d, s] + m.x["Welzow", "V", w, d, s] <= CAP_W_TO_SP_V - # Constraint: KUP combined capacity to SP + V. + # Nebenbedingung: KUP-Gesamtkapazität nach Schwarze Pumpe und Veredlung. model.cap_w_to_sp_v = pyo.Constraint(model.W, model.D, model.S, rule=cap_w_to_sp_v_rule) def cap_w_to_sp_v_b3_rule(m, w, d, s): @@ -1295,7 +1429,7 @@ def build_model( <= CAP_W_TO_SP_V_B3 ) - # Constraint: KUP combined capacity to SP + V + B3. + # Nebenbedingung: KUP-Gesamtkapazität nach Schwarze Pumpe, Veredlung und B3. model.cap_w_to_sp_v_b3 = pyo.Constraint(model.W, model.D, model.S, rule=cap_w_to_sp_v_b3_rule) def cap_rw_n_w_to_j_rule(m, w, d, s): @@ -1306,11 +1440,10 @@ def build_model( <= CAP_RW_N_W_TO_J ) - # Constraint: Combined KLP/KUP capacity to J. + # Nebenbedingung: Kombinierte KLP/KUP-Kapazität nach Jänschwalde. model.cap_rw_n_w_to_j = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_w_to_j_rule) kvb_nord = tables.get("zugdurchlass_kvb_nord") - print(kvb_nord) if kvb_nord is not None: day_map = {"Sat": "Sa", "Sun": "So", "Mon": "Mo", "Tue": "Di", "Wed": "Mi", "Thu": "Do", "Fri": "Fr"} schicht_order = ["F", "S", "N"] @@ -1346,7 +1479,7 @@ def build_model( <= v ) - # Constraint: KVB Nord short-term capacity to J (per day/shift if provided). + # Nebenbedingung: KVB-Nord-Kurzfristkapazität nach Jänschwalde je Schicht (sofern vorhanden). model.cap_kvb_nord = pyo.Constraint(model.W, model.D, model.S, rule=cap_kvb_nord_rule) def cap_rw_n_w_to_bw3_rule(m, w, d, s): @@ -1357,7 +1490,7 @@ def build_model( <= CAP_RW_N_W_TO_B3 ) - # Constraint: Combined KLP/KUP capacity to B3. + # Nebenbedingung: Kombinierte KLP/KUP-Kapazität nach Boxberg Werk 3. model.cap_rw_n_w_to_bw3 = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_w_to_bw3_rule) def cap_rw_n_j_and_w_b3_rule(m, w, d, s): @@ -1368,7 +1501,7 @@ def build_model( <= CAP_RW_N_J_AND_W_B3 ) - # Constraint: Combined capacity (KLP->J, KUP->B3). + # Nebenbedingung: Kombinierte Kapazität KLP→Jänschwalde und KUP→Boxberg Werk 3. model.cap_rw_n_j_and_w_b3 = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_j_and_w_b3_rule) def cap_rw_n_to_j_sp_v_and_w_to_b3_rule(m, w, d, s): @@ -1383,7 +1516,7 @@ def build_model( <= CAP_RW_N_JSPV_AND_W_B3 ) - # Constraint: Combined capacity (KLP->J/SP/V, KUP->B3). + # Nebenbedingung: Kombinierte Kapazität KLP→J/SP/Veredlung und KUP→Boxberg Werk 3. model.cap_rw_n_to_j_sp_v_and_w_to_b3 = pyo.Constraint( model.W, model.D, model.S, rule=cap_rw_n_to_j_sp_v_and_w_to_b3_rule ) @@ -1433,10 +1566,6 @@ def solve_model( opt.options["TimeLimit"] = time_limit opt.options["MIPGap"] = mip_gap opt.options["LogFile"] = "gurobi_first_opt_model.log" - elif solver_name == "scip": - # SCIP uses hierarchical parameter names. - opt.options["limits/time"] = time_limit - opt.options["limits/gap"] = mip_gap solve_kwargs = {"tee": True, "symbolic_solver_labels": True} if use_warmstart and hasattr(opt, "warm_start_capable"): try: diff --git a/webapp/backend/main.py b/webapp/backend/main.py index f05ba32..9c442aa 100644 --- a/webapp/backend/main.py +++ b/webapp/backend/main.py @@ -39,7 +39,7 @@ def _get_solver_availability() -> dict[str, bool]: return {"highs": False, "gurobi": False} availability: dict[str, bool] = {} - for solver_name in ("highs", "gurobi", "scip"): + for solver_name in ("highs", "gurobi"): try: solver = pyo.SolverFactory(solver_name) availability[solver_name] = bool(solver) and bool( diff --git a/webapp/frontend/src/App.jsx b/webapp/frontend/src/App.jsx index 405c87e..a388110 100644 --- a/webapp/frontend/src/App.jsx +++ b/webapp/frontend/src/App.jsx @@ -81,44 +81,7 @@ function parseSolverProgress(logText, solverName) { } }; - if (solver === "scip") { - for (const line of reversed) { - if (!metrics.gapPct) { - const m = line.match(/gap[^0-9-]*([0-9]+(?:\.[0-9]+)?)\s*%/i); - if (m) assignIfNumber("gapPct", m[1]); - } - if (!metrics.nodes) { - const m = line.match(/Solving Nodes\s*:\s*([0-9]+)/i); - if (m) assignIfNumber("nodes", m[1]); - } - if (!metrics.bestPrimal) { - const m = line.match(/Primal Bound\s*:\s*([+-]?[0-9.eE]+)/i); - if (m) assignIfNumber("bestPrimal", m[1]); - } - if (!metrics.bestDual) { - const m = line.match(/Dual Bound\s*:\s*([+-]?[0-9.eE]+)/i); - if (m) assignIfNumber("bestDual", m[1]); - } - - // Best-effort parsing of SCIP progress table rows: time|node|...|gap - if ((!metrics.gapPct || !metrics.nodes) && line.includes("|")) { - if (!metrics.nodes) { - const nodeCol = line.match(/^\s*[^|]+\|\s*([0-9]+)\s*\|/); - if (nodeCol) assignIfNumber("nodes", nodeCol[1]); - } - if (!metrics.gapPct) { - const percents = [...line.matchAll(/([0-9]+(?:\.[0-9]+)?)\s*%/g)]; - if (percents.length > 0) { - assignIfNumber("gapPct", percents[percents.length - 1][1]); - } - } - } - - if (metrics.gapPct != null && metrics.nodes != null && metrics.bestPrimal != null) { - break; - } - } - } else if (solver === "gurobi") { + if (solver === "gurobi") { for (const line of reversed) { if (!metrics.gapPct) { const m = line.match(/\bgap\s+([0-9]+(?:\.[0-9]+)?)%/i); @@ -141,7 +104,6 @@ function parseSolverProgress(logText, solverName) { } const statusPatterns = [ - /SCIP Status\s*:\s*(.+)/i, /Model status\s*:\s*(.+)/i, /Status\s*:\s*(.+)/i, ];