{ "cells": [ { "cell_type": "markdown", "id": "5d518102", "metadata": {}, "source": [ "# Rohkohle-Optimierungsmodell (Entwurf)\n", "\n", "Übersichtliche Struktur für Datenaufbereitung und ersten Modellaufbau. Dieses Notebook ist nun durchgehend kommentiert, sodass jede Code- und Markdown-Zelle den jeweiligen Zweck erklärt.\n", "\n", "Die Schritte decken Datenloading, Parametrisierung, Nebenbedingungen, Zielfunktion sowie Ergebnisaufbereitung ab." ] }, { "cell_type": "markdown", "id": "9d194127", "metadata": {}, "source": [ "## Setup\n", "\n", "Imports, Anzeigeoptionen und Laden der vorverarbeiteten Parquet-Tabellen." ] }, { "cell_type": "code", "execution_count": 1, "id": "ace0fee6", "metadata": {}, "outputs": [], "source": [ "# Basisimporte für Datenaufbereitung, Optimierung und Visualisierung\n", "import pandas as pd\n", "import numpy as np\n", "import pyomo.environ as pyo\n", "from pathlib import Path\n", "import matplotlib.pyplot as plt\n", "\n", "pd.set_option('display.max_columns', None) # alle Spalten anzeigen\n", "pd.set_option('display.width', 0) " ] }, { "cell_type": "code", "execution_count": 2, "id": "df0cf11b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dict_keys(['Verfuegbarkeiten', 'bounds_power_plants', 'bunker', 'foerderkapaz', 'kohle_mix', 'rohkohlebedarf', 'veredelung_bounds', 'verladungskap', 'zugdurchlass'])" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Alle verfügbaren Parquet-Tabellen aus dem processed-Verzeichnis laden\n", "tables = {}\n", "\n", "data_dir = Path('../data/processed')\n", "for parquet_file in sorted(data_dir.glob('*.parquet')):\n", " tables[parquet_file.stem] = pd.read_parquet(parquet_file)\n", "\n", "tables.keys()" ] }, { "cell_type": "markdown", "id": "f00d1a9f", "metadata": {}, "source": [ "## Datenaufbereitung\n", "\n", "Bedarf und Bounds aus den geladenen Tabellen vorbereiten und für die Modellparameter formen." ] }, { "cell_type": "code", "execution_count": 3, "id": "a1c72993", "metadata": {}, "outputs": [], "source": [ "bedarf = tables['rohkohlebedarf'][[\n", " 'datum', 'JW', 'SP', 'BW3', 'BW4',\n", " 'Veredel_Nochtener', 'Veredel_Welzower'\n", "]].copy()\n", "\n", "# datetime konvertieren und Wochentag ergänzen\n", "bedarf['datum'] = pd.to_datetime(bedarf['datum'])\n", "bedarf['weekday'] = bedarf['datum'].dt.weekday\n", "\n", "bounds_power_plants = tables['bounds_power_plants']" ] }, { "cell_type": "code", "execution_count": 4, "id": "ad9461a9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " kraftwerk zeitraum minus_pct plus_pct minus plus\n", "0 Jänschwalde (JW) pro Tag -0.25 0.25 -20000.0 20000.0\n", "1 Jänschwalde (JW) pro Woche NaN NaN -2000.0 2000.0\n", "2 Jänschwalde (JW) pro Monat NaN NaN -2000.0 2000.0\n", "3 Schwarze Pumpe (SP) pro Tag -0.10 0.10 -5500.0 5500.0\n", "4 Schwarze Pumpe (SP) pro Woche NaN NaN -2000.0 2000.0\n", "5 Schwarze Pumpe (SP) pro Monat NaN NaN -2000.0 2000.0\n", "6 Boxberg Werk 3 (BW3) pro Tag -0.05 0.05 -1000.0 1000.0\n", "7 Boxberg Werk 3 (BW3) pro Woche NaN NaN -1000.0 1000.0\n", "8 Boxberg Werk 3 (BW3) pro Monat NaN NaN -500.0 500.0\n", "9 Boxberg Werk 4 (BW4) pro Tag -0.05 0.05 -1000.0 1000.0\n", "10 Boxberg Werk 4 (BW4) pro Woche NaN NaN -500.0 500.0\n", "11 Boxberg Werk 4 (BW4) pro Monat NaN NaN -500.0 500.0\n", "12 Gesamt (JW+SP+BW3+BW4) pro Woche NaN NaN -2000.0 2000.0\n", "13 Gesamt (JW+SP+BW3+BW4) pro Monat NaN NaN -500.0 500.0\n" ] } ], "source": [ "# Kontrolle der Bound-Tabelle, um Spaltennamen und Kohlesorten zu prüfen\n", "print(tables['bounds_power_plants'])" ] }, { "cell_type": "markdown", "id": "438032e6", "metadata": {}, "source": [ "### Kalender- und Bound-Vorbereitung\n", "\n", "Kalenderwoche, Modell-Tags sowie Tagesgrenzen aus den Bounds ableiten. Der Abschnitt erklärt, welche Hilfsfunktionen Bounds aus den Tabellen ziehen." ] }, { "cell_type": "code", "execution_count": 5, "id": "0469f70c", "metadata": {}, "outputs": [], "source": [ "# Kalender- und Toleranzfunktionen vorbereiten\n", "# Kalenderwoche aus dem Datum ableiten\n", "bedarf['week'] = bedarf['datum'].dt.isocalendar().week\n", "\n", "# Wochentagscode (0=Mo, ..., 6=So) auf Modell-Tags abbilden\n", "weekday_map = {5: 'Sa', 6: 'So', 0: 'Mo', 1: 'Di', 2: 'Mi', 3: 'Do', 4: 'Fr'}\n", "bedarf['day'] = bedarf['weekday'].map(weekday_map)\n", "\n", "\n", "# Mapping Woche/Tag -> Datum für spätere Ergebnis-Tabellen\n", "wd_to_date = (\n", " bedarf.sort_values(\"datum\")\n", " .drop_duplicates(subset=[\"week\", \"day\"])\n", " .set_index([\"week\", \"day\"])[\"datum\"]\n", " .to_dict()\n", ")\n", "# Nur Tagesgrenzen verwenden\n", "bounds_day = bounds_power_plants[bounds_power_plants['zeitraum'] == 'pro Tag'].copy()\n", "\n", "# Passende Bounds-Zeile für ein Kraftwerk finden\n", "def match_kraftwerk_row(df: pd.DataFrame, j_key: str) -> pd.Series:\n", " if j_key == 'J':\n", " mask = df['kraftwerk'].str.contains('JW')\n", " elif j_key == 'SP':\n", " mask = df['kraftwerk'].str.contains('SP')\n", " elif j_key == 'B3':\n", " mask = df['kraftwerk'].str.contains('BW3')\n", " elif j_key == 'B4':\n", " mask = df['kraftwerk'].str.contains('BW4')\n", " else:\n", " mask = np.zeros(len(df), dtype=bool)\n", "\n", " row = df.loc[mask]\n", " if row.empty:\n", " raise ValueError(f'Keine Bounds Zeile für Kraftwerk {j_key} gefunden')\n", " return row.iloc[0]" ] }, { "cell_type": "markdown", "id": "5f72f183", "metadata": {}, "source": [ "## Modellaufbau\n", "\n", "Grundlegende Mengen und Sets definieren, bevor Parameter und Variablen gefüllt werden." ] }, { "cell_type": "code", "execution_count": 6, "id": "9c0f1d1a", "metadata": {}, "outputs": [], "source": [ "model = pyo.ConcreteModel()\n", "\n", "# Menge der Verbraucher im Modell (inkl. Veredlung)\n", "J_POWER = ['J', 'SP', 'B3', 'B4']\n", "model.J = pyo.Set(initialize=J_POWER + ['V'])\n", "\n", "# Wochen aus den Daten ableiten\n", "weeks = sorted(bedarf['week'].unique().tolist())\n", "model.W = pyo.Set(initialize=weeks)\n", "\n", "# Feste Mengen für Tage, Schichten und Quellen\n", "model.D = pyo.Set(initialize=['Sa', 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr'])\n", "model.S = pyo.Set(initialize=['F', 'S', 'N'])\n", "model.I = pyo.Set(initialize=['Reichwalde', 'Nochten', 'Welzow'])\n", "\n", "# Kohlesorten-Quellen (für Veredlung)\n", "I_W = ['Welzow'] # Welzow-Kohle\n", "I_N = ['Nochten'] # Nochtener-Kohle\n" ] }, { "cell_type": "markdown", "id": "d8d613dc", "metadata": {}, "source": [ "## Parameterdefinition\n", "\n", "Bedarfe, Veredlungsanteile und Tagesabweichungsgrenzen. Alle Parameter werden als `mutable` angelegt, damit sie aus den Daten überschrieben werden können." ] }, { "cell_type": "code", "execution_count": 7, "id": "6680154b", "metadata": {}, "outputs": [], "source": [ "# Tagesbedarf der Kraftwerke d_{j,w,d}\n", "model.d = pyo.Param(model.J, model.W, model.D, initialize=0.0, mutable=True, within=pyo.NonNegativeReals)\n", "\n", "# Veredlung: Bedarfsparameter nach Kohlesorte\n", "model.dV_N = pyo.Param(model.W, model.D, initialize=0.0, mutable=True)\n", "model.dV_W = pyo.Param(model.W, model.D, initialize=0.0, mutable=True)\n", "\n", "# Tagesabhängige Abweichungsgrenzen\n", "model.a_min_day = pyo.Param(model.J, model.W, model.D, initialize=0.0, mutable=True)\n", "model.a_max_day = pyo.Param(model.J, model.W, model.D, initialize=0.0, mutable=True)" ] }, { "cell_type": "markdown", "id": "b01f3f37", "metadata": {}, "source": [ "### Parameterbefüllung aus den Daten\n", "\n", "Bedarfs- und Abweichungswerte je Woche/Tag in das Modell schreiben. Die Kommentare in den folgenden Zellen erläutern jeden Zwischenschritt." ] }, { "cell_type": "code", "execution_count": 8, "id": "7d550976", "metadata": {}, "outputs": [], "source": [ "# Kraftwerk-Bounds (ohne Veredlung)\n", "column_map = {'JW': 'J', 'SP': 'SP', 'BW3': 'B3', 'BW4': 'B4'}\n", "bounds_by_j = {j: match_kraftwerk_row(bounds_day, j) for j in model.J if j != 'V'}\n", "\n", "# Veredlung-Bounds laden\n", "ver_bounds = tables['veredelung_bounds']\n", "v_day = ver_bounds[ver_bounds['zeitraum'] == 'pro Tag']\n", "v_week = ver_bounds[ver_bounds['zeitraum'] == 'pro Woche']\n", "v_month = ver_bounds[ver_bounds['zeitraum'] == 'pro Monat']\n", "\n", "def match_ver_row(df, label_exact):\n", " row = df[df['kohlesorte'] == label_exact]\n", " if row.empty:\n", " raise ValueError(f'Keine Veredlung-Bounds für {label_exact}')\n", " return row.iloc[0]\n", "\n", "# Vorauswahl der Bounds-Zeilen nach Kohlesorte\n", "ver_row_day_N = match_ver_row(v_day, 'Nochtener-Kohle')\n", "ver_row_day_W = match_ver_row(v_day, 'Welzower-Kohle')\n", "ver_row_day_ALL = match_ver_row(v_day, 'Gesamt (NO+WZ)')\n", "\n", "ver_row_week_N = match_ver_row(v_week, 'Nochtener-Kohle')\n", "ver_row_week_W = match_ver_row(v_week, 'Welzower-Kohle')\n", "ver_row_week_ALL = match_ver_row(v_week, 'Gesamt (NO+WZ)')\n", "\n", "ver_row_month_N = match_ver_row(v_month, 'Nochtener-Kohle')\n", "ver_row_month_W = match_ver_row(v_month, 'Welzower-Kohle')\n", "ver_row_month_ALL = match_ver_row(v_month, 'Gesamt (NO+WZ)')\n", "\n", "for _, row in bedarf.iterrows():\n", " w = int(row['week'])\n", " d = row['day']\n", "\n", " # Veredlungsbedarf nach Kohlesorte — prüfen auf NaN\n", " if (w in model.W) and (d in model.D):\n", " vN = row.get('Veredel_Nochtener', np.nan)\n", " if not pd.isna(vN):\n", " model.dV_N[w, d] = float(vN)\n", " vW = row.get('Veredel_Welzower', np.nan)\n", " if not pd.isna(vW):\n", " model.dV_W[w, d] = float(vW)\n", "\n", " # Kraftwerksbedarfe und Toleranzen\n", " for col, j in column_map.items():\n", " if not ((j in model.J) and (w in model.W) and (d in model.D)):\n", " continue\n", "\n", " val = row.get(col, np.nan)\n", " if pd.isna(val):\n", " continue\n", "\n", " d_nom = float(val)\n", " model.d[j, w, d] = d_nom\n", "\n", " b = bounds_by_j[j]\n", " lower_candidates = []\n", " upper_candidates = []\n", "\n", " minus_abs = b.get('minus')\n", " plus_abs = b.get('plus')\n", " minus_pct = b.get('minus_pct')\n", " plus_pct = b.get('plus_pct')\n", "\n", " if not pd.isna(minus_abs):\n", " lower_candidates.append(float(minus_abs))\n", " if not pd.isna(plus_abs):\n", " upper_candidates.append(float(plus_abs))\n", " if not pd.isna(minus_pct):\n", " lower_candidates.append(float(minus_pct) * d_nom)\n", " if not pd.isna(plus_pct):\n", " upper_candidates.append(float(plus_pct) * d_nom)\n", "\n", " if not lower_candidates or not upper_candidates:\n", " raise ValueError(f'Keine gültigen Toleranzen für {j}, Woche {w}, Tag {d}')\n", "\n", " a_min = max(lower_candidates)\n", " a_max = min(upper_candidates)\n", "\n", " model.a_min_day[j, w, d] = a_min\n", " model.a_max_day[j, w, d] = a_max\n" ] }, { "cell_type": "markdown", "id": "881f5ec0", "metadata": {}, "source": [ "## Variablen und abgeleitete Größen\n", "\n", "Transportentscheidungen als Integer-Variablen sowie Expressions für die abgeleiteten Liefermengen." ] }, { "cell_type": "code", "execution_count": 9, "id": "0a4884e8", "metadata": {}, "outputs": [], "source": [ "# Integer-Transportvariable k in 1000 t; tatsächliche Tonnage x = 1000 * k\n", "model.k = pyo.Var(model.I, model.J, model.W, model.D, model.S, domain=pyo.NonNegativeIntegers)\n", "\n", "def x_rule(model, i, j, w, d, s):\n", " return 1000 * model.k[i, j, w, d, s]\n", "\n", "# x als Expression statt eigene Entscheidungsvariable\n", "model.x = pyo.Expression(model.I, model.J, model.W, model.D, model.S, rule=x_rule)" ] }, { "cell_type": "code", "execution_count": 10, "id": "cb8a05e4", "metadata": {}, "outputs": [], "source": [ "# Lieferung je Kraftwerk/Woche/Tag/Schicht\n", "def y_shift_rule(model, j, w, d, s):\n", " return sum(model.x[i, j, w, d, s] for i in model.I)\n", "\n", "model.y_shift = pyo.Expression(model.J, model.W, model.D, model.S, rule=y_shift_rule)\n" ] }, { "cell_type": "markdown", "id": "d9556e33", "metadata": {}, "source": [ "## Tageslieferung und Abweichungen\n", "\n", "Berechnung der Tageslieferungen pro Kraftwerk inklusive Toleranz-Constraints auf Tagesebene." ] }, { "cell_type": "code", "execution_count": 11, "id": "5de353dc", "metadata": {}, "outputs": [], "source": [ "# Tageslieferung je Kraftwerk: Summe über alle Quellen und Schichten\n", "def y_rule(model, j, w, d):\n", " return sum(model.x[i, j, w, d, s] for i in model.I for s in model.S)\n", "\n", "model.y = pyo.Expression(model.J, model.W, model.D, rule=y_rule)\n", "\n", "# Lieferung muss innerhalb der zulässigen Toleranz um die Nachfrage liegen (nur Kraftwerke)\n", "J_delivery = [j for j in model.J if j != 'V']\n", "\n", "def delivery_tolerance_rule(model, j, w, d):\n", " return (\n", " model.d[j, w, d] + model.a_min_day[j, w, d], # Untergrenze\n", " model.y[j, w, d], # Lieferung\n", " model.d[j, w, d] + model.a_max_day[j, w, d], # Obergrenze\n", " )\n", "\n", "model.delivery_tolerance = pyo.Constraint(J_delivery, model.W, model.D, rule=delivery_tolerance_rule)\n" ] }, { "cell_type": "markdown", "id": "59d6fd9f", "metadata": {}, "source": [ "## Wochen-/Monatslieferung und Abweichung\n", "\n", "Aggregation der Tageswerte auf Wochen- und Monatsebene inklusive ihrer Toleranzrestriktionen." ] }, { "cell_type": "code", "execution_count": 12, "id": "3166c81d", "metadata": {}, "outputs": [], "source": [ "# --- Sets & Mapping Woche -> Monat ---\n", "week_to_month = (\n", " bedarf.groupby('week')['datum']\n", " .min()\n", " .dt.month\n", " .to_dict()\n", ")\n", "model.M = pyo.Set(initialize=sorted(set(week_to_month.values())))\n", "J_power = [j for j in model.J if j != 'V']\n", "\n", "# --- Helper: Bounds-Zeile für Zeitraum + Kraftwerk holen ---\n", "def match_bounds_row(df: pd.DataFrame, j_key: str, zeitraum: str) -> pd.Series:\n", " label_map = {\n", " 'J': 'Jänschwalde',\n", " 'SP': 'Schwarze Pumpe',\n", " 'B3': 'Boxberg Werk 3',\n", " 'B4': 'Boxberg Werk 4',\n", " 'ALL': 'Gesamt',\n", " }\n", " label = label_map[j_key]\n", " row = df[(df['zeitraum'] == zeitraum) & df['kraftwerk'].str.contains(label)]\n", " if row.empty:\n", " raise ValueError(f'Keine Bounds-Zeile für {j_key} ({zeitraum})')\n", " return row.iloc[0]\n", "\n", "STEP = 1000 # 1000-t-Schritt durch k\n", "MIN_STEPS = 1 # mindestens ein Schritt erlauben (bei ALL evtl. 2)\n", "\n", "def tol_from_row(row, d_nom, step=STEP, min_steps=MIN_STEPS):\n", " lowers, uppers = [], []\n", " if not pd.isna(row.get('minus')):\n", " lowers.append(float(row['minus']))\n", " if not pd.isna(row.get('plus')):\n", " uppers.append(float(row['plus']))\n", " if not pd.isna(row.get('minus_pct')):\n", " lowers.append(float(row['minus_pct']) * d_nom)\n", " if not pd.isna(row.get('plus_pct')):\n", " uppers.append(float(row['plus_pct']) * d_nom)\n", " if not lowers or not uppers:\n", " raise ValueError('Toleranz nicht definiert')\n", " \n", " a_min = max(lowers)\n", " a_max = min(uppers)\n", " # mindestens ±step*min_steps zulassen, damit 1000er-Rundung reinpasst\n", " a_min = min(a_min, -min_steps * step)\n", " a_max = max(a_max, min_steps * step)\n", " return a_min, a_max\n", "\n", "# --- Aggregierte Bedarfe & Toleranzen (mutable) ---\n", "model.d_week = pyo.Param(model.J, model.W, initialize=0.0, mutable=True)\n", "model.d_month = pyo.Param(model.J, model.M, initialize=0.0, mutable=True)\n", "\n", "model.a_min_week = pyo.Param(model.J, model.W, initialize=0.0, mutable=True)\n", "model.a_max_week = pyo.Param(model.J, model.W, initialize=0.0, mutable=True)\n", "model.a_min_month = pyo.Param(model.J, model.M, initialize=0.0, mutable=True)\n", "model.a_max_month = pyo.Param(model.J, model.M, initialize=0.0, mutable=True)\n", "\n", "# Gesamtbedarf/Toleranz (Summe über Kraftwerke)\n", "model.d_week_total = pyo.Param(model.W, initialize=0.0, mutable=True)\n", "model.d_month_total = pyo.Param(model.M, initialize=0.0, mutable=True)\n", "model.a_min_week_total = pyo.Param(model.W, initialize=0.0, mutable=True)\n", "model.a_max_week_total = pyo.Param(model.W, initialize=0.0, mutable=True)\n", "model.a_min_month_total = pyo.Param(model.M, initialize=0.0, mutable=True)\n", "model.a_max_month_total = pyo.Param(model.M, initialize=0.0, mutable=True)\n", "\n", "# --- Aggregierte Lieferungen ---\n", "def y_week_rule(model, j, w):\n", " return sum(model.y[j, w, d] for d in model.D)\n", "model.y_week = pyo.Expression(model.J, model.W, rule=y_week_rule)\n", "\n", "def y_month_rule(model, j, m):\n", " weeks_in_m = [w for w, mm in week_to_month.items() if mm == m and w in model.W]\n", " return sum(model.y_week[j, w] for w in weeks_in_m)\n", "model.y_month = pyo.Expression(model.J, model.M, rule=y_month_rule)\n", "\n", "model.y_week_total = pyo.Expression(model.W, rule=lambda m, w: sum(m.y_week[j, w] for j in J_power))\n", "model.y_month_total = pyo.Expression(model.M, rule=lambda m, mm: sum(m.y_month[j, mm] for j in J_power))\n", "\n", "# --- Befüllen von Wochen/Monat-Bedarfen und Toleranzen ---\n", "bounds_week = bounds_power_plants[bounds_power_plants['zeitraum'] == 'pro Woche']\n", "bounds_month = bounds_power_plants[bounds_power_plants['zeitraum'] == 'pro Monat']\n", "\n", "for j in J_power:\n", " for w in model.W:\n", " d_w = sum(model.d[j, w, d] for d in model.D)\n", " model.d_week[j, w] = d_w\n", " row = match_bounds_row(bounds_week, j, 'pro Woche')\n", " a_min, a_max = tol_from_row(row, d_w)\n", " model.a_min_week[j, w] = a_min\n", " model.a_max_week[j, w] = a_max\n", "\n", "for j in J_power:\n", " for m in model.M:\n", " weeks_in_m = [w for w, mm in week_to_month.items() if mm == m and w in model.W]\n", " d_m = sum(model.d_week[j, w] for w in weeks_in_m)\n", " model.d_month[j, m] = d_m\n", " row = match_bounds_row(bounds_month, j, 'pro Monat')\n", " a_min, a_max = tol_from_row(row, d_m)\n", " model.a_min_month[j, m] = a_min\n", " model.a_max_month[j, m] = a_max\n", "\n", "# Gesamtzeilen aus der Tabelle\n", "row_week_total = match_bounds_row(bounds_week, 'ALL', 'pro Woche')\n", "row_month_total = match_bounds_row(bounds_month, 'ALL', 'pro Monat')\n", "for w in model.W:\n", " d_tot = sum(model.d_week[j, w] for j in J_power)\n", " model.d_week_total[w] = d_tot\n", " a_min, a_max = tol_from_row(row_week_total, d_tot)\n", " model.a_min_week_total[w] = a_min\n", " model.a_max_week_total[w] = a_max\n", "\n", "for m in model.M:\n", " weeks_in_m = [w for w, mm in week_to_month.items() if mm == m and w in model.W]\n", " d_tot = sum(model.d_month[j, m] for j in J_power)\n", " model.d_month_total[m] = d_tot\n", " a_min, a_max = tol_from_row(row_month_total, d_tot)\n", " model.a_min_month_total[m] = a_min\n", " model.a_max_month_total[m] = a_max\n", "\n", "# --- Nebenbedingungen: Wochen- / Monats-Toleranz (nur Kraftwerke) ---\n", "def week_tolerance_rule(model, j, w):\n", " return (model.d_week[j, w] + model.a_min_week[j, w],\n", " model.y_week[j, w],\n", " model.d_week[j, w] + model.a_max_week[j, w])\n", "model.week_tolerance = pyo.Constraint(J_power, model.W, rule=week_tolerance_rule)\n", "\n", "def month_tolerance_rule(model, j, m):\n", " return (model.d_month[j, m] + model.a_min_month[j, m],\n", " model.y_month[j, m],\n", " model.d_month[j, m] + model.a_max_month[j, m])\n", "model.month_tolerance = pyo.Constraint(J_power, model.M, rule=month_tolerance_rule)\n", "\n", "def week_total_rule(model, w):\n", " return (model.d_week_total[w] + model.a_min_week_total[w],\n", " model.y_week_total[w],\n", " model.d_week_total[w] + model.a_max_week_total[w])\n", "model.week_total_tolerance = pyo.Constraint(model.W, rule=week_total_rule)\n", "\n", "def month_total_rule(model, m):\n", " return (model.d_month_total[m] + model.a_min_month_total[m],\n", " model.y_month_total[m],\n", " model.d_month_total[m] + model.a_max_month_total[m])\n", "model.month_total_tolerance = pyo.Constraint(model.M, rule=month_total_rule)\n" ] }, { "cell_type": "code", "execution_count": 13, "id": "abd9b467", "metadata": {}, "outputs": [], "source": [ "# Veredlung: Toleranzen Tag/Woche/Monat und Nebenbedingungen\n", "# Veredlung-Toleranz-Parameter\n", "model.a_min_V_day = pyo.Param(['N','W','ALL'], model.W, model.D, initialize=0.0, mutable=True)\n", "model.a_max_V_day = pyo.Param(['N','W','ALL'], model.W, model.D, initialize=0.0, mutable=True)\n", "model.a_min_V_week = pyo.Param(['N','W','ALL'], model.W, initialize=0.0, mutable=True)\n", "model.a_max_V_week = pyo.Param(['N','W','ALL'], model.W, initialize=0.0, mutable=True)\n", "model.a_min_V_month = pyo.Param(['N','W','ALL'], model.M, initialize=0.0, mutable=True)\n", "model.a_max_V_month = pyo.Param(['N','W','ALL'], model.M, initialize=0.0, mutable=True)\n", "\n", "# Tages-Toleranzen Veredlung\n", "for w in model.W:\n", " for d in model.D:\n", " dN = model.dV_N[w, d]\n", " dW = model.dV_W[w, d]\n", " dA = dN + dW\n", " model.a_min_V_day['N', w, d], model.a_max_V_day['N', w, d] = tol_from_row(ver_row_day_N, dN)\n", " model.a_min_V_day['W', w, d], model.a_max_V_day['W', w, d] = tol_from_row(ver_row_day_W, dW)\n", " model.a_min_V_day['ALL', w, d], model.a_max_V_day['ALL', w, d] = tol_from_row(ver_row_day_ALL, dA)\n", "\n", "# Wochen-Toleranzen Veredlung\n", "for w in model.W:\n", " dN = sum(model.dV_N[w, dd] for dd in model.D)\n", " dW = sum(model.dV_W[w, dd] for dd in model.D)\n", " dA = dN + dW\n", " model.a_min_V_week['N', w], model.a_max_V_week['N', w] = tol_from_row(ver_row_week_N, dN)\n", " model.a_min_V_week['W', w], model.a_max_V_week['W', w] = tol_from_row(ver_row_week_W, dW)\n", " model.a_min_V_week['ALL', w], model.a_max_V_week['ALL', w] = tol_from_row(ver_row_week_ALL, dA)\n", "\n", "# Monats-Toleranzen Veredlung\n", "for m in model.M:\n", " weeks_in_m = [w for w, mm in week_to_month.items() if mm == m and w in model.W]\n", " dN = sum(model.dV_N[w, dd] for w in weeks_in_m for dd in model.D)\n", " dW = sum(model.dV_W[w, dd] for w in weeks_in_m for dd in model.D)\n", " dA = dN + dW\n", " model.a_min_V_month['N', m], model.a_max_V_month['N', m] = tol_from_row(ver_row_month_N, dN)\n", " model.a_min_V_month['W', m], model.a_max_V_month['W', m] = tol_from_row(ver_row_month_W, dW)\n", " model.a_min_V_month['ALL', m], model.a_max_V_month['ALL', m] = tol_from_row(ver_row_month_ALL, dA)\n", "\n", "# Veredlung-Constraints (Summe über Schichten)\n", "def v_w_day_rule(m, w, d):\n", " return (m.dV_W[w, d] + m.a_min_V_day['W', w, d],\n", " sum(m.x[i, 'V', w, d, s] for i in I_W for s in m.S),\n", " m.dV_W[w, d] + m.a_max_V_day['W', w, d])\n", "model.v_w_day_tol = pyo.Constraint(model.W, model.D, rule=v_w_day_rule)\n", "\n", "def v_n_day_rule(m, w, d):\n", " return (m.dV_N[w, d] + m.a_min_V_day['N', w, d],\n", " sum(m.x[i, 'V', w, d, s] for i in I_N for s in m.S),\n", " m.dV_N[w, d] + m.a_max_V_day['N', w, d])\n", "model.v_n_day_tol = pyo.Constraint(model.W, model.D, rule=v_n_day_rule)\n", "\n", "def v_all_day_rule(m, w, d):\n", " return (m.dV_W[w, d] + m.dV_N[w, d] + m.a_min_V_day['ALL', w, d],\n", " sum(m.x[i, 'V', w, d, s] for i in I_W + I_N for s in m.S),\n", " m.dV_W[w, d] + m.dV_N[w, d] + m.a_max_V_day['ALL', w, d])\n", "model.v_all_day_tol = pyo.Constraint(model.W, model.D, rule=v_all_day_rule)\n", "\n", "# Woche\n", "def v_w_week_rule(m, w):\n", " return (sum(m.dV_W[w, d] for d in m.D) + m.a_min_V_week['W', w],\n", " sum(m.x[i, 'V', w, d, s] for i in I_W for d in m.D for s in m.S),\n", " sum(m.dV_W[w, d] for d in m.D) + m.a_max_V_week['W', w])\n", "model.v_w_week_tol = pyo.Constraint(model.W, rule=v_w_week_rule)\n", "\n", "def v_n_week_rule(m, w):\n", " return (sum(m.dV_N[w, d] for d in m.D) + m.a_min_V_week['N', w],\n", " sum(m.x[i, 'V', w, d, s] for i in I_N for d in m.D for s in m.S),\n", " sum(m.dV_N[w, d] for d in m.D) + m.a_max_V_week['N', w])\n", "model.v_n_week_tol = pyo.Constraint(model.W, rule=v_n_week_rule)\n", "\n", "def v_all_week_rule(m, w):\n", " return (sum(m.dV_W[w, d] + m.dV_N[w, d] for d in m.D) + m.a_min_V_week['ALL', w],\n", " sum(m.x[i, 'V', w, d, s] for i in I_W + I_N for d in m.D for s in m.S),\n", " sum(m.dV_W[w, d] + m.dV_N[w, d] for d in m.D) + m.a_max_V_week['ALL', w])\n", "model.v_all_week_tol = pyo.Constraint(model.W, rule=v_all_week_rule)\n", "\n", "# Monat\n", "def v_w_month_rule(m, mm):\n", " weeks_in_m = [w for w, month in week_to_month.items() if month == mm and w in m.W]\n", " return (sum(m.dV_W[w, d] for w in weeks_in_m for d in m.D) + m.a_min_V_month['W', mm],\n", " sum(m.x[i, 'V', w, d, s] for i in I_W for w in weeks_in_m for d in m.D for s in m.S),\n", " sum(m.dV_W[w, d] for w in weeks_in_m for d in m.D) + m.a_max_V_month['W', mm])\n", "model.v_w_month_tol = pyo.Constraint(model.M, rule=v_w_month_rule)\n", "\n", "def v_n_month_rule(m, mm):\n", " weeks_in_m = [w for w, month in week_to_month.items() if month == mm and w in m.W]\n", " return (sum(m.dV_N[w, d] for w in weeks_in_m for d in m.D) + m.a_min_V_month['N', mm],\n", " sum(m.x[i, 'V', w, d, s] for i in I_N for w in weeks_in_m for d in m.D for s in m.S),\n", " sum(m.dV_N[w, d] for w in weeks_in_m for d in m.D) + m.a_max_V_month['N', mm])\n", "model.v_n_month_tol = pyo.Constraint(model.M, rule=v_n_month_rule)\n", "\n", "def v_all_month_rule(m, mm):\n", " weeks_in_m = [w for w, month in week_to_month.items() if month == mm and w in m.W]\n", " return (sum(m.dV_W[w, d] + m.dV_N[w, d] for w in weeks_in_m for d in m.D) + m.a_min_V_month['ALL', mm],\n", " sum(m.x[i, 'V', w, d, s] for i in I_W + I_N for w in weeks_in_m for d in m.D for s in m.S),\n", " 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])\n", "model.v_all_month_tol = pyo.Constraint(model.M, rule=v_all_month_rule)\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02de4280", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "ad36ed63", "metadata": {}, "source": [ "## Kohlensortenmischverhältnis\n", "\n", "Eingelesene Mischungsverhältnisse pro Kraftwerk kontrollieren und ins Modell abbilden." ] }, { "cell_type": "code", "execution_count": 14, "id": "0b9070a8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " kraftwerk kohlesorte ziel_low ziel_high maximal minimal\n", "0 Jänschwalde Reichwalder-Kohle 0.15 0.30 0.50 0.0\n", "1 Jänschwalde Nochtener-Kohle 0.00 0.00 0.15 0.0\n", "2 Jänschwalde Welzower-Kohle 0.70 1.00 1.00 0.0\n", "3 Schwarze Pumpe Reichwalder-Kohle 0.00 0.30 0.40 0.0\n", "4 Schwarze Pumpe Nochtener-Kohle 0.50 1.00 1.00 0.0\n", "5 Schwarze Pumpe Welzower-Kohle 0.00 0.50 1.00 0.0\n", "6 Boxberg Werk 3 Reichwalder-Kohle 0.35 0.35 0.35 0.0\n", "7 Boxberg Werk 3 Nochtener-Kohle 0.65 0.65 0.65 0.0\n", "8 Boxberg Werk 3 Welzower-Kohle 0.00 0.00 1.00 0.0\n", "9 Boxberg Werk 4 Reichwalder-Kohle 0.30 0.70 0.80 0.0\n", "10 Boxberg Werk 4 Nochtener-Kohle 0.30 0.70 1.00 0.2\n", "11 Boxberg Werk 4 Welzower-Kohle 0.00 0.00 0.00 0.0\n" ] } ], "source": [ "# Mischungsverhältnisse je Kohlesorte pro Kraftwerk prüfen\n", "print(tables[\"kohle_mix\"])" ] }, { "cell_type": "markdown", "id": "520acdb9", "metadata": {}, "source": [ "### Harte Grenze für die Mischverhältnisse" ] }, { "cell_type": "code", "execution_count": 15, "id": "44597b29", "metadata": {}, "outputs": [], "source": [ "# Mapping und Setzen der Mischungsparameter im Modell\n", "# Mapping Kraftwerk-Label -> J-Key\n", "j_map = {\n", " 'Jänschwalde': 'J',\n", " 'Schwarze Pumpe': 'SP',\n", " 'Boxberg Werk 3': 'B3',\n", " 'Boxberg Werk 4': 'B4',\n", "}\n", "\n", "# Mapping Kohlesorte -> Quelle i\n", "i_map = {\n", " 'Reichwalder-Kohle': 'Reichwalde',\n", " 'Nochtener-Kohle': 'Nochten',\n", " 'Welzower-Kohle': 'Welzow',\n", "}\n", "\n", "mix_df = tables[\"kohle_mix\"]\n", "\n", "# α-Parameter (als Bruchteil, nicht Prozent)\n", "model.alpha_min = pyo.Param(model.I, model.J, initialize=0.0, mutable=True)\n", "model.alpha_max = pyo.Param(model.I, model.J, initialize=1.0, mutable=True)\n", "\n", "for _, row in mix_df.iterrows():\n", " j = j_map.get(row['kraftwerk'])\n", " i = i_map.get(row['kohlesorte'])\n", " if (i in model.I) and (j in model.J):\n", " model.alpha_min[i, j] = float(row['minimal']) # z.B. 0.15\n", " model.alpha_max[i, j] = float(row['maximal']) # z.B. 0.50\n", "\n", "# Tageslieferung Xi,j,w,d (Summe über Schichten)\n", "def x_day_rule(m, i, j, w, d):\n", " return sum(m.x[i, j, w, d, s] for s in m.S)\n", "model.x_day = pyo.Expression(model.I, model.J, model.W, model.D, rule=x_day_rule)\n", "\n", "# Tagesgesamte Yj,w,d (Summe über Quellen)\n", "def y_day_rule(m, j, w, d):\n", " return sum(m.x_day[i, j, w, d] for i in m.I)\n", "model.y_day = pyo.Expression(model.J, model.W, model.D, rule=y_day_rule)\n", "\n", "# Untergrenze: alpha_min * Y <= X\n", "def mix_lower_rule(m, i, j, w, d):\n", " return m.alpha_min[i, j] * m.y_day[j, w, d] <= m.x_day[i, j, w, d]\n", "\n", "# Obergrenze: X <= alpha_max * Y\n", "def mix_upper_rule(m, i, j, w, d):\n", " return m.x_day[i, j, w, d] <= m.alpha_max[i, j] * m.y_day[j, w, d]\n", "\n", "model.mix_lower = pyo.Constraint(model.I, model.J, model.W, model.D, rule=mix_lower_rule)\n", "model.mix_upper = pyo.Constraint(model.I, model.J, model.W, model.D, rule=mix_upper_rule)\n" ] }, { "cell_type": "markdown", "id": "382358f0", "metadata": {}, "source": [ "### Zielbereich für das Kohlesortenmischverhältnis" ] }, { "cell_type": "code", "execution_count": 16, "id": "b3cc2d07", "metadata": {}, "outputs": [], "source": [ "# Mapping und Setzen der Mischungsparameter im Modell\n", "# Mapping Kraftwerk-Label -> J-Key\n", "j_map = {\n", " 'Jänschwalde': 'J',\n", " 'Schwarze Pumpe': 'SP',\n", " 'Boxberg Werk 3': 'B3',\n", " 'Boxberg Werk 4': 'B4',\n", "}\n", "\n", "# Mapping Kohlesorte -> Quelle i\n", "i_map = {\n", " 'Reichwalder-Kohle': 'Reichwalde',\n", " 'Nochtener-Kohle': 'Nochten',\n", " 'Welzower-Kohle': 'Welzow',\n", "}\n", "\n", "mix_df = tables[\"kohle_mix\"]\n", "\n", "# Zielband-Parameter (als Bruchteil, nicht Prozent)\n", "model.alpha_target_low = pyo.Param(model.I, model.J, initialize=0.0, mutable=True)\n", "model.alpha_target_high = pyo.Param(model.I, model.J, initialize=1.0, mutable=True)\n", "\n", "for _, row in mix_df.iterrows():\n", " j = j_map.get(row['kraftwerk'])\n", " i = i_map.get(row['kohlesorte'])\n", " if (i in model.I) and (j in model.J):\n", " model.alpha_target_low[i, j] = float(row['ziel_low'])\n", " model.alpha_target_high[i, j] = float(row['ziel_high'])\n" ] }, { "cell_type": "code", "execution_count": 17, "id": "c6dcd7f3", "metadata": {}, "outputs": [], "source": [ "# Zielband (soft): Abweichungen werden als Slack-Variablen erfasst\n", "model.mix_target_low_dev = pyo.Var(model.I, model.J, model.W, model.D, domain=pyo.NonNegativeReals)\n", "model.mix_target_high_dev = pyo.Var(model.I, model.J, model.W, model.D, domain=pyo.NonNegativeReals)\n", "\n", "def mix_target_low_rule(m, i, j, w, d):\n", " return m.x_day[i, j, w, d] + m.mix_target_low_dev[i, j, w, d] >= m.alpha_target_low[i, j] * m.y_day[j, w, d]\n", "\n", "def mix_target_high_rule(m, i, j, w, d):\n", " 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]\n", "\n", "model.mix_target_low = pyo.Constraint(model.I, model.J, model.W, model.D, rule=mix_target_low_rule)\n", "model.mix_target_high = pyo.Constraint(model.I, model.J, model.W, model.D, rule=mix_target_high_rule)\n" ] }, { "cell_type": "markdown", "id": "a1158ce7", "metadata": {}, "source": [ "## Zielfunktion + Weiche Restriktionen\n", "\n", "Alle Zielfunktionskomponenten (Abweichungen, Glättung, Boni) werden hier zusammengesetzt." ] }, { "cell_type": "markdown", "id": "98788dcd", "metadata": {}, "source": [ "### Aufbau der Zielfunktion\n", "\n", "Wie die einzelnen Teilfunktionen zusammenwirken und welche Parameter sie skalieren." ] }, { "cell_type": "markdown", "id": "75567f6a", "metadata": {}, "source": [ "### Glättung der Schichtlieferungen\n", "\n", "Penalisiert große Schichtunterschiede je Kraftwerk, um feinere Verteilungspläne zu bevorzugen." ] }, { "cell_type": "markdown", "id": "5474cbbb", "metadata": {}, "source": [ "### Bonuskomponente\n", "\n", "Belohnt optionale Mehrlieferungen von Welzow nach Jänschwalde und kann parametrisch angepasst werden." ] }, { "cell_type": "code", "execution_count": 18, "id": "58086035", "metadata": {}, "outputs": [], "source": [ "# Bonuslogik für Welzow → Jänschwalde\n", "# Bonusparameter für Welzow → Jänschwalde (anpassbar)\n", "model.lambda_J = pyo.Param(initialize=100_000, mutable=True, within=pyo.NonNegativeReals)\n", "\n", "# Bonus: Liefermenge Welzow → J\n", "def welzow_to_J(m):\n", " return sum(\n", " m.x['Welzow', 'J', w, d, s]\n", " for w in m.W for d in m.D for s in m.S\n", " if ('Welzow', 'J', w, d, s) in m.x\n", " )\n", "model.bonus_welzow_J = pyo.Expression(rule=welzow_to_J)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "f74aaac5", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "8f1cf472", "metadata": {}, "source": [ "## Abweichungen und Hilfsvariablen\n" ] }, { "cell_type": "code", "execution_count": 19, "id": "80558550", "metadata": {}, "outputs": [], "source": [ "# Nachfrage pro j,w,d (V nutzt dV_N + dV_W, Rest d)\n", "def demand_rule(m, j, w, d):\n", " return m.dV_N[w, d] + m.dV_W[w, d] if j == 'V' else m.d[j, w, d]\n", "model.demand = pyo.Expression(model.J, model.W, model.D, rule=demand_rule)\n", "\n", "# Abweichung: Lieferung minus Nachfrage (für Reporting)\n", "model.dev = pyo.Expression(model.J, model.W, model.D,\n", " rule=lambda m, j, w, d: m.y[j, w, d] - m.demand[j, w, d])\n", "\n", "# Abweichungen nur für Kraftwerke in |dev|\n", "J_dev = list(J_power) # ohne V\n", "model.dev_pos = pyo.Var(J_dev, model.W, model.D, domain=pyo.NonNegativeReals)\n", "model.dev_neg = pyo.Var(J_dev, model.W, model.D, domain=pyo.NonNegativeReals)\n", "\n", "def dev_balance_rule(model, j, w, d):\n", " return model.dev_pos[j, w, d] - model.dev_neg[j, w, d] == model.dev[j, w, d]\n", "model.dev_balance = pyo.Constraint(J_dev, model.W, model.D, rule=dev_balance_rule)\n" ] }, { "cell_type": "markdown", "id": "01c033ac", "metadata": {}, "source": [ "## Maximalabweichung pro Tag\n" ] }, { "cell_type": "code", "execution_count": 20, "id": "fa37c381", "metadata": {}, "outputs": [], "source": [ "# Binärvariable: Tagesabweichung erreicht das Maximum? (nur Kraftwerke)\n", "model.z_max = pyo.Var(J_power, model.W, model.D, domain=pyo.Binary)\n", "\n", "def max_reached_rule(m, j, w, d):\n", " return m.dev_pos[j, w, d] <= m.a_max_day[j, w, d] * m.z_max[j, w, d]\n", "model.max_reached = pyo.Constraint(J_power, model.W, model.D, rule=max_reached_rule)\n", "\n", "D_list = list(model.D.ordered_data())\n", "def no_three_in_a_row_rule(m, j, w, idx):\n", " d1, d2, d3 = D_list[idx:idx+3]\n", " return m.z_max[j, w, d1] + m.z_max[j, w, d2] + m.z_max[j, w, d3] <= 2\n", "model.no_three_in_a_row = pyo.Constraint(J_power, model.W, range(len(D_list) - 2), rule=no_three_in_a_row_rule)\n" ] }, { "cell_type": "markdown", "id": "2e9d0b63", "metadata": {}, "source": [ "## Veredlung je Quelle\n" ] }, { "cell_type": "code", "execution_count": 21, "id": "8b2b18b1", "metadata": {}, "outputs": [], "source": [ "def y_V_nochten(m, w, d):\n", " return sum(m.x['Nochten', 'V', w, d, s] for s in m.S)\n", "\n", "def y_V_welzow(m, w, d):\n", " return sum(m.x['Welzow', 'V', w, d, s] for s in m.S)\n", "\n", "model.devV_N = pyo.Expression(model.W, model.D, rule=lambda m, w, d: y_V_nochten(m, w, d) - m.dV_N[w, d])\n", "model.devV_W = pyo.Expression(model.W, model.D, rule=lambda m, w, d: y_V_welzow(m, w, d) - m.dV_W[w, d])\n", "\n", "model.devV_N_pos = pyo.Var(model.W, model.D, domain=pyo.NonNegativeReals)\n", "model.devV_N_neg = pyo.Var(model.W, model.D, domain=pyo.NonNegativeReals)\n", "model.devV_W_pos = pyo.Var(model.W, model.D, domain=pyo.NonNegativeReals)\n", "model.devV_W_neg = pyo.Var(model.W, model.D, domain=pyo.NonNegativeReals)\n", "\n", "model.devV_N_balance = pyo.Constraint(model.W, model.D,\n", " rule=lambda m, w, d: m.devV_N_pos[w, d] - m.devV_N_neg[w, d] == m.devV_N[w, d])\n", "model.devV_W_balance = pyo.Constraint(model.W, model.D,\n", " rule=lambda m, w, d: m.devV_W_pos[w, d] - m.devV_W_neg[w, d] == m.devV_W[w, d])\n" ] }, { "cell_type": "markdown", "id": "c310d475", "metadata": {}, "source": [ "## Glättung über Schichten\n" ] }, { "cell_type": "code", "execution_count": 22, "id": "9ead659e", "metadata": {}, "outputs": [], "source": [ "model.lambda_glatt = pyo.Param(initialize=1e5, mutable=True, within=pyo.NonNegativeReals)\n", "\n", "model.m_ijwd = pyo.Expression(\n", " model.I, model.J, model.W, model.D,\n", " rule=lambda m, i, j, w, d: (1/len(m.S)) * sum(m.x[i, j, w, d, s] for s in m.S if (i,j,w,d,s) in m.x)\n", ")\n", "\n", "model.t_glatt = pyo.Var(model.I, model.J, model.W, model.D, model.S, domain=pyo.NonNegativeReals)\n", "\n", "def glatt_hi(m, i, j, w, d, s):\n", " return m.t_glatt[i,j,w,d,s] >= m.x[i,j,w,d,s] - m.m_ijwd[i,j,w,d]\n", "\n", "def glatt_lo(m, i, j, w, d, s):\n", " return m.t_glatt[i,j,w,d,s] >= -(m.x[i,j,w,d,s] - m.m_ijwd[i,j,w,d])\n", "\n", "model.glatt_hi = pyo.Constraint(model.I, model.J, model.W, model.D, model.S, rule=glatt_hi)\n", "model.glatt_lo = pyo.Constraint(model.I, model.J, model.W, model.D, model.S, rule=glatt_lo)\n" ] }, { "cell_type": "markdown", "id": "2389a64f", "metadata": {}, "source": [ "## Zielfunktion\n" ] }, { "cell_type": "code", "execution_count": 23, "id": "83595c99", "metadata": {}, "outputs": [], "source": [ "# Objective inkl. Smoothness (abweichungen über SHIFT_TOL werden bestraft)\n", "\n", "lambda_dev = 100_000 # Gewicht für Abweichungen an J\n", "lambda_v = 100_000 # Gewicht für Abweichungen an V\n", "\n", "# Smoothness weight/tolerance\n", "lambda_shift = 100_000_000 # Gewicht für Schicht-Glättung\n", "lambda_mix = 100_000_000 # Gewicht für Zielband-Abweichungen (kohle_mix ziel_low/hoch)\n", "SHIFT_TOL = 1000 # Toleranz bis zu der keine Strafe anfällt\n", "SHIFT_PAIRS = [('F', 'S'), ('S', 'N'), ('F', 'N')]\n", "model.SHIFT_PAIRS = pyo.Set(initialize=SHIFT_PAIRS, dimen=2)\n", "\n", "# Smoothness vars/constraints\n", "model.shift_dev_soft = pyo.Var(model.I, model.J, model.W, model.D, model.SHIFT_PAIRS, domain=pyo.NonNegativeReals)\n", "model.shift_excess = pyo.Var(model.I, model.J, model.W, model.D, model.SHIFT_PAIRS, domain=pyo.NonNegativeReals)\n", "\n", "model.shift_dev_abs = pyo.ConstraintList()\n", "model.shift_excess_def = pyo.ConstraintList()\n", "for i in model.I:\n", " for j in model.J:\n", " for w in model.W:\n", " for d in model.D:\n", " for s1, s2 in SHIFT_PAIRS:\n", " if (i, j, w, d, s1) in model.x and (i, j, w, d, s2) in model.x:\n", " # |x_s1 - x_s2|\n", " 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])\n", " 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])\n", " # nur Überschreitung über SHIFT_TOL wird bestraft\n", " 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)\n", "\n", "PEN_JW_NOCHTEN = 1_000_000 # Nochten -> J stark bestrafen (verhindern)\n", "BON_JW_WELZOW = 1_000_000 # Welzow -> J belohnen (fördern)\n", "\n", "PEN_SP_WELZOW = 40_000 # Welzow -> SP bestrafen (reduzieren)\n", "BON_SP_REICH = 30_000 # Reichwalde -> SP belohnen (fördern)\n", "\n", "PEN_BOX3_WELZOW = 40_000 # Welzow -> B3 bestrafen (reduzieren)\n", "BON_BOX3_REICH = 2 * 30_000 # Reichwalde -> B3 belohnen (stark fördern, 2/3)\n", "BON_BOX3_NOCHTEN = 1 * 30_000 # Nochten -> B3 belohnen (moderat fördern, 1/3)\n", "\n", "def objective_rule(model):\n", " # Abweichungsstrafen (Mengenabweichungen und V-Verteilung)\n", " deviation_penalty = (\n", " lambda_dev * sum(model.dev_pos[j, w, d] + model.dev_neg[j, w, d]\n", " for j in J_dev for w in model.W for d in model.D)\n", " + lambda_v * sum(model.devV_N_pos[w, d] + model.devV_N_neg[w, d]\n", " + model.devV_W_pos[w, d] + model.devV_W_neg[w, d]\n", " for w in model.W for d in model.D)\n", " )\n", "\n", " # Welzow-Bonus (globaler Vorteil für W->J)\n", " bonus_welzow = model.lambda_J * model.bonus_welzow_J\n", "\n", " # nur über existierende Indizes summieren\n", " def flow_sum(src, dst):\n", " return sum(model.x[i, j, w, d, s]\n", " for (i, j, w, d, s) in model.x.index_set()\n", " if i == src and j == dst)\n", "\n", " # Straf-/Bonus-Termine für bestimmte Strecken\n", " penalty_bonus_strecken = (\n", " PEN_JW_NOCHTEN * flow_sum('Nochten', 'J') # Nochten -> J teuer\n", " - BON_JW_WELZOW * flow_sum('Welzow', 'J') # Welzow -> J günstig\n", "\n", " + PEN_SP_WELZOW * flow_sum('Welzow', 'SP') # Welzow -> SP teuer\n", " - BON_SP_REICH * flow_sum('Reichwalde', 'SP') # Reichwalde -> SP günstig\n", "\n", " + PEN_BOX3_WELZOW * flow_sum('Welzow', 'B3') # Welzow -> B3 teuer\n", " - BON_BOX3_REICH * flow_sum('Reichwalde', 'B3')# Reichwalde -> B3 günstig\n", " - BON_BOX3_NOCHTEN * flow_sum('Nochten', 'B3') # Nochten -> B3 günstig\n", " )\n", "\n", " # Smoothness: nur Abweichung über SHIFT_TOL wird bestraft\n", " smoothness_penalty = lambda_shift * sum(\n", " model.shift_excess[i, j, w, d, s1, s2]\n", " for i in model.I for j in model.J for w in model.W for d in model.D\n", " for s1, s2 in model.SHIFT_PAIRS\n", " if (i, j, w, d, s1) in model.x and (i, j, w, d, s2) in model.x\n", " )\n", "\n", " # Zielband-Mix: Abweichungen von ziel_low/hoch werden bestraft\n", " mix_target_penalty = lambda_mix * sum(\n", " model.mix_target_low_dev[i, j, w, d] + model.mix_target_high_dev[i, j, w, d]\n", " for i in model.I for j in model.J for w in model.W for d in model.D\n", " )\n", "\n", " return deviation_penalty - bonus_welzow + penalty_bonus_strecken + smoothness_penalty + mix_target_penalty\n", "\n", "if hasattr(model, 'obj'):\n", " model.del_component(model.obj)\n", "model.obj = pyo.Objective(rule=objective_rule, sense=pyo.minimize)\n" ] }, { "cell_type": "markdown", "id": "ed37b283", "metadata": {}, "source": [ "## Reichwalder Kohle geht nicht zur Veredlung\n", "\n", "Nebenbedingung, die Transporte aus Reichwalde zur Veredlung explizit ausschließt." ] }, { "cell_type": "code", "execution_count": 24, "id": "1701f6e7", "metadata": {}, "outputs": [], "source": [ "def forbid_reichwalde_V_rule(m, w, d, s):\n", " return m.x['Reichwalde', 'V', w, d, s] == 0\n", "\n", "model.forbid_reichwalde_V = pyo.Constraint(model.W, model.D, model.S,\n", " rule=forbid_reichwalde_V_rule)\n" ] }, { "cell_type": "markdown", "id": "f297b423", "metadata": {}, "source": [ "## Kohlefördermengenkapazitäten\n", "\n", "Kapazitätsgrenzen der Tagebaue pro Schicht/Woche/Tag ins Modell übertragen." ] }, { "cell_type": "code", "execution_count": 25, "id": "1d2d8239", "metadata": {}, "outputs": [], "source": [ "# Förderkapazität je Tagebau/Tag/Schicht in Constraintform bringen\n", "cap_df = tables[\"foerderkapaz\"]\n", "cap_month = cap_df[cap_df['zeitraum'] == 'pro Monat']\n", "\n", "# Mapping für Tagebau-Label -> i-Key\n", "i_map = {\n", " 'Reichwalde (RW)': 'Reichwalde',\n", " 'Nochten (NO)': 'Nochten',\n", " 'Welzow-Süd': 'Welzow',\n", "}\n", "\n", "# Param für Monatsgrenzen je i\n", "model.F_max_month = pyo.Param(model.I, initialize=1e12, mutable=True)\n", "for _, row in cap_month.iterrows():\n", " i = i_map.get(row['tagebau'])\n", " if i in model.I:\n", " model.F_max_month[i] = float(row['maximal'])\n", "\n", "# Monatsförderung je i: Summe über alle j,d,s in Wochen des Monats\n", "def F_month_rule(m, i, mm):\n", " weeks_in_m = [w for w, mth in week_to_month.items() if mth == mm and w in m.W]\n", " return sum(m.x[i, j, w, d, s]\n", " for j in m.J for w in weeks_in_m for d in m.D for s in m.S)\n", "model.F_month = pyo.Expression(model.I, model.M, rule=F_month_rule)\n", "\n", "# Nebenbedingungen: Monatslimit je Tagebau\n", "model.cap_month = pyo.Constraint(\n", " model.I, model.M, rule=lambda m, i, mm: m.F_month[i, mm] <= m.F_max_month[i]\n", ")\n", "\n", "# Kombiniert RW+NO\n", "model.cap_month_RWNO = pyo.Constraint(\n", " model.M,\n", " rule=lambda m, mm: m.F_month['Reichwalde', mm] + m.F_month['Nochten', mm] <= 3_000_000.0\n", ")\n" ] }, { "cell_type": "markdown", "id": "d0ae92e0", "metadata": {}, "source": [ "## Verladungskapazität am Kohlelagerplatz Boxberg\n", "\n", "Boxberg-KLP-Beladung als Limit für zulässige Lieferungen modellieren." ] }, { "cell_type": "code", "execution_count": 26, "id": "ba2ba375", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "| verladung | zeitraum | maximal |\n", "|:----------------|:------------|----------:|\n", "| Welzow-Süd | pro Schicht | 33000 |\n", "| Welzow-Süd | pro Tag | 60000 |\n", "| Boxberg (RW+NO) | pro Schicht | 25000 |\n", "| Boxberg (RW+NO) | pro Tag | 75000 |\n" ] } ], "source": [ "# KLP-Restriktion: Boxberg-Verladung je Schicht begrenzen\n", "df = tables[\"verladungskap\"]\n", "print(df.to_markdown(index=False))" ] }, { "cell_type": "code", "execution_count": 27, "id": "1bd7cd46", "metadata": {}, "outputs": [], "source": [ "# Alternative Darstellung der KLP-Kapazitäten zur Kontrolle\n", "verladung_cap = tables[\"verladungskap\"]\n", "\n", "def cap_lookup(verladung_label: str, zeitraum_label: str) -> float:\n", " row = verladung_cap[\n", " (verladung_cap[\"verladung\"] == verladung_label)\n", " & (verladung_cap[\"zeitraum\"] == zeitraum_label)\n", " ]\n", " if row.empty:\n", " raise ValueError(f\"Keine Verladungskap für {verladung_label} / {zeitraum_label}\")\n", " return float(row.iloc[0][\"maximal\"])\n", "\n", "BOXBERG_SOURCES = [\"Reichwalde\", \"Nochten\"]\n", "BOXBERG_DEST = [\"J\", \"SP\", \"B3\", \"V\"]\n", "BOXBERG_SHIFT_CAP = cap_lookup(\"Boxberg (RW+NO)\", \"pro Schicht\")\n", "BOXBERG_DAY_CAP = cap_lookup(\"Boxberg (RW+NO)\", \"pro Tag\")\n", "\n", "WELZOW_SOURCES = [\"Welzow\"]\n", "WELZOW_DEST = [\"J\", \"SP\", \"B3\", \"V\"]\n", "WELZOW_SHIFT_CAP = cap_lookup(\"Welzow-Süd\", \"pro Schicht\")\n", "WELZOW_DAY_CAP = cap_lookup(\"Welzow-Süd\", \"pro Tag\")\n", "\n", "def boxberg_shift_cap_rule(m, w, d, s):\n", " return sum(\n", " m.x[i, j, w, d, s]\n", " for i in BOXBERG_SOURCES if i in m.I\n", " for j in BOXBERG_DEST if j in m.J\n", " ) <= BOXBERG_SHIFT_CAP\n", "model.cap_boxberg_shift = pyo.Constraint(model.W, model.D, model.S, rule=boxberg_shift_cap_rule)\n", "\n", "def boxberg_day_cap_rule(m, w, d):\n", " return sum(\n", " m.x[i, j, w, d, s]\n", " for i in BOXBERG_SOURCES if i in m.I\n", " for j in BOXBERG_DEST if j in m.J\n", " for s in m.S\n", " ) <= BOXBERG_DAY_CAP\n", "model.cap_boxberg_day = pyo.Constraint(model.W, model.D, rule=boxberg_day_cap_rule)\n", "\n", "def welzow_shift_cap_rule(m, w, d, s):\n", " return sum(\n", " m.x[i, j, w, d, s]\n", " for i in WELZOW_SOURCES if i in m.I\n", " for j in WELZOW_DEST if j in m.J\n", " ) <= WELZOW_SHIFT_CAP\n", "model.cap_welzow_shift = pyo.Constraint(model.W, model.D, model.S, rule=welzow_shift_cap_rule)\n", "\n", "def welzow_day_cap_rule(m, w, d):\n", " return sum(\n", " m.x[i, j, w, d, s]\n", " for i in WELZOW_SOURCES if i in m.I\n", " for j in WELZOW_DEST if j in m.J\n", " for s in m.S\n", " ) <= WELZOW_DAY_CAP\n", "model.cap_welzow_day = pyo.Constraint(model.W, model.D, rule=welzow_day_cap_rule)\n" ] }, { "cell_type": "markdown", "id": "511eee89", "metadata": {}, "source": [ "## Kurzfrstige Nichtverfügbarkeiten" ] }, { "cell_type": "code", "execution_count": 28, "id": "7f032640", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dict_keys(['Verfuegbarkeiten', 'bounds_power_plants', 'bunker', 'foerderkapaz', 'kohle_mix', 'rohkohlebedarf', 'veredelung_bounds', 'verladungskap', 'zugdurchlass'])" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tables.keys() # for inspection of available tables]" ] }, { "cell_type": "code", "execution_count": 29, "id": "fbc1cf9e", "metadata": {}, "outputs": [], "source": [ "avail = tables[\"Verfuegbarkeiten\"] # for inspection of available tables]\n" ] }, { "cell_type": "code", "execution_count": 30, "id": "bc4a5192", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "WARNING: DEPRECATED: Param 'cap_Welzow' declared with an implicit domain of\n", "'Any'. The default domain for Param objects is 'Any'. However, we will be\n", "changing that default to 'Reals' in the future. If you really intend the\n", "domain of this Paramto be 'Any', you can suppress this warning by explicitly\n", "specifying 'within=Any' to the Param constructor. (deprecated in 5.6.9, will\n", "be removed in (or after) 6.0) (called from\n", "/Users/nicolaimacbook/Projects/LEAG/POC1/.venv/lib/python3.13/site-\n", "packages/pyomo/core/base/indexed_component.py:785)\n", "WARNING: DEPRECATED: Param 'cap_RW_N' declared with an implicit domain of\n", "'Any'. The default domain for Param objects is 'Any'. However, we will be\n", "changing that default to 'Reals' in the future. If you really intend the\n", "domain of this Paramto be 'Any', you can suppress this warning by explicitly\n", "specifying 'within=Any' to the Param constructor. (deprecated in 5.6.9, will\n", "be removed in (or after) 6.0) (called from\n", "/Users/nicolaimacbook/Projects/LEAG/POC1/.venv/lib/python3.13/site-\n", "packages/pyomo/core/base/indexed_component.py:785)\n" ] } ], "source": [ "import math\n", "from pyomo.environ import value\n", "\n", "# Falls schon vorhanden: löschen, dann neu anlegen\n", "for name in [\"cap_Welzow\", \"cap_RW_N\", \"cap_welzow_con\", \"cap_rw_n_con\"]:\n", " if hasattr(model, name):\n", " model.del_component(name)\n", "\n", "day_map = {'Sat': 'Sa', 'Sun': 'So', 'Mon': 'Mo', 'Tue': 'Di', 'Wed': 'Mi', 'Thu': 'Do', 'Fri': 'Fr'}\n", "schicht_order = ['F', 'S', 'N'] # S1/S2/S3 -> Früh/Spät/Nacht\n", "\n", "# Params: Default = nan => kein Limit, wenn nicht überschrieben\n", "model.cap_Welzow = pyo.Param(model.W, model.D, model.S, initialize=math.nan, mutable=True)\n", "model.cap_RW_N = pyo.Param(model.W, model.D, model.S, initialize=math.nan, mutable=True)\n", "\n", "for _, row in avail.iterrows():\n", " datum = row['datum']\n", " w = datum.isocalendar().week # anpassen, falls model.W anders ist\n", " d = day_map[datum.strftime('%a')]\n", "\n", " caps_w = [row['Welzow_Sued_S1_t'], row['Welzow_Sued_S2_t'], row['Welzow_Sued_S3_t']]\n", " caps_rn = [row['Boxberg_NO_RW_S1_t'], row['Boxberg_NO_RW_S2_t'], row['Boxberg_NO_RW_S3_t']]\n", "\n", " for idx, s in enumerate(schicht_order): # ['F','S','N']\n", " val_w = caps_w[idx]\n", " if not math.isnan(val_w):\n", " model.cap_Welzow[w, d, s] = val_w\n", " val_rn = caps_rn[idx]\n", " if not math.isnan(val_rn):\n", " model.cap_RW_N[w, d, s] = val_rn\n", "\n", "\n", "# Constraints: skip bei nan, sonst Limit setzen\n", "def cap_welzow_rule(m, w, d, s):\n", " v = value(m.cap_Welzow[w, d, s])\n", " if math.isnan(v):\n", " return pyo.Constraint.Skip\n", " return sum(m.x[i, j, w, d, s]\n", " for i in ['Welzow'] for j in m.J\n", " if (i, j, w, d, s) in m.x.index_set()) <= v\n", "model.cap_welzow_con = pyo.Constraint(model.W, model.D, model.S, rule=cap_welzow_rule)\n", "\n", "def cap_rw_n_rule(m, w, d, s):\n", " v = value(m.cap_RW_N[w, d, s])\n", " if math.isnan(v):\n", " return pyo.Constraint.Skip\n", " return sum(m.x[i, j, w, d, s]\n", " for i in ['Reichwalde', 'Nochten'] for j in m.J\n", " if (i, j, w, d, s) in m.x.index_set()) <= v\n", "model.cap_rw_n_con = pyo.Constraint(model.W, model.D, model.S, rule=cap_rw_n_rule)\n" ] }, { "cell_type": "code", "execution_count": 31, "id": "e76b4aef", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "| week | day | shift | cap_Welzow | cap_RW_N |\n", "|-------:|:------|:--------|-------------:|-----------:|\n", "| 45 | Mi | F | 0 | 5000 |\n", "| 45 | Mi | S | nan | 15000 |\n", "| 46 | Mi | F | 0 | nan |\n", "| 46 | Do | F | nan | 15000 |\n", "| 47 | Mi | F | 0 | 25000 |\n", "| 47 | Mi | S | nan | 25000 |\n", "| 48 | Mi | F | 0 | 25000 |\n", "| 48 | Mi | S | nan | 25000 |\n" ] }, { "data": { "text/html": [ "
| \n", " | week | \n", "day | \n", "shift | \n", "cap_Welzow | \n", "cap_RW_N | \n", "
|---|---|---|---|---|---|
| 0 | \n", "45 | \n", "Mi | \n", "F | \n", "0.0 | \n", "5000.0 | \n", "
| 1 | \n", "45 | \n", "Mi | \n", "S | \n", "NaN | \n", "15000.0 | \n", "
| 2 | \n", "46 | \n", "Mi | \n", "F | \n", "0.0 | \n", "NaN | \n", "
| 3 | \n", "46 | \n", "Do | \n", "F | \n", "NaN | \n", "15000.0 | \n", "
| 4 | \n", "47 | \n", "Mi | \n", "F | \n", "0.0 | \n", "25000.0 | \n", "
| 5 | \n", "47 | \n", "Mi | \n", "S | \n", "NaN | \n", "25000.0 | \n", "
| 6 | \n", "48 | \n", "Mi | \n", "F | \n", "0.0 | \n", "25000.0 | \n", "
| 7 | \n", "48 | \n", "Mi | \n", "S | \n", "NaN | \n", "25000.0 | \n", "