operations_research/OR_HTML_04/programme/Excel_Bruecke.py
dschlueter 360912dbfa Aufgeraeumt: 33 PNG-Zweitfassungen, Bau-Ueberbleibsel, Ausgabepfade
Drei Aufraeumarbeiten - und zwei Funde, die dabei auffielen.

1. DIE PNG-ZWEITFASSUNGEN SIND WEG. Jedes Diagramm lag doppelt vor, als SVG
und als PNG, und kein einziges src=/href= in der Website zeigte je auf ein
PNG. Der Build kopierte sie trotzdem mit: 3,3 MB im Repository plus 3,3 MB,
die bei jeder Veroeffentlichung auf den Webserver gingen. Die 15 Generatoren
schreiben jetzt nur noch SVG, die Docstrings sind mitgezogen. Vor dem
Loeschen geprueft: Jedes PNG hatte sein gleichnamiges SVG, alle 33 waren
versioniert.

FUND 1: erzeuge_kap06_gantt.py folgte als einziger Generator nicht der
Konvention - weder svg.hashsalt noch metadata={"Date": None}. Sein SVG trug
einen echten Zeitstempel und bei jedem Lauf andere clip-path-IDs, war also
nie byteidentisch reproduzierbar, obwohl CLAUDE.md genau das fuer alle
Generatoren festhaelt. Aufgefallen nur, weil nach der PNG-Umstellung 32 von
33 SVGs bitgleich blieben und eines nicht. Jetzt byteidentisch ueber zwei
Laeufe.

FUND 2: spiegle_bilder() legte leere Verzeichnisse auf dem Webserver an. Der
Dateifilter arbeitete korrekt, aber os.walk durchlief auch __pycache__/, und
os.makedirs() erzeugte es am Ziel. Die Verzeichnisliste wird jetzt vorher
gefiltert. Gegengetestet.

2. BAU-UEBERBLEIBSEL entfernt (alle ignoriert und neu erzeugbar):
svg-inkscape/, build_v04.log, .pytest_cache/, Programme/output/, vier
__pycache__/ und die Excel-Mappen. Arbeitsbaum 36 -> 33 MB, danach null
ignorierte Ueberbleibsel.

3. Excel_Bruecke.py SCHREIBT NEBEN DAS SKRIPT statt ins
Arbeitsverzeichnis. Es benutzte blanke relative Namen; wer es aus der
Repository-Wurzel startete, verstreute dort produktionsmix.xlsx und
produktionsmix_ergebnis.xlsx. Jetzt wie die vier anderen schreibenden
Programme ueber os.path.dirname(os.path.abspath(__file__)). Nachgemessen:
Lauf aus der Wurzel legt dort null Dateien ab.

Ausserdem git gc: 653 lose Objekte gepackt, .git von 71 MB auf 28 MB -
reines Repacken, kein Inhalt beruehrt.

Geprueft: alle 33 im Buch referenzierten SVGs vorhanden, in Quelle und
Website; 16 Generatoren fehlerfrei; keine fehlenden Bilder im LaTeX-Lauf;
33 pytest-Tests; PDF unveraendert 760 Seiten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:22:26 +02:00

101 lines
3.9 KiB
Python
Generated

#!/usr/bin/env python3
# Excel_Bruecke.py
"""
Kapitel Einfuehrung: Vom Excel-Solver zu Python - dieselbe Rechnung, dieselbe Datei.
Das Programm bildet den vollstaendigen Arbeitsweg ab, den ein Excel-Modell im
Betrieb sonst von Hand geht:
Tabellenblatt lesen -> Modell bauen -> loesen -> Ergebnis zurueckschreiben
Damit es ohne Vorbereitung laeuft, legt es die Eingabedatei beim ersten Start
selbst an. Ersetzen Sie sie durch Ihre eigene Datei - solange die Spalten
gleich heissen, aendert sich am Code nichts.
Die vier Arbeitsschritte kommen aus or_kern.py (Kapitel Praxisfallen). Was
hier steht, ist nur noch das, was diesen Fall ausmacht - der Rest ist
gemeinsamer Unterbau. Wer das Modul nicht kennt: Es ist im Buch vollstaendig
abgedruckt und liegt im selben Verzeichnis.
Erzeugte Dateien (im Arbeitsverzeichnis):
produktionsmix.xlsx Eingabe: Blaetter 'Produkte' und 'Kapazitaeten'
produktionsmix_ergebnis.xlsx Ausgabe: Blaetter 'Plan' und 'Kennzahlen'
Benoetigt: pandas, openpyxl, ortools, pydantic (ueber or_kern)
"""
from __future__ import annotations
import os
import pandas as pd
from or_kern import (lade_produktionsproblem, loese_mit_glop, pruefe_loesung,
schreibe_ergebnis)
# Die Mappen liegen NEBEN diesem Programm, nicht im aktuellen
# Arbeitsverzeichnis. Sonst haengt es davon ab, aus welchem Ordner man das
# Skript startet, wo die Dateien landen - und wer es aus der
# Repository-Wurzel aufruft, verstreut sie dort.
HIER = os.path.dirname(os.path.abspath(__file__))
EINGABE = os.path.join(HIER, "produktionsmix.xlsx")
AUSGABE = os.path.join(HIER, "produktionsmix_ergebnis.xlsx")
def lege_beispieldatei_an(pfad: str) -> None:
"""Schreibt die Eingabemappe, wie sie auch aus dem Controlling kaeme."""
produkte = pd.DataFrame({
"Produkt": ["Tisch", "Stuhl", "Regal"],
"Deckungsbeitrag": [240.0, 60.0, 130.0],
"Montagestunden": [3.0, 1.0, 2.0],
"Plattenmaterial": [6.0, 1.0, 4.0],
})
kapazitaeten = pd.DataFrame({
"Ressource": ["Montagestunden", "Plattenmaterial"],
"Verfuegbar": [150.0, 240.0],
})
with pd.ExcelWriter(pfad, engine="openpyxl") as mappe:
produkte.to_excel(mappe, sheet_name="Produkte", index=False)
kapazitaeten.to_excel(mappe, sheet_name="Kapazitaeten", index=False)
if __name__ == "__main__":
if not os.path.exists(EINGABE):
lege_beispieldatei_an(EINGABE)
print(f"Beispiel-Eingabedatei angelegt: {EINGABE}")
# 1. Lesen und pruefen. Faellt hier etwas auf, ist der Fehler noch
# zuzuordnen - spaeter waere er nur noch eine merkwuerdige Zahl.
problem = lade_produktionsproblem(EINGABE)
# 2. Loesen. Einzige solverabhaengige Zeile des ganzen Programms -
# 'loese_mit_scipy' waere ein Einzeiler-Wechsel.
loesung = loese_mit_glop(problem)
# 3. Abnahmepruefung gegen die Anforderungen, ohne den Solver zu fragen.
beanstandungen = pruefe_loesung(problem, loesung)
if beanstandungen:
raise SystemExit("Abnahmepruefung fehlgeschlagen:\n - "
+ "\n - ".join(beanstandungen))
# 4. Zurueckschreiben - im Format, das die Fachabteilung ohnehin benutzt.
schreibe_ergebnis(AUSGABE, problem, loesung)
print("=" * 66)
print(" PRODUKTIONSPLAN AUS DER EXCEL-MAPPE")
print("=" * 66)
for produkt in problem.produkte:
menge = loesung.werte[produkt.name]
print(f"{produkt.name:<18} {menge:8.1f} Stueck "
f"{menge * produkt.deckungsbeitrag:12,.2f} EUR")
print("-" * 66)
print(loesung.als_bericht())
print("Abnahmepruefung: bestanden\n")
# Die Schattenpreise beantworten die Frage, die der Plan nicht stellt:
# Was waere eine zusaetzliche Einheit dieser Ressource wert?
for ressource, preis in loesung.schattenpreise.items():
print(f" eine Einheit {ressource:<18} mehr waere wert: {preis:7.2f} EUR")
print("=" * 66)
print(f"Ergebnis geschrieben nach: {AUSGABE}")