Edit on GitHub

boundml.evaluation

1from .evaluation_tools import evaluate_solvers
2from .solver_evaluation_results import SolverEvaluationResults, SolverEvaluationReport
3
4__all__ = ["evaluate_solvers", "SolverEvaluationResults", "SolverEvaluationReport"]
def evaluate_solvers( solvers: [<class 'boundml.solvers.Solver'>], instances: boundml.instances.Instances, n_instances, metrics, n_cpu=0):
19def evaluate_solvers(solvers: [Solver], instances: Instances, n_instances, metrics, n_cpu=0):
20    if n_cpu == 0:
21        n_cpu = mp.cpu_count()
22
23    n_cpu = min(n_cpu, n_instances + 1)
24
25    data = np.zeros((n_instances, len(solvers), len(metrics)))
26
27    files = {}
28    async_results = {}
29
30    # Start the jobs
31    with mp.Pool(processes=n_cpu) as pool:
32        for i, instance in zip(range(n_instances), instances):
33            for j, solver in enumerate(solvers):
34                prob_file = tempfile.NamedTemporaryFile(suffix=".lp")
35                instance.writeProblem(prob_file.name, verbose=False)
36
37                files[i,j] = prob_file
38                async_results[i,j] = pool.apply_async(_solve, [solver, prob_file.name, metrics])
39
40        print(f"{'Instance':<15}" + "".join([f"{str(solver):<{15 * len(metrics)}}" for solver in solvers]))
41        print(f"{'':<15}" + len(solvers) * ("".join([f"{metric:<15}" for metric in metrics])))
42        for i, instance in zip(range(n_instances), instances):
43            print(f"{i:<15}", end="")
44            for j, solver in enumerate(solvers):
45                line = async_results[i,j].get()
46                files[i,j].close()
47                for k, d in enumerate(line):
48                    data[i, j, k] = d
49                print("".join([f"{d:{'<15.3f' if type(d) == float else '<15'}}" for d in line]), end="", flush=True)
50
51            print()
52
53    res = SolverEvaluationResults(data, [str(s) for s in solvers], metrics)
54
55    print("=" * (15 * (len(solvers) * len(metrics) + 1)))
56
57    ss = {
58        "nnodes": 10,
59        "time": 1,
60        "gap": 1,
61    }
62
63    means = {}
64    for k, metric in enumerate(metrics):
65        mean = res.aggregate(metrics[k], lambda values: shifted_geometric_mean(values, shift=ss[metric]))
66        means[metrics[k]] = mean
67
68    info = []
69    for j in range(len(solvers)):
70        for metric in metrics:
71            info.append(means[metric][j])
72    print(f"{'sg mean': <15}" + "".join([f"{val: <15.3f}" for val in info]))
73
74    return res
class SolverEvaluationResults:
 11class SolverEvaluationResults:
 12    def __init__(self, raw_data: np.array, solvers: [str], metrics: [str]):
 13        self.data = raw_data
 14        self.solvers = solvers
 15        self.metrics = metrics
 16
 17    @property
 18    def metric_index(self) -> dict:
 19        """Returns a dictionary mapping metric names to their indices"""
 20        return {metric: idx for idx, metric in enumerate(self.metrics)}
 21
 22    def get_metric_data(self, metric: str) -> np.array:
 23        """Get all data for a specific metric"""
 24        return self.data[:, :, self.metric_index[metric]]
 25
 26    def aggregate(self, metric: str, aggregation_func: callable) -> np.array:
 27        """
 28        Apply aggregation function to a specific metric
 29        Args:
 30            metric: metric name to aggregate
 31            aggregation_func: function to apply (e.g., np.sum, np.mean)
 32        """
 33        return np.array([aggregation_func(self.get_metric_data(metric)[:, i]) for i in range(len(self.solvers))])
 34
 35    def split_instances_over(self, metric: str, condition):
 36        assert metric in self.metrics, "Cannot make a split on a non-existing metric"
 37
 38        index = self.metrics.index(metric)
 39        d = self.data[:, :, index]  # keep only the compared metrix
 40
 41        indexes = np.where(np.prod(np.apply_along_axis(condition, 1, d), axis=1))[0]
 42
 43        positives = self.data[indexes,]
 44        negatives = np.delete(self.data, indexes, axis=0)
 45        return SolverEvaluationResults(positives, self.solvers, self.metrics), SolverEvaluationResults(negatives,
 46                                                                                                       self.solvers,
 47                                                                                                       self.metrics)
 48
 49    def remove_solver(self, solver: str):
 50        index = self.solvers.index(solver)
 51        self.data = np.delete(self.data, index, axis=1)
 52        self.solvers.remove(solver)
 53
 54    def performance_profile(self, metric: str = "nnodes", ratios=np.arange(0, 1.00, .01), filename=None, plot=True, logx=True):
 55
 56        if filename:
 57            backend = matplotlib.get_backend()
 58            matplotlib.use('pgf')
 59
 60        metric_index = self.metrics.index(metric)
 61        n_instances = self.data.shape[0]
 62
 63        data = self.data[:, :, metric_index]
 64        min = np.min(data)
 65        max = np.max(data)
 66
 67        xs = ratios * (max - min) + min
 68
 69        res = []
 70        for s, solver in enumerate(self.solvers):
 71            ys = np.zeros(len(ratios))
 72            for i in range(n_instances):
 73                val = data[i, s]
 74                indexes = np.where(val <= xs)
 75                ys[indexes] += 1
 76
 77            ys /= n_instances
 78            label = solver
 79            if label == "relpscost":
 80                label = "RPB"
 81            elif "GNN" in label and "sb" in label:
 82                label = "$LSB$"
 83            elif "GNN" in label and "ub" in label:
 84                x = label.split("_")[4]
 85                label = f"$LLB^{x}$"
 86
 87            if logx:
 88                auc = np.trapezoid(ys, np.log(xs)) / np.log(max)
 89            else:
 90                auc = np.trapezoid(ys, xs) / max
 91
 92            res.append(auc)
 93            if plot:
 94                plt.plot(xs, ys, label=label)
 95
 96        if plot:
 97            plt.legend()
 98            plt.xlabel(metric)
 99            plt.ylabel("frequency")
100            plt.title(f"Performance profile w.r.t. {metric}")
101
102            if logx:
103                plt.xscale("log")
104
105            if filename:
106                plt.savefig(filename)
107                matplotlib.use(backend)
108
109            else:
110                plt.show()
111
112        return np.array(res)
113
114    def compute_report(self, *aggregations: tuple[str, callable], **kwargs):
115        data = {"solver": [s for s in self.solvers]}
116
117        for i, aggregation in enumerate(aggregations):
118            data[aggregation[0]] = list(aggregation[1](self))
119
120        return SolverEvaluationReport(data, **kwargs)
121
122    def __add__(self, other):
123        assert self.metrics == other.metrics, "Metrics must be the same when combining Results of different solvers"
124        assert self.data.shape
125        solvers = self.solvers + other.solvers
126        data = np.hstack((self.data, other.data))
127        return SolverEvaluationResults(data, solvers, self.metrics)
128
129    @staticmethod
130    def sg_metric(metric, s):
131        return (metric, lambda evaluationResults:
132        evaluationResults.aggregate(metric, lambda values: shifted_geometric_mean(values, shift=s))
133                )
134
135    @staticmethod
136    def nwins(metric, dir=1):
137        def get_wins(evaluationResults: SolverEvaluationResults):
138            data = evaluationResults.get_metric_data(metric)
139            res = []
140            for i in range(len(evaluationResults.solvers)):
141                c = 0
142                for j in range(len(data[:, i])):
143                    c += dir * data[j, i] <= dir * np.min(data[j, :])
144                res.append(c)
145            return np.array(res)
146
147        return f"wins ({metric})", get_wins
148
149    @staticmethod
150    def nsolved():
151        return ("nsolved", lambda evaluationResults: evaluationResults.aggregate("gap", lambda values: values.shape[
152                                                                                                           0] - np.count_nonzero(
153            values)))
154
155    @staticmethod
156    def auc_score(metric, **kwargs):
157        return ("AUC", lambda evaluationResults: evaluationResults.performance_profile(metric, plot=False, **kwargs))
SolverEvaluationResults( raw_data: <built-in function array>, solvers: [<class 'str'>], metrics: [<class 'str'>])
12    def __init__(self, raw_data: np.array, solvers: [str], metrics: [str]):
13        self.data = raw_data
14        self.solvers = solvers
15        self.metrics = metrics
data
solvers
metrics
metric_index: dict
17    @property
18    def metric_index(self) -> dict:
19        """Returns a dictionary mapping metric names to their indices"""
20        return {metric: idx for idx, metric in enumerate(self.metrics)}

Returns a dictionary mapping metric names to their indices

def get_metric_data(self, metric: str) -> <built-in function array>:
22    def get_metric_data(self, metric: str) -> np.array:
23        """Get all data for a specific metric"""
24        return self.data[:, :, self.metric_index[metric]]

Get all data for a specific metric

def aggregate( self, metric: str, aggregation_func: <built-in function callable>) -> <built-in function array>:
26    def aggregate(self, metric: str, aggregation_func: callable) -> np.array:
27        """
28        Apply aggregation function to a specific metric
29        Args:
30            metric: metric name to aggregate
31            aggregation_func: function to apply (e.g., np.sum, np.mean)
32        """
33        return np.array([aggregation_func(self.get_metric_data(metric)[:, i]) for i in range(len(self.solvers))])

Apply aggregation function to a specific metric Args: metric: metric name to aggregate aggregation_func: function to apply (e.g., np.sum, np.mean)

def split_instances_over(self, metric: str, condition):
35    def split_instances_over(self, metric: str, condition):
36        assert metric in self.metrics, "Cannot make a split on a non-existing metric"
37
38        index = self.metrics.index(metric)
39        d = self.data[:, :, index]  # keep only the compared metrix
40
41        indexes = np.where(np.prod(np.apply_along_axis(condition, 1, d), axis=1))[0]
42
43        positives = self.data[indexes,]
44        negatives = np.delete(self.data, indexes, axis=0)
45        return SolverEvaluationResults(positives, self.solvers, self.metrics), SolverEvaluationResults(negatives,
46                                                                                                       self.solvers,
47                                                                                                       self.metrics)
def remove_solver(self, solver: str):
49    def remove_solver(self, solver: str):
50        index = self.solvers.index(solver)
51        self.data = np.delete(self.data, index, axis=1)
52        self.solvers.remove(solver)
def performance_profile( self, metric: str = 'nnodes', ratios=array([0. , 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1 , 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2 , 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3 , 0.31, 0.32, 0.33, 0.34, 0.35, 0.36, 0.37, 0.38, 0.39, 0.4 , 0.41, 0.42, 0.43, 0.44, 0.45, 0.46, 0.47, 0.48, 0.49, 0.5 , 0.51, 0.52, 0.53, 0.54, 0.55, 0.56, 0.57, 0.58, 0.59, 0.6 , 0.61, 0.62, 0.63, 0.64, 0.65, 0.66, 0.67, 0.68, 0.69, 0.7 , 0.71, 0.72, 0.73, 0.74, 0.75, 0.76, 0.77, 0.78, 0.79, 0.8 , 0.81, 0.82, 0.83, 0.84, 0.85, 0.86, 0.87, 0.88, 0.89, 0.9 , 0.91, 0.92, 0.93, 0.94, 0.95, 0.96, 0.97, 0.98, 0.99]), filename=None, plot=True, logx=True):
 54    def performance_profile(self, metric: str = "nnodes", ratios=np.arange(0, 1.00, .01), filename=None, plot=True, logx=True):
 55
 56        if filename:
 57            backend = matplotlib.get_backend()
 58            matplotlib.use('pgf')
 59
 60        metric_index = self.metrics.index(metric)
 61        n_instances = self.data.shape[0]
 62
 63        data = self.data[:, :, metric_index]
 64        min = np.min(data)
 65        max = np.max(data)
 66
 67        xs = ratios * (max - min) + min
 68
 69        res = []
 70        for s, solver in enumerate(self.solvers):
 71            ys = np.zeros(len(ratios))
 72            for i in range(n_instances):
 73                val = data[i, s]
 74                indexes = np.where(val <= xs)
 75                ys[indexes] += 1
 76
 77            ys /= n_instances
 78            label = solver
 79            if label == "relpscost":
 80                label = "RPB"
 81            elif "GNN" in label and "sb" in label:
 82                label = "$LSB$"
 83            elif "GNN" in label and "ub" in label:
 84                x = label.split("_")[4]
 85                label = f"$LLB^{x}$"
 86
 87            if logx:
 88                auc = np.trapezoid(ys, np.log(xs)) / np.log(max)
 89            else:
 90                auc = np.trapezoid(ys, xs) / max
 91
 92            res.append(auc)
 93            if plot:
 94                plt.plot(xs, ys, label=label)
 95
 96        if plot:
 97            plt.legend()
 98            plt.xlabel(metric)
 99            plt.ylabel("frequency")
100            plt.title(f"Performance profile w.r.t. {metric}")
101
102            if logx:
103                plt.xscale("log")
104
105            if filename:
106                plt.savefig(filename)
107                matplotlib.use(backend)
108
109            else:
110                plt.show()
111
112        return np.array(res)
def compute_report(self, *aggregations: tuple[str, callable], **kwargs):
114    def compute_report(self, *aggregations: tuple[str, callable], **kwargs):
115        data = {"solver": [s for s in self.solvers]}
116
117        for i, aggregation in enumerate(aggregations):
118            data[aggregation[0]] = list(aggregation[1](self))
119
120        return SolverEvaluationReport(data, **kwargs)
@staticmethod
def sg_metric(metric, s):
129    @staticmethod
130    def sg_metric(metric, s):
131        return (metric, lambda evaluationResults:
132        evaluationResults.aggregate(metric, lambda values: shifted_geometric_mean(values, shift=s))
133                )
@staticmethod
def nwins(metric, dir=1):
135    @staticmethod
136    def nwins(metric, dir=1):
137        def get_wins(evaluationResults: SolverEvaluationResults):
138            data = evaluationResults.get_metric_data(metric)
139            res = []
140            for i in range(len(evaluationResults.solvers)):
141                c = 0
142                for j in range(len(data[:, i])):
143                    c += dir * data[j, i] <= dir * np.min(data[j, :])
144                res.append(c)
145            return np.array(res)
146
147        return f"wins ({metric})", get_wins
@staticmethod
def nsolved():
149    @staticmethod
150    def nsolved():
151        return ("nsolved", lambda evaluationResults: evaluationResults.aggregate("gap", lambda values: values.shape[
152                                                                                                           0] - np.count_nonzero(
153            values)))
@staticmethod
def auc_score(metric, **kwargs):
155    @staticmethod
156    def auc_score(metric, **kwargs):
157        return ("AUC", lambda evaluationResults: evaluationResults.performance_profile(metric, plot=False, **kwargs))
class SolverEvaluationReport:
159class SolverEvaluationReport:
160    def __init__(self, data=None, header=None, df_=None):
161        assert (data is None) != (df_ is None), "Only one of data and df_ must be given"
162
163        if df_ is not None:
164            self.df = df_
165            return
166
167        if header is not None:
168            data_ = {}
169            for key in data:
170                if key != "solver":
171                    data_[(header, key)] = data[key]
172                else:
173                    data_[("", key)] = data[key]
174
175        else:
176            data_ = data
177
178        self.df = pd.DataFrame(data_)
179        if header is not None:
180            self.df.set_index(("","solver"), inplace=True)
181
182    def __str__(self):
183        return tabulate(self.df, headers="keys", tablefmt='grid', showindex=False)
184
185    def to_latex(self, *args, **kwargs):
186        return self.df.to_latex(index=False, *args, **kwargs)
187
188    def __add__(self, other):
189        print(self.df.to_dict(orient='list'))
190        print(other.df.to_dict(orient='list'))
191        df2 = pd.concat(
192            [self.df, other.df],
193            axis=1
194        )
195
196        df2 = df2.reset_index().rename(columns={'index': ('', 'solver')})
197        return SolverEvaluationReport(df_ = df2)
SolverEvaluationReport(data=None, header=None, df_=None)
160    def __init__(self, data=None, header=None, df_=None):
161        assert (data is None) != (df_ is None), "Only one of data and df_ must be given"
162
163        if df_ is not None:
164            self.df = df_
165            return
166
167        if header is not None:
168            data_ = {}
169            for key in data:
170                if key != "solver":
171                    data_[(header, key)] = data[key]
172                else:
173                    data_[("", key)] = data[key]
174
175        else:
176            data_ = data
177
178        self.df = pd.DataFrame(data_)
179        if header is not None:
180            self.df.set_index(("","solver"), inplace=True)
df
def to_latex(self, *args, **kwargs):
185    def to_latex(self, *args, **kwargs):
186        return self.df.to_latex(index=False, *args, **kwargs)