# Fichier: python_cheats/cheatsheets/pulp.txt
# Cheatsheet PuLP Python - Guide Complet


[OK] INTRODUCTION

# PuLP: Bibliothèque Python pour la programmation linéaire (LP) et mixte (MIP)
# Permet de modéliser et résoudre des problèmes d'optimisation
# Interface pour plusieurs solveurs: CBC, GLPK, CPLEX, Gurobi, etc.

# Installation
# pip install pulp

import pulp
from pulp import *


[OK] CONCEPTS DE BASE


# Programmation linéaire (LP):
# Optimiser une fonction objective linéaire
# Sous contraintes linéaires
# Variables continues

# Programmation linéaire mixte (MIP):
# Variables continues ET entières/binaires
# Plus complexe mais plus flexible

# Composants d'un problème:
# 1. Variables de décision
# 2. Fonction objective (à maximiser ou minimiser)
# 3. Contraintes


[OK] CRÉER UN PROBLÈME


# Problème de minimisation
prob = LpProblem("Mon_Probleme", LpMinimize)

# Problème de maximisation
prob = LpProblem("Mon_Probleme", LpMaximize)

# Nom du problème (optionnel mais recommandé)
prob = LpProblem("Optimisation_Production", LpMaximize)

# Sense du problème
# LpMinimize = -1 (minimiser)
# LpMaximize = 1 (maximiser)

# Accéder au sense
print(prob.sense)  # 1 ou -1


[OK] VARIABLES DE DÉCISION


# Variable continue (par défaut)
x = LpVariable("x", lowBound=0)           # x >= 0
y = LpVariable("y", lowBound=0, upBound=10)  # 0 <= y <= 10
z = LpVariable("z")                       # Sans bornes

# Variable entière
n = LpVariable("n", lowBound=0, cat='Integer')
# ou
n = LpVariable("n", lowBound=0, cat=LpInteger)

# Variable binaire (0 ou 1)
b = LpVariable("b", cat='Binary')
# ou
b = LpVariable("b", cat=LpBinary)

# Catégories disponibles:
# 'Continuous' ou LpContinuous (défaut)
# 'Integer' ou LpInteger
# 'Binary' ou LpBinary

# Variable sans borne inférieure
x = LpVariable("x", lowBound=None)

# Variable sans borne supérieure
x = LpVariable("x", upBound=None)

# Variable libre (sans bornes)
x = LpVariable("x", lowBound=None, upBound=None)

# Dictionnaire de variables
# Très utile pour problèmes avec indices
x = LpVariable.dicts("x", ['A', 'B', 'C'], lowBound=0)
# Crée: x['A'], x['B'], x['C']

# Avec indices numériques
x = LpVariable.dicts("x", range(5), lowBound=0)
# Crée: x[0], x[1], x[2], x[3], x[4]

# Matrice de variables (2D)
x = LpVariable.dicts("x", 
                     (range(3), range(4)),
                     lowBound=0)
# Crée: x[(0,0)], x[(0,1)], ..., x[(2,3)]

# Avec noms personnalisés
products = ['Produit_A', 'Produit_B', 'Produit_C']
factories = ['Usine_1', 'Usine_2']
x = LpVariable.dicts("production",
                     ((p, f) for p in products for f in factories),
                     lowBound=0,
                     cat='Integer')
# x[('Produit_A', 'Usine_1')], etc.


[OK] FONCTION OBJECTIVE


# Définir l'objectif (minimisation)
prob = LpProblem("test", LpMinimize)
x = LpVariable("x", lowBound=0)
y = LpVariable("y", lowBound=0)

# Objectif: minimiser 2x + 3y
prob += 2*x + 3*y

# Alternative (explicite)
prob.setObjective(2*x + 3*y)

# Objectif de maximisation
prob = LpProblem("test", LpMaximize)
prob += 5*x + 4*y  # Maximiser profit

# Objectif avec constante
prob += 2*x + 3*y + 100

# Objectif avec somme
products = ['A', 'B', 'C']
profit = {'A': 10, 'B': 15, 'C': 20}
x = LpVariable.dicts("x", products, lowBound=0)

prob += lpSum([profit[p] * x[p] for p in products])

# Objectif complexe
prob += lpSum([profit[p] * x[p] for p in products]) - \
        lpSum([cost[p] * x[p] for p in products])


[OK] CONTRAINTES


# Ajouter contraintes
prob = LpProblem("test", LpMaximize)
x = LpVariable("x", lowBound=0)
y = LpVariable("y", lowBound=0)

# Contrainte d'inégalité (<=)
prob += x + y <= 10, "Contrainte_Capacite"

# Contrainte d'inégalité (>=)
prob += 2*x + 3*y >= 5, "Contrainte_Minimum"

# Contrainte d'égalité
prob += x + y == 8, "Contrainte_Egalite"

# Nom de contrainte (optionnel mais recommandé)
prob += x <= 5, "Max_X"
prob += y <= 7, "Max_Y"

# Contraintes multiples
for i in range(5):
    prob += x[i] <= 10, f"Max_x_{i}"

# Contrainte avec lpSum
products = ['A', 'B', 'C']
x = LpVariable.dicts("x", products, lowBound=0)
weights = {'A': 2, 'B': 3, 'C': 5}

prob += lpSum([weights[p] * x[p] for p in products]) <= 50, "Poids_Max"

# Contrainte logique (si-alors avec variables binaires)
b = LpVariable("b", cat='Binary')
x = LpVariable("x", lowBound=0, upBound=100)

# Si b = 1, alors x >= 20
M = 100  # Big M
prob += x >= 20 - M*(1-b), "Logic_1"
prob += x <= M*b, "Logic_2"

# Contrainte OR (au moins une vraie)
b1 = LpVariable("b1", cat='Binary')
b2 = LpVariable("b2", cat='Binary')
prob += b1 + b2 >= 1, "Au_moins_une"

# Contrainte XOR (exactement une vraie)
prob += b1 + b2 == 1, "Exactement_une"

# Contrainte AND (toutes vraies)
prob += b1 == 1, "B1_vrai"
prob += b2 == 1, "B2_vrai"


[OK] RÉSOUDRE LE PROBLÈME


# Résoudre avec solveur par défaut (CBC)
status = prob.solve()

# Résoudre avec solveur spécifique
status = prob.solve(PULP_CBC_CMD())
status = prob.solve(GLPK_CMD())
status = prob.solve(CPLEX_CMD())
status = prob.solve(GUROBI_CMD())

# Options du solveur
status = prob.solve(PULP_CBC_CMD(msg=1))      # Afficher logs
status = prob.solve(PULP_CBC_CMD(msg=0))      # Pas de logs
status = prob.solve(PULP_CBC_CMD(timeLimit=60))  # Timeout 60s
status = prob.solve(PULP_CBC_CMD(gapRel=0.01))   # Gap relatif 1%

# Vérifier le statut
print(LpStatus[status])
# 'Optimal' - Solution optimale trouvée
# 'Not Solved' - Pas encore résolu
# 'Infeasible' - Aucune solution faisable
# 'Unbounded' - Problème non borné
# 'Undefined' - Statut indéfini

# Constantes de statut
if status == LpStatusOptimal:
    print("Solution optimale trouvée!")
elif status == LpStatusInfeasible:
    print("Problème infaisable")
elif status == LpStatusUnbounded:
    print("Problème non borné")

# Statuts disponibles:
# LpStatusOptimal = 1
# LpStatusNotSolved = 0
# LpStatusInfeasible = -1
# LpStatusUnbounded = -2
# LpStatusUndefined = -3


[OK] RÉCUPÉRER LES RÉSULTATS


# Valeur de la fonction objective
print(f"Valeur optimale: {value(prob.objective)}")
print(f"Valeur optimale: {prob.objective.value()}")

# Valeur des variables
x = LpVariable("x", lowBound=0)
y = LpVariable("y", lowBound=0)

print(f"x = {x.varValue}")
print(f"y = {y.varValue}")

# Alternative
print(f"x = {value(x)}")
print(f"y = {value(y)}")

# Toutes les variables
for v in prob.variables():
    print(f"{v.name} = {v.varValue}")

# Filtrer variables non nulles
for v in prob.variables():
    if v.varValue > 0:
        print(f"{v.name} = {v.varValue}")

# Variables d'un dictionnaire
x = LpVariable.dicts("x", ['A', 'B', 'C'], lowBound=0)
for key in x:
    print(f"x[{key}] = {x[key].varValue}")

# Contraintes et leur valeur (slack/surplus)
for name, constraint in prob.constraints.items():
    print(f"{name}: {constraint.value()}")


[OK] EXEMPLE COMPLET: PROBLÈME DE PRODUCTION


# Problème: Maximiser le profit de production
# Produits: A et B
# Profit: A=10€, B=15€
# Ressources limitées: 100h main d'œuvre, 80 unités matière première
# A nécessite: 2h et 3 unités
# B nécessite: 3h et 2 unités

# Créer problème
prob = LpProblem("Production_Optimale", LpMaximize)

# Variables
x_A = LpVariable("Production_A", lowBound=0, cat='Integer')
x_B = LpVariable("Production_B", lowBound=0, cat='Integer')

# Fonction objective
prob += 10*x_A + 15*x_B, "Profit_Total"

# Contraintes
prob += 2*x_A + 3*x_B <= 100, "Main_Oeuvre"
prob += 3*x_A + 2*x_B <= 80, "Matiere_Premiere"

# Résoudre
status = prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if status == LpStatusOptimal:
    print(f"Production A: {x_A.varValue}")
    print(f"Production B: {x_B.varValue}")
    print(f"Profit: {value(prob.objective)}€")


[OK] PROBLÈME DE TRANSPORT (EXEMPLE CLASSIQUE)


# Minimiser coût de transport de m entrepôts vers n magasins
warehouses = ['W1', 'W2', 'W3']
stores = ['S1', 'S2', 'S3', 'S4']

# Coûts de transport
costs = {
    ('W1', 'S1'): 4, ('W1', 'S2'): 6, ('W1', 'S3'): 8, ('W1', 'S4'): 5,
    ('W2', 'S1'): 5, ('W2', 'S2'): 4, ('W2', 'S3'): 7, ('W2', 'S4'): 6,
    ('W3', 'S1'): 6, ('W3', 'S2'): 5, ('W3', 'S3'): 4, ('W3', 'S4'): 3,
}

# Offre et demande
supply = {'W1': 100, 'W2': 150, 'W3': 200}
demand = {'S1': 80, 'S2': 120, 'S3': 150, 'S4': 100}

# Créer problème
prob = LpProblem("Transport_Problem", LpMinimize)

# Variables: quantité transportée de chaque entrepôt à chaque magasin
routes = [(w, s) for w in warehouses for s in stores]
x = LpVariable.dicts("Route", routes, lowBound=0, cat='Continuous')

# Objectif: minimiser coût total
prob += lpSum([costs[(w, s)] * x[(w, s)] for (w, s) in routes]), "Total_Cost"

# Contraintes d'offre (ne pas dépasser capacité entrepôt)
for w in warehouses:
    prob += lpSum([x[(w, s)] for s in stores]) <= supply[w], f"Supply_{w}"

# Contraintes de demande (satisfaire demande magasin)
for s in stores:
    prob += lpSum([x[(w, s)] for w in warehouses]) >= demand[s], f"Demand_{s}"

# Résoudre
status = prob.solve(PULP_CBC_CMD(msg=0))

# Afficher résultats
if status == LpStatusOptimal:
    print(f"Coût total: {value(prob.objective)}")
    for (w, s) in routes:
        if x[(w, s)].varValue > 0:
            print(f"{w} -> {s}: {x[(w, s)].varValue}")


[OK] PROBLÈME D'AFFECTATION


# Affecter n tâches à n personnes pour minimiser le coût total
people = ['Alice', 'Bob', 'Charlie']
tasks = ['Task1', 'Task2', 'Task3']

# Coûts (temps en heures)
costs = {
    ('Alice', 'Task1'): 5, ('Alice', 'Task2'): 3, ('Alice', 'Task3'): 6,
    ('Bob', 'Task1'): 4, ('Bob', 'Task2'): 6, ('Bob', 'Task3'): 5,
    ('Charlie', 'Task1'): 7, ('Charlie', 'Task2'): 4, ('Charlie', 'Task3'): 3,
}

# Problème
prob = LpProblem("Assignment_Problem", LpMinimize)

# Variables binaires: 1 si personne i fait tâche j
assignments = [(p, t) for p in people for t in tasks]
x = LpVariable.dicts("Assignment", assignments, cat='Binary')

# Objectif
prob += lpSum([costs[(p, t)] * x[(p, t)] for (p, t) in assignments])

# Contraintes: chaque personne fait exactement une tâche
for p in people:
    prob += lpSum([x[(p, t)] for t in tasks]) == 1, f"Person_{p}"

# Contraintes: chaque tâche est faite par exactement une personne
for t in tasks:
    prob += lpSum([x[(p, t)] for p in people]) == 1, f"Task_{t}"

# Résoudre
status = prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if status == LpStatusOptimal:
    print(f"Temps total: {value(prob.objective)}h")
    for (p, t) in assignments:
        if x[(p, t)].varValue == 1:
            print(f"{p} -> {t}")


[OK] PROBLÈME DU SAC À DOS (KNAPSACK)


# Maximiser valeur d'objets dans un sac avec capacité limitée
items = ['Item1', 'Item2', 'Item3', 'Item4', 'Item5']
values = {'Item1': 10, 'Item2': 40, 'Item3': 30, 'Item4': 50, 'Item5': 35}
weights = {'Item1': 5, 'Item2': 4, 'Item3': 6, 'Item4': 3, 'Item5': 7}
capacity = 15

# Problème
prob = LpProblem("Knapsack", LpMaximize)

# Variables binaires: 1 si objet est pris
x = LpVariable.dicts("Item", items, cat='Binary')

# Objectif: maximiser valeur totale
prob += lpSum([values[i] * x[i] for i in items])

# Contrainte: ne pas dépasser capacité
prob += lpSum([weights[i] * x[i] for i in items]) <= capacity

# Résoudre
status = prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if status == LpStatusOptimal:
    print(f"Valeur maximale: {value(prob.objective)}")
    print("Objets sélectionnés:")
    total_weight = 0
    for i in items:
        if x[i].varValue == 1:
            print(f"  {i} (valeur={values[i]}, poids={weights[i]})")
            total_weight += weights[i]
    print(f"Poids total: {total_weight}/{capacity}")


[OK] PROBLÈME DE COUVERTURE (SET COVER)


# Couvrir tous les éléments avec minimum de sous-ensembles
universe = [1, 2, 3, 4, 5, 6]
subsets = {
    'S1': [1, 2, 3],
    'S2': [2, 4],
    'S3': [3, 4, 5],
    'S4': [4, 5, 6],
    'S5': [1, 6]
}
costs = {'S1': 5, 'S2': 10, 'S3': 8, 'S4': 12, 'S5': 7}

# Problème
prob = LpProblem("Set_Cover", LpMinimize)

# Variables: 1 si sous-ensemble est sélectionné
x = LpVariable.dicts("Subset", subsets.keys(), cat='Binary')

# Objectif: minimiser coût total
prob += lpSum([costs[s] * x[s] for s in subsets])

# Contraintes: chaque élément doit être couvert au moins une fois
for elem in universe:
    prob += lpSum([x[s] for s in subsets if elem in subsets[s]]) >= 1, \
            f"Cover_{elem}"

# Résoudre
status = prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if status == LpStatusOptimal:
    print(f"Coût minimum: {value(prob.objective)}")
    print("Sous-ensembles sélectionnés:")
    for s in subsets:
        if x[s].varValue == 1:
            print(f"  {s}: {subsets[s]} (coût={costs[s]})")


[OK] PROBLÈME DE PLANIFICATION (SCHEDULING)


# Planifier des tâches avec contraintes de précédence
tasks = ['T1', 'T2', 'T3', 'T4', 'T5']
durations = {'T1': 3, 'T2': 4, 'T3': 2, 'T4': 5, 'T5': 3}
precedences = [('T1', 'T2'), ('T1', 'T3'), ('T2', 'T4'), ('T3', 'T4'), ('T4', 'T5')]

# Problème: minimiser durée totale du projet
prob = LpProblem("Scheduling", LpMinimize)

# Variables: temps de début de chaque tâche
start = LpVariable.dicts("Start", tasks, lowBound=0, cat='Continuous')

# Variable auxiliaire: temps de fin du projet
makespan = LpVariable("Makespan", lowBound=0)

# Objectif: minimiser makespan
prob += makespan

# Contraintes: makespan >= fin de chaque tâche
for t in tasks:
    prob += makespan >= start[t] + durations[t], f"Finish_{t}"

# Contraintes de précédence
for (t1, t2) in precedences:
    prob += start[t2] >= start[t1] + durations[t1], f"Precedence_{t1}_{t2}"

# Résoudre
status = prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if status == LpStatusOptimal:
    print(f"Durée du projet: {value(prob.objective)}")
    for t in tasks:
        print(f"{t}: début={start[t].varValue}, fin={start[t].varValue + durations[t]}")


[OK] PROBLÈME DE DÉCOUPE (CUTTING STOCK)


# Minimiser le nombre de barres standard pour produire des pièces
bar_length = 100  # Longueur barre standard
pieces = ['P1', 'P2', 'P3']
lengths = {'P1': 45, 'P2': 35, 'P3': 20}
demands = {'P1': 10, 'P2': 8, 'P3': 15}

# Patterns de découpe possibles (générés à l'avance)
patterns = {
    'Pattern1': {'P1': 2, 'P2': 0, 'P3': 0},  # 2x45 = 90
    'Pattern2': {'P1': 1, 'P2': 1, 'P3': 0},  # 45+35 = 80
    'Pattern3': {'P1': 1, 'P2': 0, 'P3': 2},  # 45+2x20 = 85
    'Pattern4': {'P1': 0, 'P2': 2, 'P3': 1},  # 2x35+20 = 90
    'Pattern5': {'P1': 0, 'P2': 1, 'P3': 3},  # 35+3x20 = 95
    'Pattern6': {'P1': 0, 'P2': 0, 'P3': 5},  # 5x20 = 100
}

# Problème
prob = LpProblem("Cutting_Stock", LpMinimize)

# Variables: nombre de barres découpées selon chaque pattern
x = LpVariable.dicts("Pattern", patterns.keys(), lowBound=0, cat='Integer')

# Objectif: minimiser nombre total de barres
prob += lpSum([x[p] for p in patterns])

# Contraintes: satisfaire demande pour chaque pièce
for piece in pieces:
    prob += lpSum([patterns[p][piece] * x[p] for p in patterns]) >= demands[piece], \
            f"Demand_{piece}"

# Résoudre
status = prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if status == LpStatusOptimal:
    print(f"Nombre de barres: {value(prob.objective)}")
    for p in patterns:
        if x[p].varValue > 0:
            print(f"{p}: {x[p].varValue} barres - {patterns[p]}")


[OK] PROBLÈME DE LOCALISATION D'INSTALLATIONS


# Décider où ouvrir des entrepôts pour minimiser coûts
warehouses = ['W1', 'W2', 'W3']
customers = ['C1', 'C2', 'C3', 'C4']

# Coûts fixes d'ouverture
fixed_costs = {'W1': 1000, 'W2': 1200, 'W3': 800}

# Coûts de transport par unité
transport_costs = {
    ('W1', 'C1'): 5, ('W1', 'C2'): 7, ('W1', 'C3'): 8, ('W1', 'C4'): 6,
    ('W2', 'C1'): 6, ('W2', 'C2'): 5, ('W2', 'C3'): 6, ('W2', 'C4'): 7,
    ('W3', 'C1'): 8, ('W3', 'C2'): 6, ('W3', 'C3'): 5, ('W3', 'C4'): 4,
}

# Demande des clients
demand = {'C1': 100, 'C2': 150, 'C3': 120, 'C4': 80}

# Capacité des entrepôts
capacity = {'W1': 200, 'W2': 250, 'W3': 300}

# Problème
prob = LpProblem("Facility_Location", LpMinimize)

# Variables binaires: 1 si entrepôt est ouvert
open_warehouse = LpVariable.dicts("Open", warehouses, cat='Binary')

# Variables continues: quantité livrée de w à c
flow = LpVariable.dicts("Flow", 
                        [(w, c) for w in warehouses for c in customers],
                        lowBound=0)

# Objectif: minimiser coûts fixes + coûts transport
prob += lpSum([fixed_costs[w] * open_warehouse[w] for w in warehouses]) + \
        lpSum([transport_costs[(w, c)] * flow[(w, c)] 
               for w in warehouses for c in customers])

# Contraintes: satisfaire demande
for c in customers:
    prob += lpSum([flow[(w, c)] for w in warehouses]) >= demand[c], \
            f"Demand_{c}"

# Contraintes: respecter capacité
for w in warehouses:
    prob += lpSum([flow[(w, c)] for c in customers]) <= \
            capacity[w] * open_warehouse[w], f"Capacity_{w}"

# Résoudre
status = prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if status == LpStatusOptimal:
    print(f"Coût total: {value(prob.objective)}")
    print("\nEntrepôts ouverts:")
    for w in warehouses:
        if open_warehouse[w].varValue == 1:
            print(f"  {w} (coût fixe: {fixed_costs[w]})")
    
    print("\nLivraisons:")
    for w in warehouses:
        for c in customers:
            if flow[(w, c)].varValue > 0:
                print(f"  {w} -> {c}: {flow[(w, c)].varValue}")


[OK] PROBLÈME DE TOURNÉES (TSP SIMPLIFIÉ)


# Problème du voyageur de commerce (version simple avec contraintes)
cities = ['A', 'B', 'C', 'D']
distances = {
    ('A', 'B'): 10, ('A', 'C'): 15, ('A', 'D'): 20,
    ('B', 'A'): 10, ('B', 'C'): 35, ('B', 'D'): 25,
    ('C', 'A'): 15, ('C', 'B'): 35, ('C', 'D'): 30,
    ('D', 'A'): 20, ('D', 'B'): 25, ('D', 'C'): 30,
}

# Problème
prob = LpProblem("TSP", LpMinimize)

# Variables binaires: 1 si on va de i à j
x = LpVariable.dicts("Route", 
                     [(i, j) for i in cities for j in cities if i != j],
                     cat='Binary')

# Objectif: minimiser distance totale
prob += lpSum([distances[(i, j)] * x[(i, j)] 
               for i in cities for j in cities if i != j])

# Contraintes: entrer dans chaque ville exactement une fois
for j in cities:
    prob += lpSum([x[(i, j)] for i in cities if i != j]) == 1, \
            f"Enter_{j}"

# Contraintes: sortir de chaque ville exactement une fois
for i in cities:
    prob += lpSum([x[(i, j)] for j in cities if i != j]) == 1, \
            f"Leave_{i}"

# Note: contraintes de sous-tours nécessaires pour TSP complet
# (non incluses ici pour simplicité)

# Résoudre
status = prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if status == LpStatusOptimal:
    print(f"Distance totale: {value(prob.objective)}")
    print("Tournée:")
    for i in cities:
        for j in cities:
            if i != j and x[(i, j)].varValue == 1:
                print(f"  {i} -> {j}")


[OK] lpSum() - FONCTION CLÉS


# lpSum() est optimisée pour sommes de variables PuLP
# Plus efficace que sum() standard

# Somme simple
x = LpVariable.dicts("x", range(5), lowBound=0)
total = lpSum([x[i] for i in range(5)])

# Somme avec coefficients
coeffs = [2, 3, 4, 5, 6]
expr = lpSum([coeffs[i] * x[i] for i in range(5)])

# Somme de produits
y = LpVariable.dicts("y", range(5), lowBound=0)
expr = lpSum([x[i] * y[i] for i in range(5)])

# Somme conditionnelle
expr = lpSum([x[i] for i in range(10) if i % 2 == 0])

# Somme 2D
matrix = LpVariable.dicts("m", (range(3), range(4)), lowBound=0)
total = lpSum([matrix[(i, j)] for i in range(3) for j in range(4)])

# Alternative: sum() fonctionne aussi mais moins efficace
total = sum([x[i] for i in range(5)])

# lpSum vs sum
# lpSum() : optimisé pour PuLP, plus rapide pour grandes sommes
# sum()   : fonction Python standard, fonctionne mais moins efficace


[OK] lpDot() - PRODUIT SCALAIRE


# Produit scalaire de deux vecteurs
from pulp import lpDot

x = LpVariable.dicts("x", range(5), lowBound=0)
coeffs = [1, 2, 3, 4, 5]

# Avec lpDot
expr = lpDot(coeffs, [x[i] for i in range(5)])

# Équivalent avec lpSum
expr = lpSum([coeffs[i] * x[i] for i in range(5)])

# lpDot est plus concis et lisible


[OK] value() - OBTENIR VALEURS


# Obtenir valeur d'une expression
x = LpVariable("x", lowBound=0)
y = LpVariable("y", lowBound=0)

# Après résolution
prob.solve()

# Valeur variable
val_x = value(x)
val_y = value(y)

# Valeur expression
expr = 2*x + 3*y
val_expr = value(expr)

# Valeur objective
val_obj = value(prob.objective)

# Si non résolu, retourne None
if value(x) is not None:
    print(f"x = {value(x)}")


[OK] EXPORT/IMPORT DE MODÈLES


# Écrire modèle au format LP
prob.writeLP("mon_probleme.lp")

# Format MPS (standard industrie)
prob.writeMPS("mon_probleme.mps")

# Format JSON
import json
prob_dict = prob.toDict()
with open("mon_probleme.json", "w") as f:
    json.dump(prob_dict, f)

# Lire modèle depuis fichier
prob = LpProblem.fromMPS("mon_probleme.mps")
prob = LpProblem.fromDict(prob_dict)

# Afficher modèle (debug)
print(prob)


[OK] SOLVEURS DISPONIBLES


# CBC (défaut, open-source, inclus avec PuLP)
solver = PULP_CBC_CMD()

# GLPK (GNU Linear Programming Kit)
solver = GLPK_CMD()

# CPLEX (commercial, très puissant)
solver = CPLEX_CMD()

# Gurobi (commercial, très rapide)
solver = GUROBI_CMD()

# COIN-OR (suite open-source)
solver = COIN_CMD()

# Options communes à tous les solveurs
solver = PULP_CBC_CMD(
    msg=1,              # 0=pas de log, 1=logs
    timeLimit=300,      # Timeout en secondes
    gapRel=0.01,       # Gap de tolérance relatif (1%)
    gapAbs=0.1,        # Gap de tolérance absolu
    threads=4,         # Nombre de threads
    options=['option1', 'option2']  # Options spécifiques
)

# Vérifier solveurs disponibles
print(listSolvers())
print(listSolvers(onlyAvailable=True))

# Utiliser premier solveur disponible
solver = getSolver('PULP_CBC_CMD', msg=False)


[OK] OPTIONS AVANCÉES CBC


solver = PULP_CBC_CMD(
    msg=1,                    # Verbosité
    maxSeconds=300,          # Timeout
    fracGap=0.01,           # Gap fractionnaire
    maxNodes=1000000,       # Max nœuds arbre recherche
    threads=4,              # Parallélisme
    presolve=1,             # Pré-résolution (0=off, 1=on)
    cuts=1,                 # Coupes (0=off, 1=on)
    strong=5,               # Strong branching
    options=['printingOptions all']  # Options CBC raw
)


[OK] PROBLÈMES MULTI-OBJECTIFS


# PuLP ne supporte pas nativement multi-objectifs
# Solutions:

# 1. Somme pondérée
prob = LpProblem("Multi_Objective", LpMaximize)
x = LpVariable.dicts("x", range(5), lowBound=0)

# Deux objectifs: profit et qualité
profit = lpSum([10*x[i] for i in range(5)])
quality = lpSum([5*x[i] for i in range(5)])

# Objectif combiné avec poids
w1, w2 = 0.7, 0.3  # Poids
prob += w1 * profit + w2 * quality

# 2. Optimisation lexicographique (hiérarchique)
# Optimiser objectif 1, puis objectif 2 sans dégrader objectif 1

# Étape 1: optimiser profit
prob1 = LpProblem("Obj1", LpMaximize)
prob1 += profit
prob1.solve()
max_profit = value(prob1.objective)

# Étape 2: optimiser qualité avec contrainte sur profit
prob2 = LpProblem("Obj2", LpMaximize)
prob2 += quality
prob2 += profit >= 0.95 * max_profit  # 95% de l'optimal
prob2.solve()

# 3. Méthode epsilon-contrainte
# Optimiser un objectif, autres deviennent contraintes
prob = LpProblem("Epsilon", LpMaximize)
prob += profit
prob += quality >= 100  # Seuil minimum qualité
prob.solve()


[OK] VARIABLES SEMI-CONTINUES


# Variable qui est soit 0, soit dans [L, U]
# Semi-continuous: x = 0 ou L <= x <= U

x = LpVariable("x", lowBound=10, upBound=100)
b = LpVariable("b", cat='Binary')

# Si b=0, alors x=0; si b=1, alors 10 <= x <= 100
prob += x <= 100 * b
prob += x >= 10 * b


[OK] VARIABLES SOS (Special Ordered Sets)


# SOS Type 1: au plus une variable non-nulle
# SOS Type 2: au plus deux variables adjacentes non-nulles

# PuLP ne supporte pas directement SOS
# Mais peut être simulé avec variables binaires

# SOS1: au plus une variable > 0
x = LpVariable.dicts("x", range(5), lowBound=0, upBound=100)
b = LpVariable.dicts("b", range(5), cat='Binary')

# x[i] > 0 ssi b[i] = 1
M = 100
for i in range(5):
    prob += x[i] <= M * b[i]

# Au plus un b[i] = 1
prob += lpSum([b[i] for i in range(5)]) <= 1


[OK] PROBLÈME DE MÉLANGE (BLENDING)


# Mélanger ingrédients pour satisfaire spécifications
ingredients = ['I1', 'I2', 'I3']

# Propriétés des ingrédients (%)
properties = {
    'protein': {'I1': 20, 'I2': 15, 'I3': 10},
    'fat': {'I1': 5, 'I2': 10, 'I3': 15},
    'fiber': {'I1': 10, 'I2': 5, 'I3': 20}
}

# Coûts
costs = {'I1': 10, 'I2': 8, 'I3': 6}

# Spécifications du produit final (quantité totale = 100kg)
target_quantity = 100
min_protein = 15  # Au moins 15% protéines
max_fat = 12      # Au plus 12% graisse
min_fiber = 8     # Au moins 8% fibres

# Problème
prob = LpProblem("Blending", LpMinimize)

# Variables: quantité de chaque ingrédient
x = LpVariable.dicts("Ingredient", ingredients, lowBound=0)

# Objectif: minimiser coût
prob += lpSum([costs[i] * x[i] for i in ingredients])

# Contrainte: quantité totale
prob += lpSum([x[i] for i in ingredients]) == target_quantity

# Contraintes de qualité
prob += lpSum([properties['protein'][i] * x[i] for i in ingredients]) >= \
        min_protein * target_quantity

prob += lpSum([properties['fat'][i] * x[i] for i in ingredients]) <= \
        max_fat * target_quantity

prob += lpSum([properties['fiber'][i] * x[i] for i in ingredients]) >= \
        min_fiber * target_quantity

# Résoudre
prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if prob.status == LpStatusOptimal:
    print(f"Coût minimum: {value(prob.objective)}")
    for i in ingredients:
        print(f"{i}: {x[i].varValue}kg ({x[i].varValue/target_quantity*100:.1f}%)")


[OK] PROBLÈME DE PORTEFEUILLE (PORTFOLIO)


# Sélectionner actions pour maximiser rendement avec risque limité
stocks = ['S1', 'S2', 'S3', 'S4', 'S5']

# Rendements attendus (%)
returns = {'S1': 12, 'S2': 15, 'S3': 8, 'S4': 20, 'S5': 10}

# Risques (écart-type %)
risks = {'S1': 10, 'S2': 20, 'S3': 5, 'S4': 25, 'S5': 8}

# Budget total
budget = 100000

# Contraintes
max_risk = 15  # Risque moyen maximal
min_stocks = 2  # Au moins 2 actions
max_per_stock = 0.4  # Max 40% dans une action

# Problème
prob = LpProblem("Portfolio", LpMaximize)

# Variables: montant investi dans chaque action
x = LpVariable.dicts("Investment", stocks, lowBound=0)

# Variables binaires: 1 si action sélectionnée
y = LpVariable.dicts("Selected", stocks, cat='Binary')

# Objectif: maximiser rendement espéré
prob += lpSum([returns[s] * x[s] / budget for s in stocks])

# Contrainte: budget total
prob += lpSum([x[s] for s in stocks]) == budget

# Contrainte: risque moyen
prob += lpSum([risks[s] * x[s] for s in stocks]) / budget <= max_risk

# Contrainte: minimum d'actions
prob += lpSum([y[s] for s in stocks]) >= min_stocks

# Contrainte: max par action
for s in stocks:
    prob += x[s] <= max_per_stock * budget

# Lier x et y (si x[s] > 0 alors y[s] = 1)
for s in stocks:
    prob += x[s] <= budget * y[s]

# Résoudre
prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if prob.status == LpStatusOptimal:
    print(f"Rendement attendu: {value(prob.objective):.2f}%")
    print("\nAllocations:")
    total_risk = 0
    for s in stocks:
        if x[s].varValue > 0:
            pct = x[s].varValue / budget * 100
            print(f"{s}: {x[s].varValue:.0f}€ ({pct:.1f}%)")
            total_risk += risks[s] * x[s].varValue
    print(f"\nRisque moyen: {total_risk/budget:.2f}%")


[OK] PROBLÈME DE LOT-SIZING


# Planifier production sur plusieurs périodes
periods = range(1, 7)  # 6 périodes

# Demande par période
demand = {1: 100, 2: 150, 3: 200, 4: 120, 5: 180, 6: 140}

# Coûts
production_cost = 10  # Par unité
setup_cost = 500      # Fixe par période si production
holding_cost = 2      # Stockage par unité par période

# Capacités
max_production = 300  # Max par période
max_inventory = 500   # Stockage max

# Problème
prob = LpProblem("Lot_Sizing", LpMinimize)

# Variables
produce = LpVariable.dicts("Produce", periods, lowBound=0, cat='Integer')
inventory = LpVariable.dicts("Inventory", periods, lowBound=0, cat='Integer')
setup = LpVariable.dicts("Setup", periods, cat='Binary')

# Objectif: minimiser coût total
prob += lpSum([production_cost * produce[t] + 
               setup_cost * setup[t] + 
               holding_cost * inventory[t] 
               for t in periods])

# Contrainte: équilibre stock
inventory[0] = 0  # Stock initial
for t in periods:
    if t == 1:
        prob += inventory[t] == produce[t] - demand[t]
    else:
        prob += inventory[t] == inventory[t-1] + produce[t] - demand[t]

# Contrainte: capacité production
for t in periods:
    prob += produce[t] <= max_production * setup[t]

# Contrainte: stockage max
for t in periods:
    prob += inventory[t] <= max_inventory

# Résoudre
prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if prob.status == LpStatusOptimal:
    print(f"Coût total: {value(prob.objective):.0f}€")
    print("\nPlan de production:")
    print("Période | Production | Stock | Setup")
    print("-" * 45)
    for t in periods:
        print(f"   {t}    |    {produce[t].varValue:.0f}     |  {inventory[t].varValue:.0f}  |  {'Oui' if setup[t].varValue == 1 else 'Non'}")


[OK] PROBLÈME DE COLORATION DE GRAPHE


# Attribuer couleurs aux nœuds (aucun voisin même couleur)
nodes = ['A', 'B', 'C', 'D', 'E']
edges = [('A', 'B'), ('A', 'C'), ('B', 'C'), ('B', 'D'), ('C', 'D'), ('D', 'E')]
colors = range(5)  # 5 couleurs possibles

# Problème: minimiser nombre de couleurs
prob = LpProblem("Graph_Coloring", LpMinimize)

# Variables: x[n,c] = 1 si nœud n a couleur c
x = LpVariable.dicts("Color", 
                     [(n, c) for n in nodes for c in colors],
                     cat='Binary')

# Variables: y[c] = 1 si couleur c est utilisée
y = LpVariable.dicts("Used", colors, cat='Binary')

# Objectif: minimiser nombre de couleurs
prob += lpSum([y[c] for c in colors])

# Contrainte: chaque nœud a exactement une couleur
for n in nodes:
    prob += lpSum([x[(n, c)] for c in colors]) == 1

# Contrainte: voisins ont couleurs différentes
for (n1, n2) in edges:
    for c in colors:
        prob += x[(n1, c)] + x[(n2, c)] <= 1

# Lier x et y
for n in nodes:
    for c in colors:
        prob += x[(n, c)] <= y[c]

# Résoudre
prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if prob.status == LpStatusOptimal:
    print(f"Nombre de couleurs: {value(prob.objective):.0f}")
    for n in nodes:
        for c in colors:
            if x[(n, c)].varValue == 1:
                print(f"{n}: Couleur {c}")


[OK] WARM START


# Fournir solution initiale au solveur (accélère résolution)
prob = LpProblem("test", LpMaximize)
x = LpVariable("x", lowBound=0, upBound=10)
y = LpVariable("y", lowBound=0, upBound=10)

prob += x + y
prob += 2*x + y <= 15

# Définir valeurs initiales
x.setInitialValue(5)
y.setInitialValue(5)

# Résoudre avec warm start
prob.solve(PULP_CBC_CMD(warmStart=True))


[OK] CALLBACKS ET MONITORING


# PuLP ne supporte pas directement callbacks
# Mais on peut monitorer avec polling

import time

def solve_with_monitoring(prob, interval=1):
    """Résoudre avec monitoring périodique"""
    # Lancer résolution dans thread séparé
    import threading
    
    status = [None]
    
    def solve_thread():
        status[0] = prob.solve(PULP_CBC_CMD(msg=1))
    
    thread = threading.Thread(target=solve_thread)
    thread.start()
    
    # Monitorer
    while thread.is_alive():
        time.sleep(interval)
        print("Résolution en cours...")
    
    thread.join()
    return status[0]


[OK] ANALYSE DE SENSIBILITÉ


# Analyser impact de changements de paramètres
def sensitivity_analysis(base_prob, param_name, param_values):
    """Analyser sensibilité à un paramètre"""
    results = []
    
    for val in param_values:
        # Copier problème (attention: copie superficielle)
        prob = base_prob.copy()
        
        # Modifier paramètre
        # ... (dépend du contexte)
        
        # Résoudre
        prob.solve(PULP_CBC_CMD(msg=0))
        
        if prob.status == LpStatusOptimal:
            results.append({
                'param_value': val,
                'objective': value(prob.objective)
            })
    
    return results


[OK] RELAXATION LINÉAIRE


# Résoudre version continue du problème entier
prob_mip = LpProblem("MIP", LpMaximize)
x = LpVariable("x", lowBound=0, cat='Integer')
y = LpVariable("y", lowBound=0, cat='Integer')

prob_mip += 3*x + 4*y
prob_mip += x + 2*y <= 10

# Résoudre MIP
prob_mip.solve()
mip_value = value(prob_mip.objective)

# Relaxation: changer variables en continues
prob_lp = LpProblem("LP", LpMaximize)
x_lp = LpVariable("x", lowBound=0, cat='Continuous')
y_lp = LpVariable("y", lowBound=0, cat='Continuous')

prob_lp += 3*x_lp + 4*y_lp
prob_lp += x_lp + 2*y_lp <= 10

prob_lp.solve()
lp_value = value(prob_lp.objective)

print(f"MIP: {mip_value}")
print(f"LP relaxation: {lp_value}")
print(f"Gap: {(lp_value - mip_value) / lp_value * 100:.2f}%")


[OK] CONTRAINTES INDICATRICES


# Si condition alors contrainte active
# Utiliser big-M

x = LpVariable("x", lowBound=0, upBound=100)
y = LpVariable("y", lowBound=0, upBound=100)
b = LpVariable("b", cat='Binary')

M = 1000  # Big M

# Si b=1, alors x + y >= 50
prob += x + y >= 50 - M*(1-b)

# Si b=0, contrainte désactivée (toujours satisfaite)
# Si b=1, contrainte x + y >= 50 active


[OK] CONTRAINTES DISJONCTIVES (OR)


# Au moins une contrainte doit être satisfaite
x = LpVariable("x", lowBound=0)
y = LpVariable("y", lowBound=0)

# (x <= 5) OR (y <= 5)
b1 = LpVariable("b1", cat='Binary')
b2 = LpVariable("b2", cat='Binary')

M = 1000
prob += x <= 5 + M*(1-b1)  # Si b1=1, x <= 5
prob += y <= 5 + M*(1-b2)  # Si b2=1, y <= 5
prob += b1 + b2 >= 1        # Au moins une active


[OK] DÉBOGAGE


# 1. Vérifier faisabilité
prob.solve(PULP_CBC_CMD(msg=1))
if prob.status == LpStatusInfeasible:
    print("Problème infaisable!")
    # Identifier contraintes conflictuelles

# 2. Afficher modèle
print(prob)

# 3. Sauvegarder modèle pour inspection
prob.writeLP("debug.lp")

# 4. Vérifier contraintes individuellement
for name, constraint in prob.constraints.items():
    print(f"{name}: {constraint}")

# 5. Vérifier bornes variables
for v in prob.variables():
    print(f"{v.name}: [{v.lowBound}, {v.upBound}]")

# 6. Mode verbose
prob.solve(PULP_CBC_CMD(msg=1, options=['printingOptions all']))


[OK] BONNES PRATIQUES


# [OK] Toujours nommer problème, variables, contraintes
# [OK] Utiliser LpVariable.dicts pour variables indexées
# [OK] Préférer lpSum() à sum() pour grandes sommes
# [OK] Vérifier statut avant accéder résultats
# [OK] Utiliser cat='Integer' ou 'Binary' si nécessaire
# [OK] Définir bornes appropriées (accélère résolution)
# [OK] Exporter modèle (.lp) pour vérification
# [OK] Tester avec petites instances d'abord
# [OK] Utiliser big-M le plus petit possible
# [OK] Normaliser coefficients (éviter très grands/petits)

# [X] Ne pas oublier de résoudre avant accéder valeurs
# [X] Ne pas utiliser Big-M trop grand (problèmes numériques)
# [X] Ne pas créer variables redondantes
# [X] Attention aux problèmes non-linéaires (PuLP = linéaire)
# [X] Ne pas ignorer status Infeasible/Unbounded


[OK] LIMITES DE PULP


# PuLP est pour programmation LINÉAIRE seulement
# Ne supporte PAS:
# [X] Contraintes non-linéaires (x², x*y, log(x), etc.)
# [X] Objectifs non-linéaires
# [X] Programmation quadratique native (QP)
# [X] Programmation stochastique
# [X] Problèmes très grands (>millions variables)

# Alternatives pour non-linéaire:
# - Pyomo (plus flexible)
# - CVXPY (convexe)
# - scipy.optimize (général)
# - Gekko (dynamique)


[OK] COMPARAISON AVEC ALTERNATIVES


# PuLP vs Pyomo:
# PuLP: Plus simple, syntaxe intuitive, moins de features
# Pyomo: Plus puissant, abstractions avancées, courbe apprentissage

# PuLP vs CVXPY:
# PuLP: LP/MIP
# CVXPY: Optimisation convexe (DCP)

# PuLP vs OR-Tools (Google):
# PuLP: Interface simple, multi-solveurs
# OR-Tools: Plus rapide, plus d'algorithmes, plus complexe

# PuLP vs scipy.optimize:
# PuLP: Modélisation déclarative, LP/MIP
# scipy.optimize: Impératif, non-linéaire, local


[OK] RESSOURCES


# Documentation officielle:
# https://coin-or.github.io/pulp/

# Tutoriels:
# https://coin-or.github.io/pulp/CaseStudies/index.html

# Solveurs open-source:
# CBC: https://github.com/coin-or/Cbc
# GLPK: https://www.gnu.org/software/glpk/

# Livres:
# - "Model Building in Mathematical Programming" (H.P. Williams)
# - "Integer Programming" (Wolsey)


[OK] AIDE-MÉMOIRE RAPIDE


# Créer problème
prob = LpProblem("Name", LpMaximize)  # ou LpMinimize

# Variables
x = LpVariable("x", lowBound=0, upBound=10, cat='Continuous')
y = LpVariable("y", cat='Integer')
b = LpVariable("b", cat='Binary')
vars = LpVariable.dicts("v", indices, lowBound=0)

# Objectif
prob += expression

# Contraintes
prob += expression <= value, "name"
prob += expression >= value, "name"
prob += expression == value, "name"

# Sommes
lpSum([coef[i] * var[i] for i in indices])

# Résoudre
status = prob.solve(PULP_CBC_CMD(msg=0))

# Résultats
if status == LpStatusOptimal:
    print(value(prob.objective))
    print(var.varValue)
    
# Exporter
prob.writeLP("model.lp")


[OK] EXEMPLES COMPLETS SUPPLÉMENTAIRES


# Disponibles dans la documentation officielle:
# - Sudoku Solver
# - Wedding Seating Problem  
# - Bin Packing
# - Cutting Stock Advanced
# - Network Flow
# - Job Shop Scheduling
# - Nurse Rostering
# - etc.

# Template de base pour tout problème:
"""
1. Définir le problème (LpProblem)
2. Créer variables de décision (LpVariable)
3. Définir fonction objective (prob +=)
4. Ajouter contraintes (prob +=)
5. Résoudre (prob.solve())
6. Analyser résultats (status, varValue)
"""