class ErrorFileReader(StatisticReader):
    """
    reads information from error files
    """
    name = "ErrorFileReader"
    regular_exp = re.compile("returned with error code (\d+)")
    datakey = Key.ErrorCode
    context = Key.CONTEXT_ERRFILE

    def extractStatistic(self, line):
        match = self.regular_exp.search(line)
        if match:
            returncode = match.groups()[0]
            self.addData(self.datakey, int(returncode)) class GapReader(StatisticReader):
    """
    reads the primal dual gap at the end of the solving
    """
    name = 'GapReader'
    regular_exp = re.compile('^Gap                :')
    datakey = Key.Gap
    datatype = float
    lineindex = 2

    def extractStatistic(self, line):
        if self.regular_exp.match(line):
            gapasword = misc.getWordAtIndex(line, self.lineindex)

            # if the gap is infinite, no data is passed to the test run
            if gapasword != "infinite":
                gap = self.turnIntoFloat(gapasword)
                self.addData(self.datakey, gap) class TimeLimitReader(StatisticReader):
    """
    extracts the time limit for a problem
    """
    name = 'TimeLimitReader'
    timelimitreadkeys = {
                   StatisticReader.SOLVERTYPE_SCIP : '@05',
                   StatisticReader.SOLVERTYPE_CPLEX : '@05',
                   StatisticReader.SOLVERTYPE_GUROBI : "@05",
                   StatisticReader.SOLVERTYPE_CBC : "@05",
                   StatisticReader.SOLVERTYPE_XPRESS : "@05",
                   StatisticReader.SOLVERTYPE_COUENNE : "^@05"}

    datakey = Key.TimeLimit

    def extractStatistic(self, line):
        if re.search(self.timelimitreadkeys[StatisticReader.solvertype], line):
            self.addData(self.datakey, float(line.split()[-1])) class ListReader(StatisticReader):
    """
    reads a list matching a regular expression
    """
    name = "ListReader"

    def __init__(self, regpattern=None, name=None):
        """
        construct a new list reader to parse key-value pairs from a given context

        List readers parse key-value pairs of the form

        (regpattern-match 1) value
        (regpattern-match 2) value
        (regpattern-match 3) value

        The matching regpattern is used as data key

        Parameters:
        -----------

        regpattern : a pattern (regular expression supported) that suitable lines must match

        name : a name for this reader
        """
        if regpattern is None:
            raise ValueError("Error: No 'regpattern' specified for reader %s" % str(name))
        self.regular_exp = re.compile(regpattern)
        self.regpattern = regpattern
        if name is None:
            name = ListReader.name
        self.name = name

    def getEditableAttributes(self):
        return ["name", "regpattern"]

    def set_context(self, contextname):
        self.context = self.contextname2contexts.get(contextname, self.context)

    def getRequiredOptionsByAttribute(self, attr):
        if attr == "context":
            return list(self.contextname2contexts.keys())
        return None

    def getLineData(self, line):
        match = self.regular_exp.match(line)
        if match is not None:
            datakey = match.group(1)
            strval = match.group(2)
            try:
                val = int(strval)
            except ValueError:
                val = float(strval)
            return (datakey, val)
        return None

    def extractStatistic(self, line):
        data = self.getLineData(line)
        if data is not None:
            self.addData(data[0], data[1]) class BestSolInfeasibleReader(StatisticReader):
    """
    catches the expression 'best solution is not feasible in original problem'

    @return: False if above expression is found in the log and the best solution is thus not feasible, otherwise True
    """
    name = 'BestSolInfeasibleReader'
    regular_exp = re.compile('best solution is not feasible in original problem')
    datakey = Key.BestSolutionInfeasible

    def extractStatistic(self, line):
        if self.regular_exp.search(line):
            self.addData(self.datakey, True) class MaxDepthReader(StatisticReader):
    """
    reads the maximum depth
    """
    name = 'MaxDepthReader'
    regular_exp = re.compile('  max depth        :')
    datakey = Key.MaximumDepth
    datatype = int
    lineindex = 3 class MetaDataReader(StatisticReader):
    """
    Read lines of the form
    @Key Value
    from meta, out and err file and stores 'Value' in a Field 'Key'.
    """
    context = [Key.CONTEXT_METAFILE, Key.CONTEXT_LOGFILE, Key.CONTEXT_ERRFILE]

    metadataexp = re.compile("^@\S{3,}\s+\S+$")
    name = 'MetaDataReader'
    datakey = Key.MetaData

    def extractStatistic(self, line):
        """ Read metadata from specified line

        Parameters
        ----------
        line
            string to be read from. has to have the form
                @attribute datum
        """
        if self.metadataexp.match(line):
#            TODO better to allow more spaces?
            [attr, datum] = line[1:].split()
            datum = datum.split('\n')[0]
            self.testrun.metadatadict[attr] = datum class NodeNameReader(StatisticReader):
    """
    Read nodename data from a line in outfile like the output of 'uname -a' (currently only on Linux).
    """
    context = [Key.CONTEXT_LOGFILE]

    nodenameexp = re.compile('^Linux (\S*) .* GNU/Linux')
    name = 'NodeNameReader'
    datakey = Key.NodeName
    nodename = None

    def extractStatistic(self, line):
        """ Save the hostname from a line like the output of 'uname -a' (currently only on Linux) to add at the end of the problem.

        Parameters
        ----------
        line from the outfile
        """
        matched = self.nodenameexp.match(line)
        if matched:
            self.nodename = matched.groups()[0]

    def execEndOfProb(self):
        """
        At the end of each problem, add the current nodename to the data.
        """
        if self.nodename is not None:
            self.addData(self.datakey, self.nodename)
        self.nodename = None class NodesReader(StatisticReader):
    """
    reads the total number of solving nodes of the branch and bound search
    """
    name = 'NodesReader'
    regular_exp = re.compile("^  nodes \(total\)    :")
    datakey = Key.Nodes
    datatype = int
    lineindex = 3 class RootNodeFixingsReader(StatisticReader):
    """
    reads the number of variable fixings during root node
    """
    name = 'RootNodeFixingsReader'
    regular_exp = re.compile('^  root node')
    datakey = Key.RootNodeFixings
    datatype = int
    lineindex = 4 class SettingsFileReader(StatisticReader):
    """
    parses settings from a settings file

    parses the type, the default value, the name and the current value for every parameter
    # [type: int, range: [-536870912,536870911], default: 100000]
    nodeselection/bfs/stdpriority = 1000000
    """
    name = "SettingsFileReader"
    regular_exp_name = re.compile("^([\w/]+) = (\S+)")
    regular_exp_type = re.compile("^# \[type: (\w+),.*default: ([^\]]+)\]")
    context = Key.CONTEXT_SETFILE

    typemap = {
               "real" : float,
               "char" : str,
               "string" : str,
               "int" : int,
               "bool" : StatisticReader.boolfunction,
               "longint" : int
               }
    """ map from a parameter type to a python standard data type """

    def extractStatistic(self, line):
        match_type = self.regular_exp_type.match(line)
        if match_type:
            self.type = match_type.groups()[0]
            self.default = match_type.groups()[1]
        else:
            match_name = self.regular_exp_name.match(line)
            if match_name:
                name = match_name.groups()[0]
                value = match_name.groups()[1]
                typefunction = self.typemap.get(self.type, str)
                try:
                    self.testrun.addParameterValue(name, typefunction(value))
                    self.testrun.addDefaultParameterValue(name, typefunction(self.default))
                except ValueError:
                    # when an error occurs, just return a string
                    self.testrun.addParameterValue(name, value)
                    self.testrun.addDefaultParameterValue(name, self.default) class TimeToFirstReader(StatisticReader):
    name = 'TimeToFirstReader'
    regular_exp = re.compile('  First Solution   :')
    datakey = Key.TimeToFirstSolution
    datatype = float
    lineindex = 3

    def extractStatistic(self, line):
        if self.regular_exp.match(line):
            try:
                timetofirst = float(misc.getNumberAtIndex(line, self.lineindex))
                self.addData(self.datakey, timetofirst)

            except TypeError:
                pass class TimeToBestReader(StatisticReader):
    name = 'TimeToBestReader'
    regular_exp = re.compile('  Primal Bound     :')
    datakey = Key.TimeToBestSolution
    datatype = float
    lineindex = 3

    def extractStatistic(self, line):
        if self.regular_exp.match(line):
            try:
                self.addData(self.datakey, float(misc.getNumberAtIndex(line, self.lineindex)))
            except TypeError:
                pass class ObjsenseReader(StatisticReader):
    name = 'ObjsenseReader'
    regular_exp = re.compile("^  Objective        : (\w*),")
    datakey = Key.ObjectiveSense
    minimize = 1
    maximize = -1
    orig_prob_state = False
    orig_prob_expr = re.compile('^Original Problem   :')
    pres_prob_expr = re.compile('^Presolved Problem  :')

    def extractStatistic(self, line):
        if self.orig_prob_expr.match(line):
            self.orig_prob_state = True
            return
        if self.pres_prob_expr.match(line):
            self.orig_prob_state = False
            return

        match = self.regular_exp.match(line)
        if self.orig_prob_state and match:
            objsense = self.minimize
            if match.groups()[0] == "maximize":
                objsense = self.maximize

            self.addData(self.datakey, objsense) class DateTimeReader(StatisticReader):
    """
    reads in the start and finish time from a timestamp in given in Milliseconds

    If found, the corresponding data keys are Datetime_Start and Datetime_End
    """
    name = 'DateTimeReader'  # : the name for this reader
    datetimestartexp = re.compile(r"^@03 ([0-9]+)")  # : the expression for the date time start
    datetimeendexp = re.compile(r"^@04 ([0-9]+)")  # : the expression for the date time after termination
    datetimestartkey = Key.DatetimeStart  # : data key for start of run
    datetimeendkey = Key.DatetimeEnd  # : data key for end of run

    datetimekw = {datetimestartkey:datetimestartexp, datetimeendkey:datetimeendexp}

    def extractStatistic(self, line):
        for key, exp in list(self.datetimekw.items()):
            matched = exp.match(line)
            if matched:
                timestamp = int(matched.groups()[0])
                #time = misc.convertTimeStamp(timestamp)
                self.addData(key, timestamp)
                break class SolCheckerReader(StatisticReader):
    """reads the solution checker output, if available, that can be used for validation

    The output looks like this
Read MPS: 1
MIP has 49606 vars and 26069 constraints
Read SOL: 1
Objective value computed by solver: 82895.99999986154

Integrality tolerance: 1/10000
Linear tolerance: 1/10000
Objective tolerance: 1/10000

Check SOL: Integrality 1 Constraints 1 Objective 1
Maximum violations: Integrality 3.902288396999999e-06 Constraints 2.81133904261283e-05 Objective 1.47339791e-12
    """
    name = "SolCheckerReader"

    def extractStatistic(self, line : str):
        if line.startswith("Read SOL: 1"):
            self.addData(Key.SolCheckerRead, True)
        elif line.startswith("Read SOL: 0"):
            self.addData(Key.SolCheckerRead, False)

        if line.startswith("Check SOL: Integrality 1 Constraints 1 Objective 1"):
            self.addData(Key.SolCheckerFeas, True)
        elif line.startswith("Check SOL:"):
            self.addData(Key.SolCheckerFeas, False) class TableReader(StatisticReader):
    """
    Reader which tries to read various data arranged in table format -
    Format Foo: a     b
             1  : 0     5
             2  : 7     9

    will be parsed into flattened data keys Foo_a_1, Foo_a_2, Foo_b_1 etc. and added to the testrun with their corresponding
    (numeric) table entry

    It can also read single column tables of the form
    Foo:
    a  : 1
    b  : 2
    ...

    and will store the corresponding values under Foo_a, and Foo_b
    """
    name = 'TableReader'
    tableids = ['Presolvers', 'Constraints', 'Constraint Timings', 'Propagators', 'Propagator Timings', 'Conflict Analysis',
                'Separators', 'Branching Rules', 'Diving Statistics', 'Diving (single)', 'Diving (adaptive)', 'LP', 'Branching Analysis', 'Primal Heuristics', 'Concurrent Solvers', 'Integrals']
    columnids = ['Root Node', 'Total Time', 'B&B Tree']
    active = False
    spacesepcolumnnames = ['LP Iters']
    replacecolumnnames = [''.join(sscname.split()) for sscname in spacesepcolumnnames]
    wrongtableid = 'Wrong'
    tableid = wrongtableid


    def convertToFloat(self, x):
        try:
            return float(x)
        except:
            return None

    def extractStatistic(self, line):
        if re.match('^SCIP Status', line):
            self.active = True

        elif self.active and re.match('[a-zA-Z]', line):
            if not re.search(':', line):
                return None
            try:
                colonidx = line.index(':')
            except:
                # print(StatisticReader.problemname, line)
                raise Exception()
            tableid = line[:colonidx].rstrip()
            self.tableid = ''.join(tableid.split())
            if tableid in self.tableids:

                preprocessedline = line[colonidx + 1:]
                for expr, replacement in zip(self.spacesepcolumnnames, self.replacecolumnnames):
                    preprocessedline = re.sub(expr, replacement, preprocessedline)

                    # we are parsing a table
                self.columns = preprocessedline.split()

                    # we are only parsing a single column
            elif tableid in self.columnids:
                self.columns = []
            else:
                self.tableid = self.wrongtableid

        elif self.active and self.tableid != self.wrongtableid:
            try:
                colonidx = line.index(':')
            except:
                self.tableid = self.wrongtableid
                return None

            rowname = ''.join(line[:colonidx].split())

            # distinguish between single columns and tables with multiple columns
            if self.columns != []:

                # treat tables (tables with at least two data columns)
                datakeys = ['_'.join((self.tableid, column, rowname)) for column in self.columns]
                data = list(map(self.convertToFloat, misc.numericExpression.findall(line[colonidx + 1:])))
            else:
                # treat vectors (tables with only one data column)
                datakeys = ['_'.join((self.tableid, rowname))]
                # TODO This works, why is eclipse complaining?
                data = [self.convertToFloat(misc.numericExpression.search(line, colonidx + 1).group(0))]

            # determine minimum length (necessary if more headers were recognized than actual available data)
            minlen = min(len(datakeys), len(data))

            storekeys = [re.sub("[%&()]", ".", x) for x in datakeys[:minlen]]

            self.addData(storekeys, data[:minlen])

    def execEndOfProb(self):
        self.active = False class CustomTableReader(TableReader):
    """customizable plugin statistics reader for user plugin data tables
    """

    def __init__(self, name = "CustomTableReader", tableid = None, columnid = None):
        """Constructs custom data table reader

        Parameters
        ----------
        tableid : string representing table identifier

        singlecolumname : string representing a column identifier
        """

        if tableid is None and columnid is None:
            raise Exception("Both tableid and singlecolumname are None")
        self.set_tableid(tableid)
        self.set_columnid(columnid)
        self.name = name

    def set_tableid(self, tableid):
        self.tableid = tableid
        if tableid is not None:
            self.tableids = [tableid]
        else:
            self.tableids = []

    def set_columnid(self, columnid):
        self.columnid = columnid
        if columnid is not None:
            self.columnids = [columnid]
        else:
            self.columnids = []


    def getEditableAttributes(self):
        return TableReader.getEditableAttributes(self) + ["tableid", "singlecolumname"] class VariableReader(StatisticReader):
    """
    Reader class to parse SCIP variable and constraint counts for original and transformed problem
    """
    name = 'VariableReader'
    varexp = re.compile(r'^  Variables        :')
    consexp = re.compile(r'^  Constraints      :')
    varkeys = ['Vars', 'BinVars', 'IntVars', 'ImplVars', 'ContVars']
    conskeys = ["InitialNCons", "MaxNCons"]
    problemtype = None
    
    def extractStatistic(self, line):
        
        # parse the problem type (original or presolved)
        if line.startswith('Presolved Problem  :'):
            self.problemtype = "PresolvedProblem"
        elif line.startswith('Original Problem   :'):
            self.problemtype = "OriginalProblem"
     
        # check if the SCIP variable expression is matched by line
        elif self.problemtype and self.varexp.match(line):
            nvariables = list(map(int, misc.numericExpression.findall(line)[:len(self.varkeys)]))
            datakeys = ["%s_%s" % (self.problemtype, key) for key in self.varkeys]
            self.addData(datakeys, nvariables)

        # check if the constraint expression is matched by line
        elif self.problemtype and self.consexp.match(line):
            nconns = list(map(int, misc.numericExpression.findall(line)[:len(self.conskeys)]))

            datakeys = ["%s_%s" % (self.problemtype, key) for key in self.conskeys]
            self.addData(datakeys, nconns)
            
            # reset the problem type
            self.problemtype = None
            
        return None class CustomReader(StatisticReader):
    """
    Reader to be initialised interactively through IPET or from an interactive python shell
    """
    name = 'CustomReader'
    regexp = 'Custom'
    datakey = 'Custom'
    data = None

    METHOD_FIRST = 1
    METHOD_LAST = 2
    METHOD_SUM = 3
    METHOD_MIN = 4
    METHOD_MAX = 5
    METHOD_COUNT = 6

    str2method = {
                  "first" : METHOD_FIRST,
                  "last" : METHOD_LAST,
                  "sum" : METHOD_SUM,
                  "min" : METHOD_MIN,
                  "max" : METHOD_MAX,
                  "count" : METHOD_COUNT
                  }


    requiredoptions = {
            "datatype" : ["float", "int"],
            "method" : list(str2method.keys())
        }

    def __init__(self, name = None, regpattern = None, datakey = None, index = 0, datatype = "float", method = "last", active = True):
        """
        constructor of a custom reader to parse additional simple solver output from log file context

        Parameters:
        -----------

        name : a name to distinguish this reader from the others

        regpattern : A string or regular expression pattern to detect lines from which output should be read

        datakey : The data key under which the parsed datum gets stored for every problem

        index : The zero-based index of the number in the specified line (only numbers count)

        datatype : choose 'int' or 'float'

        method : how to treat multiple occurrences of this data within one problem; 'count' occurrences or parse 'first', 'last', 'sum', 'min' or 'max'
        """

        IpetNode.__init__(self, active)

        if regpattern is None:
            raise ValueError("Error: No 'regpattern' specified for reader with name %s" % str(name))

        if name in [None, ""]:
            self.name = datakey + "Reader"
            self.username = False
        else:
            self.name = name
            self.username = True

        self.set_datakey(datakey)

        self.set_index(index)

        self.regpattern = regpattern
        self.set_regpattern(regpattern)

        self.method = method
        self.methodint = self.METHOD_LAST
        self.set_method(method)

        self.set_datatype(datatype)

    def getEditableAttributes(self):
        return ['name', 'regpattern', 'datakey', 'index', 'datatype', 'method'] + IpetNode.getEditableAttributes(self)

    def getRequiredOptionsByAttribute(self, attr):
        return self.requiredoptions.get(attr, IpetNode.getRequiredOptionsByAttribute(self, attr))

    def extractStatistic(self, line):
        if self.regexp.search(line):

            logging.debug("Custom Reader {} found match in line \n{}".format(self.name, line.strip()))
            logging.debug("Numerical expression matches: {}".format(", ".join(misc.numericExpression.findall(line))))

            previousdata = self.testrun.getCurrentProblemData(self.datakey)
            if self.methodint == CustomReader.METHOD_COUNT:
                if previousdata is None:
                    self.addData(self.datakey, 1)
                else:
                    self.addData(self.datakey, previousdata + 1)
                return

            try:
                data = misc.getNumberAtIndex(line, self.index)
                data = self.datatypemethod(data)


                if self.methodint == CustomReader.METHOD_FIRST:
                    if previousdata is None:
                        self.addData(self.datakey, data)

                elif self.methodint == CustomReader.METHOD_LAST:
                    self.addData(self.datakey, data)

                elif self.methodint == CustomReader.METHOD_SUM:
                    if previousdata is None:
                        previousdata = 0

                    self.addData(self.datakey, data + previousdata)

                elif self.methodint == CustomReader.METHOD_MIN:
                    if previousdata is None:
                        self.addData(self.datakey, data)
                    elif data < previousdata:
                        self.addData(self.datakey, data)

                elif self.methodint == CustomReader.METHOD_MAX:
                    if previousdata is None:
                        self.addData(self.datakey, data)
                    elif data > previousdata:
                        self.addData(self.datakey, data)

            except:
                logger.warn("Reader %s could not retrieve data at index %d from matching line '%s'", self.getName(), self.index, line)
                pass


        return None

    def setDataType(self, sometype):
        """
        recognizes data types (e.g., 'float' or 'int') and sets reader data type to this value
        """
        try:
            self.datatypemethod = getattr(builtins, sometype)
            self.datatype = sometype
        except:
            logger.debug("Error: Could not recognize data type %s, using float" % sometype)
            self.datatypemethod = float
            self.datatype = 'float'
    def set_datatype(self, datatype):
        self.setDataType(datatype)

    def set_method(self, method):
        self.methodint = self.str2method.get(method, self.methodint)
        self.method = method

    def set_regpattern(self, regpattern):
        self.regexp = re.compile(regpattern)
        self.regpattern = regpattern

    def set_name(self, name):
        if name == self.getName():
            return
        if name in ["", None]:
            self.name = self.datakey + "Reader"
            self.username = False
        else:
            self.name = name
            self.username = True


    def set_datakey(self, datakey):
        self.datakey = datakey
        if not self.username:
            self.name = self.datakey + "Reader"

    def set_index(self, index):
        self.index = int(index) class TraceFileReader(StatisticReader):
    """
    classdocs
    """
    active = False
    context = Key.CONTEXT_TRACEFILE
    tracefilestartexpression = "* Trace Record Definition"
    input = "InputFileName,ModelType,SolverName,OptionFile,Direction,NumberOfEquations,NumberOfVariables,NumberOfDiscreteVariables,NumberOfNonZeros,NumberOfNonlinearNonZeros," + \
            "ModelStatus,SolverStatus,ObjectiveValue,ObjectiveValueEstimate,SolverTime,ETSolver,NumberOfIterations,NumberOfNodes"
    datakeys = input.split(",")[1:]
    
    floatmap = {"NA":None,
                "-INF":-FLOAT_INFINITY,
                "+INF":+FLOAT_INFINITY
                }

    def prepareData(self, value):
        value = self.floatmap.get(value, value)
        if value is None:
            return None

        for datatype in (int, float):
            try:
                return datatype(value)
            except ValueError:
                pass

        return value

    def extractStatistic(self, line):
        if line.startswith(self.tracefilestartexpression):
            self.active = True
        elif self.active and not line.startswith("*"):
            splitline = line.split(",")
            probname = splitline[0]
            datavalues = list(map(self.prepareData, splitline[1:]))

            logger.debug("Trace File Reader adds data for problem %s", probname)
            self.testrun.addDataByName(self.datakeys, datavalues, probname) class Manager(Observable):
    """
    manages all manageables of a certain type of which many objects might exist and need to be listed / browsed frequently
    """

    def __init__(self, listofmanageables=[], activate=False):
        """
        constructs a new Manager
        
        constructs a new Manager by creating an empty dictionary. Fills the dictionary with a list
        of manageables, if non-empty. All elements can be optionally activated.
        """
        self.stringrepresentations = {}
        self.activeset = set()
        for manageable in listofmanageables:
            self.addManageable(manageable)
            if activate:
                self.activate(manageable)

    def addManageable(self, manageable):
        """
        add manageable to dictionary - ensures that only one manageable with that string representation is stored
        """
        stringrepresentation = self.getStringRepresentation(manageable)
        self.stringrepresentations[stringrepresentation] = manageable

    def getStringRepresentation(self, manageable):
        """
        return an manageable's string representation
        """
        if type(manageable) is str:
            return manageable
        else:
            try:
                return manageable.getName()
            except AttributeError:
                return str(manageable)

    def getManageable(self, stringrepresentation):
        """
        returns the manageable belonging to a string representation, or None, if no such manageable is available
        """
        return self.stringrepresentations.get(stringrepresentation, None)

    def deleteManageable(self, manageable):
        """
        delete an manageable from the manager
        """
        for key, val in list(self.stringrepresentations.items()):
            if val == manageable:
                oldstringrepresentation = key
                break
        del self.stringrepresentations[oldstringrepresentation]
        self.deactivate([manageable])

    def reinsertManageable(self, manageable):
        """
        reinserts a manageable after a possible name change that was not 
        
        reinserts a manageable after its name has changed
        """
        active = self.isActive(manageable)
        self.deleteManageable(manageable)
        self.addManageable(manageable)
        if active:
            self.activate([manageable])

    def editObjectAttribute(self, manageable, attributename, newattribute):
        """
        edit an objects attribute and ensure that a changed object representation is directly
        processed
        """
        oldname = self.getStringRepresentation(manageable)
        manageable.editAttribute(attributename, newattribute)
        newname = self.getStringRepresentation(manageable)
        print(newname, newattribute)
        if oldname != newname:
            self.chgManageableName(manageable, oldname, newname)
        self.notify(Message("Changed attribute %s of %s to %s" % (attributename, newname, newattribute), Message.MESSAGETYPE_INFO))

    def chgManageableName(self, manageable, oldname, newname):
        """
        changes a manageables name, if possible
        """
        if newname != oldname:
            if self.getManageable(newname) is not None:
                raise KeyError("An element of name %s is already listed" % (newname))
            del self.stringrepresentations[oldname]
            self.stringrepresentations[newname] = manageable

    def getManageables(self, onlyactive=False):
        """
        returns all (or only active) manageables
        """
        if onlyactive:
            return list(self.activeset)
        else:
            return list(self.stringrepresentations.values())

    def getAllRepresentations(self, onlyactive=False):
        """
        returns a list of all string representations
        """
        if not onlyactive:
            return list(self.stringrepresentations.keys())
        else:
            return [self.getStringRepresentation(manageable) for manageable in self.activeset]

    def activate(self, manageables):
        """
        adds a manageable to the active set
        """
        for manageable in manageables:
            if self.getStringRepresentation(manageable) not in self.stringrepresentations:
                raise KeyError("%s is not managed by this manager - call addManageable() first" % (self.getStringRepresentation(manageable)))
            if not manageable in self.activeset:
                self.activeset.add(manageable)

        if manageables != []:
            self.notify(Message("Activated %s" % ", ".join(map(self.getStringRepresentation, manageables)), messagetype=Message.MESSAGETYPE_INFO))

    def addAndActivate(self, manageable):
        """
        adds a manageable to the manager and activates it
        """
        self.addManageable(manageable)
        self.activate([manageable])

    def getActiveSet(self):
        """
        returns the set of active objects managed by the manager
        """
        return self.activeset

    def deactivate(self, manageables):
        """
        removes a list of manageables from the active set of managed objects - elements stays present and can be activated again
        """
        for manageable in manageables:
            try:
                self.activeset.remove(manageable)
            except KeyError:
                pass
        self.notify(Message("Deactivated %s" % ", ".join(map(self.getStringRepresentation, manageables)), messagetype=Message.MESSAGETYPE_INFO))


    def countManageables(self, onlyactive):
        """
        count the number of (optionally only the active) manageables
        """
        if onlyactive:
            return len(self.activeset)
        else:
            return len(list(self.stringrepresentations.keys()))

    def isActive(self, manageable):
        """
        is the manageable part of the active set of this manager
        """
        return manageable in self.activeset class IpetNode(Editable):
    """
    A Node is an interface to an XML-like ancestor-structure
    """
    active = True
    _attributes2options = {"active":[True, False]}
    deprecatedattrdir = {}
    
    def __init__(self, active = True, **kw):
        """
        constructs a new IpetNode

        Parameters
        ----------
        active : True or "True" if this element should be active, False otherwise
        """
        self.set_active(active)

        for d, message in self.deprecatedattrdir.items():
            if d in kw:
                logger.warning("Warning : The attribute '{0}' is deprecated and will be ignored; {1}. ".format(d, message))

    def addChild(self, child):
        """
        append a child to this node's children
        """
        pass
    
    def getChildren(self):
        """
        return a list of all children of this node
        """
        return None
    
    def acceptsAsChild(self, child):
        """
        returns whether this node accepts node 'child' as child
        """
        return False
    
    def removeChild(self, child):
        """
        remove the child from this node's children.
        """
        pass
    
    def isActive(self):
        """
        returns True if this node is currently active, False otherwise
        """
        return self.active
    
    def set_active(self, active):
        self.active = True if active in [True, "True"] else False

    def getEditableAttributes(self):
        """
        returns all editable attributes
        """
        return ["active"]

    def getRequiredOptionsByAttribute(self, attr):
        return self._attributes2options.get(attr)

    @staticmethod
    def getNodeTag():
        return "Node" class SCIPSolver(Solver):

    solverId = "SCIP"
    recognition_expr = re.compile("^SCIP version ")
    primalbound_expr = re.compile("^Primal Bound       : (\S+)")
    dualbound_expr = re.compile("^Dual Bound         : (\S+)")
    solvingtime_expr = re.compile("^Solving Time \(sec\) : (\S+)")
    version_expr = re.compile("SCIP version (\S+)")
    limitreached_expr = re.compile("((?:^SCIP Status        :)|(?:\[(?:.*) (reached|interrupt)\]))")
    nodes_expr = re.compile("(?:Solving Nodes      : *\d+ \(total of (\d+) nodes in \d+ runs\)$|Solving Nodes      : *(\d+)$|  nodes \(total\)    : *(\d+) \()")
    extrasol_expr = re.compile("^feasible solution found .* after (.*) seconds, objective value (\S*)")
    soplexgithash_expr = re.compile("^  SoPlex .+\[GitHash: (\S+)\]")
    violbound_expr = re.compile("^  bounds           : \S+ (\S+)$")
    violint_expr = re.compile("^  integrality      : (\S+)")
    viollp_expr = re.compile("^  LP rows          : \S+ (\S+)$")
    violcons_expr = re.compile("^  constraints      : \S+ (\S+)$")

    # variables needed for primal bound history
    primalboundhistory_exp = re.compile('^\s+time\s+\| .* \|\s+primalbound\s+\|\s+gap')
    heurdispcharexp = re.compile('^[^ \d]')
    """ all lines starting with a non-whitespace and non-digit character """

    shorttablecheckexp = re.compile('s\|')
    firstsolexp = re.compile('^  First Solution   :')

    # variables needed for dual bound history
    regular_exp = re.compile('\|')  # compile the regular expression to speed up reader

    solverstatusmap = {
        "no problem exists" : Key.SolverStatusCodes.Readerror,
        "SCIP Status        : problem is solved \[optimal solution found\]":Key.SolverStatusCodes.Optimal,
        "SCIP Status        : problem is solved \[infeasible\]": Key.SolverStatusCodes.Infeasible,
        "SCIP Status        : problem is solved \[infeasible or unbounded\]": Key.SolverStatusCodes.InfOrUnbounded,
        "SCIP Status        : solving was interrupted \[time limit reached\]" : Key.SolverStatusCodes.TimeLimit,
        "SCIP Status        : solving was interrupted \[memory limit reached\]" : Key.SolverStatusCodes.MemoryLimit,
        "SCIP Status        : solving was interrupted \[node limit reached\]" : Key.SolverStatusCodes.NodeLimit,
        "SCIP Status        : solving was interrupted \[gap limit reached\]" : Key.SolverStatusCodes.GapLimit,
        "SCIP Status        : solving was interrupted" : Key.SolverStatusCodes.Interrupted,
    }

    def __init__(self, **kw):
        super(SCIPSolver, self).__init__(**kw)

    def reset(self):
        """ reset stored data
        """
        Solver.reset(self)
        # variables needed for dual bound history
        self.lastdualbound = misc.FLOAT_INFINITY
        self.lasttime = -1
        self.dualboundindex = -1
        self.primalboundindex = -1

    def extractPrimalboundHistory(self, line : str):
        """ Extract the sequence of primal bounds
        """

        #
        # check if an additional line is printed for a feasible solution before presolving
        #
        extrasolmatch = self.extrasol_expr.match(line)
        if extrasolmatch:
            pointInTime, PrimalBound = extrasolmatch.groups()
            # store newly found (time, primal bound) tuple if it differs from the last primal bound
            self.addHistoryData(Key.PrimalBoundHistory, pointInTime, PrimalBound)
            return
        elif not self.isTableLine(line):
            return

        # history reader should be in a table. check if a display char indicates a new primal bound
        if self.heurdispcharexp.match(line):

            allmatches = misc.numericExpression.findall(line[:line.rindex("|")])

            if len(allmatches) == 0:
                return

            splitline = line.split("|")
            pointInTime = allmatches[0]
            PrimalBound = splitline[self.primalboundindex]
            # in the case of ugscip, we reacted on a disp char, so no problem at all.
            self.addHistoryData(Key.PrimalBoundHistory, pointInTime, PrimalBound)


    def extractDualboundHistory(self, line : str):
        """ Extract the sequence of dual bounds
        """
        if not self.isTableLine(line):
            return

        try:
            # TODO This works, why is eclipse complaining?
            lineelems = misc.tablenumericExpression.findall(line)
            # parse time and dual bound from the table
            time = lineelems[0]
            dualbound = line.split("|")[self.dualboundindex]

            # store newly found (time, dual bound) tuple if it differs from the last dual bound
            self.addHistoryData(Key.DualBoundHistory, time, dualbound)

        except ValueError:
            return None
        except IndexError:
            return None

    def isTableLine(self, line : str) -> bool:
        """ decide if line is a data line of the table
        """
        if self.primalboundhistory_exp.match(line):
            columnheaders = list(map(str.strip, line.split('|')))
            self.dualboundindex = columnheaders.index('dualbound')
            self.primalboundindex = columnheaders.index('primalbound')
            return True
        elif self.shorttablecheckexp.search(line):
            return True
        return False

    def extractMoreData(self, line : str):
        """Handle more than just the version
        """
        for keyword in ["mode", "LP solver", "GitHash"]:
            data = re.search(r'SCIP.*\[%s: ([^]]*)\]' % keyword, line)
            if data:
                if keyword == "LP solver":
                    data = data.groups()[0]
                    lp_data = data.split(" ")
                    if len(lp_data) == 1:
                        self.addData("LPSolver", data)
                    else:
                        self.addData("LPSolver", lp_data[0])
                        self.addData("LPSolverVersion", " ".join(lp_data[1:]))
                else:
                    self.addData(keyword, data.groups()[0])
        soplexhashmatch = self.soplexgithash_expr.match(line)
        if soplexhashmatch:
            self.addData("SpxGitHash", soplexhashmatch.groups()[0])

    def extractPath(self, line : str):
        """Extract the path info
        """
        if line.startswith('loaded parameter file'):
            absolutesettingspath = line[len('loaded parameter file '):].strip('<>')
            self.addData(Key.SettingsPathAbsolute, absolutesettingspath)
            settings = os.path.basename(absolutesettingspath)
            settings = os.path.splitext(settings)[0]

    def extractOptionalInformation(self, line : str):
        """Extract the path info
        """
        self.extractPath(line)
        self.extractMoreData(line) class CbcSolver(Solver):

    solverId = "CBC"
    recognition_expr = re.compile("Welcome to the CBC MILP Solver")
    primalbound_expr = re.compile("^Objective value:\s*(\S*)")
    dualbound_expr = re.compile("^(?:Lower bound\s*|Objective value\s*):\s*(\S*)")
    solvingtime_expr = re.compile("Total time \(CPU seconds\):\s*(\S*)")
    version_expr = re.compile("^Version: (\S+)")
    nodes_expr = re.compile("^Enumerated nodes: *(\S+)")

    solverstatusmap = {"Result - Optimal solution found" : Key.SolverStatusCodes.Optimal,
                       "Result - Stopped on time limit" : Key.SolverStatusCodes.TimeLimit,
                       "Result - Problem proven infeasible" : Key.SolverStatusCodes.Infeasible,
                       "Problem is unbounded" : Key.SolverStatusCodes.Unbounded,
                       "Pre-processing says infeasible or unbounded" : Key.SolverStatusCodes.InfOrUnbounded,
                       "\*\* Current model not valid" : Key.SolverStatusCodes.Readerror
                       #                       "" : Key.SolverStatusCodes.MemoryLimit,
                       #                       "" : Key.SolverStatusCodes.NodeLimit,
                       #                       "" : Key.SolverStatusCodes.Interrupted
                       }

    def __init__(self, **kw):
        super(CbcSolver, self).__init__(**kw)

    def extractPrimalboundHistory(self, line : str):
        """ Extract the sequence of primal bounds
        """
        if "Integer solution of " in line:
            self.readBoundAndTime(line, 4, -2, timestripchars = "(")
        return None class XpressSolver(Solver):

    solverId = "XPRESS"
    recognition_expr = re.compile("FICO Xpress")
    primalbound_expr = re.compile("Objective value =\s*(\S*)")
    dualbound_expr = re.compile("Best Bound =\s*(\S*)")
    solvingtime_expr = re.compile(" \*\*\* Search.*\*\*\*\s*Time:\s*(\S*)")
    version_expr = re.compile(".*Optimizer.* v([0-9.]*) ")
    nodes_expr = re.compile("^Nodes explored = (.*)$")


    # TODO does this work? Benchmarks seem to be broken
    solverstatusmap = {r"Best integer solution found" : Key.SolverStatusCodes.Optimal,
                       "Problem is integer infeasible" : Key.SolverStatusCodes.Infeasible,
                       "Problem is unbounded" : Key.SolverStatusCodes.Unbounded,
                       "STOPPING - MAXTIME limit reached" : Key.SolverStatusCodes.TimeLimit,
                       "\?(45|20)" : Key.SolverStatusCodes.MemoryLimit,
                       #                       "" : Key.SolverStatusCodes.NodeLimit,
                       r" \*\*\* Search unfinished \*\*\*" : Key.SolverStatusCodes.Interrupted,
                      r"\?(66|1038|1039)" : Key.SolverStatusCodes.Readerror
                       }

    objsensemap = {
            "^Minimizing MILP" : Key.ObjectiveSenseCode.MINIMIZE,
            "^Maximizing MILP" : Key.ObjectiveSenseCode.MAXIMIZE,
        }

    # variables needed for primal bound history extraction
    xpresscutidx = -1

    def __init__(self, **kw):
        super(XpressSolver, self).__init__(**kw)

    def extractPrimalboundHistory(self, line : str):
        """ Extract the sequence of primal bounds
        """
        if "BestSoln" in line:
            self.xpresscutidx = line.index("BestSoln") + len("BestSoln")
        elif re.search("^[a-zA-Z*](\d| )", line):
            self.readBoundAndTime(line, -1, -1, cutidx = self.xpresscutidx)
        elif line.startswith(" \*\*\* Heuristic solution found: "):
            self.readBoundAndTime(line, -4, -2)

    def extractStatus(self, line:str):
        if self.getData(Key.SolverStatus) in [Key.SolverStatusCodes.Crashed, Key.SolverStatusCodes.Optimal]:
            Solver.extractStatus(self, line)

    def cleanupData(self):
        """Clean up Xpress solver data

        in addition to the clean up step of the solver class, the dual bound reported by XPress
        can be wrong in that it exceeds the primal bound. In that case, the primal bound is
        taken as a reference and the dual bound is adapted.
        """

        super(XpressSolver, self).cleanupData()

        pb = self.getData(Key.PrimalBound)
        db = self.getData(Key.DualBound)

        if  pb is not None and db is not None:
            if self.getData(Key.ObjectiveSense) == Key.ObjectiveSenseCode.MAXIMIZE:
                db = max(db, pb)
            elif self.getData(Key.ObjectiveSense) == Key.ObjectiveSenseCode.MINIMIZE:
                db = min(db, pb)

            self.addData(Key.DualBound, db) class GurobiSolver(Solver):

    solverId = "GUROBI"
    recognition_expr = re.compile("Gurobi Optimizer version")
    primalbound_expr = re.compile("^(?:Best|Optimal) objective +([^\s,]+),*")
    dualbound_expr = re.compile("^(?:Best objective \S+, best bound|Optimal objective) +([^\s,]+),*")
    solvingtime_expr = re.compile("^(?:Explored \d* nodes .* in|Solved in .* iterations and) (\S*) seconds")
    version_expr = re.compile("Gurobi Optimizer version (\S+)")
    nodes_expr = re.compile("Explored (\d+) nodes")

    solverstatusmap = {"(Optimal solution found|Solved with barrier|Solved in \d+ iterations)" : Key.SolverStatusCodes.Optimal,
                       "Model is infeasible$" : Key.SolverStatusCodes.Infeasible,
                       "Model is infeasible or unbounded" : Key.SolverStatusCodes.InfOrUnbounded,
                        "Time limit reached" : Key.SolverStatusCodes.TimeLimit,
                        "^(ERROR 10001|Out of memory)" : Key.SolverStatusCodes.MemoryLimit,
#                       "" : Key.SolverStatusCodes.NodeLimit,
#                       "" : Key.SolverStatusCodes.Interrupted
                        "^ERROR 10003" : Key.SolverStatusCodes.Readerror,
                        "^Model is unbounded" : Key.SolverStatusCodes.Unbounded,
                       }

    # variables needed for bound history
    inTable = False
    gurobiextralist = []

    def __init__(self, **kw):
        super(GurobiSolver, self).__init__(**kw)

    def extractPrimalboundHistory(self, line : str):
        """ Extract the sequence of primal bounds
        """
        if "Found heuristic solution" in line:
            self.gurobiextralist.append(line.split()[-1])
        if "Expl Unexpl |  Obj  Depth" in line:
            self.inTable = True
        elif self.inTable and line.endswith("s\n") and self.gurobiextralist != []:
            pointInTime = line.split()[-1].strip("s")
            self.addHistoryData(Key.PrimalBoundHistory, pointInTime, self.gurobiextralist[-1])
            self.gurobiextralist = []
        elif self.inTable and line.startswith("H") or line.startswith("*"):
            self.readBoundAndTime(line, -5, -1, timestripchars = "s")

        elif "Cutting planes:" in line and self.inTable:
            self.inTable = False
        elif self.gurobiextralist != [] and "Explored " in line:
            pointInTime = line.split()[-2]
            self.addHistoryData(Key.PrimalBoundHistory, pointInTime, self.gurobiextralist[-1])
            self.gurobiextralist = []
        if "Explored " in line and "simplex iterations" in line:
            self.inTable = False
        return None

    def extractDualboundHistory(self, line : str):
        """ Extract the sequence of dual bounds
        """
        if "Expl Unexpl |  Obj  Depth" in line:
            self.inTable = True
        elif self.inTable and line.endswith("s\n"):
            self.readBoundAndTimeDual(line, -4, -1, timestripchars = "s")

        elif "Cutting planes:" in line and self.inTable:
            self.inTable = False
        if "Explored " in line and "simplex iterations" in line:
            self.inTable = False
        if self.gurobiextralist != [] and "Explored " in line:
            pointInTime = line.split()[-2]
            self.addHistoryData(Key.DualBoundHistory, pointInTime, self.gurobiextralist[-1])
            self.gurobiextralist = []
        return None

    def extractPrimalbound(self, line : str):
        """overrides method in super class Solver

        Gurobi may report the best objective of the barrier algorithm, which needs to
        overridden by the MIP result
        """

        # delete any previous matching line
        if self.primalbound_expr.match(line):
            self.deleteData(Key.PrimalBound)

        # invoke primal bound extraction of super class
        super(GurobiSolver, self).extractPrimalbound(line)

    def extractOptionalInformation(self, line : str):
        if "Explored " in line and "simplex iterations" in line:
            nnodes = line.split()[1]
            self.addData('Nodes', int(nnodes))
        return None class CplexSolver(Solver):

    solverId = "CPLEX"
    recognition_expr = re.compile("^Welcome to IBM\(R\) ILOG\(R\) CPLEX\(R\) Interactive Optimizer")
    primalbound_expr = re.compile("^MIP -.*Objective =\s*(\S+)")
    dualbound_expr = re.compile("^(?:Current MIP best bound|^MIP - Integer optimal solution:  Objective) =\s*(\S+)")
    solvingtime_expr = re.compile("Solution time =\s*(\S+)")
    version_expr = re.compile("^Welcome to IBM\(R\) ILOG\(R\) CPLEX\(R\) Interactive Optimizer (\S+)")
    nodes_expr = re.compile("Solution time = .* sec\.  Iterations = \d+  Nodes = (\S+)")


    elapsedtime_expr = re.compile("^Elapsed time = .+ sec. \(.* ticks, tree = .* MB, solutions = \d+\)")

    solverstatusmap = {"MIP - Integer optimal" : Key.SolverStatusCodes.Optimal,
                       "MIP - Integer infeasible\." : Key.SolverStatusCodes.Infeasible,
                       "MIP - Time limit exceeded" : Key.SolverStatusCodes.TimeLimit,
                       "MIP - Integer unbounded" : Key.SolverStatusCodes.Unbounded,
                       "MIP - Integer infeasible or unbounded." : Key.SolverStatusCodes.InfOrUnbounded,
                       "CPLEX Error  1001: Out of memory\." : Key.SolverStatusCodes.MemoryLimit,
                       "No file read\." : Key.SolverStatusCodes.Readerror,

                       #                       "" : Key.SolverStatusCodes.NodeLimit,
                       #                       "" : Key.SolverStatusCodes.Interrupted
                       }

    objsensemap = {
        "^CPLEX> Problem is a minimization problem" : Key.ObjectiveSenseCode.MINIMIZE,
        "^CPLEX> Problem is a maximization problem" : Key.ObjectiveSenseCode.MAXIMIZE,
        }

    # variables needed for primal bound history extraction
    easyCPLEX = False
    lastelapsedtime = 0.0
    nnodessincelastelapsedtime = 0
    lastnnodes = 0
    cpxprimals = []
    inTable = False

    def __init__(self, **kw):
        super(CplexSolver, self).__init__(**kw)

    def extractOptionalInformation(self, line):
        """Extract the settings
        """
        self.extractSettings(line)

    def extractSettings(self, line):
        """Extract the settings
        """
        if "CPLEX> Non-default parameters written to file" in line:
            self.addData(Key.Settings, line.split('.')[-3])

    # TODO DualBoundHistory
    def extractPrimalboundHistory(self, line : str):
        """ Extract the sequence of primal bounds
        """
        if self.easyCPLEX and "Found incumbent of value" in line:
            splitline = line.split()
            self.readBoundAndTime(line, splitline.index("Found") + 4, splitline.index("Found") + 6)
        elif not self.easyCPLEX:
            if "Welcome to IBM(R) ILOG(R) CPLEX(R)" in line:
                self.lastelapsedtime = 0.0
                self.nnodessincelastelapsedtime = 0
                self.lastnnodes = 0
                self.cpxprimals = []
            if "   Node  Left     Objective  IInf  Best Integer    Best Bound    ItCnt     Gap" in line:
                self.inTable = True
            elif self.inTable and ("cuts applied:" in line or "Root node processing" in line):
                self.inTable = False
            elif self.inTable and "Repeating presolve." in line:
                self.inTable = False
            elif self.inTable and len(line) > 0 and (line.startswith(" ") or line.startswith("*")):
                if line == "\n":
                    return None
                nodeinlineidx = 7
                while line[nodeinlineidx] != " " and line[nodeinlineidx] != "+":
                    nodeinlineidx += 1
                nnodes = int(line[:nodeinlineidx].split()[-1].strip('*+')) + 1
                if line.startswith("*"):
                    if '+' in line:
                        primalbound = line.split()[-3]
                    else:
                        primalbound = line.split()[-4]
                    self.cpxprimals.append((nnodes, primalbound))
                self.lastnnodes = nnodes
            elif self.elapsedtime_expr.match(line):
                thetime = float(line.split()[3])
                self.processCpxprimals(thetime)

                self.nnodessincelastelapsedtime = self.lastnnodes
                self.lastelapsedtime = thetime

            elif "Solution time =" in line:
                thetime = float(line.split()[3])
                self.processCpxprimals(thetime)

    def processCpxprimals(self, currenttime):
        solvednodes = (self.lastnnodes - self.nnodessincelastelapsedtime)
        timespentonnode = (currenttime - self.lastelapsedtime) / max(1.0, float(solvednodes))
        assert currenttime >= self.lastelapsedtime
        for node, bound in self.cpxprimals:
            estimatedtime = self.lastelapsedtime + (node - self.nnodessincelastelapsedtime) * timespentonnode
            self.addHistoryData(Key.PrimalBoundHistory, estimatedtime, bound)

        self.cpxprimals = [] class FiberSCIPSolver(SCIPSolver):
    """Reads data from FiberSCIP output, that ressembles SCIP output a lot, except for the table.
    """
    solverId = "FiberSCIP"
    recognition_expr = re.compile("^The following solver is parallelized by UG")
    primalbound_expr = re.compile("^  Primal Bound     : (\S+)")
    dualbound_expr = re.compile("^  Dual Bound       : (\S+)")
    solvingtime_expr = re.compile("^Total Time         : (\S+)")
    version_expr = re.compile("^The following solver is parallelized by UG version (\S+)")
    nodes_expr = re.compile("  nodes \(total\)    : *(\d+)")

    # variables needed for primal bound history
    heurdispcharexp = re.compile('^\* ')
    """ all lines starting with a single (!!!) asterisk. UG sometimes prints double asterisks """

    ugtableexp = re.compile('^\s+Time\s+Nodes')

    solverstatusmap = {
        "SCIP Status        : problem is solved" : Key.SolverStatusCodes.Optimal,
        "SCIP Status        : solving was interrupted \[ hard time limit reached \]" : Key.SolverStatusCodes.TimeLimit,
    }

    def __init__(self, **kw):
        super(FiberSCIPSolver, self).__init__(**kw)


    def isTableLine(self, line : str) -> bool:
        if self.ugtableexp.match(line):
            return True
        elif self.heurdispcharexp.match(line):
            return True
        return False

    def extractPrimalboundHistory(self, line : str):
        """ Extract the sequence of primal bounds
        """

        if not self.isTableLine(line):
            return

        # history reader should be in a table. check if a display char indicates a new primal bound
        if self.heurdispcharexp.match(line):
            allmatches = misc.numericExpression.findall(line)[:5]

            if len(allmatches) == 0:
                return

            pointInTime = allmatches[0]
            PrimalBound = allmatches[-1]
            # in the case of ugscip, we reacted on a disp char, so no problem at all.
            self.addHistoryData(Key.PrimalBoundHistory, pointInTime, PrimalBound)

    def extractDualboundHistory(self, line : str):
        """Do not read a dual bound history, override method in SCIP solver

        There is no simple way to distinguish UG table lines from other output unless the primal bound changes.
        """
        pass class MatlabSolver(Solver):
    '''Solver class for the Matlab Intlinprog solver
    '''
    solverId = "Matlab"
    recognition_expr = re.compile(" *< M A T L A B \(R\) >")
    primalbound_expr = re.compile("PrimalBound (\S*)")
    dualbound_expr = re.compile("DualBound (\S*)")
    nodes_expr = re.compile("^BBnodes (\S+)")

    solverstatusmap = {"Intlinprog stopped.* because the objective value is within" : Key.SolverStatusCodes.Optimal,
                       "Intlinprog stopped because it exceeded the time limit" : Key.SolverStatusCodes.TimeLimit,
                       "Intlinprog stopped because the root LP problem is unbounded" : Key.SolverStatusCodes.Unbounded,
                       "Intlinprog stopped because no [a-z]* point" : Key.SolverStatusCodes.Infeasible,
                       "Intlinprog stopped because it exceeded its allocated memory" : Key.SolverStatusCodes.MemoryLimit,
                       "Intlinprog stopped because it reached the maximum number of nodes" : Key.SolverStatusCodes.NodeLimit,
                       "MPSREAD stopped because it encountered an invalid MPS format" : Key.SolverStatusCodes.Readerror,
                       "Intlinprog stopped" : Key.SolverStatusCodes.Interrupted
                       }

    def __init__(self, **kw):
        super(MatlabSolver, self).__init__(**kw) class MosekSolver(Solver):
    '''Solver class for Mosek
    '''

    solverId = "Mosek"
    recognition_expr = re.compile("MOSEK")
    version_expr = re.compile("MOSEK Version (.*)$")
    solvingtime_expr = re.compile("Optimizer terminated\. Time: (.*)$")
    primalbound_expr = re.compile("^Objective of best integer solution : (.*)$")
    dualbound_expr = re.compile("^Best objective bound               : (.*)$")
    nodes_expr = re.compile("^Number of branches                 : (.*)$")

    solverstatusmap = {"^  Solution status : .*OPTIMAL" : Key.SolverStatusCodes.Optimal,
                       "^  Problem status  : PRIMAL_INFEASIBLE$" : Key.SolverStatusCodes.Infeasible,
                       "^(  Problem status  : UNKNOWN|  Solution status : PRIMAL_FEASIBLE)" : Key.SolverStatusCodes.TimeLimit,
                       "^  Problem status  : PRIMAL_INFEASIBLE_OR_UNBOUNDED" : Key.SolverStatusCodes.InfOrUnbounded,
                       "^mosek.Error: \(1101\)" : Key.SolverStatusCodes.Readerror,
                       "^mosek.Error: \(1051\)" : Key.SolverStatusCodes.MemoryLimit
                       }

    def __init__(self, **kw):
        super(MosekSolver, self).__init__(**kw) class MipclSolver(Solver):
    '''Solver class for the MIPCL solver
    '''

    solverId = "MIPCL"
    recognition_expr = re.compile("^MIPCL")
    version_expr = re.compile("^MIPCL version (.*)$")
    solvingtime_expr = re.compile("^Solution time: (.*)$")
    dualbound_expr = re.compile("^     lower-bound: (.*)$")
    #dualbound_expr = re.compile("^Objective value: (.*) - optimality proven")
    primalbound_expr = re.compile("^Objective value: (\S+)")
    nodes_expr = re.compile("Branch-and-Cut nodes: (.*)$")



    solverstatusmap = {"^Objective value: .* - optimality proven" : Key.SolverStatusCodes.Optimal,
                       "^This problem is infeasible" : Key.SolverStatusCodes.Infeasible,
                       "^Time limit reached" : Key.SolverStatusCodes.TimeLimit
                       }

    # members needed for primal bound history
    inTable = False
    primalboundhistory_exp = re.compile("^      Time     Nodes    Leaves   Sols       Best Solution         Lower Bound     Gap%")
    endtable = re.compile('^===========================================')

    def __init__(self, **kw):
        super(MipclSolver, self).__init__(**kw)


    def extractDualbound(self, line : str):
        Solver.extractDualbound(self, line)
        # Mipcl only reports a primal bound in case it solved to completion
        if self.primalbound_expr.match(line) and re.search("optimality proven$", line):
            self.extractByExpression(line, self.primalbound_expr, Key.DualBound)



    def extractHistory(self, line : str):
        """ Extract the sequence of primal bounds
        """

        return

        if not self.isTableLine(line):
            return

        # history reader should be in a table. check if a display char indicates a new primal bound
        if self.inTable:
            allmatches = misc.numericExpressionOrInf.findall(line)
            if len(allmatches) == 0:
                return

            pointInTime = allmatches[0]
            pb = allmatches[4]
            db = allmatches[5]
            # in the case of ugscip, we reacted on a disp char, so no problem at all.
            self.addHistoryData(Key.PrimalBoundHistory, pointInTime, pb)
            self.addHistoryData(Key.DualBoundHistory, pointInTime, db)

    def isTableLine(self, line):
        if self.primalboundhistory_exp.match(line):
            self.inTable = True
            return False
        elif self.inTable and self.endtable.match(line):
            self.inTable = False
            return False
        return self.inTable class NuoptSolver(Solver):

    solverId = "Nuopt"
    recognition_expr = re.compile("^MSI Numerical Optimizer")
    primalbound_expr = re.compile("^VALUE_OF_OBJECTIVE *(\S+)")
    dualbound_expr = re.compile("^(?:Lower bound\s*|Objective value\s*):\s*(\S*)")
    solvingtime_expr = re.compile("^ELAPSED_TIME\(sec\.\) \s*(\S+)")
    version_expr = re.compile("^^MSI Numerical Optimizer (\S+)")
    nodes_expr = re.compile("^PARTIAL_PROBLEM_COUNT \s*(\S+)")
    gap_expr = re.compile("^GAP *(\S+)")
    no_feasible_sol_expr = re.compile("^\(NUOPT 2[02]\) .* \(no feas\.sol\.\)")
    viol_expr = re.compile("^\(NUOPT (14|34|36)\)")

    solverstatusmap = {"^STATUS *OPTIMAL" : Key.SolverStatusCodes.Optimal,
                       "^STATUS *NON_OPTIMAL" : Key.SolverStatusCodes.TimeLimit,
                       "^\(NUOPT 16\) Infeasible MIP" : Key.SolverStatusCodes.Infeasible,
                       "^__STDIN__:\d+: \(MPS FILE \d+\)" : Key.SolverStatusCodes.Readerror,
                       "^(\<preprocess begin\>\.+)*\(NUOPT 12\)" : Key.SolverStatusCodes.MemoryLimit,
                       #                       "" : Key.SolverStatusCodes.NodeLimit,
                       #                       "" : Key.SolverStatusCodes.Interrupted
                       }

    def __init__(self, **kw):
        super(NuoptSolver, self).__init__(**kw)

    def extractDualbound(self, line : str):
        """
        Nuopt only reports the gap, but not the actual dual bound.
        """
        gap_match = self.gap_expr.match(line)

        if gap_match:
            gap = float(gap_match.groups()[0])

            #
            #  A gap of -1 is reported if no solution was found
            #
            if gap >= 0:
                self.addData(Key.DualBound, self.getData(Key.PrimalBound) - gap)
        elif self.solvingtime_expr.match(line) and self.getData(Key.SolverStatus) == Key.SolverStatusCodes.Optimal:
            self.addData(Key.DualBound, self.getData(Key.PrimalBound))

    def extractPrimalbound(self, line : str):
        self.extractByExpression(line, self.primalbound_expr, Key.PrimalBound, float)

        # ## remove primal bound if no feasible solution has been found
        #
        # Nuopt reports 0 in this case
        #
        if self.no_feasible_sol_expr.match(line) or self.viol_expr.match(line):
            self.deleteData(Key.PrimalBound)
            self.deleteData(Key.DualBound) class SasSolver(Solver):
    '''Solver class for Sas
    '''

    solverId = "SAS"
    recognition_expr = re.compile("^NOTE: SAS \(r\) Proprietary Software")
    version_expr = re.compile("^NOTE: SAS \(r\) Proprietary Software (\S+)")
    solvingtime_expr = re.compile("^ *Solution Time *(\S+)$")
    primalbound_expr = re.compile("^ *Objective Value *(\S+)$")
    dualbound_expr = re.compile("^ *Best Bound *(\S+)$")
    nodes_expr = re.compile("^ *Nodes *(\S+)$")

    solverstatusmap = {"^ *Solution Status *(Conditionally)* Optimal" : Key.SolverStatusCodes.Optimal,
                       "^ *Solution Status *Infeasible$" : Key.SolverStatusCodes.Infeasible,
                       "^ *Solution Status *Time Limit Reached" : Key.SolverStatusCodes.TimeLimit,
                       "^ *Solution Status *Infeasible or Unbounded" : Key.SolverStatusCodes.InfOrUnbounded,
                       "^ERROR: The MPS file is incomplete\." : Key.SolverStatusCodes.Readerror,
                       "^ *Solution Status *Out of Memory" : Key.SolverStatusCodes.MemoryLimit
                       }

    def __init__(self, **kw):
        super(SasSolver, self).__init__(**kw) FLOAT_INFINITY = 1e20
FLOAT_LARGE = 1e15
DEFAULT_MIN_GEOM_MEAN = 1.0
    FLOAT_INFINITY = infinity
def sortingKeyContext(context):
def filenameGetContext(filename):
def getWordAtIndex(line : str, index : int) -> str:
def getNumberAtIndex(line : str, index : int) -> str:
def getSoluFileProbName(probname):
def floatPrint(number, digitsbef=16, digitsafter=9, dispchar='g'):
def getTexName(name):
def getCplexGap(value, referencevalue):
def getGap(value : float, referencevalue : float, useCplexGap : bool=False) -> float:
def listGetArithmeticMean(listofnumbers):
def iqr(listofnumbers):
def lQuart(listofnumbers):
def uQuart(listofnumbers):
def shmean(listofnumbers, shiftby=10.0):
def gemean(listofnumbers, mingeommean=DEFAULT_MIN_GEOM_MEAN):
def listGetGeomMean(listofnumbers, mingeommean=DEFAULT_MIN_GEOM_MEAN):
def listGetShiftedGeometricMean(listofnumbers, shiftby=10.0):
def getVariabilityScore(listofnumbers):
def isInfinite(value : float) -> bool:
def getInfinity() -> float:
def setInfinity(infinity : float):
def cutString(string, char='_', maxlength=-1):
def listGetMinColWidth(listofitems):
def strConcat(strings : list) -> str:
def meanOrConcat(l : list):
def saveAsXML(nodeobject, filename : str) -> None:
def convertTimeStamp(timestamp : float) -> str: CONTEXT_LOGFILE = 1  # the log file of a solver which most readers are reading from
CONTEXT_ERRFILE = 2  # the error file of a solver
CONTEXT_SETFILE = 3  # the settings file used for solving
CONTEXT_TRACEFILE = 5
CONTEXT_METAFILE = 0  # the metadata
    MINIMIZE = 1
    MAXIMIZE = -1
class SolverStatusCodes:
class ProblemStatusCodes:
class ObjectiveSenseCode:
    def getBestStatus(*args):
    def getWorstStatus(*args):
def solverToProblemStatusCode(solverstatus : int) -> str: class IPETInconsistencyError(BaseException):

    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return repr(self.msg) CONTEXT_ERRFILE = 2  # the error file of a solver CONTEXT_LOGFILE = 1  # the log file of a solver which most readers are reading from
import os
import logging
import xml.etree.ElementTree as ElementTree
from .StatisticReader import ErrorFileReader, GapReader, TimeLimitReader, ListReader, \
    BestSolInfeasibleReader, MaxDepthReader, MetaDataReader, NodeNameReader, NodesReader, RootNodeFixingsReader, \
    SettingsFileReader, TimeToFirstReader, TimeToBestReader, ObjsenseReader, DateTimeReader, SolCheckerReader
from .StatisticReader_TableReader import TableReader, CustomTableReader
from .StatisticReader_VariableReader import VariableReader
from .StatisticReader_CustomReader import CustomReader
from .TraceFileReader import TraceFileReader
from ipet.concepts.Manager import Manager
from ipet.concepts.IPETNode import IpetNode
from ipet.parsing.Solver import SCIPSolver, CbcSolver, XpressSolver, GurobiSolver, \
    CplexSolver, FiberSCIPSolver, MatlabSolver, MosekSolver, MipclSolver, NuoptSolver, SasSolver
from ipet.misc import misc
from ipet import Key
from ipet.IPETError import IPETInconsistencyError
from ipet.Key import CONTEXT_ERRFILE, CONTEXT_LOGFILE
"""
The MIT License (MIT)

Copyright (c) 2018 Zuse Institute Berlin, www.zib.de

Permissions are granted as stated in the license file you have obtained
with this software. If you find the library useful for your purpose,
please refer to README.md for how to cite IPET.

@author: Gregor Hendel
"""
# CbcSolver, CouenneSolver, \
#     XpressSolver, GurobiSolver, CplexSolver

logger = logging.getLogger(__name__)


class ReaderManager(Manager, IpetNode):