-
iliya.saroukha authorediliya.saroukha authored
simplexe.py 19.29 KiB
"""
Author: Niklaus Eggenberg
Created: 23.02.2023
Simplex Class - contains the code to solve an LP via simplex method
"""
# run command:
# cd
# python c:\HEPIA\neg-teaching-material\Simplexe\main.py C:\HEPIA\neg-teaching-material\2022-2023\2e\lp_test.txt
import numpy as np
import os.path
from os import path
from src.constants import Constants, OptimizationType, OptStatus
import time
import sys
class Simplexe:
def __init__(self, A, B, C, objType):
self.IsPhaseI = False
self.__isRemovingAuxVariables = False
self.__PivotCount = 0
self.__PhaseIPivotCount = 0
self.__PrintDetails = False
self.Costs = C
self.AMatrix = A
self.RHS = B
self.ObjectiveType = objType
self.OptStatus = OptStatus.Unknown
# some calculated stuff
self.NumRows = self.AMatrix.shape[0]
self.NumCols = self.AMatrix.shape[1]
self.hasCycle = False
# self.initBasicVariable = self.__basicVariables.copy()
def SolveProblem(self):
self.__start = time.time()
self.__initTableau()
# make sure we have a feasible solution - go to PhaseI
# phase I runs only if
if (self.__isSolutionFeasible() != True):
print("Initial solution is unfeasible - starting Phase I...")
self.__solvePhaseI()
if self.OptStatus == OptStatus.Infeasible:
self.__end = time.time()
print(
"-------------------------------------------------------------------------------")
print(
"Pase I has non-zero optimum : STOP - problem has not feasible solution!")
print(
"-------------------------------------------------------------------------------")
print("Execution statistics")
self.PrintSolution()
print("------------------------------------------------")
print("LP is INFEASIBLE - no solution exists!")
print("------------------------------------------------")
sys.exit()
else:
self.PrintTableau("Initial feasible solution")
else:
self.OptStatus = OptStatus.Feasible
self.__performPivots()
#
self.__end = time.time()
if self.OptStatus == OptStatus.NotBounded:
print(
"-------------------------------------------------------------------------------")
print("Found unbounded column => solution has no optimal solution")
print(
"-------------------------------------------------------------------------------")
print("Execution statistics")
self.PrintSolution()
if self.OptStatus == OptStatus.NotBounded:
print("------------------------------------------------")
print("LP is UNBOUNDED!")
print("------------------------------------------------")
def __performPivots(self):
# we DO have a feasible solution - go on with the simplex
while (self.OptStatus == OptStatus.Feasible):
self.__pivotTableau(self.__selectPivot())
# self.PrintTableau("After pivot")
def PrintTableau(self, header):
print("------------------------------------------------")
print("Simplex tableau: ", header, " Opt status: ", self.OptStatus)
print("------------------------------------------------")
# make sure to print all in one line - note that the width of the command prompt might wrap automatically
# but output to file works just perfect!
varNames = [self.__varName(item) for item in self.__basicVariables]
varNames.append(self.__padStr("* OBJ *"))
tmpArray = np.array([varNames]).T
tableau = np.append(tmpArray, self.__tableau.copy(), axis=1)
with np.printoptions(precision=3, suppress=True, linewidth=np.inf):
print(tableau)
def ObjValue(self):
objVal = self.__tableau[-1, -1]
# phase I has no associated LP => we always have a min !
if self.IsPhaseI:
return -objVal
return objVal if self.ObjectiveType == OptimizationType.Max else -objVal
def PrintSolution(self):
if self.OptStatus == OptStatus.Optimal:
if self.__PrintDetails:
self.PrintTableau("FINAL TABLEAU")
print("------------------------------------------------")
print("Non-zero variables")
nonZeros = []
print(self.__basicVariables)
for rowId, baseColId in enumerate(self.__basicVariables):
# NOTE: in Phase 1, the LAST row is original objective => skip it:
if (not self.IsPhaseI) & (baseColId < 0) & (rowId < len(self.__basicVariables) - 1):
print("ERROR: basic variable with negative index at position ",
rowId, " value " + baseColId)
sys.exit()
if baseColId >= 0:
nonZeros.append([baseColId, "{} = {}".format(self.__varName(
baseColId), "%.4f" % self.__getBasicVariableValue(rowId, baseColId))])
# sort array
nonZeros = sorted(nonZeros, key=lambda tup: tup[0])
for val in nonZeros:
print(val[1])
else:
print("------------------------------------------------")
print("No solution found...")
#
print("------------------------------------------------")
print("Num rows / cols : ", self.NumRows, " / ", self.NumCols)
print("Tableau dimensions: : ", self.__tableau.shape)
print("Optimiser status : ", self.OptStatus)
print("Objective Value : ", self.ObjValue())
print("Nbr pivots Phase I : ", self.__PhaseIPivotCount)
print("Nbr pivots Phase II : ", self.__PivotCount)
print("Total exec time [s] : ", self.__end - self.__start)
print("------------------------------------------------")
def __padStr(self, str):
return str.ljust(8, " ")
def __varName(self, colId):
if colId < 0:
return "* OBJ_0 *"
if colId < self.NumCols:
return self.__padStr("x[{}]".format(colId))
return self.__padStr("z[{}]".format(colId - self.NumCols))
def __initTableau(self):
# initializes the tableau from the previously loaded LP
# tableau is
# -------------
# | A | I | b |
# |-----------|
# | c | 0 | 0 |
# -------------
# NOTE: to make things easier for Phase I and signs, always make sure b is >= 0 (if originally we have b < 0, simply change the signs of the entire row)
# make sure to perform a deep copy of the coefficients - also make sure all are 2D arrays (i.d. add arrays as [])
tmpA = self.AMatrix.copy()
tmpI = np.identity(self.NumRows)
tmpB = np.array([self.RHS.copy()]).T
# now append an identity matrix to the right and the b vector
self.__tableau = np.append(tmpA, tmpI, axis=1)
self.__tableau = np.append(self.__tableau, tmpB, axis=1)
# last row
tmpC = np.array(
[np.concatenate((self.Costs.copy(), np.zeros(self.NumRows + 1)))])
self.__tableau = np.append(self.__tableau, tmpC, axis=0)
self.__basicVariables = np.arange(
self.NumCols, self.NumCols + self.NumRows, 1, dtype=int)
self.__initBasicVariable = self.__basicVariables.copy()
self.TableauRowCount, self.TableauColCount = self.__tableau.shape
for rowId in range(self.NumRows):
if self.__tableau[rowId, -1] < -Constants.EPS:
self.__tableau[rowId, :] = self.__tableau[rowId, :] * -1.
if self.__PrintDetails:
self.PrintTableau("Initial tableau")
# returns next pivot as an array of leaving row id and entering col Id
# return None if there is pivot (sets optimisation status accoringly before returning)
def __selectPivot(self):
# print(self.__basicVariables)
colId = self.__selectEnteringColumn()
if ((not colId) | colId < 0):
# no more entering column => optimiser status is OPTIMAL (must be - we never select a pivot if the tableau is unfeasible)
self.OptStatus = OptStatus.Optimal
return None
# get leaving row id
rowId = self.__selectLeavingRow(colId)
if (rowId < 0):
# there is a negative reduced cost column that never hits any constraint => problem is NotBounded!
self.OptStatus = OptStatus.NotBounded
return None
# normal case : we do have a standard pivot!
# print(f"Before : {self.__basicVariables}")
# print(f"RowID = {rowId}")
# idx_to_replace = np.argwhere(self.__basicVariables == rowId)[0][0]
self.__basicVariables[rowId] = colId
# print(f"After : {self.__basicVariables}")
# print(idx_to_replace)
return [rowId, colId]
def __getBasicVariableValue(self, rowId, baseColId):
# the value of the basic variable is RHS / coeff in basic variable's colum
return self.__tableau[rowId, -1] / self.__tableau[rowId, baseColId]
def __solvePhaseI(self):
# initializes and solves the Phase I simplexe automatically
phaseISplx = Simplexe(self.AMatrix, self.RHS,
self.Costs, self.ObjectiveType)
phaseISplx.initPhaseI(self)
# if optimal solution of Phase I is NOT 0 => problem has no feasible solution
if phaseISplx.ObjValue() > Constants.EPS:
self.OptStatus = OptStatus.Infeasible
return
# make sure we eliminate ALL auxiliary variables from the current base (auxiliary vars are determined by their id : the all have id >= num cols + num rows)
if self.__PrintDetails:
phaseISplx.PrintTableau("Optimal solution Phase I")
phaseISplx.RemoveAuxiliaryBasicVariables()
phaseISplx.PrintSolution()
# we have a feasible solution => extract feasible solution to proceed with Phase II
# -> first part of the tableau (original matrix)
self.__tableau = phaseISplx.__tableau[0:self.NumRows,
0:self.NumCols+self.NumRows]
# -> append the objective row - previous last row of PhaseI tableau
self.__tableau = np.append(self.__tableau, np.array(
[phaseISplx.__tableau[-2, 0:self.NumCols+self.NumRows]]), axis=0)
# -> append the RHS for all except the last row
self.__tableau = np.append(self.__tableau, np.array(
[phaseISplx.__tableau[0:-1, -1]]).T, axis=1)
# finally the basic variables indices
self.__basicVariables = phaseISplx.__basicVariables[0:-1]
self.__PhaseIPivotCount = phaseISplx.__PivotCount
self.OptStatus = OptStatus.Feasible
def RemoveAuxiliaryBasicVariables(self):
# after resolving PhaseI, we might still have some auxiliary varaibles in the current basis
# if so, make sure we remove them - they always have index >= initial number of columns (i.e. self.NumCols here)
maxId = np.argmax(self.__basicVariables)
while (self.__basicVariables[maxId] >= self.NumCols):
# leaving row is the one corresponding to the auxiliary variable, i.e. row with index maxId
# we now have to retrieve which is the ORIGINAL SLACK COLUMN corresponding to the SAME row as the AUXILIARY variable (there is always one)
# in Phase I, we have 1 additional row (corresponding to the original objective row) => the id of the original SLACK is id of AUXILIARY - (numRows - 1)
originSlackVarId = self.__basicVariables[maxId] - (
self.NumRows - 1)
# pivot ROW ID is current index of auxiliary variable (i.e. maxId), and entering column is the original slack variable, i.e. originSlackVarId
# - perform pivot
self.__isRemovingAuxVariables = True
self.__pivotTableau([maxId, originSlackVarId])
self.__isRemovingAuxVariables = False
# update the max Id
maxId = np.argmax(self.__basicVariables)
# stop measuring time
self.__end = time.time()
###################################################################
########## METHODS TO BE IMPLEMENTED ##########
###################################################################
# returns cheks if the current solution is feasible or not (return True if feasible)
def __isSolutionFeasible(self):
# we MUST have that all BASIC variables are >= 0 to have a FEASIBLE solution
# to iterate over basic variables do:
for rowId, baseColId in enumerate(self.__basicVariables):
if self.__getBasicVariableValue(rowId, baseColId) < 0:
return False
return True
# returns index of entering col Id
# return None or -1 if there is pivot
def __selectEnteringColumn(self):
# find an entering column for pivot => return -1 if none is found!
# print(self.__tableau[0])
# for rowId, baseColId in enumerate(self.__basicVariables):
# print(self.__tableau[rowId, baseColId])
#
if self.__PivotCount != 0 and np.array_equal(self.__basicVariables,
self.__initBasicVariable):
self.hasCycle = True
if self.hasCycle:
for idx, val in enumerate(self.__tableau[-1, :-1]):
if val < 0:
return idx
return -1
else:
if np.size(self.__tableau[-1, :-1][np.where(self.__tableau[-1, :-1]
< 0)]) == 0:
return -1
return np.argmin(self.__tableau[-1, :-1])
# returns leaving row ID
# return None or -1 if there is pivot (sets optimisation status accoringly before returning)
def __selectLeavingRow(self, pivotColId):
# given the entering column, find the leaving row - return the index of the row or -1 if none is found
# iterate using :
# rowCount = self.TableauRowCount - 2 if self.IsPhaseI else self.TableauRowCount - 1
# for index in range(rowCount):
# rowCount = self.TableauRowCount - 2 if self.IsPhaseI else \
# self.TableauRowCount - 1
#
# lhs = self.__tableau[:rowCount, pivotColId]
# rhs = self.__tableau[:rowCount, -1]
#
# ratios = rhs / lhs
#
# candidates = np.where(ratios > 0, ratios, np.inf)
#
# if len((np.unique(candidates))) == 0:
# return -1
#
# return candidates.argmin()
minRatio = float('inf')
leavingRow = -1
rowCount = self.TableauRowCount - 2 if self.IsPhaseI else \
self.TableauRowCount - 1
for index in range(rowCount):
# Si le ration est positif
if self.__tableau[index][pivotColId] > 0:
ratio = self.__tableau[index][-1] / \
self.__tableau[index][pivotColId]
if ratio < minRatio:
minRatio = ratio
leavingRow = index
# print("leaving row :", leavingRow) # vérification ok
return leavingRow
def __pivotTableau(self, pivotIDs):
if pivotIDs is None or pivotIDs[0] < 0 or pivotIDs[1] < 0:
# no pivot => optimiser status is updated in pivot selection => return!
return
# ---- update stats
self.__PivotCount += 1
#
pivotRowId = pivotIDs[0]
pivotColId = pivotIDs[1]
# print("Pivot is ", pivotRowId, " ", pivotColId)
pivotVal = self.__tableau[pivotRowId, pivotColId]
# double-check pivot is actually positive
if pivotVal < Constants.EPS:
# pivoting around a negative value is allowed ONLY if we're doing it while removing auxiliary variables from PhaseI
if not self.__isRemovingAuxVariables:
print(
"ERROR: pivot on non-positive value {} at [{},{}]".format(pivotVal, pivotRowId, pivotColId))
self.OptStatus = OptStatus.ERROR
sys.exit()
# perform pivot (above code ensures the pivot is possible!)
# iterate using
# pivotRow = self.__tableau[pivotRowId, :] / pivotVal
# for rowId in range(self.TableauRowCount):
# Calculer la ligne du pivot normalisée (avoir la valeur 1 à la colonne du pivot)
pivotRow = self.__tableau[pivotRowId, :] / pivotVal
# Pour chaque ligne autre que le pivot
for rowId in range(self.TableauRowCount):
if rowId != pivotRowId:
# Mettre la colonne du pivot à 0
multiplier = self.__tableau[rowId, pivotColId]
self.__tableau[rowId, :] -= multiplier * pivotRow
self.__tableau[pivotRowId, :] = pivotRow
# raise Exception(
# 'Not implemented', 'Simplexe.__pivotTableau: missing code to be implemented.')
# For a given unfeasible initial problem, this method creates the corresponding Tableau of the Phase I, then launches the optimization of Phase I
def initPhaseI(self, simplexe):
# create augmented tableau for Phase I => current
# -- Original ----- # -- Auxiliary -----
# | A | I | b | | A | I | I | b |
# |-----------| |---------------|
# | c | 0 | 0 | | c | 0 | 0 | 0 |
# |-----------| |---------------|
# | d |-1 | 0 | S |
# d = - sum(coeff in column) and S is the -sum vector b (NOTE: we already made sure all elements in b are non-negative!)
# store the fact we are actually using a Phase I - to make sure we don't select a pivot in the "original" objective row!
self.IsPhaseI = True
self.__start = time.time()
# new dimensions (note : all original variables are considered as "normal" variables, whereas all auxiliary ones are now slacks!)
self.NumRows = simplexe.NumRows + 1
self.NumCols = simplexe.NumCols + simplexe.NumRows
self.OptStatus = OptStatus.Feasible
# copy tableau excep LAST row and last column (we will add them last)
tmpCopy = simplexe.__tableau.copy()
self.__tableau = tmpCopy[0:-1, 0:-1]
# add the auxiliary variables and their objective row
self.__tableau = np.append(
self.__tableau, np.identity(simplexe.NumRows), axis=1)
# add the RHS column
self.__tableau = np.append(
self.__tableau, np.array([tmpCopy[0:-1, -1]]).T, axis=1)
# create the tableau for the PhaseI from above initialized arrays!!!
raise Exception('Not implemented',
'Simplexe.initPhaseI: missing code to be implemented.')
# set dimensions
self.TableauRowCount, self.TableauColCount = self.__tableau.shape
if self.__PrintDetails:
self.PrintTableau("Phase 1 Tableau")
self.__performPivots()