fix: Fachwort Fluss nicht mehr aus eigenen Prompts; englische Zahlen und Teilfragen
- Routing-Prompt verlangt bei Dijkstra Restriktionsnamen in der Sprache der Aufgabe statt Flusserhaltung; Solver-Prompt ohne Fachwort (löste Korrekturrunden aus) - JARGON_EVERYDAY: beeinflusst/Einfluss/Überfluss zählen weder als Vorkommen noch als Erklärung eines Fachbegriffs - normalize_number liest eindeutige englische Zahlen (1,234.56; 12,345,678) richtig - QUESTION_PATTERN erkennt Part/Question/Task N Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0f1ab04a8d
commit
0582737b22
4 changed files with 44 additions and 5 deletions
|
|
@ -78,9 +78,15 @@ JARGON = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Alltagswörter, die einen Fachbegriff-Stamm enthalten. Sie zählen weder als Vorkommen noch als Erklärung –
|
||||||
|
# beobachtet: „beeinflusst“ in der Zusammenfassung galt als Erklärung von „Flusserhaltung“ (20260917-160618).
|
||||||
|
JARGON_EVERYDAY = re.compile(r"beeinfluss|einfluss|überfluss|ueberfluss|flussdiagramm")
|
||||||
|
|
||||||
|
|
||||||
def unexplained_jargon(sichtbar: str, erklaerung: str) -> List[str]:
|
def unexplained_jargon(sichtbar: str, erklaerung: str) -> List[str]:
|
||||||
"""Fachbegriffe, die im sichtbaren Text vorkommen, in der Erklärung aber nicht."""
|
"""Fachbegriffe, die im sichtbaren Text vorkommen, in der Erklärung aber nicht."""
|
||||||
sichtbar, erklaerung = sichtbar.lower(), erklaerung.lower()
|
sichtbar = JARGON_EVERYDAY.sub(" ", sichtbar.lower())
|
||||||
|
erklaerung = JARGON_EVERYDAY.sub(" ", erklaerung.lower())
|
||||||
return sorted(name for name, staemme in JARGON.items()
|
return sorted(name for name, staemme in JARGON.items()
|
||||||
if any(s in sichtbar for s in staemme) and not any(s in erklaerung for s in staemme))
|
if any(s in sichtbar for s in staemme) and not any(s in erklaerung for s in staemme))
|
||||||
|
|
||||||
|
|
@ -104,8 +110,9 @@ def check_jargon(spec: OptimizationModelSpecification) -> List[CheckIssue]:
|
||||||
# Zahl im Fließtext: 1.234,56 / 0,05 / 12 / 3.8 – ohne Zahlen, die Teil eines Wortes sind (A02, x1)
|
# Zahl im Fließtext: 1.234,56 / 0,05 / 12 / 3.8 – ohne Zahlen, die Teil eines Wortes sind (A02, x1)
|
||||||
NUMBER_PATTERN = re.compile(r"(?<![A-Za-z0-9_])\d+(?:[.,]\d+)*(?![A-Za-z0-9_])")
|
NUMBER_PATTERN = re.compile(r"(?<![A-Za-z0-9_])\d+(?:[.,]\d+)*(?![A-Za-z0-9_])")
|
||||||
# Teilfragen-Marker am Zeilenanfang: "a)", "- b)", "**Teil 2:**", "1." – bewusst eng, um
|
# Teilfragen-Marker am Zeilenanfang: "a)", "- b)", "**Teil 2:**", "1." – bewusst eng, um
|
||||||
# Aufzählungen im Fließtext nicht als Frage zu zählen.
|
# Aufzählungen im Fließtext nicht als Frage zu zählen. Englische Marker ("Part 2", "Question 1", "Task 3")
|
||||||
QUESTION_PATTERN = re.compile(r"^[\s\-*>]*\**\s*(?:\(?([a-h])\)|(?:Teil|Aufgabe|Frage)\s+(\d+)|([1-9])\.)\s*[:.)]?\s+\S",
|
# gehören dazu: sonst fehlt bei englischen Aufgaben das Nachfassen bei übersehenen Teilfragen.
|
||||||
|
QUESTION_PATTERN = re.compile(r"^[\s\-*>]*\**\s*(?:\(?([a-h])\)|(?:Teil|Aufgabe|Frage|Part|Question|Task)\s+(\d+)|([1-9])\.)\s*[:.)]?\s+\S",
|
||||||
re.MULTILINE)
|
re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -115,8 +122,16 @@ def normalize_number(roh: str) -> str:
|
||||||
Der Punkt ist mehrdeutig: Tausendertrenner (deutsch) oder Dezimalpunkt (englisch, Nummerierung).
|
Der Punkt ist mehrdeutig: Tausendertrenner (deutsch) oder Dezimalpunkt (englisch, Nummerierung).
|
||||||
Entschieden wird am Komma (dann ist der Punkt Tausendertrenner) und sonst an der Gruppenlänge:
|
Entschieden wird am Komma (dann ist der Punkt Tausendertrenner) und sonst an der Gruppenlänge:
|
||||||
genau drei Ziffern hinter dem letzten Punkt heißt Tausender, alles andere Dezimalpunkt.
|
genau drei Ziffern hinter dem letzten Punkt heißt Tausender, alles andere Dezimalpunkt.
|
||||||
|
|
||||||
|
Englische Schreibweise wird erkannt, wo sie eindeutig ist: '1,234.56' (Punkt nach dem Komma) und
|
||||||
|
'12,345,678' (mehrere Dreiergruppen). Ein einzelnes '1,500' bleibt deutsch gelesen (1,5) – beides ist
|
||||||
|
möglich, und die Pipeline ist auf deutsche Aufgaben ausgelegt. Vorher wurde '1,234.56' zu 1.23456.
|
||||||
"""
|
"""
|
||||||
if "," in roh:
|
englisch = (("." in roh and roh.rfind(".") > roh.rfind(",") > -1) # 1,234.56
|
||||||
|
or (roh.count(",") > 1 and "." not in roh and all(len(t) == 3 for t in roh.split(",")[1:]))) # 12,345,678
|
||||||
|
if englisch:
|
||||||
|
roh = roh.replace(",", "")
|
||||||
|
elif "," in roh:
|
||||||
roh = roh.replace(".", "").replace(",", ".")
|
roh = roh.replace(".", "").replace(",", ".")
|
||||||
elif "." in roh:
|
elif "." in roh:
|
||||||
teile = roh.split(".")
|
teile = roh.split(".")
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ Entscheide streng nach folgenden Routing-Regeln (die Zuordnung Klasse → Stack
|
||||||
- Verlangt die Aufgabe ausdrücklich ein bestimmtes Graphverfahren (typisch: „Lösen Sie mit dem Dijkstra-Algorithmus“), ist dieser Stack zu wählen – ein gleichwertiges Flussmodell erfüllt die Aufgabenstellung nicht. [→ REQUESTED_METHOD_IGNORED]
|
- Verlangt die Aufgabe ausdrücklich ein bestimmtes Graphverfahren (typisch: „Lösen Sie mit dem Dijkstra-Algorithmus“), ist dieser Stack zu wählen – ein gleichwertiges Flussmodell erfüllt die Aufgabenstellung nicht. [→ REQUESTED_METHOD_IGNORED]
|
||||||
- VORAUSSETZUNG: alle Bogengewichte sind nicht negativ. Gibt es negative Gewichte (Erlöse, Gutschriften), ist Dijkstra NICHT anwendbar – dann Flussmodell als MILP über HiGHS wählen und das in routing_rationale begründen. [→ check_graph]
|
- VORAUSSETZUNG: alle Bogengewichte sind nicht negativ. Gibt es negative Gewichte (Erlöse, Gutschriften), ist Dijkstra NICHT anwendbar – dann Flussmodell als MILP über HiGHS wählen und das in routing_rationale begründen. [→ check_graph]
|
||||||
- Die Entscheidungsvariablen sind die Bögen des Weges; deklariere sie als binäre \(x_{ij}\), auch wenn der Algorithmus selbst ohne sie auskommt.
|
- Die Entscheidungsvariablen sind die Bögen des Weges; deklariere sie als binäre \(x_{ij}\), auch wenn der Algorithmus selbst ohne sie auskommt.
|
||||||
|
- Benenne die Restriktionen in der Sprache der Aufgabe, nicht mit Netzwerk-Fachwörtern: z. B. „Start_Woche_1“ (der Plan beginnt in Woche 1), „Weiter_ab_Woche_k“ (wer in Woche k ankommt, bestellt dort erneut), „Ende_nach_Woche_4“ – nicht „Flusserhaltung“, „Quellfluss“, „Senke“. Diese Namen stehen im Report vor dem Leser. [→ UNEXPLAINED_JARGON]
|
||||||
|
|
||||||
GRÖSSENORDNUNG: Schätze aus dem Produkt der Indexmengen, wie viele Variablen das Modell bekommt, und nenne die Zahl in routing_rationale. Praktische Grenzen: LP ~10.000 Variablen, MILP/CP ~100–1.000 diskrete (NP-schwer), QP/SOCP ~1.000 stetige, MIQP/NLP ~100; kürzeste Wege sind auch bei \(10^5\) Knoten unkritisch. Über der Grenze bleibt der fachlich richtige Stack – die Folge („Optimalität im Zeitlimit voraussichtlich nicht beweisbar“) gehört als Annahme ausgewiesen, nicht das Modell vereinfacht.
|
GRÖSSENORDNUNG: Schätze aus dem Produkt der Indexmengen, wie viele Variablen das Modell bekommt, und nenne die Zahl in routing_rationale. Praktische Grenzen: LP ~10.000 Variablen, MILP/CP ~100–1.000 diskrete (NP-schwer), QP/SOCP ~1.000 stetige, MIQP/NLP ~100; kürzeste Wege sind auch bei \(10^5\) Knoten unkritisch. Über der Grenze bleibt der fachlich richtige Stack – die Folge („Optimalität im Zeitlimit voraussichtlich nicht beweisbar“) gehört als Annahme ausgewiesen, nicht das Modell vereinfacht.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ API je target_stack:
|
||||||
Startlösung (Warmstart): `sol = model.createSol(); model.setSolVal(sol, var, wert)` für jede Variable, dann `model.addSol(sol)` vor `model.optimize()`.
|
Startlösung (Warmstart): `sol = model.createSol(); model.setSolVal(sol, var, wert)` für jede Variable, dann `model.addSol(sol)` vor `model.optimize()`.
|
||||||
- NetworkX (Dijkstra): `import networkx as nx`; `G = nx.DiGraph()`; Bögen mit `G.add_edge(i, j, weight=c)`; `laenge, pfad = nx.single_source_dijkstra(G, quelle, ziel, weight="weight")` liefert Weglänge und Knotenfolge; alle endgültigen Marken in der Reihenfolge ihrer Vergabe: `marken = nx.single_source_dijkstra_path_length(G, quelle, weight="weight")` (das Wörterbuch ist in genau dieser Reihenfolge gefüllt). Ist das Ziel nicht erreichbar, wirft `single_source_dijkstra` `nx.NetworkXNoPath` – dann Status "infeasible" melden.
|
- NetworkX (Dijkstra): `import networkx as nx`; `G = nx.DiGraph()`; Bögen mit `G.add_edge(i, j, weight=c)`; `laenge, pfad = nx.single_source_dijkstra(G, quelle, ziel, weight="weight")` liefert Weglänge und Knotenfolge; alle endgültigen Marken in der Reihenfolge ihrer Vergabe: `marken = nx.single_source_dijkstra_path_length(G, quelle, weight="weight")` (das Wörterbuch ist in genau dieser Reihenfolge gefüllt). Ist das Ziel nicht erreichbar, wirft `single_source_dijkstra` `nx.NetworkXNoPath` – dann Status "infeasible" melden.
|
||||||
WICHTIG: Bei negativen Bogengewichten wirft networkx KEINEN Fehler, liefert aber ein falsches Ergebnis (geprüft: 3 statt 2). Prüfe die Gewichte im Programm und melde Status "error" mit einem Hinweis, falls eines negativ ist.
|
WICHTIG: Bei negativen Bogengewichten wirft networkx KEINEN Fehler, liefert aber ein falsches Ergebnis (geprüft: 3 statt 2). Prüfe die Gewichte im Programm und melde Status "error" mit einem Hinweis, falls eines negativ ist.
|
||||||
Melde den Graphen im Argument graph (siehe unten) – ohne ihn kann der Report die verlangte Zeichnung nicht erzeugen. Die Restriktionen der Spezifikation (Flusserhaltung) sind trotzdem in constraints auszuweisen, ausgewertet am gefundenen Weg.
|
Melde den Graphen im Argument graph (siehe unten) – ohne ihn kann der Report die verlangte Zeichnung nicht erzeugen. Die Restriktionen der Spezifikation (Start, Ende und Weiterführung an jedem Zwischenknoten) sind trotzdem in constraints auszuweisen, ausgewertet am gefundenen Weg.
|
||||||
- Ipopt (cyipopt): `from cyipopt import minimize_ipopt`; `res = minimize_ipopt(zielfunktion, x0=startwerte, bounds=[(lo, hi), ...], constraints=[{"type": "ineq"|"eq", "fun": g}], options={"print_level": 0, "sb": "yes"})`; minimiert immer (bei Maximierung Vorzeichen drehen und Zielwert zurückrechnen); `g(x) >= 0` bei "ineq", `g(x) == 0` bei "eq"; Erfolg `res.success`, Werte `res.x`, Zielwert `res.fun`.
|
- Ipopt (cyipopt): `from cyipopt import minimize_ipopt`; `res = minimize_ipopt(zielfunktion, x0=startwerte, bounds=[(lo, hi), ...], constraints=[{"type": "ineq"|"eq", "fun": g}], options={"print_level": 0, "sb": "yes"})`; minimiert immer (bei Maximierung Vorzeichen drehen und Zielwert zurückrechnen); `g(x) >= 0` bei "ineq", `g(x) == 0` bei "eq"; Erfolg `res.success`, Werte `res.x`, Zielwert `res.fun`.
|
||||||
Ipopt findet nur LOKALE Optima: Starte von mindestens 5 verschiedenen, zulässigen Startpunkten und behalte das beste Ergebnis. Melde status "optimal" nur, wenn das Problem nachweislich konvex ist (dann ist lokal = global), sonst "feasible" und erwähne in note bzw. headline, dass es sich um die beste gefundene lokale Lösung handelt. Findet kein Startpunkt eine Lösung, melde "infeasible" bzw. "error".
|
Ipopt findet nur LOKALE Optima: Starte von mindestens 5 verschiedenen, zulässigen Startpunkten und behalte das beste Ergebnis. Melde status "optimal" nur, wenn das Problem nachweislich konvex ist (dann ist lokal = global), sonst "feasible" und erwähne in note bzw. headline, dass es sich um die beste gefundene lokale Lösung handelt. Findet kein Startpunkt eine Lösung, melde "infeasible" bzw. "error".
|
||||||
Berichte die Streuung über die Startpunkte im Argument multistart (siehe unten) – sie ist der einzige Hinweis darauf, ob das Optimum stabil ist. Verwende feste Startpunkte oder einen festen Zufallsstartwert (z. B. `np.random.default_rng(0)`), damit derselbe Lauf dasselbe Ergebnis liefert.
|
Berichte die Streuung über die Startpunkte im Argument multistart (siehe unten) – sie ist der einzige Hinweis darauf, ob das Optimum stabil ist. Verwende feste Startpunkte oder einen festen Zufallsstartwert (z. B. `np.random.default_rng(0)`), damit derselbe Lauf dasselbe Ergebnis liefert.
|
||||||
|
|
|
||||||
|
|
@ -446,3 +446,26 @@ def test_objective_label_is_still_stripped():
|
||||||
from checks.model_checks import _objective_without_label
|
from checks.model_checks import _objective_without_label
|
||||||
assert _objective_without_label("Z = 12 x").strip() == "12 x"
|
assert _objective_without_label("Z = 12 x").strip() == "12 x"
|
||||||
assert _objective_without_label("\\sum_{i=0}^{2} c_i x_i").startswith("\\sum")
|
assert _objective_without_label("\\sum_{i=0}^{2} c_i x_i").startswith("\\sum")
|
||||||
|
|
||||||
|
|
||||||
|
def test_everyday_words_neither_trigger_nor_explain_jargon():
|
||||||
|
from checks.model_checks import unexplained_jargon
|
||||||
|
# beobachtet: „beeinflusst“ in der Zusammenfassung galt als Erklärung von „Flusserhaltung“
|
||||||
|
assert unexplained_jargon("Flusserhaltung", "Die Bestellmenge beeinflusst die Lagerkosten.") == ["Fluss"]
|
||||||
|
assert unexplained_jargon("Der Einfluss des Preises", "Zusammenfassung") == []
|
||||||
|
assert unexplained_jargon("Flusserhaltung", "Der Fluss durch einen Knoten bleibt erhalten.") == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_unambiguous_english_numbers_are_read_correctly():
|
||||||
|
from checks.model_checks import normalize_number
|
||||||
|
assert normalize_number("1,234.56") == "1234.56" # vorher 1.23456
|
||||||
|
assert normalize_number("12,345,678") == "12345678"
|
||||||
|
assert normalize_number("1,500") == "1.5" # mehrdeutig: bleibt deutsch gelesen
|
||||||
|
assert normalize_number("1.234,56") == "1234.56" # deutsch unverändert
|
||||||
|
|
||||||
|
|
||||||
|
def test_english_sub_question_markers_are_recognised():
|
||||||
|
from checks.model_checks import QUESTION_PATTERN
|
||||||
|
for zeile in ("Part 2: Solve the model", "* **Question 1: How many units**", "- Task 3: Compare"):
|
||||||
|
assert QUESTION_PATTERN.search(zeile), zeile
|
||||||
|
assert not QUESTION_PATTERN.search("In part 2 of the text we see") # Fließtext bleibt Fließtext
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue