Optimisation avec Python – Real Python

By | juin 22, 2020

python pour débutant

PuLP possède une API de programmation linéaire plus pratique que SciPy. Vous n'avez pas besoin de modifier mathématiquement votre problème ou d'utiliser des vecteurs et des matrices. Tout est plus propre et moins sujet aux erreurs.

Maintenant que vous avez importé PuLP, vous pouvez résoudre vos problèmes.

Exemple 1

Vous allez maintenant résoudre ce système avec PuLP:

mmst-lp-py-eq-2

La première étape consiste à initialiser une instance de LpProblem pour représenter votre modèle:

# Créer le modèle
modèle = LpProblem(Nom="petit problème", sens=LpMaximize)

Vous utilisez le sens paramètre pour choisir d'effectuer ou non la minimisation (LpMinimize ou 1, qui est la valeur par défaut) ou la maximisation (LpMaximize ou -1). Ce choix affectera le résultat de votre problème.

Une fois que vous avez le modèle, vous pouvez définir les variables de décision comme des instances de la LpVariable classe:

# Initialiser les variables de décision
X = LpVariable(Nom="X", lowBound=0)
y = LpVariable(Nom="y", lowBound=0)

Vous devez fournir une borne inférieure avec lowBound = 0 car la valeur par défaut est l'infini négatif. Le paramètre upBound définit la limite supérieure, mais vous pouvez l'omettre ici car elle est définie par défaut sur l'infini positif.

Le paramètre facultatif chat définit la catégorie d'une variable de décision. Si vous travaillez avec des variables continues, vous pouvez utiliser la valeur par défaut "Continu".

Vous pouvez utiliser les variables X et y pour créer d'autres objets PuLP qui représentent des expressions et des contraintes linéaires:

>>>

>>> expression = 2 * X + 4 * y
>>> type(expression)


>>> contrainte = 2 * X + 4 * y > = 8
>>> type(contrainte)

Lorsque vous multipliez une variable de décision par un scalaire ou que vous créez une combinaison linéaire de plusieurs variables de décision, vous obtenez une instance de pulp.LpAffineExpression qui représente une expression linéaire.

De même, vous pouvez combiner des expressions linéaires, des variables et des scalaires avec les opérateurs ==, <=, ou > = pour obtenir des instances de pulp.LpConstraint qui représentent les contraintes linéaires de votre modèle.

Dans cette optique, l'étape suivante consiste à créer les contraintes et la fonction objectif ainsi qu'à les affecter à votre modèle. Vous n'avez pas besoin de créer de listes ou de matrices. Il suffit d'écrire des expressions Python et d'utiliser le + = pour les ajouter au modèle:

# Ajoutez les contraintes au modèle
modèle + = (2 * X + y <= 20, "red_constraint")
modèle + = (4 * X - 5 * y > = -dix, "blue_constraint")
modèle + = (-X + 2 * y > = -2, "yellow_constraint")
modèle + = (-X + 5 * y == 15, "green_constraint")

Dans le code ci-dessus, vous définissez des tuples qui contiennent les contraintes et leurs noms. LpProblem vous permet d'ajouter des contraintes à un modèle en les spécifiant sous forme de tuples. Le premier élément est un LpConstraint exemple. Le deuxième élément est un nom lisible par l'homme pour cette contrainte.

La définition de la fonction objectif est très similaire:

# Ajouter la fonction objectif au modèle
obj_func = X + 2 * y
modèle + = obj_func

Alternativement, vous pouvez utiliser une notation plus courte:

# Ajouter la fonction objectif au modèle
modèle + = X + 2 * y

Vous avez maintenant la fonction objectif ajoutée et le modèle défini.

Pour les problèmes plus importants, il est souvent plus pratique d'utiliser lpSum () avec une liste ou une autre séquence que de répéter la + opérateur. Par exemple, vous pouvez ajouter la fonction objectif au modèle avec cette instruction:

# Ajouter la fonction objectif au modèle
modèle + = lpSum([[[[X, 2 * y])

Il produit le même résultat que l'instruction précédente.

Vous pouvez maintenant voir la définition complète de ce modèle:

>>>

>>> modèle
petit problème:
MAXIMISER
1 * x + 2 * y + 0
SUJET À
contrainte_ rouge: 2 x + y <= 20

contrainte_bleu: 4 x - 5 y> = -10

contrainte_ jaune: - x + 2 y> = -2

contrainte_ verte: - x + 5 y = 15

VARIABLES
x Continu
y Continu

La représentation sous forme de chaîne du modèle contient toutes les données pertinentes: les variables, les contraintes, l'objectif et leurs noms.

Enfin, vous êtes prêt à résoudre le problème. Vous pouvez le faire en appelant .résoudre() sur votre objet modèle. Si vous souhaitez utiliser le solveur par défaut (CBC), vous n'avez pas besoin de passer d'arguments:

# Résoudre le problème
statut = modèle.résoudre()

.résoudre() appelle le solveur sous-jacent, modifie le modèle et renvoie l'état entier de la solution, qui sera 1 si l'optimum est trouvé. Pour le reste des codes d'état, voir LpStatus[].

Vous pouvez obtenir les résultats d'optimisation en tant qu'attributs de modèle. La fonction valeur() et la méthode correspondante .valeur() renvoie les valeurs réelles des attributs:

>>>

>>> impression(F"statut: modèle.statut, LpStatus[[[[modèle.statut]")
état: 1, optimal

>>> impression(F"objectif: modèle.objectif.valeur()")
objectif: 16.8181817

>>> pour var dans modèle.les variables():
...     impression(F"var.Nom: var.valeur()")
...
x: 7.7272727
y: 4.5454545

>>> pour Nom, contrainte dans modèle.contraintes.articles():
...     impression(F"Nom: contrainte.valeur()")
...
contrainte_ rouge: -9.99999993922529e-08
contrainte_ bleue: 18.181818300000003
contrainte_ jaune: 3,3636362999999996
contrainte_ verte: -2,0000000233721948e-07)

model.objective détient la valeur de la fonction objectif, model.constraints contient les valeurs des variables slack et les objets X et y avoir les valeurs optimales des variables de décision. model.variables () renvoie une liste avec les variables de décision:

>>>

>>> modèle.les variables()
[x, y]
>>> modèle.les variables()[[[[0] est X
Vrai
>>> modèle.les variables()[[[[1] est y
Vrai

Comme vous pouvez le voir, cette liste contient les objets exacts créés avec le constructeur de LpVariable.

Les résultats sont approximativement les mêmes que ceux obtenus avec SciPy.

Vous pouvez voir quel solveur a été utilisé en appelant .solver:

>>>

>>> modèle.solveur

La sortie vous informe que le solveur est CBC. Vous n'avez pas spécifié de solveur, PuLP a donc appelé celui par défaut.

Si vous souhaitez exécuter un solveur différent, vous pouvez le spécifier comme argument de .résoudre(). Par exemple, si vous souhaitez utiliser GLPK et que vous l'avez déjà installé, vous pouvez utiliser solveur = GLPK (msg = False) dans la dernière ligne. N'oubliez pas que vous devrez également l'importer:

Maintenant que vous avez importé GLPK, vous pouvez l'utiliser à l'intérieur .résoudre():

# Créer le modèle
modèle = LpProblem(Nom="petit problème", sens=LpMaximize)

# Initialiser les variables de décision
X = LpVariable(Nom="X", lowBound=0)
y = LpVariable(Nom="y", lowBound=0)

# Ajoutez les contraintes au modèle
modèle + = (2 * X + y <= 20, "red_constraint")
modèle + = (4 * X - 5 * y > = -dix, "blue_constraint")
modèle + = (-X + 2 * y > = -2, "yellow_constraint")
modèle + = (-X + 5 * y == 15, "green_constraint")

# Ajouter la fonction objectif au modèle
modèle + = lpSum([[[[X, 2 * y])

# Résoudre le problème
statut = modèle.résoudre(solveur=GLPK(msg=Faux))

le msg Le paramètre est utilisé pour afficher les informations du solveur. msg = Faux désactive l'affichage de ces informations. Si vous souhaitez inclure les informations, omettez simplement msg ou définir msg = True.

Votre modèle est défini et résolu, vous pouvez donc inspecter les résultats de la même manière que dans le cas précédent:

>>>

>>> impression(F"statut: modèle.statut, LpStatus[[[[modèle.statut]")
état: 1, optimal

>>> impression(F"objectif: modèle.objectif.valeur()")
objectif: 16.81817

>>> pour var dans modèle.les variables():
...     impression(F"var.Nom: var.valeur()")
...
x: 7.72727
y: 4.54545

>>> pour Nom, contrainte dans modèle.contraintes.articles():
...     impression(F"Nom: contrainte.valeur()")
...
red_constraint: -1.0000000000509601e-05
contrainte_ bleue: 18.181830000000005
contrainte_ jaune: 3,3636299999999997
contrainte_ verte: -2,000000000279556e-05

Vous avez pratiquement obtenu le même résultat avec GLPK qu'avec SciPy et CBC.

Voyons quel solveur a été utilisé cette fois:

>>>

>>> modèle.solveur

Comme vous l'avez défini ci-dessus avec la déclaration mise en évidence model.solve (solveur = GLPK (msg = False)), le solveur est GLPK.

Vous pouvez également utiliser PuLP pour résoudre des problèmes de programmation linéaire à nombres mixtes. Pour définir un entier ou une variable binaire, passez simplement cat = "Entier" ou cat = "Binaire" à LpVariable. Tout le reste reste le même:

# Créer le modèle
modèle = LpProblem(Nom="petit problème", sens=LpMaximize)

# Initialiser les variables de décision: x est entier, y est continu
X = LpVariable(Nom="X", lowBound=0, chat="Entier")
y = LpVariable(Nom="y", lowBound=0)

# Ajoutez les contraintes au modèle
modèle + = (2 * X + y <= 20, "red_constraint")
modèle + = (4 * X - 5 * y > = -dix, "blue_constraint")
modèle + = (-X + 2 * y > = -2, "yellow_constraint")
modèle + = (-X + 5 * y == 15, "green_constraint")

# Ajouter la fonction objectif au modèle
modèle + = lpSum([[[[X, 2 * y])

# Résoudre le problème
statut = modèle.résoudre()

Dans cet exemple, vous avez une variable entière et obtenez des résultats différents d'avant:

>>>

>>> impression(F"statut: modèle.statut, LpStatus[[[[modèle.statut]")
état: 1, optimal

>>> impression(F"objectif: modèle.objectif.valeur()")
objectif: 15,8

>>> pour var dans modèle.les variables():
...     impression(F"var.Nom: var.valeur()")
...
x: 7.0
y: 4,4

>>> pour Nom, contrainte dans modèle.contraintes.articles():
...     impression(F"Nom: contrainte.valeur()")
...
red_constraint: -1.5999999999999996
contrainte_bleu: 16,0
contrainte_ jaune: 3,8000000000000007
contrainte_ verte: 0,0)

>>> modèle.solveur

Maintenant X est un entier, comme spécifié dans le modèle. (Techniquement, il contient une valeur flottante avec zéro après le point décimal.) Ce fait modifie la solution entière. Voyons cela sur le graphique:

mmst-lp-py-fig-6

Comme vous pouvez le voir, la solution optimale est le point vert le plus à droite sur le fond gris. Ceci est la solution réalisable avec les plus grandes valeurs des deux X et y, lui donnant la valeur maximale de la fonction objectif.

GLPK est également capable de résoudre de tels problèmes.

Exemple 2

Vous pouvez maintenant utiliser PuLP pour résoudre le problème d'allocation des ressources ci-dessus:

mmst-lp-py-eq-4

L'approche pour définir et résoudre le problème est la même que dans l'exemple précédent:

# Définissez le modèle
modèle = LpProblem(Nom="allocation des ressources", sens=LpMaximize)

# Définir les variables de décision
X = je: LpVariable(Nom=F"Xje", lowBound=0) pour je dans gamme(1, 5)

# Ajouter des contraintes
modèle + = (lpSum(X.valeurs()) <= 50, "main d'oeuvre")
modèle + = (3 * X[[[[1] + 2 * X[[[[2] + X[[[[3] <= 100, "material_a")
modèle + = (X[[[[2] + 2 * X[[[[3] + 3 * X[[[[4] <= 90, "material_b")

# Fixer l'objectif
modèle + = 20 * X[[[[1] + 12 * X[[[[2] + 40 * X[[[[3] + 25 * X[[[[4]

# Résoudre le problème d'optimisation
statut = modèle.résoudre()

# Obtenez les résultats
impression(F"statut: modèle.statut, LpStatus[[[[modèle.statut]")
impression(F"objectif: modèle.objectif.valeur()")

pour var dans X.valeurs():
    impression(F"var.Nom: var.valeur()")

pour Nom, contrainte dans modèle.contraintes.articles():
    impression(F"Nom: contrainte.valeur()")

Dans ce cas, vous utilisez le dictionnaire X pour stocker toutes les variables de décision. Cette approche est pratique car les dictionnaires peuvent stocker les noms ou indices des variables de décision sous forme de clés et les LpVariable objets en tant que valeurs. Listes ou tuples de LpVariable les instances peuvent également être utiles.

Le code ci-dessus produit le résultat suivant:

état: 1, optimal
objectif: 1900.0
x1: 5,0
x2: 0,0
x3: 45,0
x4: 0,0
effectif: 0,0
material_a: -40.0
material_b: 0.0

Comme vous pouvez le voir, la solution est cohérente avec celle obtenue avec SciPy. La solution la plus rentable est de produire 5,0 unités du premier produit et 45,0 unités du troisième produit par jour.

Rendons ce problème plus compliqué et intéressant. Supposons que l'usine ne puisse pas produire les premier et troisième produits en parallèle en raison d'un problème de machine. Quelle est la solution la plus rentable dans ce cas?

Vous avez maintenant une autre contrainte logique: si X₁ est positif, alors X₃ doit être nul et vice versa. C'est là que les variables de décision binaires sont très utiles. Vous utiliserez deux variables de décision binaires, y₁ et y₃, cela indiquera si le premier ou le troisième produit est généré:

    1 modèle = LpProblem(Nom="allocation des ressources", sens=LpMaximize)
    2 
    3 # Définir les variables de décision
    4 X = je: LpVariable(Nom=F"Xje", lowBound=0) pour je dans gamme(1, 5)
    5 y = je: LpVariable(Nom=F"yje", chat="Binaire") pour je dans (1, 3)
    6 
    7 # Ajouter des contraintes
    8 modèle + = (lpSum(X.valeurs()) <= 50, "main d'oeuvre")
    9 modèle + = (3 * X[[[[1] + 2 * X[[[[2] + X[[[[3] <= 100, "material_a")
dix modèle + = (X[[[[2] + 2 * X[[[[3] + 3 * X[[[[4] <= 90, "material_b")
11 
12 M = 100
13 modèle + = (X[[[[1] <= y[[[[1] * M, "x1_constraint")
14 modèle + = (X[[[[3] <= y[[[[3] * M, "x3_constraint")
15 modèle + = (y[[[[1] + y[[[[3] <= 1, "y_constraint")
16 
17 # Fixer un objectif
18 modèle + = 20 * X[[[[1] + 12 * X[[[[2] + 40 * X[[[[3] + 25 * X[[[[4]
19 
20 # Résoudre le problème d'optimisation
21 statut = modèle.résoudre()
22 
23 impression(F"statut: modèle.statut, LpStatus[[[[modèle.statut]")
24 impression(F"objectif: modèle.objectif.valeur()")
25 
26 pour var dans modèle.les variables():
27     impression(F"var.Nom: var.valeur()")
28 
29 pour Nom, contrainte dans modèle.contraintes.articles():
30     impression(F"Nom: contrainte.valeur()")

Le code est très similaire à l'exemple précédent à l'exception des lignes en surbrillance. Voici les différences:

  • Ligne 5 définit les variables de décision binaires y[1] et y[3] tenue dans le dictionnaire y.

  • Ligne 12 définit un nombre arbitrairement grand M. La valeur 100 est assez grand dans ce cas car vous ne pouvez pas avoir plus de 100 unités par jour.

  • Ligne 13 dit que si y[1] est nul, alors X[1] doit être nul, sinon il peut s'agir de n'importe quel nombre non négatif.

  • Ligne 14 dit que si y[3] est nul, alors X[3] doit être nul, sinon il peut s'agir de n'importe quel nombre non négatif.

  • Ligne 15 dit que soit y[1] ou y[3] est nul (ou les deux le sont), donc soit X[1] ou X[3] doit également être nul.

Voici la solution:

état: 1, optimal
objectif: 1800.0
x1: 0,0
x2: 0,0
x3: 45,0
x4: 0,0
y1: 0,0
y3: 1,0
effectif: -5,0
material_a: -55.0
material_b: 0.0
x1_constraint: 0.0
x3_constraint: -55,0
contrainte y: 0,0

Il s'avère que l'approche optimale consiste à exclure le premier produit et à n'en produire que le troisième.