#!/usr/bin/env python3
import json
import csv
import matplotlib
matplotlib.use('Agg') # Use non-interactive backend
import matplotlib.pyplot as plt
import numpy as np
from pathlib import Path
import glob
import argparse
class TargetProximityPlotter:
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 calculate_percentage_deviation(self, values, instance_name):
"""Calculate percentage deviation from optimal value - reused from original plotter"""
if instance_name not in self.objectives:
raise ValueError(f"No objective value found for instance {instance_name}")
optimal_value = self.objectives[instance_name]
# Check if any values are significantly better than the known optimum
tolerance = 1e-4 * np.abs(optimal_value)
significantly_better = values < (optimal_value - tolerance)
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
percentage_deviations = (values - new_optimal_value) / new_optimal_value * 100
return percentage_deviations
def load_runs_for_algorithm_instance(self, algorithm, instance):
"""Load all individual runs for an algorithm-instance combination"""
algorithm_path = self.data_path / algorithm / instance
csv_files = list(algorithm_path.glob('best_candidates_*.csv'))
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}")
all_runs = []
for csv_file in csv_files:
try:
with open(csv_file, 'r') as f:
reader = csv.DictReader(f)
iterations = []
evaluations = []
for row in reader:
if 'iteration' in row and 'evaluation' in row:
iterations.append(float(row['iteration']))
evaluations.append(float(row['evaluation']))
if iterations and evaluations:
# Convert to percentage deviation
values = np.array(evaluations)
percentage_devs = self.calculate_percentage_deviation(values, instance)
run_data = {
'iteration': np.array(iterations),
'percentage_deviation': percentage_devs
}
all_runs.append(run_data)
except Exception as e:
print(f"Error reading {csv_file}: {e}")
return all_runs if all_runs else None
def calculate_target_proximity_over_time_for_algorithm_instance(self, algorithm, instance, target_pct):
"""Calculate fraction of runs within target proximity at each iteration"""
runs = self.load_runs_for_algorithm_instance(algorithm, instance)
if not runs:
return None
num_runs = len(runs)
# Find common iteration grid across all runs (similar to original plotter)
all_iterations = set()
for run in runs:
all_iterations.update(run['iteration'].tolist())
common_grid = sorted(list(all_iterations))
# For each iteration, count how many runs are within target
fractions_over_time = []
iterations = []
for eval_point in common_grid:
within_target_count = 0
for run in runs:
# Find the best deviation achieved up to this evaluation point
mask = run['iteration'] <= eval_point
if np.any(mask):
best_deviation_so_far = np.min(run['percentage_deviation'][mask])
if best_deviation_so_far <= target_pct:
within_target_count += 1
fraction = within_target_count / num_runs
fractions_over_time.append(fraction)
iterations.append(eval_point)
return {
'iterations': np.array(iterations),
'fractions': np.array(fractions_over_time)
}
def calculate_algorithm_average_over_time(self, algorithm, problem_groups, target_pct):
"""Calculate average target proximity over time across problem groups for one algorithm"""
all_group_data = []
for group in problem_groups:
group_data = []
for instance in group:
result = self.calculate_target_proximity_over_time_for_algorithm_instance(
algorithm, instance, target_pct
)
if result:
group_data.append(result)
if group_data:
all_group_data.extend(group_data)
if not all_group_data:
return None
# Find common iteration grid across all problems
all_iterations = set()
for data in all_group_data:
all_iterations.update(data['iterations'].tolist())
common_grid = sorted(list(all_iterations))
# Interpolate each problem's data to common grid and average
averaged_fractions = []
for eval_point in common_grid:
fractions_at_eval = []
for data in all_group_data:
# Find the fraction at this evaluation point (or the last known value)
mask = data['iterations'] <= eval_point
if np.any(mask):
last_known_fraction = data['fractions'][mask][-1]
fractions_at_eval.append(last_known_fraction)
else:
# Before any evaluations, fraction is 0
fractions_at_eval.append(0.0)
averaged_fractions.append(np.mean(fractions_at_eval))
return {
'iterations': np.array(common_grid),
'fractions': np.array(averaged_fractions)
}
def create_plot(self):
"""Create the target proximity plot"""
fig, ax = plt.subplots(1, 1, figsize=self.config['plot_settings']['figsize'])
# Support both single target and multiple targets
if 'targets' in self.config:
targets = self.config['targets'] # Multiple targets
else:
targets = [self.config['target']] # Single target (backward compatibility)
problem_groups = self.config['problem_groups']
# First pass: collect all algorithm data for all targets to find global max iteration
all_algorithm_data = {}
global_max_iteration = 0
for algorithm in self.config['algorithms']:
alg_name = algorithm['name']
print(f"Processing algorithm: {alg_name}")
# Collect data for each target
target_data_list = []
for target_pct in targets:
# Get average results over time across problem groups for this target
alg_data = self.calculate_algorithm_average_over_time(alg_name, problem_groups, target_pct)
if alg_data:
target_data_list.append(alg_data)
global_max_iteration = max(global_max_iteration, alg_data['iterations'].max())
if target_data_list:
all_algorithm_data[alg_name] = target_data_list
else:
print(f"No data found for algorithm {alg_name}")
# Second pass: average across targets and extend all data to global max, then plot
for algorithm in self.config['algorithms']:
alg_name = algorithm['name']
alg_label = algorithm['label']
alg_color = algorithm.get('color', '#000000')
linestyle = algorithm.get('linestyle', '-')
if alg_name not in all_algorithm_data:
continue
target_data_list = all_algorithm_data[alg_name]
# Find common iteration grid across all targets for this algorithm
all_iterations = set()
for data in target_data_list:
all_iterations.update(data['iterations'].tolist())
all_iterations.add(global_max_iteration) # Include global max
common_grid = sorted(list(all_iterations))
# Average fractions across targets at each iteration point
averaged_fractions = []
std_fractions = []
for eval_point in common_grid:
fractions_at_eval = []
for data in target_data_list:
# Find the fraction at this evaluation point (or the last known value)
mask = data['iterations'] <= eval_point
if np.any(mask):
last_known_fraction = data['fractions'][mask][-1]
fractions_at_eval.append(last_known_fraction)
else:
# Before any evaluations, fraction is 0
fractions_at_eval.append(0.0)
averaged_fractions.append(np.mean(fractions_at_eval))
std_fractions.append(np.std(fractions_at_eval))
averaged_fractions = np.array(averaged_fractions)
std_fractions = np.array(std_fractions)
# Plot the averaged line
ax.plot(common_grid, averaged_fractions,
color=alg_color,
linestyle=linestyle,
label=alg_label,
linewidth=2,
drawstyle='steps-post') # Step plot like original plotter
# Add fill_between for standard deviation bands (if enabled)
if self.config['plot_settings'].get('show_std', False):
lower_bound = averaged_fractions - std_fractions
upper_bound = averaged_fractions + std_fractions
# Ensure bounds stay within [0, 1] range
lower_bound = np.maximum(lower_bound, 0.0)
upper_bound = np.minimum(upper_bound, 1.0)
alpha_fill = self.config['plot_settings'].get('alpha_fill', 0.2)
ax.fill_between(common_grid,
lower_bound,
upper_bound,
color=alg_color,
alpha=alpha_fill,
step='post')
# Configure plot
ax.set_xlabel(self.config['plot_settings']['xlabel'], fontsize=18)
ax.set_ylabel(self.config['plot_settings']['ylabel'], fontsize=18)
ax.set_title(self.config['plot_settings']['title'], fontsize=20)
# Set logarithmic x-axis if requested
if self.config['plot_settings'].get('log_x', False):
ax.set_xscale('log')
# Set tick label font sizes
ax.tick_params(axis='both', which='major', labelsize=16)
ax.tick_params(axis='both', which='minor', labelsize=14)
# Set y-axis to [-0.1, 1.1] range for better visibility of 0.0 and 1.0 values
ax.set_ylim(-0.1, 1.1)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{x:.1f}'))
if self.config['plot_settings'].get('grid', True):
ax.grid(True, alpha=0.3)
if self.config['plot_settings'].get('legend', True):
ax.legend(fontsize=14)
plt.tight_layout()
# Save plot
output_file = self.output_dir / f"target_proximity_{self.config['plot_name']}.svg"
plt.savefig(output_file, format='svg', bbox_inches='tight')
print(f"Plot saved to: {output_file}")
def main():
parser = argparse.ArgumentParser(description='Plot target proximity analysis for constraint optimization results')
parser.add_argument('config', help='Path to JSON configuration file')
args = parser.parse_args()
plotter = TargetProximityPlotter(args.config)
plotter.create_plot()
if __name__ == '__main__':
main()