~ruther/ctu-fee-eoa

ref: 48d0912f632e44005cedd27f13de6f4925e3312a ctu-fee-eoa/codes/py_plotter/plotter.py -rw-r--r-- 11.3 KiB
48d0912f — Rutherther feat: add a simple python plotter 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
#!/usr/bin/env python3

import json
import pandas as pd
import matplotlib
matplotlib.use('Agg')  # Use non-interactive backend
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
from pathlib import Path
import glob
import argparse

class ConstraintOptimizationPlotter:
    def __init__(self, config_path):
        with open(config_path, 'r') as f:
            self.config = json.load(f)

        self.data_path = Path(self.config['data_path'])
        self.output_dir = Path(self.config['output_dir'])
        self.output_dir.mkdir(exist_ok=True)

        # Load objectives for percentage deviation calculation
        objectives_path = Path(__file__).parent / 'objectives.json'
        with open(objectives_path, 'r') as f:
            self.objectives = json.load(f)

    def get_csv_pattern(self, plot_type):
        patterns = {
            'best_candidates': 'best_candidates_*.csv',
            'constraint_violation': 'constraint_violation/constraint_violations_*.csv',
            'feasible_fraction': 'feasible_fraction/feasible_fractions_*.csv'
        }
        return patterns.get(plot_type, 'best_candidates_*.csv')

    def get_value_column(self, plot_type):
        columns = {
            'best_candidates': 'evaluation',  # Changed from 'fitness' to 'evaluation'
            'constraint_violation': 'avg_constraint_violation',
            'feasible_fraction': 'feasible_fraction'
        }
        return columns.get(plot_type, 'evaluation')

    def blend_colors(self, color1, color2, alpha=0.5):
        """Blend two colors together"""
        c1 = mcolors.to_rgb(color1)
        c2 = mcolors.to_rgb(color2)
        blended = tuple(alpha * c1[i] + (1 - alpha) * c2[i] for i in range(3))
        return mcolors.to_hex(blended)

    def get_combined_color(self, algorithm_color, instance_color, combination_method="blend"):
        """Combine algorithm and instance colors based on the specified method"""
        if combination_method == "blend":
            return self.blend_colors(algorithm_color, instance_color, alpha=0.6)
        elif combination_method == "algorithm":
            return algorithm_color
        elif combination_method == "instance":
            return instance_color
        else:
            return self.blend_colors(algorithm_color, instance_color, alpha=0.5)

    def calculate_percentage_deviation(self, values, instance_name):
        """Calculate percentage deviation from optimal value for best_candidates plots"""
        if self.config['plot_type'] != 'best_candidates' or instance_name not in self.objectives:
            return values

        optimal_value = self.objectives[instance_name]

        # Check if any values are significantly better than the known optimum
        # Allow small tolerance for numerical precision
        tolerance = 1e-4 * np.abs(optimal_value)
        significantly_better = values < (optimal_value - tolerance)
        # print(values - optimal_value)

        if np.any(significantly_better):
            better_indices = np.where(significantly_better)[0]
            best_found = np.min(values[better_indices])
            improvement = optimal_value - best_found
            improvement_pct = improvement / np.abs(optimal_value) * 100

            print(f"WARNING: Found {np.sum(significantly_better)} values better than known optimum for {instance_name}!")
            print(f"Known optimum: {optimal_value}")
            print(f"Best found: {best_found}")
            print(f"Improvement: {improvement} ({improvement_pct:.3f}%)")
            print(f"Using best found value as new reference point.")

            # Update the optimal value to the best found for this calculation
            optimal_value = best_found


        new_optimal_value = optimal_value
        if optimal_value < 0:
            new_optimal_value = - optimal_value
            values = values + 2*new_optimal_value

        # Calculate percentage deviation: (current - optimal) / |optimal| * 100
        # This will always be >= 0 since current >= optimal
        percentage_deviations = (values - new_optimal_value) / new_optimal_value * 100
        return percentage_deviations

    def load_data_for_algorithm_instance(self, algorithm, instance):
        csv_pattern = self.get_csv_pattern(self.config['plot_type'])

        algorithm_path = self.data_path / algorithm / instance
        csv_files = list(algorithm_path.glob(csv_pattern))

        if not csv_files:
            print(f"Warning: No CSV files found for {algorithm}/{instance}")
            return None

        print(f"Found {len(csv_files)} files for {algorithm}/{instance}")

        value_col = self.get_value_column(self.config['plot_type'])
        all_data = []

        for csv_file in csv_files:
            try:
                df = pd.read_csv(csv_file)
                if 'iteration' in df.columns and value_col in df.columns:
                    all_data.append(df[['iteration', value_col]].copy())
            except Exception as e:
                print(f"Error reading {csv_file}: {e}")

        if not all_data:
            return None

        # Find the maximum function evaluation (iteration value) across all runs
        max_evaluation = max(df['iteration'].max() for df in all_data)

        # Create a common evaluation grid - collect all unique evaluation points
        all_evaluations = set()
        for df in all_data:
            all_evaluations.update(df['iteration'].tolist())
        all_evaluations.add(max_evaluation)  # Ensure max is included
        common_grid = sorted(list(all_evaluations))

        aligned_data = []

        for df in all_data:
            df_copy = df.copy().sort_values('iteration').reset_index(drop=True)

            # Interpolate/extend this run to the common grid
            aligned_values = []
            current_value = df_copy[value_col].iloc[0] if len(df_copy) > 0 else 0
            df_idx = 0

            for eval_point in common_grid:
                # Update current_value if we have data at this evaluation point or before
                while df_idx < len(df_copy) and df_copy['iteration'].iloc[df_idx] <= eval_point:
                    current_value = df_copy[value_col].iloc[df_idx]
                    df_idx += 1

                aligned_values.append(current_value)

            aligned_data.append(aligned_values)

        # All runs now have the same length and evaluation points
        values_matrix = np.column_stack(aligned_data)
        values_matrix = values_matrix.astype(np.float64)
        iterations = np.array(common_grid)

        # Apply percentage deviation calculation for best_candidates
        if self.config['plot_type'] == 'best_candidates':
            # Apply percentage deviation to each column (run) separately
            for i in range(values_matrix.shape[1]):
                values_matrix[:, i] = self.calculate_percentage_deviation(values_matrix[:, i], instance)

        mean_values = np.mean(values_matrix, axis=1)
        std_values = np.std(values_matrix, axis=1)

        return {
            'iterations': iterations,
            'mean': mean_values,
            'std': std_values,
            'runs': len(all_data)
        }

    def create_plot(self):
        fig, ax = plt.subplots(1, 1, figsize=self.config['plot_settings']['figsize'])

        combination_method = self.config['plot_settings'].get('color_combination', 'blend')

        for instance in self.config['instances']:
            instance_name = instance['name'] if isinstance(instance, dict) else instance
            instance_label = instance.get('label', instance_name) if isinstance(instance, dict) else instance_name
            instance_color = instance.get('color', '#000000') if isinstance(instance, dict) else '#000000'

            for algorithm in self.config['algorithms']:
                alg_name = algorithm['name']
                alg_label = algorithm['label']
                alg_color = algorithm.get('color', '#000000')
                linestyle = algorithm['linestyle']

                # Get combined color
                combined_color = self.get_combined_color(alg_color, instance_color, combination_method)

                data = self.load_data_for_algorithm_instance(alg_name, instance_name)

                if data is None:
                    print(f"No data found for {alg_name}/{instance_name}")
                    continue

                # Handle data values (percentage deviation for best_candidates, absolute for others)
                mean_values = data['mean']
                std_values = data['std']

                # For log scale with negative values (non-best_candidates plots), take absolute value
                if self.config['plot_settings'].get('log_y', False) and self.config['plot_type'] != 'best_candidates':
                    mean_values = np.abs(mean_values)

                ax.plot(data['iterations'], mean_values,
                       color=combined_color,
                       linestyle=linestyle,
                       label=f"{alg_label} - {instance_label}",
                       linewidth=2,
                       drawstyle='steps-post')

                # Add fill_between for standard deviation bands (if enabled)
                if self.config['plot_settings'].get('show_std', True):
                    lower_bound = mean_values - std_values
                    upper_bound = mean_values + std_values

                    # For log scale, ensure positive values
                    if self.config['plot_settings'].get('log_y', False):
                        lower_bound = np.maximum(lower_bound, 0.01)

                    ax.fill_between(data['iterations'],
                                   lower_bound,
                                   upper_bound,
                                   color=combined_color,
                                   alpha=self.config['plot_settings']['alpha_fill'])

        ax.set_xlabel(self.config['plot_settings']['xlabel'], fontsize=14)
        ax.set_ylabel(self.config['plot_settings']['ylabel'], fontsize=14)
        ax.set_title(self.config['plot_settings']['title'], fontsize=16)

        # Set log scale if requested
        if self.config['plot_settings'].get('log_x', False):
            ax.set_xscale('log')
        if self.config['plot_settings'].get('log_y', False):
            ax.set_yscale('log')

        # Format y-axis with percentages for best_candidates plots
        if self.config['plot_type'] == 'best_candidates':
            ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{x:.1f}%'))

        # Set tick label font sizes
        ax.tick_params(axis='both', which='major', labelsize=12)
        ax.tick_params(axis='both', which='minor', labelsize=10)

        if self.config['plot_settings']['grid']:
            ax.grid(True, alpha=0.3)

        if self.config['plot_settings']['legend']:
            ax.legend()

        plt.tight_layout()

        instance_names = [inst['name'] if isinstance(inst, dict) else inst for inst in self.config['instances']]
        output_file = self.output_dir / f"{self.config['plot_type']}_{'_'.join(instance_names)}.svg"
        plt.savefig(output_file, format='svg', bbox_inches='tight')
        print(f"Plot saved to: {output_file}")

def main():
    parser = argparse.ArgumentParser(description='Plot constraint optimization results')
    parser.add_argument('config', help='Path to JSON configuration file')
    args = parser.parse_args()

    plotter = ConstraintOptimizationPlotter(args.config)
    plotter.create_plot()

if __name__ == '__main__':
    main()