891 lines
29 KiB
Python
891 lines
29 KiB
Python
# %%
|
|
from gurobipy import *
|
|
import json
|
|
import random
|
|
|
|
|
|
MEN = ["Herbert", "Christoph", "Martin", "Christian", "Marlon"]
|
|
WOMEN = ["Gaby", "Teresa", "Iris", "Anne-Christin", "Jenni", "Birgit", "Johanna"]
|
|
NAMES = MEN + WOMEN
|
|
|
|
PAIRS_2018 = [
|
|
("Martin", "Birgit"),
|
|
("Birgit", "Teresa"),
|
|
("Teresa", "Herbert"),
|
|
("Herbert", "Peter"),
|
|
("Peter", "Iris"),
|
|
("Iris", "Gaby"),
|
|
("Gaby", "Christoph"),
|
|
("Christoph", "Martin"),
|
|
("Christian", "Anne-Christin"),
|
|
("Anne-Christin", "Jenni"),
|
|
]
|
|
PAIRS_2019 = [
|
|
("Peter", "Herbert"),
|
|
("Herbert", "Gaby"),
|
|
("Gaby", "Christian"),
|
|
("Christian", "Renate"),
|
|
("Renate", "Anne-Christin"),
|
|
("Anne-Christin", "Martin"),
|
|
("Martin", "Iris"),
|
|
("Iris", "Teresa"),
|
|
("Teresa", "Jenni"),
|
|
("Jenni", "Christoph"),
|
|
("Christoph", "Birgit"),
|
|
("Birgit", "Peter"),
|
|
]
|
|
PAIRS_2021 = [
|
|
("Peter", "Teresa"),
|
|
("Teresa", "Christian"),
|
|
("Christian", "Birgit"),
|
|
("Birgit", "Jenni"),
|
|
("Jenni", "Iris"),
|
|
("Iris", "Herbert"),
|
|
("Herbert", "Martin"),
|
|
("Martin", "Christoph"),
|
|
("Christoph", "Gaby"),
|
|
("Gaby", "Anne-Christin"),
|
|
("Anne-Christin", "Peter"),
|
|
]
|
|
PAIRS_2022 = [
|
|
("Peter", "Christian"),
|
|
("Christian", "Herbert"),
|
|
("Herbert", "Anne-Christin"),
|
|
("Anne-Christin", "Birgit"),
|
|
("Birgit", "Iris"),
|
|
("Iris", "Christoph"),
|
|
("Christoph", "Teresa"),
|
|
("Teresa", "Gaby"),
|
|
("Gaby", "Martin"),
|
|
("Martin", "Jenni"),
|
|
("Jenni", "Peter"),
|
|
]
|
|
PAIRS_2022 = [
|
|
("Peter", "Christian"),
|
|
("Christian", "Herbert"),
|
|
("Herbert", "Anne-Christin"),
|
|
("Anne-Christin", "Birgit"),
|
|
("Birgit", "Iris"),
|
|
("Iris", "Christoph"),
|
|
("Christoph", "Teresa"),
|
|
("Teresa", "Gaby"),
|
|
("Gaby", "Martin"),
|
|
("Martin", "Jenni"),
|
|
("Jenni", "Peter"),
|
|
]
|
|
PAIRS_2023 = [
|
|
("Herbert", "Jenni"),
|
|
("Christoph", "Herbert"),
|
|
("Martin", "Johanna"),
|
|
("Christian", "Christoph"),
|
|
("Gaby", "Birgit"),
|
|
("Teresa", "Anne-Christin"),
|
|
("Iris", "Christian"),
|
|
("Anne-Christin", "Iris"),
|
|
("Jenni", "Gaby"),
|
|
("Birgit", "Martin"),
|
|
("Johanna", "Teresa"),
|
|
]
|
|
|
|
PAIRS_2024 = [
|
|
("Johanna", "Herbert"),
|
|
("Herbert", "Teresa"),
|
|
("Teresa", "Iris"),
|
|
("Iris", "Martin"),
|
|
("Martin", "Anne-Christin"),
|
|
("Anne-Christin", "Jenni"),
|
|
("Jenni", "Christoph"),
|
|
("Christoph", "Birgit"),
|
|
("Birgit", "Marlon"),
|
|
("Marlon", "Gaby"),
|
|
("Gaby", "Christian"),
|
|
("Christian", "Johanna"),
|
|
]
|
|
|
|
|
|
SEX_PAIRS = [
|
|
("Herbert", "Birgit"),
|
|
("Anne-Christin", "Christoph"),
|
|
("Christian", "Jenni"),
|
|
("Iris", "Jenni"),
|
|
("Iris", "Christian"),
|
|
# ("Teresa", "Martin"),
|
|
# ("Gaby", "Peter"),
|
|
("Johanna", "Christoph"),
|
|
("Johanna", "Anne-Christin"),
|
|
("Marlon", "Christoph"),
|
|
("Marlon", "Anne-Christin"),
|
|
("Marlon", "Johanna"),
|
|
|
|
]
|
|
|
|
|
|
# %%
|
|
|
|
m = Model("Silvester")
|
|
|
|
x = {}
|
|
y = {}
|
|
|
|
for i in NAMES:
|
|
for j in NAMES:
|
|
x[i, j] = m.addVar(vtype=GRB.BINARY)
|
|
if i == j:
|
|
x[i, j].ub = 0
|
|
|
|
sex_penalty = m.addVar(vtype=GRB.CONTINUOUS)
|
|
old_penalty_2018 = m.addVar(vtype=GRB.CONTINUOUS)
|
|
old_penalty_2019 = m.addVar(vtype=GRB.CONTINUOUS)
|
|
old_penalty_2021 = m.addVar(vtype=GRB.CONTINUOUS)
|
|
old_penalty_2022 = m.addVar(vtype=GRB.CONTINUOUS)
|
|
old_penalty_2023 = m.addVar(vtype=GRB.CONTINUOUS)
|
|
old_penalty_2024 = m.addVar(vtype=GRB.CONTINUOUS)
|
|
old_penalty_2024_reverse = m.addVar(vtype=GRB.CONTINUOUS)
|
|
pair_penalty = m.addVar(vtype=GRB.CONTINUOUS)
|
|
|
|
m.update()
|
|
print("Finished Variable set-up")
|
|
|
|
|
|
# kein gegenseitiges Beschenken
|
|
m.addConstrs(x[i, j] + x[j, i] <= 1 for i in NAMES for j in NAMES)
|
|
|
|
# falls ein Teilnehmer im nächsten Jahr ausfällt
|
|
m.addConstrs(
|
|
x[i, j] + x[j, k] + x[k, i] <= 2 for i in NAMES for j in NAMES for k in NAMES
|
|
)
|
|
|
|
|
|
# add constraints
|
|
m.addConstr(
|
|
quicksum(x[p] + x[p[1], p[0]] for p in SEX_PAIRS if p[0] in NAMES and p[1] in NAMES)
|
|
<= pair_penalty
|
|
)
|
|
m.addConstr(
|
|
quicksum(x[i, j] for i in MEN for j in MEN)
|
|
+ quicksum(x[i, j] for i in WOMEN for j in WOMEN)
|
|
<= sex_penalty
|
|
)
|
|
|
|
m.addConstr(
|
|
quicksum(x[p[0], p[1]] for p in PAIRS_2018 if p[0] in NAMES and p[1] in NAMES)
|
|
<= old_penalty_2018
|
|
)
|
|
m.addConstr(
|
|
quicksum(x[p[0], p[1]] for p in PAIRS_2019 if p[0] in NAMES and p[1] in NAMES)
|
|
<= old_penalty_2019
|
|
)
|
|
m.addConstr(
|
|
quicksum(x[p[0], p[1]] for p in PAIRS_2021 if p[0] in NAMES and p[1] in NAMES)
|
|
<= old_penalty_2021
|
|
)
|
|
m.addConstr(
|
|
quicksum(x[p[0], p[1]] for p in PAIRS_2022 if p[0] in NAMES and p[1] in NAMES)
|
|
<= old_penalty_2022
|
|
)
|
|
m.addConstr(
|
|
quicksum(x[p[0], p[1]] for p in PAIRS_2023 if p[0] in NAMES and p[1] in NAMES)
|
|
<= old_penalty_2023
|
|
)
|
|
m.addConstr(
|
|
quicksum(x[p[0], p[1]] for p in PAIRS_2024 if p[0] in NAMES and p[1] in NAMES)
|
|
<= old_penalty_2024
|
|
)
|
|
|
|
# kein zurueckschenken
|
|
# m.addConstr(
|
|
# quicksum(x[p[1], p[0]] for p in PAIRS_2018 if p[0] in NAMES and p[1] in NAMES)
|
|
# <= old_penalty_2018
|
|
# )
|
|
# m.addConstr(
|
|
# quicksum(x[p[1], p[0]] for p in PAIRS_2019 if p[0] in NAMES and p[1] in NAMES)
|
|
# <= old_penalty_2019
|
|
# )
|
|
# m.addConstr(
|
|
# quicksum(x[p[1], p[0]] for p in PAIRS_2021 if p[0] in NAMES and p[1] in NAMES)
|
|
# <= old_penalty_2021
|
|
# )
|
|
# m.addConstr(
|
|
# quicksum(x[p[1], p[0]] for p in PAIRS_2022 if p[0] in NAMES and p[1] in NAMES)
|
|
# <= old_penalty_2022
|
|
# )
|
|
# m.addConstr(
|
|
# quicksum(x[p[1], p[0]] for p in PAIRS_2023 if p[0] in NAMES and p[1] in NAMES)
|
|
# <= old_penalty_2023
|
|
# )
|
|
m.addConstr(
|
|
quicksum(x[p[1], p[0]] for p in PAIRS_2024 if p[0] in NAMES and p[1] in NAMES)
|
|
<= old_penalty_2024_reverse
|
|
)
|
|
# m.addConstr(x['Teresa','Christoph'] == 0)
|
|
|
|
|
|
|
|
# MARLON
|
|
# allowed_names = ['Birgit', 'Gaby', 'Herbert']
|
|
# m.addConstr(quicksum(x["Johanna", name] for name in allowed_names) == 1)
|
|
# m.addConstr(quicksum(x["Marlon", name] for name in allowed_names) == 1)
|
|
|
|
|
|
for i in NAMES:
|
|
m.addConstr(quicksum(x[i, j] for j in NAMES) == 1)
|
|
m.addConstr(quicksum(x[j, i] for j in NAMES) == 1)
|
|
|
|
# Set objective
|
|
m.modelSense = GRB.MINIMIZE
|
|
m.setObjective(
|
|
0.01 * quicksum(random.uniform(0, 1) * x[key] for key in x.keys())
|
|
+ 10
|
|
* (
|
|
# 5 * sex_penalty
|
|
+ 100 * pair_penalty
|
|
+ 5000 * old_penalty_2024
|
|
+ 100 * old_penalty_2024_reverse
|
|
+ 1000 * old_penalty_2023
|
|
+ 500 * old_penalty_2022
|
|
+ 100 * old_penalty_2021
|
|
+ 50 * old_penalty_2019
|
|
+ old_penalty_2018
|
|
)
|
|
)
|
|
|
|
print("Finished Contraint set-up")
|
|
|
|
def generate_html_report(PAIRS, c, violations_data, obj_data):
|
|
"""Generate a beautiful HTML report for the solution"""
|
|
from datetime import datetime
|
|
|
|
current_time = datetime.now().strftime('%d.%m.%Y um %H:%M:%S')
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Silvester Wichteln 2025 - Lösung #{c}</title>
|
|
<style>
|
|
* {{
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}}
|
|
body {{
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
padding: 20px;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
}}
|
|
.container {{
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 15px;
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
overflow: hidden;
|
|
}}
|
|
header {{
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 40px;
|
|
text-align: center;
|
|
}}
|
|
h1 {{
|
|
font-size: 2.5em;
|
|
margin-bottom: 10px;
|
|
}}
|
|
.subtitle {{
|
|
font-size: 1.2em;
|
|
opacity: 0.9;
|
|
}}
|
|
.content {{
|
|
padding: 40px;
|
|
}}
|
|
section {{
|
|
margin-bottom: 40px;
|
|
}}
|
|
h2 {{
|
|
color: #667eea;
|
|
border-bottom: 3px solid #667eea;
|
|
padding-bottom: 10px;
|
|
margin-bottom: 20px;
|
|
font-size: 1.8em;
|
|
}}
|
|
.pairs-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
}}
|
|
.pair-card {{
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
transition: transform 0.3s;
|
|
}}
|
|
.pair-card:hover {{
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
|
}}
|
|
.pair-arrow {{
|
|
font-size: 1.5em;
|
|
color: #667eea;
|
|
margin: 5px 0;
|
|
}}
|
|
.history-table {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 20px;
|
|
}}
|
|
.history-table th, .history-table td {{
|
|
padding: 12px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #ddd;
|
|
}}
|
|
.history-table th {{
|
|
background: #667eea;
|
|
color: white;
|
|
font-weight: bold;
|
|
}}
|
|
.history-table tr:hover {{
|
|
background: #f5f7fa;
|
|
}}
|
|
.violation-section {{
|
|
background: #fff5f5;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
border-left: 5px solid #fc8181;
|
|
}}
|
|
.violation-section.no-violations {{
|
|
background: #f0fff4;
|
|
border-left-color: #68d391;
|
|
}}
|
|
.violation-item {{
|
|
margin: 10px 0;
|
|
padding: 10px;
|
|
background: white;
|
|
border-radius: 5px;
|
|
}}
|
|
.violation-badge {{
|
|
display: inline-block;
|
|
padding: 5px 10px;
|
|
border-radius: 20px;
|
|
font-size: 0.9em;
|
|
font-weight: bold;
|
|
margin-right: 10px;
|
|
}}
|
|
.badge-error {{
|
|
background: #fc8181;
|
|
color: white;
|
|
}}
|
|
.badge-success {{
|
|
background: #68d391;
|
|
color: white;
|
|
}}
|
|
.objective-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
}}
|
|
.objective-card {{
|
|
background: linear-gradient(135deg, #ffeaa7 0%, #fdcb6e 100%);
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
}}
|
|
.objective-label {{
|
|
font-weight: bold;
|
|
color: #666;
|
|
margin-bottom: 5px;
|
|
}}
|
|
.objective-value {{
|
|
font-size: 1.5em;
|
|
color: #333;
|
|
font-weight: bold;
|
|
}}
|
|
.summary-box {{
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
font-size: 1.3em;
|
|
font-weight: bold;
|
|
margin-top: 20px;
|
|
}}
|
|
.summary-valid {{
|
|
background: #68d391;
|
|
color: white;
|
|
}}
|
|
.summary-invalid {{
|
|
background: #fc8181;
|
|
color: white;
|
|
}}
|
|
.year-badge {{
|
|
display: inline-block;
|
|
padding: 3px 8px;
|
|
border-radius: 5px;
|
|
font-size: 0.8em;
|
|
margin-right: 5px;
|
|
background: #667eea;
|
|
color: white;
|
|
}}
|
|
@media print {{
|
|
body {{
|
|
background: white;
|
|
}}
|
|
.container {{
|
|
box-shadow: none;
|
|
}}
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>🎁 Silvester Wichteln 2025</h1>
|
|
<div class="subtitle">Lösung #{c} - Generiert am {current_time}</div>
|
|
</header>
|
|
|
|
<div class="content">
|
|
<section>
|
|
<h2>Geschenkpaare 2025</h2>
|
|
<div class="pairs-grid">
|
|
"""
|
|
|
|
for giver, receiver in PAIRS:
|
|
html += f""" <div class="pair-card">
|
|
<div style="font-weight: bold; color: #667eea;">{giver}</div>
|
|
<div class="pair-arrow">↓</div>
|
|
<div style="font-weight: bold; color: #764ba2;">{receiver}</div>
|
|
</div>
|
|
"""
|
|
|
|
html += """ </div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Historie pro Person</h2>
|
|
<table class="history-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Person</th>
|
|
<th>2018</th>
|
|
<th>2019</th>
|
|
<th>2021</th>
|
|
<th>2022</th>
|
|
<th>2023</th>
|
|
<th>2024</th>
|
|
<th>2025</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
"""
|
|
|
|
for n in NAMES:
|
|
hist_2018 = [t[1] for t in PAIRS_2018 if t[0] == n]
|
|
hist_2019 = [t[1] for t in PAIRS_2019 if t[0] == n]
|
|
hist_2021 = [t[1] for t in PAIRS_2021 if t[0] == n]
|
|
hist_2022 = [t[1] for t in PAIRS_2022 if t[0] == n]
|
|
hist_2023 = [t[1] for t in PAIRS_2023 if t[0] == n]
|
|
hist_2024 = [t[1] for t in PAIRS_2024 if t[0] == n]
|
|
hist_2025 = [t[1] for t in PAIRS if t[0] == n]
|
|
|
|
html += f""" <tr>
|
|
<td><strong>{n}</strong></td>
|
|
<td>{', '.join(hist_2018) if hist_2018 else '-'}</td>
|
|
<td>{', '.join(hist_2019) if hist_2019 else '-'}</td>
|
|
<td>{', '.join(hist_2021) if hist_2021 else '-'}</td>
|
|
<td>{', '.join(hist_2022) if hist_2022 else '-'}</td>
|
|
<td>{', '.join(hist_2023) if hist_2023 else '-'}</td>
|
|
<td>{', '.join(hist_2024) if hist_2024 else '-'}</td>
|
|
<td><strong style="color: #667eea;">{', '.join(hist_2025) if hist_2025 else '-'}</strong></td>
|
|
</tr>
|
|
"""
|
|
|
|
html += """ </tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Verletzungsprüfung</h2>
|
|
"""
|
|
|
|
violations_found = violations_data.get('violations_found', False)
|
|
violation_class = "" if violations_found else "no-violations"
|
|
|
|
html += f""" <div class="violation-section {violation_class}">
|
|
"""
|
|
|
|
# Sex pair violations
|
|
sex_pair_violations = violations_data.get('sex_pair_violations', [])
|
|
if sex_pair_violations:
|
|
html += """ <div class="violation-item">
|
|
<span class="violation-badge badge-error">❌ VERLETZUNG</span>
|
|
<strong>Verbotene Paare:</strong>
|
|
<ul style="margin-top: 10px; margin-left: 20px;">
|
|
"""
|
|
for giver, receiver in sex_pair_violations:
|
|
html += f" <li>{giver} → {receiver}</li>\n"
|
|
html += """ </ul>
|
|
</div>
|
|
"""
|
|
else:
|
|
html += """ <div class="violation-item">
|
|
<span class="violation-badge badge-success">✓ OK</span>
|
|
<strong>Verbotene Paare:</strong> Keine Verletzungen
|
|
</div>
|
|
"""
|
|
|
|
# Previous year violations
|
|
prev_year_violations = violations_data.get('prev_year_violations', [])
|
|
if prev_year_violations:
|
|
html += """ <div class="violation-item">
|
|
<span class="violation-badge badge-error">❌ VERLETZUNG</span>
|
|
<strong>Vorjahrespaare:</strong>
|
|
<ul style="margin-top: 10px; margin-left: 20px;">
|
|
"""
|
|
for year, giver, receiver, direction in prev_year_violations:
|
|
html += f" <li><span class='year-badge'>{year}</span> {giver} → {receiver} ({direction})</li>\n"
|
|
html += """ </ul>
|
|
</div>
|
|
"""
|
|
else:
|
|
html += """ <div class="violation-item">
|
|
<span class="violation-badge badge-success">✓ OK</span>
|
|
<strong>Vorjahrespaare:</strong> Keine Verletzungen
|
|
</div>
|
|
"""
|
|
|
|
# Mutual violations
|
|
mutual_violations = violations_data.get('mutual_violations', [])
|
|
if mutual_violations:
|
|
html += """ <div class="violation-item">
|
|
<span class="violation-badge badge-error">❌ VERLETZUNG</span>
|
|
<strong>Gegenseitiges Beschenken:</strong>
|
|
<ul style="margin-top: 10px; margin-left: 20px;">
|
|
"""
|
|
for g1, r1, g2, r2 in mutual_violations:
|
|
html += f" <li>{g1} ↔ {r1}</li>\n"
|
|
html += """ </ul>
|
|
</div>
|
|
"""
|
|
else:
|
|
html += """ <div class="violation-item">
|
|
<span class="violation-badge badge-success">✓ OK</span>
|
|
<strong>Gegenseitiges Beschenken:</strong> Keine Verletzungen
|
|
</div>
|
|
"""
|
|
|
|
# Assignment violations
|
|
assignment_issues = violations_data.get('assignment_issues', {})
|
|
if assignment_issues.get('missing_givers') or assignment_issues.get('missing_receivers') or \
|
|
assignment_issues.get('duplicate_givers') or assignment_issues.get('duplicate_receivers'):
|
|
html += """ <div class="violation-item">
|
|
<span class="violation-badge badge-error">❌ VERLETZUNG</span>
|
|
<strong>Zuordnungsfehler:</strong>
|
|
<ul style="margin-top: 10px; margin-left: 20px;">
|
|
"""
|
|
if assignment_issues.get('missing_givers'):
|
|
html += f" <li>Fehlende Geber: {', '.join(assignment_issues['missing_givers'])}</li>\n"
|
|
if assignment_issues.get('missing_receivers'):
|
|
html += f" <li>Fehlende Empfänger: {', '.join(assignment_issues['missing_receivers'])}</li>\n"
|
|
if assignment_issues.get('duplicate_givers'):
|
|
html += f" <li>Doppelte Geber: {', '.join(set(assignment_issues['duplicate_givers']))}</li>\n"
|
|
if assignment_issues.get('duplicate_receivers'):
|
|
html += f" <li>Doppelte Empfänger: {', '.join(set(assignment_issues['duplicate_receivers']))}</li>\n"
|
|
html += """ </ul>
|
|
</div>
|
|
"""
|
|
else:
|
|
html += """ <div class="violation-item">
|
|
<span class="violation-badge badge-success">✓ OK</span>
|
|
<strong>Zuordnungen:</strong> Alle Teilnehmer geben und erhalten genau einmal
|
|
</div>
|
|
"""
|
|
|
|
html += f""" </div>
|
|
|
|
<div class="summary-box {'summary-valid' if not violations_found else 'summary-invalid'}">
|
|
{'✓ KEINE VERLETZUNGEN - LÖSUNG IST GÜLTIG' if not violations_found else '⚠️ VERLETZUNGEN ERKANNT'}
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Zielfunktionswerte</h2>
|
|
<div class="objective-grid">
|
|
"""
|
|
|
|
for label, value, weighted in obj_data:
|
|
html += f""" <div class="objective-card">
|
|
<div class="objective-label">{label}</div>
|
|
<div class="objective-value">{value:.4f}</div>
|
|
<div style="font-size: 0.9em; color: #666; margin-top: 5px;">→ {weighted:.2f}</div>
|
|
</div>
|
|
"""
|
|
|
|
html += f""" </div>
|
|
<div style="text-align: center; margin-top: 20px; padding: 20px; background: #f5f7fa; border-radius: 10px;">
|
|
<strong style="font-size: 1.3em;">Gesamter Zielfunktionswert: {obj_data[0][1]:.4f}</strong>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
return html
|
|
|
|
|
|
objVal = 0
|
|
c = 0
|
|
final_solution_data = None
|
|
|
|
while objVal == 0:
|
|
|
|
c += 1
|
|
m.optimize()
|
|
|
|
PAIRS = []
|
|
for key in x.keys():
|
|
if x[key].x > 0.1:
|
|
PAIRS.append(key)
|
|
x[key].ub = 0
|
|
|
|
print("\n" + "="*80)
|
|
print(f"SOLUTION #{c}")
|
|
print("="*80)
|
|
|
|
print("\n--- GIFT PAIRS (2025) ---")
|
|
for p in PAIRS:
|
|
print(f" {p[0]} -> {p[1]}")
|
|
|
|
with open(f"sol_2024_{c}.json", "w") as f:
|
|
f.write(json.dumps(PAIRS))
|
|
|
|
print("\n--- HISTORY PER PERSON ---")
|
|
for n in NAMES:
|
|
print(n)
|
|
print("\t2018", [t[1] for t in PAIRS_2018 if t[0] == n])
|
|
print("\t2019", [t[1] for t in PAIRS_2019 if t[0] == n])
|
|
print("\t2021", [t[1] for t in PAIRS_2021 if t[0] == n])
|
|
print("\t2022", [t[1] for t in PAIRS_2022 if t[0] == n])
|
|
print("\t2023", [t[1] for t in PAIRS_2023 if t[0] == n])
|
|
print("\t2024", [t[1] for t in PAIRS_2024 if t[0] == n])
|
|
print("\t2025", [t[1] for t in PAIRS if t[0] == n])
|
|
|
|
# Check for violations
|
|
print("\n" + "="*80)
|
|
print("VIOLATION CHECKS")
|
|
print("="*80)
|
|
|
|
violations_found = False
|
|
|
|
# Check same-sex pairs
|
|
# same_sex_violations = []
|
|
# for giver, receiver in PAIRS:
|
|
# if (giver in MEN and receiver in MEN) or (giver in WOMEN and receiver in WOMEN):
|
|
# same_sex_violations.append((giver, receiver))
|
|
|
|
# if same_sex_violations:
|
|
# violations_found = True
|
|
# print("\n❌ SAME-SEX PAIR VIOLATIONS:")
|
|
# for giver, receiver in same_sex_violations:
|
|
# print(f" {giver} -> {receiver}")
|
|
# else:
|
|
# print("\n✓ No same-sex pair violations")
|
|
|
|
# Check sex pairs (forbidden pairs)
|
|
sex_pair_violations = []
|
|
for giver, receiver in PAIRS:
|
|
if (giver, receiver) in SEX_PAIRS or (receiver, giver) in SEX_PAIRS:
|
|
sex_pair_violations.append((giver, receiver))
|
|
|
|
if sex_pair_violations:
|
|
violations_found = True
|
|
print("\n❌ SEX PAIR VIOLATIONS (forbidden pairs):")
|
|
for giver, receiver in sex_pair_violations:
|
|
print(f" {giver} -> {receiver}")
|
|
else:
|
|
print("\n✓ No sex pair violations")
|
|
|
|
# Check previous year violations
|
|
prev_year_violations = []
|
|
|
|
for giver, receiver in PAIRS:
|
|
# Check 2018
|
|
if (giver, receiver) in PAIRS_2018:
|
|
prev_year_violations.append(("2018", giver, receiver, "same direction"))
|
|
if (receiver, giver) in PAIRS_2018:
|
|
prev_year_violations.append(("2018", giver, receiver, "reverse"))
|
|
|
|
# Check 2019
|
|
if (giver, receiver) in PAIRS_2019:
|
|
prev_year_violations.append(("2019", giver, receiver, "same direction"))
|
|
if (receiver, giver) in PAIRS_2019:
|
|
prev_year_violations.append(("2019", giver, receiver, "reverse"))
|
|
|
|
# Check 2021
|
|
if (giver, receiver) in PAIRS_2021:
|
|
prev_year_violations.append(("2021", giver, receiver, "same direction"))
|
|
if (receiver, giver) in PAIRS_2021:
|
|
prev_year_violations.append(("2021", giver, receiver, "reverse"))
|
|
|
|
# Check 2022
|
|
if (giver, receiver) in PAIRS_2022:
|
|
prev_year_violations.append(("2022", giver, receiver, "same direction"))
|
|
if (receiver, giver) in PAIRS_2022:
|
|
prev_year_violations.append(("2022", giver, receiver, "reverse"))
|
|
|
|
# Check 2023
|
|
if (giver, receiver) in PAIRS_2023:
|
|
prev_year_violations.append(("2023", giver, receiver, "same direction"))
|
|
if (receiver, giver) in PAIRS_2023:
|
|
prev_year_violations.append(("2023", giver, receiver, "reverse"))
|
|
|
|
# Check 2024
|
|
if (giver, receiver) in PAIRS_2024:
|
|
prev_year_violations.append(("2024", giver, receiver, "same direction"))
|
|
if (receiver, giver) in PAIRS_2024:
|
|
prev_year_violations.append(("2024", giver, receiver, "reverse"))
|
|
|
|
if prev_year_violations:
|
|
violations_found = True
|
|
print("\n❌ PREVIOUS YEAR PAIR VIOLATIONS:")
|
|
for year, giver, receiver, direction in prev_year_violations:
|
|
print(f" {year}: {giver} -> {receiver} ({direction})")
|
|
else:
|
|
print("\n✓ No previous year pair violations")
|
|
|
|
# Check mutual gifting (should not happen)
|
|
mutual_violations = []
|
|
pair_set = set(PAIRS)
|
|
for giver, receiver in PAIRS:
|
|
if (receiver, giver) in pair_set:
|
|
mutual_violations.append((giver, receiver, receiver, giver))
|
|
|
|
if mutual_violations:
|
|
violations_found = True
|
|
print("\n❌ MUTUAL GIFTING VIOLATIONS:")
|
|
for g1, r1, g2, r2 in mutual_violations:
|
|
print(f" {g1} <-> {r1} (mutual)")
|
|
else:
|
|
print("\n✓ No mutual gifting violations")
|
|
|
|
# Check if everyone gives and receives exactly once
|
|
givers = [p[0] for p in PAIRS]
|
|
receivers = [p[1] for p in PAIRS]
|
|
missing_givers = set(NAMES) - set(givers)
|
|
missing_receivers = set(NAMES) - set(receivers)
|
|
duplicate_givers = [name for name in givers if givers.count(name) > 1]
|
|
duplicate_receivers = [name for name in receivers if receivers.count(name) > 1]
|
|
|
|
assignment_issues = {}
|
|
if missing_givers or missing_receivers or duplicate_givers or duplicate_receivers:
|
|
violations_found = True
|
|
print("\n❌ ASSIGNMENT VIOLATIONS:")
|
|
if missing_givers:
|
|
print(f" Missing givers: {missing_givers}")
|
|
assignment_issues['missing_givers'] = missing_givers
|
|
if missing_receivers:
|
|
print(f" Missing receivers: {missing_receivers}")
|
|
assignment_issues['missing_receivers'] = missing_receivers
|
|
if duplicate_givers:
|
|
print(f" Duplicate givers: {set(duplicate_givers)}")
|
|
assignment_issues['duplicate_givers'] = duplicate_givers
|
|
if duplicate_receivers:
|
|
print(f" Duplicate receivers: {set(duplicate_receivers)}")
|
|
assignment_issues['duplicate_receivers'] = duplicate_receivers
|
|
else:
|
|
print("\n✓ All participants give and receive exactly once")
|
|
|
|
# Summary
|
|
print("\n" + "="*80)
|
|
if violations_found:
|
|
print("⚠️ VIOLATIONS DETECTED")
|
|
else:
|
|
print("✓ NO VIOLATIONS - SOLUTION IS VALID")
|
|
print("="*80)
|
|
|
|
# Objective value details
|
|
print(f"\nObjective value: {m.objVal}")
|
|
# print(f" Sex penalty: {sex_penalty.x} -> {100*sex_penalty.x}")
|
|
print(f" Pair penalty: {pair_penalty.x} -> {100*pair_penalty.x}")
|
|
print(f" 2024 penalty: {old_penalty_2024.x} -> {5000*old_penalty_2024.x}")
|
|
print(f" 2024 reverse penalty: {old_penalty_2024_reverse.x} -> {100*old_penalty_2024_reverse.x}")
|
|
print(f" 2023 penalty: {old_penalty_2023.x} -> {1000*old_penalty_2023.x}")
|
|
print(f" 2022 penalty: {old_penalty_2022.x} -> {500*old_penalty_2022.x}")
|
|
print(f" 2021 penalty: {old_penalty_2021.x} -> {100*old_penalty_2021.x}")
|
|
print(f" 2019 penalty: {old_penalty_2019.x} -> {50*old_penalty_2019.x}")
|
|
print(f" 2018 penalty: {old_penalty_2018.x} -> {10*old_penalty_2018.x}")
|
|
print("="*80 + "\n")
|
|
|
|
# Store data for HTML report
|
|
violations_data = {
|
|
'violations_found': violations_found,
|
|
'sex_pair_violations': sex_pair_violations,
|
|
'prev_year_violations': prev_year_violations,
|
|
'mutual_violations': mutual_violations,
|
|
'assignment_issues': assignment_issues
|
|
}
|
|
|
|
obj_data = [
|
|
("Gesamtwert", m.objVal, m.objVal),
|
|
("Pair Penalty", pair_penalty.x, 100 * pair_penalty.x),
|
|
("2024 Penalty", old_penalty_2024.x, 5000 * old_penalty_2024.x),
|
|
("2024 Reverse", old_penalty_2024_reverse.x, 100 * old_penalty_2024_reverse.x),
|
|
("2023 Penalty", old_penalty_2023.x, 1000 * old_penalty_2023.x),
|
|
("2022 Penalty", old_penalty_2022.x, 500 * old_penalty_2022.x),
|
|
("2021 Penalty", old_penalty_2021.x, 100 * old_penalty_2021.x),
|
|
("2019 Penalty", old_penalty_2019.x, 50 * old_penalty_2019.x),
|
|
("2018 Penalty", old_penalty_2018.x, 10 * old_penalty_2018.x),
|
|
]
|
|
|
|
final_solution_data = {
|
|
'PAIRS': PAIRS,
|
|
'c': c,
|
|
'violations_data': violations_data,
|
|
'obj_data': obj_data
|
|
}
|
|
|
|
objVal = m.objVal
|
|
|
|
# Generate HTML report after optimization loop
|
|
if final_solution_data:
|
|
html_report = generate_html_report(
|
|
final_solution_data['PAIRS'],
|
|
final_solution_data['c'],
|
|
final_solution_data['violations_data'],
|
|
final_solution_data['obj_data']
|
|
)
|
|
|
|
html_filename = f"silvester2025_report_{final_solution_data['c']}.html"
|
|
with open(html_filename, "w", encoding="utf-8") as f:
|
|
f.write(html_report)
|
|
|
|
print(f"\n{'='*80}")
|
|
print(f"HTML Report erstellt: {html_filename}")
|
|
print(f"{'='*80}\n")
|
|
|
|
# %%
|
|
|
|
import matplotlib.pyplot as plt
|
|
import networkx as nx
|
|
|
|
G = nx.DiGraph()
|
|
G.clear()
|
|
G.add_nodes_from(NAMES)
|
|
G.add_edges_from(PAIRS)
|
|
|
|
pos = nx.kamada_kawai_layout(G)
|
|
nx.draw_networkx_nodes(G, pos, node_size=500)
|
|
nx.draw_networkx_labels(G, pos)
|
|
nx.draw_networkx_edges(G, pos, edgelist=PAIRS, edge_color="r", arrows=True)
|
|
# nx.draw_networkx_edges(G, pos, edgelist=black_edges, arrows=False)
|
|
plt.savefig(f'silvester2025_{c}.png')
|
|
# plt.savefig(f'silvester2022_{c}.pdf')
|
|
plt.show()
|
|
|
|
|
|
# %%
|