Cvičení 3: Rozhodnutí ano/ne a logické vazby¶

Řešení.

Otevřete notebook v Jupyteru nebo Google Colabu. Buňky spouštějte shora dolů pomocí Shift+Enter. Hlavní příklad i krátké demonstrace obsahují úplný kód; v sekci 9 je malý úkol k samostatnému doplnění. V sekci 10 najdete odkaz na samostatný domácí úkol. Data jsou modelová.

Používáme Python 3.12 nebo novější a PuLP 3.3.2, stejně jako ve C2. Nejprve spusťte instalační buňku v sekci 0; případný požadavek na restart jádra (v Colabu relace) se zobrazí přímo v jejím výstupu. Následující kód je určen pro tuto verzi; PuLP 4 má jiné rozhraní.

0. Příprava prostředí — spusťte jako první¶

Tato buňka nainstaluje PuLP 3.3.2 do prostředí aktuálního notebooku, pokud správná verze chybí. Stačí ji spustit; příkazy není potřeba upravovat.

Pokud už máte v paměti PuLP 4, buňka po instalaci výpočet zastaví a vyzve k restartu relace/jádra. V Colabu otevřete nabídku Runtime (Běhové prostředí) a zvolte restart relace; v Jupyteru Kernel → Restart Kernel. Potom spusťte notebook znovu shora. Samotné opakování import pulp starou verzi z paměti neodstraní. Po hlášení „Připraveno“ můžete pokračovat. Instalace vyžaduje připojení k internetu.

import sys
import subprocess
from importlib.metadata import version, PackageNotFoundError

REQUIRED_PULP = "3.3.2"
try:
    installed_pulp = version("pulp")
except PackageNotFoundError:
    installed_pulp = None

if installed_pulp != REQUIRED_PULP:
    subprocess.check_call([
        sys.executable, "-m", "pip", "install", "--quiet",
        "--disable-pip-version-check", "--no-cache-dir", "pulp==3.3.2"
    ])
if version("pulp") != REQUIRED_PULP:
    raise RuntimeError("PuLP 3.3.2 se nenainstalovalo. Zkontrolujte výpis instalace výše.")

loaded_pulp = sys.modules.get("pulp")
if loaded_pulp is not None and getattr(loaded_pulp, "__version__", None) != REQUIRED_PULP:
    raise RuntimeError(
        "PuLP 3.3.2 je nainstalované, ale v paměti zůstává jiná verze. "
        "V Colabu zvolte Runtime > Restart session (případně Restart runtime). "
        "V Jupyteru zvolte Kernel > Restart Kernel. Potom spusťte všechny buňky shora."
    )
print("Připraveno: PuLP 3.3.2 je nainstalované. Pokračujte dalšími buňkami.")
Připraveno: PuLP 3.3.2 je nainstalované. Pokračujte dalšími buňkami.

Co si dnes vyzkoušíme¶

V C2 jsme sestavili model a přečetli výsledek. Dnes se naučíme zapsat podmínky typu „nejvýše jedna možnost“, „právě jedna možnost“ a „B jen tehdy, když A“.

Nejprve si řekneme, které kombinace mají být dovoleny. Potom napíšeme omezení a ověříme, zda skutečně vyjadřuje zamýšlené pravidlo.

import pulp

if pulp.__version__ != "3.3.2":
    raise RuntimeError("Spusťte sekci 0 a postupujte podle pokynu k restartu.")
print("PuLP:", pulp.__version__)
TOL = 1e-7
PuLP: 3.3.2

1. Zadání: program studentské akce¶

Připravujeme studentskou akci a vybíráme z pěti aktivit. Každou buď uskutečníme celou, nebo ji vynecháme. Žádnou neopakujeme a zatím nemáme předepsáno, kolik jich musíme vybrat.

Máme nejvýše 10 tisíc Kč a celkem 10 člověkohodin organizátorů. Člověkohodina je hodina práce jednoho člověka: dva lidé pracující dvě hodiny spotřebují čtyři člověkohodiny. Nejde o délku programu a nesestavujeme časový rozvrh.

Kód Aktivita Náklady [tis. Kč] Čas [člověkohodiny] Přínos [body]
A Robotická dílna 4 3 9
B Programovací dílna 3 4 8
C Vědecký kvíz 3 2 6
D Astronomická přednáška 2 2 4
E Noční pozorování 3 3 9

Data jsou umělá. Body vyjadřují hodnocení organizátorů a v tomto modelu se sčítají; nejsou počtem různých návštěvníků. Také náklady a čas se sčítají, nezávisle na ostatních vybraných aktivitách. Hledáme výběr s nejvyšším celkovým přínosem.

V základní úloze další podmínky nejsou. Organizační pravidla přidáme později, každé nejprve zvlášť.

2. Proměnná znamená rozhodnutí ano/ne¶

Pro každou aktivitu $i\in\{A,B,C,D,E\}$ zavedeme proměnnou $$y_i=\begin{cases}1,&\text{aktivitu uskutečníme},\\0,&\text{aktivitu vynecháme}.\end{cases}$$ Taková proměnná je binární. Neměří počet návštěvníků ani množství aktivity. Hodnota $0{,}5$ by znamenala půl aktivity, což zadání nedovoluje.

Model na papíře¶

$$\begin{aligned} \max\quad &9y_A+8y_B+6y_C+4y_D+9y_E &&\text{(body)},\\ \text{za podmínek}\quad &4y_A+3y_B+3y_C+2y_D+3y_E\le10 &&\text{(tisíce Kč)},\\ &3y_A+4y_B+2y_C+2y_D+3y_E\le10 &&\text{(člověkohodiny)},\\ &y_A,y_B,y_C,y_D,y_E\in\{0,1\}. \end{aligned}$$ Například člen $4y_A$ znamená: robotická dílna stojí 4 tisíce Kč, pokud ji zvolíme, a 0 Kč, pokud ji vynecháme.

3. Sestavení modelu po krocích¶

Krok 1: vstupní data¶

S pěti aktivitami už se vyplatí uložit údaje do slovníků. cost["A"] znamená náklad aktivity A. Seznam activities určuje, přes které aktivity budeme procházet.

Hodnota rozpočtu je 10, protože náklady zadáváme v tisících Kč. Záměnou za 10000 bychom vytvořili jinou úlohu.

activities = ["A", "B", "C", "D", "E"]
cost = {"A": 4, "B": 3, "C": 3, "D": 2, "E": 3}
hours = {"A": 3, "B": 4, "C": 2, "D": 2, "E": 3}
benefit = {"A": 9, "B": 8, "C": 6, "D": 4, "E": 9}
budget = 10
time_limit = 10

print(cost["A"])
4

Krok 2: prázdná maximalizační úloha¶

LpProblem a LpMaximize známe ze C2. Při opakovaném sestavování začněte znovu touto buňkou a pokračujte dolů, aby se omezení nepřidávala podruhé.

model = pulp.LpProblem("student_event", pulp.LpMaximize)

Krok 3: pět binárních proměnných¶

cat="Binary" dovoluje pouze hodnoty 0 a 1. Samotné meze lowBound=0, upBound=1 u spojité proměnné by nestačily: dovolovaly by i zlomky.

Do prázdného slovníku y vložíme jednu proměnnou pro každou aktivitu. Cyklus se postupně provede pro A, B, C, D a E. Zápis f"y_{i}" vytvoří názvy y_A, y_B a tak dále. y["A"] zatím označuje proměnnou modelu, nikoli nalezené číslo.

y = {}
for i in activities:
    y[i] = pulp.LpVariable(f"y_{i}", cat="Binary")

print(y)
{'A': y_A, 'B': y_B, 'C': y_C, 'D': y_D, 'E': y_E}

Krok 4: účelová funkce jako součet¶

Na papíře jsme napsali pět členů. pulp.lpSum(benefit[i] * y[i] for i in activities) udělá totéž: pro každou aktivitu vytvoří člen a všechny sečte. Následující buňka zatím pouze vypíše výraz $9y_A+8y_B+6y_C+4y_D+9y_E$.

print(pulp.lpSum(benefit[i] * y[i] for i in activities))
9*y_A + 8*y_B + 6*y_C + 4*y_D + 9*y_E

Teď výraz přidáme do modelu jako účelovou funkci. Stejně jako ve C2 používáme +=; výraz nemá nerovnost ani rovnost.

model += pulp.lpSum(benefit[i] * y[i] for i in activities), "benefit"

Krok 5: rozpočet¶

Za vybranou aktivitu zaplatíme její celý náklad, za nevybranou nic. Všechny náklady se sečtou a nesmějí překročit společný rozpočet.

model += pulp.lpSum(cost[i] * y[i] for i in activities) <= budget, "budget"

Krok 6: čas organizátorů¶

Stejným způsobem sečteme člověkohodiny. Rozpočet a čas jsou dvě různé podmínky s různými jednotkami.

model += pulp.lpSum(hours[i] * y[i] for i in activities) <= time_limit, "time"

Krok 7: kontrola zapsaného modelu¶

Ve výpisu najděte účelovou funkci, obě omezení a pět proměnných. PuLP 3.3.2 může binární proměnné vypisovat jako 0 <= y_A <= 1 Integer. Celočíselnost společně s těmito mezemi právě dovoluje jen 0 a 1.

print(model)
student_event:
MAXIMIZE
9*y_A + 8*y_B + 6*y_C + 4*y_D + 9*y_E + 0.0
SUBJECT TO
budget: 4 y_A + 3 y_B + 3 y_C + 2 y_D + 3 y_E <= 10

time: 3 y_A + 4 y_B + 2 y_C + 2 y_D + 3 y_E <= 10

VARIABLES
0 <= y_A <= 1 Integer
0 <= y_B <= 1 Integer
0 <= y_C <= 1 Integer
0 <= y_D <= 1 Integer
0 <= y_E <= 1 Integer

4. Řešení a čtení výstupu¶

Model vyřešíme pomocí CBC. Nejdříve přečteme stav, teprve po potvrzení Optimal hodnoty proměnných a hodnotu účelové funkce. Výpis 1.0 znamená vybranou aktivitu, 0.0 vynechanou.

Seznam na konci vybere kódy s hodnotou větší než 0.5. U binárního řešení tím pohodlně odlišíme jedničky od nul; není to postup pro zaokrouhlení spojitého řešení.

status = model.solve(pulp.PULP_CBC_CMD(msg=False))
print(pulp.LpStatus[status])
if status == pulp.LpStatusOptimal:
    for i in activities:
        print(i, y[i].value())
    print("Vybrané aktivity:", [i for i in activities if y[i].value() > 0.5])
    print("Přínos [body]:", pulp.value(model.objective))
Optimal
A 1.0
B 1.0
C 0.0
D 0.0
E 1.0
Vybrané aktivity: ['A', 'B', 'E']
Přínos [body]: 26.0

Výsledek znamená: uskutečnit A, B a E, s celkovým přínosem 26 bodů. Oba workshopy jsou zatím povoleny a pozorování zatím nevyžaduje přednášku; tyto podmínky v základním modelu nejsou.

Kontrola dosazením do zadání¶

Z nalezených nul a jedniček přepočítáme náklady, čas a přínos. Zde sčítáme čísla, proto stačí vestavěné sum. Pomocí all(...) ověříme, že každá hodnota leží u nuly nebo jedničky. Tolerance TOL = 1e-7 je absolutní, v jednotkách dané kontroly. assert zastaví buňku, pokud kontrola neprojde.

if status == pulp.LpStatusOptimal:
    used_cost = sum(cost[i] * y[i].value() for i in activities)
    used_time = sum(hours[i] * y[i].value() for i in activities)
    checked_benefit = sum(benefit[i] * y[i].value() for i in activities)
    binary_ok = all(
        min(abs(y[i].value()), abs(y[i].value() - 1)) <= TOL
        for i in activities
    )
    print("Náklady [tis. Kč]:", used_cost, "/", budget)
    print("Čas [člověkohodiny]:", used_time, "/", time_limit)
    print("Přínos přepočtený z dat [body]:", checked_benefit)
    print("Binarita:", binary_ok)
    assert used_cost <= budget + TOL and used_time <= time_limit + TOL
    assert binary_ok
    assert abs(checked_benefit - pulp.value(model.objective)) <= TOL
Náklady [tis. Kč]: 10.0 / 10
Čas [člověkohodiny]: 10.0 / 10
Přínos přepočtený z dat [body]: 26.0
Binarita: True

Spotřebujeme 10 tisíc Kč a 10 člověkohodin. Při přepočtu vycházíme z původních dat; jejich slovníky proto ještě porovnáme s tabulkou zadání. Samotná kontrola přípustnosti neprokazuje optimalitu, tu zde přebíráme ze stavu řešiče.

Každou následující variantu sestavíme jako nový model s novými proměnnými. Vždy vyjdeme ze základu a přidáme jen pravidlo právě probírané sekce. Pravidla tedy zatím nekumulujeme.

5. Nejvýše jedna možnost¶

Organizátoři chtějí vybrat nejvýše jeden z workshopů A a B. Smíme vybrat A, B nebo ani jeden; nesmíme vybrat oba.

$y_A$ $y_B$ $y_A+y_B$ Dovoleno?
0 0 0 ano
0 1 1 ano
1 0 1 ano
1 1 2 ne

Tabulka přesně odpovídá omezení $y_A+y_B\le1$. Tabulka posuzuje pouze toto pravidlo; celý model musí splnit i rozpočet a čas. Než spustíte další buňky, zkuste odhadnout, zda původní výběr A, B, E projde.

exclusive_model = pulp.LpProblem("exclusive", pulp.LpMaximize)
exclusive_y = {}
for i in activities:
    exclusive_y[i] = pulp.LpVariable(f"exclusive_y_{i}", cat="Binary")
exclusive_model += pulp.lpSum(benefit[i] * exclusive_y[i] for i in activities), "benefit"
exclusive_model += pulp.lpSum(cost[i] * exclusive_y[i] for i in activities) <= budget, "budget"
exclusive_model += pulp.lpSum(hours[i] * exclusive_y[i] for i in activities) <= time_limit, "time"

exclusive_model += exclusive_y["A"] + exclusive_y["B"] <= 1, "one_workshop_at_most"
exclusive_status = exclusive_model.solve(pulp.PULP_CBC_CMD(msg=False))
print(pulp.LpStatus[exclusive_status])
if exclusive_status == pulp.LpStatusOptimal:
    for i in activities:
        print(i, exclusive_y[i].value())
    print("Vybrané aktivity:", [i for i in activities if exclusive_y[i].value() > 0.5])
    print("Přínos [body]:", pulp.value(exclusive_model.objective))
Optimal
A 1.0
B 0.0
C 1.0
D 0.0
E 1.0
Vybrané aktivity: ['A', 'C', 'E']
Přínos [body]: 24.0

Vybereme A, C a E, přínos 24 bodů. Původní výběr obsahoval oba workshopy a nové pravidlo porušoval. Teď ověříme kapacity, binaritu i novou podmínku.

if exclusive_status == pulp.LpStatusOptimal:
    used_cost = sum(cost[i] * exclusive_y[i].value() for i in activities)
    used_time = sum(hours[i] * exclusive_y[i].value() for i in activities)
    checked_benefit = sum(benefit[i] * exclusive_y[i].value() for i in activities)
    binary_ok = all(
        min(abs(exclusive_y[i].value()), abs(exclusive_y[i].value() - 1)) <= TOL
        for i in activities
    )
    print("Náklady [tis. Kč]:", used_cost, "/", budget)
    print("Čas [člověkohodiny]:", used_time, "/", time_limit)
    print("Přínos přepočtený z dat [body]:", checked_benefit)
    print("Binarita:", binary_ok)
    assert used_cost <= budget + TOL and used_time <= time_limit + TOL
    assert binary_ok
    assert abs(checked_benefit - pulp.value(exclusive_model.objective)) <= TOL
    rule_ok = exclusive_y["A"].value() + exclusive_y["B"].value() <= 1 + TOL
    print("Nejvýše jedna z A a B:", rule_ok)
    assert rule_ok
Náklady [tis. Kč]: 10.0 / 10
Čas [člověkohodiny]: 8.0 / 10
Přínos přepočtený z dat [body]: 24.0
Binarita: True
Nejvýše jedna z A a B: True

6. Právě jedna možnost¶

Nyní začneme znovu od základního modelu. Organizátoři vyžadují právě jednu z aktivit C a D: buď kvíz, nebo astronomickou přednášku. Tentokrát nesmíme vynechat obě.

$y_C$ $y_D$ $y_C+y_D$ Dovoleno?
0 0 0 ne
0 1 1 ano
1 0 1 ano
1 1 2 ne

Pravidlo zapisujeme $y_C+y_D=1$, v Pythonu pomocí ==. Jedno = by bylo přiřazení. Ve srovnání s „nejvýše jednou“ navíc zakazujeme součet 0.

Do této varianty nepřenášíme omezení na workshopy A a B.

exactly_model = pulp.LpProblem("exactly", pulp.LpMaximize)
exactly_y = {}
for i in activities:
    exactly_y[i] = pulp.LpVariable(f"exactly_y_{i}", cat="Binary")
exactly_model += pulp.lpSum(benefit[i] * exactly_y[i] for i in activities), "benefit"
exactly_model += pulp.lpSum(cost[i] * exactly_y[i] for i in activities) <= budget, "budget"
exactly_model += pulp.lpSum(hours[i] * exactly_y[i] for i in activities) <= time_limit, "time"

exactly_model += exactly_y["C"] + exactly_y["D"] == 1, "one_of_quiz_and_talk"
exactly_status = exactly_model.solve(pulp.PULP_CBC_CMD(msg=False))
print(pulp.LpStatus[exactly_status])
if exactly_status == pulp.LpStatusOptimal:
    for i in activities:
        print(i, exactly_y[i].value())
    print("Vybrané aktivity:", [i for i in activities if exactly_y[i].value() > 0.5])
    print("Přínos [body]:", pulp.value(exactly_model.objective))
Optimal
A 1.0
B 0.0
C 1.0
D 0.0
E 1.0
Vybrané aktivity: ['A', 'C', 'E']
Přínos [body]: 24.0

Opět vybereme A, C a E, přínos 24 bodů. Stejný výsledek neznamená stejná omezení: například prázdný výběr vyhovuje pravidlu z předchozí sekce, ale ne požadavku právě jedné z C a D.

if exactly_status == pulp.LpStatusOptimal:
    used_cost = sum(cost[i] * exactly_y[i].value() for i in activities)
    used_time = sum(hours[i] * exactly_y[i].value() for i in activities)
    checked_benefit = sum(benefit[i] * exactly_y[i].value() for i in activities)
    binary_ok = all(
        min(abs(exactly_y[i].value()), abs(exactly_y[i].value() - 1)) <= TOL
        for i in activities
    )
    print("Náklady [tis. Kč]:", used_cost, "/", budget)
    print("Čas [člověkohodiny]:", used_time, "/", time_limit)
    print("Přínos přepočtený z dat [body]:", checked_benefit)
    print("Binarita:", binary_ok)
    assert used_cost <= budget + TOL and used_time <= time_limit + TOL
    assert binary_ok
    assert abs(checked_benefit - pulp.value(exactly_model.objective)) <= TOL
    rule_ok = abs(exactly_y["C"].value() + exactly_y["D"].value() - 1) <= TOL
    print("Právě jedna z C a D:", rule_ok)
    assert rule_ok
Náklady [tis. Kč]: 10.0 / 10
Čas [člověkohodiny]: 8.0 / 10
Přínos přepočtený z dat [body]: 24.0
Binarita: True
Právě jedna z C a D: True

7. Jedna aktivita vyžaduje druhou¶

Znovu začneme od základního modelu. Organizátoři tentokrát stanovili, že noční pozorování E smí proběhnout jen společně s astronomickou přednáškou D, která účastníky připraví. Samotná přednáška D je dovolena.

$y_D$ $y_E$ Dovoleno? Význam
0 0 ano nevybereme ani jednu
0 1 ne pozorování bez přednášky
1 0 ano samotná přednáška
1 1 ano přednáška i pozorování

Potřebujeme zakázat jedinou kombinaci: $y_E=1$, $y_D=0$. To přesně zajistí $y_E\le y_D$.

Pozor na směr: $y_D\le y_E$ by vyjadřovalo opačný požadavek. Rovnost $y_E=y_D$ by zakázala i samotnou přednášku a znamenala by „obě, nebo žádná“. Ani jedno není naše zadání.

requires_model = pulp.LpProblem("requires", pulp.LpMaximize)
requires_y = {}
for i in activities:
    requires_y[i] = pulp.LpVariable(f"requires_y_{i}", cat="Binary")
requires_model += pulp.lpSum(benefit[i] * requires_y[i] for i in activities), "benefit"
requires_model += pulp.lpSum(cost[i] * requires_y[i] for i in activities) <= budget, "budget"
requires_model += pulp.lpSum(hours[i] * requires_y[i] for i in activities) <= time_limit, "time"

requires_model += requires_y["E"] <= requires_y["D"], "observation_requires_talk"
requires_status = requires_model.solve(pulp.PULP_CBC_CMD(msg=False))
print(pulp.LpStatus[requires_status])
if requires_status == pulp.LpStatusOptimal:
    for i in activities:
        print(i, requires_y[i].value())
    print("Vybrané aktivity:", [i for i in activities if requires_y[i].value() > 0.5])
    print("Přínos [body]:", pulp.value(requires_model.objective))
Optimal
A 1.0
B 1.0
C 1.0
D 0.0
E 0.0
Vybrané aktivity: ['A', 'B', 'C']
Přínos [body]: 23.0

Vybereme A, B a C, přínos 23 bodů. Přednášku ani pozorování nezvolíme. Nové pravidlo totiž nevyžaduje uskutečnit E; pouze stanoví podmínku pro případ, že E vybereme. Nulové hodnoty D a E tedy pravidlo splňují.

if requires_status == pulp.LpStatusOptimal:
    used_cost = sum(cost[i] * requires_y[i].value() for i in activities)
    used_time = sum(hours[i] * requires_y[i].value() for i in activities)
    checked_benefit = sum(benefit[i] * requires_y[i].value() for i in activities)
    binary_ok = all(
        min(abs(requires_y[i].value()), abs(requires_y[i].value() - 1)) <= TOL
        for i in activities
    )
    print("Náklady [tis. Kč]:", used_cost, "/", budget)
    print("Čas [člověkohodiny]:", used_time, "/", time_limit)
    print("Přínos přepočtený z dat [body]:", checked_benefit)
    print("Binarita:", binary_ok)
    assert used_cost <= budget + TOL and used_time <= time_limit + TOL
    assert binary_ok
    assert abs(checked_benefit - pulp.value(requires_model.objective)) <= TOL
    rule_ok = requires_y["E"].value() <= requires_y["D"].value() + TOL
    print("E pouze při D:", rule_ok)
    assert rule_ok
Náklady [tis. Kč]: 10.0 / 10
Čas [člověkohodiny]: 9.0 / 10
Přínos přepočtený z dat [body]: 23.0
Binarita: True
E pouze při D: True

8. Porovnání variant¶

Každý řádek vychází ze stejného základního modelu a přidává právě uvedené pravidlo.

Varianta Přidané pravidlo Výběr Přínos [body] Náklady [tis. Kč] Čas [člověkohodiny]
Základ žádné A, B, E 26 10 10
Nejvýše jedna $y_A+y_B\le1$ A, C, E 24 10 8
Právě jedna $y_C+y_D=1$ A, C, E 24 10 8
E vyžaduje D $y_E\le y_D$ A, B, C 23 10 9

Nové omezení při maximalizaci téže účelové funkce nemůže nejlepší dosažitelný přínos zvýšit: dovoluje jen některé z původně přípustných výběrů. U jiných dat se optimální hodnota nemusí změnit; nové pravidlo také může způsobit nepřípustnost.

Pro vlastní projekt vždy začněte slovním významem pravidla. Vyzkoušejte několik povolených a zakázaných kombinací, zapište omezení a nakonec ověřte, že nalezené řešení splňuje původní požadavek. Samotný stav Optimal neodhalí, že jste napsali obrácenou implikaci.

9. Krátký samostatný úkol¶

Vraťte se k základní úloze a nyní přidejte současně dvě podmínky:

  • Z workshopů A a B lze vybrat nejvýše jeden.
  • Pozorování E smí proběhnout pouze při uskutečnění přednášky D.

Podmínku na právě jednu z C a D tentokrát nepřidávejte. Nejprve odhadněte, zda některý z dosavadních optimálních výběrů projde oběma požadavky. Potom sestavte nový model, vyřešte jej a vypište stav, výběr a přínos. Dosazením ověřte rozpočet, čas, binaritu i obě nová pravidla. Zkuste to bez kopírování hotových variant; kroky v sekci 3 slouží jako nápověda.

practice_model = pulp.LpProblem("practice", pulp.LpMaximize)
practice_y = {}
for i in activities:
    practice_y[i] = pulp.LpVariable(f"practice_y_{i}", cat="Binary")
practice_model += pulp.lpSum(benefit[i] * practice_y[i] for i in activities), "benefit"
practice_model += pulp.lpSum(cost[i] * practice_y[i] for i in activities) <= budget, "budget"
practice_model += pulp.lpSum(hours[i] * practice_y[i] for i in activities) <= time_limit, "time"

practice_model += practice_y["A"] + practice_y["B"] <= 1, "one_workshop_at_most"
practice_model += practice_y["E"] <= practice_y["D"], "observation_requires_talk"
practice_status = practice_model.solve(pulp.PULP_CBC_CMD(msg=False))
print(pulp.LpStatus[practice_status])
if practice_status == pulp.LpStatusOptimal:
    for i in activities:
        print(i, practice_y[i].value())
    print("Vybrané aktivity:", [i for i in activities if practice_y[i].value() > 0.5])
    print("Přínos [body]:", pulp.value(practice_model.objective))
Optimal
A 1.0
B 0.0
C 0.0
D 1.0
E 1.0
Vybrané aktivity: ['A', 'D', 'E']
Přínos [body]: 22.0
if practice_status == pulp.LpStatusOptimal:
    used_cost = sum(cost[i] * practice_y[i].value() for i in activities)
    used_time = sum(hours[i] * practice_y[i].value() for i in activities)
    checked_benefit = sum(benefit[i] * practice_y[i].value() for i in activities)
    binary_ok = all(
        min(abs(practice_y[i].value()), abs(practice_y[i].value() - 1)) <= TOL
        for i in activities
    )
    print("Náklady [tis. Kč]:", used_cost, "/", budget)
    print("Čas [člověkohodiny]:", used_time, "/", time_limit)
    print("Přínos přepočtený z dat [body]:", checked_benefit)
    print("Binarita:", binary_ok)
    assert used_cost <= budget + TOL and used_time <= time_limit + TOL
    assert binary_ok
    assert abs(checked_benefit - pulp.value(practice_model.objective)) <= TOL
    rule_ok = practice_y["A"].value() + practice_y["B"].value() <= 1 + TOL
    print("Nejvýše jedna z A a B:", rule_ok)
    assert rule_ok
    rule_ok = practice_y["E"].value() <= practice_y["D"].value() + TOL
    print("E pouze při D:", rule_ok)
    assert rule_ok
Náklady [tis. Kč]: 9.0 / 10
Čas [člověkohodiny]: 8.0 / 10
Přínos přepočtený z dat [body]: 22.0
Binarita: True
Nejvýše jedna z A a B: True
E pouze při D: True

Žádný z předchozích optimálních výběrů oběma pravidly neprojde: A, B, E a A, B, C obsahují oba workshopy; A, C, E obsahuje pozorování bez přednášky.

Nový výsledek je A, D a E, přínos 22 bodů. Spotřebujeme 9 tisíc Kč a 8 člověkohodin. Z workshopů zvolíme pouze A a pozorování má doprovodnou přednášku D. Zbývající zdroje nemusíme vyčerpat: podmínky mají tvar horních mezí, nikoli rovností.

10. Domácí úkol: malý výrobní projekt¶

Otevřete notebook k domácímu doplnění. Úloha se třemi kapalnými směsmi procvičuje samostatný postup: slovní zadání, oddělená data, model v PuLP, kontrolu výsledku a jednu změnu kapacity. Instalační buňka je stejná jako zde a ve C2.

Do notebooku doplňte kód i vlastní vysvětlení výsledků. Před dokončením restartujte jádro a spusťte všechny buňky shora. Řešení je v samostatném souboru, stejně jako u ostatních cvičení.