1
0
This repository has been archived on 2025-03-06. You can view files and clone it, but cannot push or open issues or pull requests.
Jip J. Dekker f2a1c4e389 Squashed 'software/mza/' content from commit f970a59b17
git-subtree-dir: software/mza
git-subtree-split: f970a59b177c13ca3dd8aaef8cc6681d83b7e813
2021-07-11 16:34:30 +10:00

520 lines
27 KiB
Python

## This class compares and summarizes instance solution results
## for a given set of optimization/satisfiability instances.
## Looks for contradictions. Heuristic performance indicators.
## (c) Gleb Belov@monash.edu 2017
from collections import OrderedDict
# import prettytable
import utils, io ## functools
from utils import strNL
#####################################################################################
################### class CompareLogs ##################
#####################################################################################
## It receives a list of dictionaries, keyed by instances files,
## containing values of solutions status etc., and performs the checks + summary.
class CompareLogs:
def __init__( self ):
self.lResLogs = [] ## empty list of logs/methods to compare
self.hdrSummary = [ ## These are column headers for overall per-method summary
( "s_MethodName", "logfile/test name", "Logfile and possibly test alias" ),
( "n_Reported", "Nout", "Total number of instances" ),
( "n_CheckFailed","Nbad", "Number of failed solution checks" ),
( "n_ErrorsBackend", "NerrB", "Number of backend errors. TODO need still to consider feasible solutions if ERROR status" ),
( "n_ErrorsLogical", "NerrL", "Number of logical errors, such as different solver and MZN obj values" ),
( "n_OPT", "Nopt", "Number of reported optimal" ),
( "n_FEAS", "Nfea", "Number of reported feasible" ),
( "n_SATALL", "NsatA", "Number of reported SAT-COMPLETE" ),
( "n_SAT", "Nsat", "Number of reported SAT" ),
( "n_INFEAS", "Ninfeas", "Number of reported UNSAT" ),
( "n_NOFZN", "NoFZN", "Number of failed flattenings" ),
( "n_UNKNOWN", "Nunkn", "Number of unknown results" ),
( "t_Flatten", "TFlt", "Total flattening time" )
]
self.hdrRanking = [ ## These are column headers for ranking analysis
## ( "nmMeth", "logfile/test/method name" ),
( "OOpt", "Number of instances where ONLY this method is OPTIMAL" ),
( "OSaC", "Number of instances where ONLY this method is SAT-COMPLETE" ),
( "OFeas", "Number of instances where ONLY this method is FEASIBLE and none is optimal" ),
( "OSat", "Number of instances where ONLY this method is SAT and none is optimal" ),
( "OInfeas","Number of instances where ONLY this method is INFeasible" ),
( "BPri", "Number of instances where this method has a better PRIMAL BOUND" ),
( "BDua", "Number of instances where this method has a better DUAL BOUND" )
## TODO: ranks here ( "n_UNKNOWN", "Nunkn" )
]
hdrTable = OrderedDict( [ ## Possible headers for table printout
( "stt", "The solver status" ), ## TODO an error should be separate flag, not a status
( "chk", "The solution checking status" ),
( "objMZN", "The MZN obj value" ),
( "objSLV", "The solver obj value" ),
( "bnd", "The solver dual bound" ),
( "tAll", "Total running wall time" ),
( "tFlt", "Flattening time" ),
( "tBest", "A best solution's finding time" ),
( "sns", "Model sense (min/max/sat)" ),
( "errH", "Solver errors" ),
( "errL", "Logical errors" )
] )
## which of those to print for each method
hdrTable2P = "stt objMZN objSLV bnd tFlt tBest"
hdrTable2P_spl = hdrTable2P.split( " " )
mapStatShort = { ## Short status names
2: "OPT",
1: "FEAS",
4: "SATA",
3: "SAT",
0: "UNKN",
-1: "UNSAT",
-2: "UNBND",
-3: "UNSorUNB",
-4: "ERR_H"
}
mapProblemSense = {
1: "max",
0: "sat",
-1: "min",
-2: "nosns"
}
## Add a method's log
def addLog( self, sName ):
self.lResLogs.append( ( OrderedDict(), [ sName, '' ] ) ) ## a tuple of dict and list of names ([filename, nick])
return self.getLastLog()
def getLastLog( self ):
assert 0<len( self.lResLogs )
return self.lResLogs[-1]
def getMethodName( self, lNames ): ## produce a sigle string from a list to create a method name
return " ".join( lNames )
## Return the union of all instances in all logs
def getInstanceUnion( self ):
## return list( functools.reduce(set.union, (set(d[0].keys()) for d in self.lResLogs)) )
r0 = OrderedDict()
for d in self.lResLogs:
r0.update( OrderedDict.fromkeys( d[0] ) )
return r0.keys()
## Return the intersection of all instances in all logs
def getInstanceIntersection( self ):
assert False
return [] ## TODO
## This compares all instances specified in the list of pairs (full name, short name)
## If comparing by-instance from outside, follow this pattern
def compareAllInstances( self, lInstances ):
self.initListComparison()
for sInst in lInstances:
self.compareInstance( sInst )
self.summarizeCmp()
self.summarizeFinalHdr()
self.summarize()
## Init stats etc.
def initListComparison( self ):
self.nInstCompared = 0
self.nInstWithOptSense = 0
## Using warning strings... self.lInstContradOptSense = []
self.lCmpVecs = [] # List of summary vectors for each method
self.mCmpVecVals = {} # Maps to the "quantity" parts of those
self.mCmpVecQual = {} # Maps to the "quality" parts
print( "" ) ## Newline
print( "=============== PER-INSTANCE RESULTS TABLE, HEADERS: ===============" )
for hdrLine in self.hdrTable.items():
print( " ", hdrLine )
print( "====================================================================" )
print( "No.\tinst", end='\t')
for mLog, lN in self.lResLogs: ## Select method and its name list
lNames = self.getMethodName(lN)
av = OrderedDict({ "s_MethodName": lNames })
aq = OrderedDict({ "s_MethodName": lNames })
self.lCmpVecs.append( ( lNames, av, aq ) )
self.mCmpVecVals[ lNames ] = av
self.mCmpVecQual[ lNames ] = aq
for hdr in self.hdrTable2P_spl: ## Print table headers
print( hdr, end='\t' )
print( "" ) ## Newline
self.mInfeas, self.mNoFZN, self.mFail, self.mError = {}, {}, {}, {}
self.nContrStatus, self.nContrOptVal, self.nContrBounds = 0, 0, 0
self.matrRanking = utils.MatrixByStr(
[ ( self.getMethodName( lNames ), "No long name" ) for mLog, lNames in self.lResLogs ],
self.hdrRanking )
self.matrRankingMsg = utils.MatrixByStr(
[ ( self.getMethodName( lNames ), "No long name" ) for mLog, lNames in self.lResLogs ],
self.hdrRanking, [] )
self.nNoOptAndAtLeast2Feas = 0
############################## Output strings. TODO into a map
self.ioContrSense = io.StringIO()
self.ioContrStatus = io.StringIO()
self.ioContrObjValMZN = io.StringIO()
self.ioBadObjValueStatusOpt = io.StringIO()
self.ioBadObjValueStatusFeas = io.StringIO()
self.ioContrOptVal = io.StringIO()
self.ioContrBounds = io.StringIO()
self.ioBadChecks = io.StringIO()
self.ioErrors = io.StringIO()
## Compare methods on the given instance
def compareInstance( self, sInst ):
self.initInstanceComparison( sInst[0] )
self.tryFindProblemSense( sInst[0] )
self.compileInstanceResults( sInst[0] )
self.checkContradictions( sInst[0] )
self.rankPerformance( sInst[0] )
self.doInstanceSummary( sInst )
## Summarize up to current instance
def summarizeCurrent( self, lLogNames ):
lNames = self.getMethodName(lLogNames)
mCmpVals = self.mCmpVecVals[lNames]
for hdr in self.hdrSummary:
print( hdr[1], ":",
mCmpVals[hdr[0]] if hdr[0] in mCmpVals else 0,
sep='', end=' ' )
## Summarize comparisons / ranking
def summarizeCmp( self ):
print(
self.matrRankingMsg.stringifyLists( " METHODS' STAND-OUTS",
" METHOD",
" PARAMETER" ) + \
"\n------------------ SOLUTION CHECKS FAILURES ------------------\n\n" + \
self.ioBadChecks.getvalue() + \
"\n\n------------------ OBJECTIVE SENSE CONTRADICTIONS ------------------\n\n" + \
self.ioContrSense.getvalue() + \
"\n\n------------------ OBJECTIVE VALUE MZN / SOLVER CONTRADICTIONS ------------------\n\n" + \
self.ioContrObjValMZN.getvalue() + \
"\n\n------------------ OBJECTIVE VALUE BAD, STATUS OPTIMAL ------------------\n\n" + \
self.ioBadObjValueStatusOpt.getvalue() + \
"\n\n------------------ OBJECTIVE VALUE BAD, STATUS FEASIBLE ------------------\n\n" + \
self.ioBadObjValueStatusFeas.getvalue() + \
"\n\n------------------ STATUS CONTRADICTIONS ------------------\n\n" + \
self.ioContrStatus.getvalue() + \
"\n\n------------------ OBJECTIVE VALUE CONTRADICTIONS ------------------\n\n" + \
self.ioContrOptVal.getvalue() + \
"\n\n------------------ DUAL BOUND CONTRADICTIONS ------------------\n\n" + \
self.ioContrBounds.getvalue() + \
"\n\n------------------ ERRORS REPORTED BY SOLVERS ------------------\n\n" + \
self.ioErrors.getvalue() + "\n" + \
"\n\n------------------ RANKING ------------------\n\n" + \
"\n".join( [ " " + str( hl ) for hl in self.hdrRanking ] ) + \
"\n---------------------------------------------\n" + \
self.matrRanking.stringify2D()
)
## Summary headers
def summarizeFinalHdr( self ):
return \
"\n".join( [ " " + str((hdrLine[1], hdrLine[2])) for hdrLine in self.hdrSummary ] ) + \
"\n=================================================="
## Summarize
def summarize( self ):
return \
utils.MyTab().tabulate(
[ [ lcv[1][hdr[0]] if hdr[0] in lcv[1] else 0
for hdr in self.hdrSummary ]
for lcv in self.lCmpVecs ],
[ pr[1] for pr in self.hdrSummary ]
)
###############################################################################################
####################### LEVEL 2 #########################
###############################################################################################
def initInstanceComparison( self, sInst ):
self.lOpt, self.lSatAll, self.lFeas, self.lSat, self.lInfeas = [], [], [], [], []
self.mOptVal, self.lOptVal, self.lPrimBnd, self.lDualBnd = OrderedDict(), [], [], []
self.nInstCompared += 1
self.nReported = 0 ## How many methods reported for this instances
## Detailed table line for this instance
self.aDetThisInst = { self.getMethodName( ll[1] ) : {} for ll in self.lResLogs }
def tryFindProblemSense( self, sInst ):
self.sSenses = {}
self.nOptSenseGiven = -2;
for mLog, lNames in self.lResLogs:
if sInst in mLog:
mSlv = mLog[ sInst ][ "__SOLVE__" ] ## __SOLVE__ always there???
if "Problem_Sense" in mSlv:
self.sSenses[ mSlv["Problem_Sense"][0] ] = lNames # mSlv["Problem_Sense"][1]
if 1<len( self.sSenses ):
print( "WARNING: DIFFERENT OBJ SENSES REPORTED for the instance ", sInst,
": ", self.sSenses, sep='', file=self.ioContrSense )
elif 1==len( self.sSenses ):
self.nOptSenseGiven = list(self.sSenses.keys())[0]
def compileInstanceResults( self, sInst ):
for mLog, lN in self.lResLogs: ## Select method and its name list
lNames = self.getMethodName(lN)
aDetThis = self.aDetThisInst[ lNames ] ## Result table line section
if sInst in mLog:
self.nReported += 1
aResultThisInst = OrderedDict({ "n_Reported": 1 })
aResultThisInst[ "n_CheckFailed" ] = 0
mRes = mLog[ sInst ] ## The method's entry for this instance
aDetThis[ "chk" ] = "ok"
if "SOLUTION_CHECKS_FAILED" in mRes and \
0<mRes["SOLUTION_CHECKS_FAILED"]:
aResultThisInst[ "n_CheckFailed" ] = 1
aDetThis[ "chk" ] = "BAD"
utils.addMapValues( self.mCmpVecVals[lNames], aResultThisInst )
print( "WARNING: SOLUTION CHECK(S) FAILED for the instance ", sInst,
", method '", lNames, "'.", sep='', file = self.ioBadChecks )
continue ## TODO. Param?
aResultThisInst[ "n_ErrorsBackend" ] = 0
aResultThisInst[ "n_ErrorsLogical" ] = 0
aDetThis [ "errH" ] = 0
aDetThis [ "errL" ] = 0
mSlv = mRes[ "__SOLVE__" ]
dObj_MZN = utils.try_float( mSlv.get( "ObjVal_MZN" ) )
aDetThis [ "objMZN" ] = dObj_MZN
dObj_SLV = utils.try_float( mSlv.get( "ObjVal_Solver" ) )
aDetThis [ "objSLV" ] = dObj_SLV
dBnd_SLV = utils.try_float( mSlv.get( "DualBnd_Solver" ) )
aDetThis [ "bnd" ] = dBnd_SLV
dTime_All = utils.try_float( mSlv.get( "TimeReal_All" ) )
aDetThis [ "tAll" ] = dTime_All
dTime_Flt = utils.try_float( mSlv.get( "Time_Flt" ) )
aResultThisInst[ "t_Flatten" ] = dTime_Flt if dTime_Flt is not None else dTime_All ##?? Assume flattening stopped
aDetThis [ "tFlt" ] = dTime_Flt
dTime_Last = utils.try_float( mSlv.get( "TimeReal_LastStatus" ) )
aDetThis [ "tBest" ] = dTime_Last
## Compare obj vals
dObj, bObj_MZN = (dObj_MZN, True) if \
None!=dObj_MZN and abs( dObj_MZN ) < 1e45 else (mSlv.get("ObjVal_MZN"), False)
## Assuming solver value is better if different. WHY? Well it' happened both ways
dObj, bObj_SLV = (dObj_SLV, True) if \
None!=dObj_SLV and abs( dObj_SLV ) < 1e45 else (dObj, False)
if bObj_MZN and bObj_SLV:
if abs( dObj_MZN-dObj_SLV ) > 1e-6 * max( abs(dObj_MZN), abs(dObj_SLV) ):
aResultThisInst[ "n_ErrorsLogical" ] += 1
aDetThis [ "errL" ] += 1
print ( " WARNING: DIFFERENT MZN / SOLVER OBJ VALUES for the instance ", sInst,
", method '", lNames, "' : ",
dObj_MZN, " / ", dObj_SLV, sep='', file=self.ioContrObjValMZN)
## Retrieve solution status
if "Sol_Status" in mSlv:
n_SolStatus = mSlv[ "Sol_Status" ][0]
else:
n_SolStatus = 0
## Retrieve dual bound
dBnd = None
if None!=dBnd_SLV and abs( dBnd_SLV ) < 1e45:
dBnd = dBnd_SLV
self.lDualBnd.append( ( dBnd_SLV, lNames ) ) ## Even infeas instances can have dual bound?
## Trying to deduce opt sense if not given:
if 1==len(self.sSenses):
nSense = next(iter(self.sSenses.keys()))
else:
nSense = -2 ## ??
aDetThis[ "sns" ] = self.mapProblemSense[ nSense ]
self.bOptProblem = True if 0!=nSense else False ## or (None!=dBnd or None!=dObj)
### ... here assumed it's an opt problem by default... why... need to check bounds first??
## Handle optimality / SAT completed
if 2==n_SolStatus:
if not self.bOptProblem:
self.lSatAll.append( lNames )
aResultThisInst[ "n_SATALL" ] = 1
aDetThis[ "stt" ] = self.mapStatShort[ 4 ]
else: ## Assume it's an optimization problem????? TODO
self.lOpt.append( lNames ) ## Append the optimal method list
aResultThisInst[ "n_OPT" ] = 1
aDetThis[ "stt" ] = self.mapStatShort[ 2 ]
if None==dObj or abs( dObj ) >= 1e45:
aResultThisInst[ "n_ErrorsLogical" ] += 1
aDetThis [ "errL" ] += 1
print ( " WARNING: OPTIMAL STATUS BUT BAD OBJ VALUE, instance ", sInst,
", method '", lNames, "': '",
( "" if None==dObj else str(dObj) ), "', result record: ", # mRes,
",, dObj_MZN: ", dObj_MZN, sep='', file=self.ioBadObjValueStatusOpt )
else:
self.mOptVal[ dObj ] = lNames ## Could have used OrderedDict
self.lOptVal.append( (dObj, lNames) ) ## To have both a map and the order
self.lPrimBnd.append( (dObj, lNames) )
## Handle feasibility / SAT
elif 1==n_SolStatus:
if not self.bOptProblem:
self.lSat.append( lNames )
aResultThisInst[ "n_SAT" ] = 1
aDetThis[ "stt" ] = self.mapStatShort[ 3 ]
else: ## Assume it's an optimization problem????? TODO
self.lFeas.append( lNames ) ## Append the optimal method list
aResultThisInst[ "n_FEAS" ] = 1
aDetThis[ "stt" ] = self.mapStatShort[ 1 ]
if None==dObj or abs( dObj ) >= 1e45:
aResultThisInst[ "n_ErrorsLogical" ] += 1
aDetThis [ "errL" ] += 1
print ( " WARNING: feasible status but bad obj value, instance ", sInst,
", method '", lNames, "' :'",
( "" if None==dObj else str(dObj) ), "', result record: ", # mRes,
sep='', file=self.ioBadObjValueStatusFeas )
else:
self.lPrimBnd.append( (dObj, lNames) )
## Handle infeasibility
elif -1>=n_SolStatus and -3<=n_SolStatus:
self.lInfeas.append( lNames )
aResultThisInst[ "n_INFEAS" ] = 1
aDetThis[ "stt" ] = self.mapStatShort[ n_SolStatus ]
self.mInfeas. setdefault( sInst, [] )
self.mInfeas[ sInst ].append( lNames )
## Handle ERROR?
elif -4==n_SolStatus:
aResultThisInst[ "n_ErrorsBackend" ] = 1
aDetThis [ "errH" ] += 1
aDetThis[ "stt" ] = self.mapStatShort[ n_SolStatus ] ## Should not happen TODO
self.mError. setdefault( sInst, [] ).append( lNames )
print( "ERROR REPORTED for the instance ", sInst, ", method '", lNames,
"', result record: ", ## mRes,
sep='', file=self.ioErrors )
else:
aResultThisInst[ "n_UNKNOWN" ] = 1
aDetThis[ "stt" ] = self.mapStatShort[ 0 ]
## Handle NOFZN
if None==dTime_Flt:
aResultThisInst[ "n_NOFZN" ] = 1
self.mNoFZN. setdefault( sInst, [] ).append( lNames )
## Handle FAIL???
# LAST:
utils.addMapValues( self.mCmpVecVals[lNames], aResultThisInst )
###
### Now compare between differen methods: CONTRADICTIONS
###
def checkContradictions( self, sInst ):
self.fContr = False
if len(self.lOpt)+len(self.lFeas)+len(self.lSatAll)+len(self.lSat) > 0 and len(self.lInfeas) > 0:
self.nContrStatus += 1
self.fContr = True
print( "CONTRADICTION of STATUS: instance " + str(sInst) + ": " + \
"\n OPTIMAL: " + strNL( "\n ", self.lOpt) + \
"\n FEAS: " + strNL( "\n ", self.lFeas) + \
"\n SAT_COMPLETE: " + strNL( "\n ", self.lSatAll) + \
"\n SAT: " + strNL( "\n ", self.lSat) + \
"\n INFEAS: " + strNL( "\n ", self.lInfeas), file= self.ioContrStatus )
if len(self.mOptVal) > 1:
self.nContrOptVal += 1
self.fContr = True
print( "CONTRADICTION of OPTIMAL VALUES: " + str(sInst) + \
": " + strNL( "\n ", self.mOptVal.items()), file=self.ioContrOptVal )
self.nOptSense=0; ## Take as SAT by default
if len(self.lPrimBnd)>0 and len(self.lDualBnd)>0 and len(self.lOpt)<self.nReported:
lKeysP, lValP = zip(*self.lPrimBnd)
lKeysD, lValD = zip(*self.lDualBnd)
nPMin, nPMax, nDMin, nDMax = \
min(lKeysP), max(lKeysP), \
min(lKeysD), max(lKeysD)
if nPMax <= nDMin + 1e-6 and nPMin < nDMax - 1e-6:
self.nOptSense=1 ## maximize
elif nPMin >= nDMax - 1e-6 and nPMax > nDMin + 1e-6:
self.nOptSense=-1 ## minimize
elif nPMax > nDMin + 1e-6 and nPMin < nDMax - 1e-6 or \
nPMin < nDMax - 1e-6 and nPMax > nDMin + 1e-6:
self.nContrBounds += 1
self.fContr = True
print( "CONTRADICTION of BOUNDS: instance " + str(sInst) + \
":\n PRIMALS: " + strNL( "\n ", self.lPrimBnd) + \
",\n DUALS: " + strNL( "\n ", self.lDualBnd),
file = self.ioContrBounds )
else:
self.nOptSense=0 ## SAT
if 1==len(self.sSenses) and self.nOptSense!=0:
if self.nOptSense!=self.nOptSenseGiven: ## access the 'given' opt sense
print( "CONTRADICITON of IMPLIED OBJ SENSE: Instance "+ str(sInst) + \
": primal bounds " + strNL( "\n ", self.lPrimBnd) + \
" and dual bounds "+ strNL( "\n ", self.lDualBnd) + \
" together imply opt sense " + str(self.nOptSense) + \
", while result logs say "+ str(self.nOptSenseGiven), file=self.ioContrBounds )
## else accepting nOptSense as it is
###
### Now compare between differen methods: DIFFERENCES AND RANKING
###
def rankPerformance( self, sInst ):
### Accepting the opt sense from result tables, if given
if self.nOptSenseGiven!=-2:
self.nOptSense = self.nOptSenseGiven
### Compare methods on this instance:
if not self.fContr and self.nReported == len(self.lResLogs):
if len(self.lOpt) == 1:
self.matrRanking[self.lOpt[0], "OOpt"] += 1
self.matrRankingMsg[self.lOpt[0], "OOpt"].append( \
str(sInst) + ": the ONLY OPTIMAL")
elif len(self.lSatAll) == 1:
self.matrRanking[self.lSatAll[0], "OSaC"] += 1
self.matrRankingMsg[self.lSatAll[0], "OSaC"].append( \
str(sInst) + ": the ONLY SAT-COMPLETE")
elif len(self.lOpt) == 0 and len(self.lFeas) == 1:
self.matrRanking[self.lFeas[0], "OFeas"] += 1
self.matrRankingMsg[self.lFeas[0], "OFeas"].append( \
str(sInst) + ": the ONLY FEASIBLE")
elif len(self.lSatAll) == 0 and len(self.lSat) == 1:
self.matrRanking[self.lSat[0], "OSat"] += 1
self.matrRankingMsg[self.lSat[0], "OSat"].append( \
str(sInst) + ": the ONLY SAT")
elif len(self.lOpt) == 0 and len(self.lFeas) > 1:
self.nNoOptAndAtLeast2Feas += 1
elif len(self.lInfeas) == 1:
self.matrRanking[self.lInfeas[0], "OInfeas"] += 1
self.matrRankingMsg[self.lInfeas[0], "OInfeas"].append( \
str(sInst) + ": the ONLY INFeasible")
if not self.fContr \
and 0==len(self.lInfeas) and 1<len(self.lFeas) and 0!=self.nOptSense:
self.lPrimBnd.sort()
if self.nOptSense>0:
self.lPrimBnd.reverse()
dBnd, dNM = zip(*self.lPrimBnd)
dBetter = (dBnd[0]-dBnd[1]) * self.nOptSense
if 1e-2 < dBetter: ## Param? TODO
self.matrRanking[dNM[0], "BPri"] += 1
self.matrRankingMsg[dNM[0], "BPri"].append( str(sInst) \
+ ": the best OBJ VALUE by " + str(dBetter) \
+ "\n PRIMAL BOUNDS AVAILABLE: " + strNL( "\n ", self.lPrimBnd))
if not self.fContr \
and 0==len(self.lInfeas) and 1<len(self.lDualBnd) and 0!=self.nOptSense:
self.lDualBnd.sort()
if self.nOptSense<0:
self.lDualBnd.reverse()
dBnd, dNM = zip(*self.lDualBnd)
dBetter = (dBnd[1]-dBnd[0]) * self.nOptSense
if 1e-2 < dBetter: ## Param/adaptive? TODO
self.matrRanking[dNM[0], "BDua"] += 1
self.matrRankingMsg[dNM[0], "BDua"].append( str(sInst) \
+ ": the best DUAL BOUND by " + str(dBetter) \
+ "\n DUAL BOUNDS AVAILABLE: " + strNL( "\n ", self.lDualBnd))
###
### Now print a table line summarizing the instance for different methods
###
def doInstanceSummary( self, sInst ):
lIL = [ sInst[1] ] ## The short instance name
print( self.nInstCompared, ".\t", sInst[1], sep='', end='\t' )
for mLog, lN in self.lResLogs: ## Select method and its name list
lNames = self.getMethodName(lN)
aDetThis = self.aDetThisInst[ lNames ] ## Result table line section
if sInst[0] in mLog:
for hdr in self.hdrTable2P_spl:
if hdr not in self.hdrTable:
print( '?', end='\t' )
elif aDetThis.get( hdr ) is None:
print( "-", end='\t' )
else:
print( aDetThis.get(hdr), end='\t' )
else:
for hdr in self.hdrTable2P_spl:
print( '-' if hdr in self.hdrTable else '?', end='\t' )
print( "" ) ## Newline