diff --git a/examples/perf_benchmark/batch_benchmark.py b/examples/perf_benchmark/batch_benchmark.py index e4ba67227..e489b7d0d 100644 --- a/examples/perf_benchmark/batch_benchmark.py +++ b/examples/perf_benchmark/batch_benchmark.py @@ -1,4 +1,3 @@ -from benchmark import BenchmarkArgs from benchmark_plotter import plot_batch_benchmark import argparse import subprocess @@ -6,6 +5,74 @@ from datetime import datetime import pandas as pd +# Create a struct to store the arguments +class BenchmarkArgs: + def __init__(self, renderer_name, rasterizer, n_envs, n_steps, resX, resY, camera_posX, camera_posY, camera_posZ, camera_lookatX, camera_lookatY, camera_lookatZ, camera_fov, mjcf, benchmark_result_file_path): + self.renderer_name = renderer_name + self.rasterizer = rasterizer + self.n_envs = n_envs + self.n_steps = n_steps + self.resX = resX + self.resY = resY + self.camera_posX = camera_posX + self.camera_posY = camera_posY + self.camera_posZ = camera_posZ + self.camera_lookatX = camera_lookatX + self.camera_lookatY = camera_lookatY + self.camera_lookatZ = camera_lookatZ + self.camera_fov = camera_fov + self.mjcf = mjcf + self.benchmark_result_file_path = benchmark_result_file_path + + @staticmethod + def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("-d", "--renderer_name", type=str, default="batch_renderer") + parser.add_argument("-r", "--rasterizer", action="store_true", default=False) + parser.add_argument("-n", "--n_envs", type=int, default=1024) + parser.add_argument("-s", "--n_steps", type=int, default=1) + parser.add_argument("-x", "--resX", type=int, default=1024) + parser.add_argument("-y", "--resY", type=int, default=1024) + parser.add_argument("-i", "--camera_posX", type=float, default=1.5) + parser.add_argument("-j", "--camera_posY", type=float, default=0.5) + parser.add_argument("-k", "--camera_posZ", type=float, default=1.5) + parser.add_argument("-l", "--camera_lookatX", type=float, default=0.0) + parser.add_argument("-m", "--camera_lookatY", type=float, default=0.0) + parser.add_argument("-o", "--camera_lookatZ", type=float, default=0.5) + parser.add_argument("-v", "--camera_fov", type=float, default=45) + parser.add_argument("-f", "--mjcf", type=str, default="xml/franka_emika_panda/panda.xml") + parser.add_argument("-g", "--benchmark_result_file_path", type=str, default="benchmark.csv") + args = parser.parse_args() + benchmark_args = BenchmarkArgs( + renderer_name=args.renderer_name, + rasterizer=args.rasterizer, + n_envs=args.n_envs, + n_steps=args.n_steps, + resX=args.resX, + resY=args.resY, + camera_posX=args.camera_posX, + camera_posY=args.camera_posY, + camera_posZ=args.camera_posZ, + camera_lookatX=args.camera_lookatX, + camera_lookatY=args.camera_lookatY, + camera_lookatZ=args.camera_lookatZ, + camera_fov=args.camera_fov, + mjcf=args.mjcf, + benchmark_result_file_path=args.benchmark_result_file_path, + ) + print(f"Benchmark with args:") + print(f" renderer_name: {benchmark_args.renderer_name}") + print(f" rasterizer: {benchmark_args.rasterizer}") + print(f" n_envs: {benchmark_args.n_envs}") + print(f" n_steps: {benchmark_args.n_steps}") + print(f" resolution: {benchmark_args.resX}x{benchmark_args.resY}") + print(f" camera_pos: ({benchmark_args.camera_posX}, {benchmark_args.camera_posY}, {benchmark_args.camera_posZ})") + print(f" camera_lookat: ({benchmark_args.camera_lookatX}, {benchmark_args.camera_lookatY}, {benchmark_args.camera_lookatZ})") + print(f" camera_fov: {benchmark_args.camera_fov}") + print(f" mjcf: {benchmark_args.mjcf}") + print(f" benchmark_result_file_path: {benchmark_args.benchmark_result_file_path}") + return benchmark_args + class BatchBenchmarkArgs: def __init__(self, use_full_list, continue_from): self.use_full_list = use_full_list @@ -25,7 +92,8 @@ def create_batch_args(benchmark_result_file_path, use_full_list=False): # Create a list of all the possible combinations of arguments # and return them as a list of BenchmarkArgs full_mjcf_list = ["xml/franka_emika_panda/panda.xml", "xml/unitree_g1/g1.xml", "xml/unitree_go2/go2.xml"] - rasterizer_list = [True, False] + full_renderer_list = ["batch_renderer", "pyrender"] + full_rasterizer_list = [True, False] full_batch_size_list = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384] square_resolution_list = [ (64, 64), (128, 128), (256, 256), (512, 512), (1024, 1024), (2048, 2048), (4096, 4096), (8192, 8192) @@ -39,12 +107,14 @@ def create_batch_args(benchmark_result_file_path, use_full_list=False): full_resolution_list = square_resolution_list + four_three_resolution_list + sixteen_nine_resolution_list # Minimal mjcf, resolution, and batch size + minimal_renderer_list = ["batch_renderer", "pyrender"] + minimal_rasterizer_list = [True] minimal_mjcf_list = [ "xml/franka_emika_panda/panda.xml" ] minimal_batch_size_list = [ #2048, 3072, 4096, 6144, 8192, 12288, 16384 - 2048, 4096, 8192, 16384 + 1024, 2048 ] #minimal_batch_size_list = full_batch_size_list minimal_resolution_list = [ @@ -53,10 +123,14 @@ def create_batch_args(benchmark_result_file_path, use_full_list=False): ] if use_full_list: + renderer_list = full_renderer_list + rasterizer_list = full_rasterizer_list mjcf_list = full_mjcf_list resolution_list = full_resolution_list batch_size_list = full_batch_size_list else: + renderer_list = minimal_renderer_list + rasterizer_list = minimal_rasterizer_list mjcf_list = minimal_mjcf_list resolution_list = minimal_resolution_list batch_size_list = minimal_batch_size_list @@ -75,32 +149,35 @@ def create_batch_args(benchmark_result_file_path, use_full_list=False): batch_args_dict = {} # Build hierarchical structure - for rasterizer in rasterizer_list: - batch_args_dict[rasterizer] = {} - for mjcf in mjcf_list: - batch_args_dict[rasterizer][mjcf] = {} - for batch_size in batch_size_list: - batch_args_dict[rasterizer][mjcf][batch_size] = {} - for resolution in resolution_list: - resX, resY = resolution - # Create benchmark args for this combination - args = BenchmarkArgs( - rasterizer=rasterizer, - n_envs=batch_size, - n_steps=n_steps, - resX=resX, - resY=resY, - camera_posX=camera_pos[0], - camera_posY=camera_pos[1], - camera_posZ=camera_pos[2], - camera_lookatX=camera_lookat[0], - camera_lookatY=camera_lookat[1], - camera_lookatZ=camera_lookat[2], - camera_fov=camera_fov, - mjcf=mjcf, - benchmark_result_file_path=benchmark_result_file_path - ) - batch_args_dict[rasterizer][mjcf][batch_size][(resX,resY)] = args + for renderer in renderer_list: + batch_args_dict[renderer] = {} + for rasterizer in rasterizer_list: + batch_args_dict[renderer][rasterizer] = {} + for mjcf in mjcf_list: + batch_args_dict[renderer][rasterizer][mjcf] = {} + for batch_size in batch_size_list: + batch_args_dict[renderer][rasterizer][mjcf][batch_size] = {} + for resolution in resolution_list: + resX, resY = resolution + # Create benchmark args for this combination + args = BenchmarkArgs( + renderer_name=renderer, + rasterizer=rasterizer, + n_envs=batch_size, + n_steps=n_steps, + resX=resX, + resY=resY, + camera_posX=camera_pos[0], + camera_posY=camera_pos[1], + camera_posZ=camera_pos[2], + camera_lookatX=camera_lookat[0], + camera_lookatY=camera_lookat[1], + camera_lookatZ=camera_lookat[2], + camera_fov=camera_fov, + mjcf=mjcf, + benchmark_result_file_path=benchmark_result_file_path + ) + batch_args_dict[renderer][rasterizer][mjcf][batch_size][(resX,resY)] = args return batch_args_dict @@ -118,7 +195,7 @@ def create_benchmark_result_file(continue_from_file_path): benchmark_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") benchmark_result_file_path = f"{benchmark_data_directory}/batch_benchmark_{benchmark_timestamp}.csv" with open(benchmark_result_file_path, "w") as f: - f.write("result,mjcf,rasterizer,n_envs,n_steps,resX,resY,camera_posX,camera_posY,camera_posZ,camera_lookatX,camera_lookatY,camera_lookatZ,camera_fov,time_taken,time_taken_per_env,fps,fps_per_env\n") + f.write("result,mjcf,renderer,rasterizer,n_envs,n_steps,resX,resY,camera_posX,camera_posY,camera_posZ,camera_lookatX,camera_lookatY,camera_lookatZ,camera_fov,time_taken,time_taken_per_env,fps,fps_per_env\n") print(f"Created new benchmark result file: {benchmark_result_file_path}") return benchmark_result_file_path @@ -135,7 +212,7 @@ def get_previous_runs(continue_from_file_path): for _, row in df.iterrows(): run_info = ( row['mjcf'], - row['rasterizer'], + row['renderer'], row['n_envs'], (row['resX'], row['resY']), row['result'] # 'succeeded' or 'failed' @@ -144,67 +221,83 @@ def get_previous_runs(continue_from_file_path): return previous_runs -def run_batch_benchmark(batch_args_dict, benchmark_script_path, previous_runs=None): +def get_benchmark_script_path(renderer_name): + current_dir = os.path.dirname(os.path.abspath(__file__)) + if renderer_name == "batch_renderer": + return f"{current_dir}/benchmark.py" + elif renderer_name == "pyrender": + return f"{current_dir}/benchmark_pyrender.py" + else: + raise ValueError(f"Invalid renderer name: {renderer_name}") + +def run_batch_benchmark(batch_args_dict, previous_runs=None): if previous_runs is None: previous_runs = [] + + for renderer in batch_args_dict: + benchmark_script_path = get_benchmark_script_path(renderer) + if not os.path.exists(benchmark_script_path): + raise FileNotFoundError(f"Benchmark script not found: {benchmark_script_path}") + print(f"Running benchmark for {renderer}") - for rasterizer in batch_args_dict: - for mjcf in batch_args_dict[rasterizer]: - for batch_size in batch_args_dict[rasterizer][mjcf]: - last_resolution_failed = False - for resolution in batch_args_dict[rasterizer][mjcf][batch_size]: - if last_resolution_failed: - break - - # Check if this run was in a previous execution - run_info = (mjcf, rasterizer, batch_size, resolution) - skip_this_run = False - - for prev_run in previous_runs: - if run_info == prev_run[:4]: # Compare only the run parameters, not the status - skip_this_run = True - if prev_run[4] == 'failed': - # Skip this and subsequent resolutions if it failed before - last_resolution_failed = True + for rasterizer in batch_args_dict[renderer]: + for mjcf in batch_args_dict[renderer][rasterizer]: + for batch_size in batch_args_dict[renderer][rasterizer][mjcf]: + last_resolution_failed = False + for resolution in batch_args_dict[renderer][rasterizer][mjcf][batch_size]: + if last_resolution_failed: break - - if skip_this_run: - continue - # Run the benchmark - batch_args = batch_args_dict[rasterizer][mjcf][batch_size][resolution] - - # launch a process to run the benchmark - cmd = ["python3", benchmark_script_path] - if batch_args.rasterizer: - cmd.append("-r") - cmd.extend([ - "-n", str(batch_args.n_envs), - "-s", str(batch_args.n_steps), - "-x", str(batch_args.resX), - "-y", str(batch_args.resY), - "-i", str(batch_args.camera_posX), - "-j", str(batch_args.camera_posY), - "-k", str(batch_args.camera_posZ), - "-l", str(batch_args.camera_lookatX), - "-m", str(batch_args.camera_lookatY), - "-o", str(batch_args.camera_lookatZ), - "-v", str(batch_args.camera_fov), - "-f", batch_args.mjcf, - "-g", batch_args.benchmark_result_file_path - ]) - try: - process = subprocess.Popen(cmd) - return_code = process.wait() - if return_code != 0: - raise subprocess.CalledProcessError(return_code, cmd) - except Exception as e: - print(f"Error running benchmark: {str(e)}") - last_resolution_failed = True - # Write failed result without timing data - with open(batch_args.benchmark_result_file_path, 'a') as f: - f.write(f'failed,{batch_args.mjcf},{batch_args.rasterizer},{batch_args.n_envs},{batch_args.n_steps},{batch_args.resX},{batch_args.resY},{batch_args.camera_posX},{batch_args.camera_posY},{batch_args.camera_posZ},{batch_args.camera_lookatX},{batch_args.camera_lookatY},{batch_args.camera_lookatZ},{batch_args.camera_fov},,,,\n') - break + # Check if this run was in a previous execution + run_info = (mjcf, rasterizer, batch_size, resolution) + skip_this_run = False + + for prev_run in previous_runs: + if run_info == prev_run[:4]: # Compare only the run parameters, not the status + skip_this_run = True + if prev_run[4] == 'failed': + # Skip this and subsequent resolutions if it failed before + last_resolution_failed = True + break + + if skip_this_run: + continue + + # Run the benchmark + batch_args = batch_args_dict[renderer][rasterizer][mjcf][batch_size][resolution] + + # launch a process to run the benchmark + cmd = ["python3", benchmark_script_path] + if batch_args.rasterizer: + cmd.append("-r") + cmd.extend([ + "-d", batch_args.renderer_name, + "-n", str(batch_args.n_envs), + "-s", str(batch_args.n_steps), + "-x", str(batch_args.resX), + "-y", str(batch_args.resY), + "-i", str(batch_args.camera_posX), + "-j", str(batch_args.camera_posY), + "-k", str(batch_args.camera_posZ), + "-l", str(batch_args.camera_lookatX), + "-m", str(batch_args.camera_lookatY), + "-o", str(batch_args.camera_lookatZ), + "-v", str(batch_args.camera_fov), + "-f", batch_args.mjcf, + "-g", batch_args.benchmark_result_file_path + ]) + try: + process = subprocess.Popen(cmd) + return_code = process.wait() + if return_code != 0: + raise subprocess.CalledProcessError(return_code, cmd) + except Exception as e: + print(f"Error running benchmark: {str(e)}") + last_resolution_failed = True + # Write failed result without timing data + with open(batch_args.benchmark_result_file_path, 'a') as f: + f.write(f'failed,{batch_args.mjcf},{batch_args.renderer_name},{batch_args.rasterizer},{batch_args.n_envs},{batch_args.n_steps},{batch_args.resX},{batch_args.resY},{batch_args.camera_posX},{batch_args.camera_posY},{batch_args.camera_posZ},{batch_args.camera_lookatX},{batch_args.camera_lookatY},{batch_args.camera_lookatZ},{batch_args.camera_fov},,,,\n') + break def main(): batch_benchmark_args = parse_args() @@ -213,14 +306,9 @@ def main(): # Get list of previous runs if continuing from a previous run previous_runs = get_previous_runs(batch_benchmark_args.continue_from) - # Run benchmark in batch - current_dir = os.path.dirname(os.path.abspath(__file__)) - benchmark_script_path = f"{current_dir}/benchmark.py" - if not os.path.exists(benchmark_script_path): - raise FileNotFoundError(f"Benchmark script not found: {benchmark_script_path}") - + # Run benchmark in batch batch_args_dict = create_batch_args(benchmark_result_file_path, use_full_list=batch_benchmark_args.use_full_list) - run_batch_benchmark(batch_args_dict, benchmark_script_path, previous_runs) + run_batch_benchmark(batch_args_dict, previous_runs) # Generate plots plot_batch_benchmark(benchmark_result_file_path) diff --git a/examples/perf_benchmark/benchmark.py b/examples/perf_benchmark/benchmark.py index 9c474ebea..4508e8f85 100644 --- a/examples/perf_benchmark/benchmark.py +++ b/examples/perf_benchmark/benchmark.py @@ -4,69 +4,7 @@ import numpy as np import genesis as gs import torch - -# Create a struct to store the arguments -class BenchmarkArgs: - def __init__(self, rasterizer, n_envs, n_steps, resX, resY, camera_posX, camera_posY, camera_posZ, camera_lookatX, camera_lookatY, camera_lookatZ, camera_fov, mjcf, benchmark_result_file_path): - self.rasterizer = rasterizer - self.n_envs = n_envs - self.n_steps = n_steps - self.resX = resX - self.resY = resY - self.camera_posX = camera_posX - self.camera_posY = camera_posY - self.camera_posZ = camera_posZ - self.camera_lookatX = camera_lookatX - self.camera_lookatY = camera_lookatY - self.camera_lookatZ = camera_lookatZ - self.camera_fov = camera_fov - self.mjcf = mjcf - self.benchmark_result_file_path = benchmark_result_file_path - -def parse_args(): - parser = argparse.ArgumentParser() - parser.add_argument("-r", "--rasterizer", action="store_true", default=False) - parser.add_argument("-n", "--n_envs", type=int, default=1024) - parser.add_argument("-s", "--n_steps", type=int, default=1) - parser.add_argument("-x", "--resX", type=int, default=1024) - parser.add_argument("-y", "--resY", type=int, default=1024) - parser.add_argument("-i", "--camera_posX", type=float, default=1.5) - parser.add_argument("-j", "--camera_posY", type=float, default=0.5) - parser.add_argument("-k", "--camera_posZ", type=float, default=1.5) - parser.add_argument("-l", "--camera_lookatX", type=float, default=0.0) - parser.add_argument("-m", "--camera_lookatY", type=float, default=0.0) - parser.add_argument("-o", "--camera_lookatZ", type=float, default=0.5) - parser.add_argument("-v", "--camera_fov", type=float, default=45) - parser.add_argument("-f", "--mjcf", type=str, default="xml/franka_emika_panda/panda.xml") - parser.add_argument("-g", "--benchmark_result_file_path", type=str, default="benchmark.csv") - args = parser.parse_args() - benchmark_args = BenchmarkArgs( - rasterizer=args.rasterizer, - n_envs=args.n_envs, - n_steps=args.n_steps, - resX=args.resX, - resY=args.resY, - camera_posX=args.camera_posX, - camera_posY=args.camera_posY, - camera_posZ=args.camera_posZ, - camera_lookatX=args.camera_lookatX, - camera_lookatY=args.camera_lookatY, - camera_lookatZ=args.camera_lookatZ, - camera_fov=args.camera_fov, - mjcf=args.mjcf, - benchmark_result_file_path=args.benchmark_result_file_path, - ) - print(f"Benchmark with args:") - print(f" rasterizer: {benchmark_args.rasterizer}") - print(f" n_envs: {benchmark_args.n_envs}") - print(f" n_steps: {benchmark_args.n_steps}") - print(f" resolution: {benchmark_args.resX}x{benchmark_args.resY}") - print(f" camera_pos: ({benchmark_args.camera_posX}, {benchmark_args.camera_posY}, {benchmark_args.camera_posZ})") - print(f" camera_lookat: ({benchmark_args.camera_lookatX}, {benchmark_args.camera_lookatY}, {benchmark_args.camera_lookatZ})") - print(f" camera_fov: {benchmark_args.camera_fov}") - print(f" mjcf: {benchmark_args.mjcf}") - print(f" benchmark_result_file_path: {benchmark_args.benchmark_result_file_path}") - return benchmark_args +from batch_benchmark import BenchmarkArgs def init_gs(benchmark_args): ########################## init ########################## @@ -155,7 +93,7 @@ def run_benchmark(scene, benchmark_args): # warmup scene.step() - rgb, depth, _, _ = scene.batch_render() + rgb, depth, _, _ = scene.render_all_cams() # fill gpu cache with random data fill_gpu_cache_with_random_data() @@ -165,7 +103,7 @@ def run_benchmark(scene, benchmark_args): start_time = time() for i in range(n_steps): - rgb, depth, _, _ = scene.batch_render(force_render=True) + rgb, depth, _, _ = scene.render_all_cams(force_render=True) end_time = time() time_taken = end_time - start_time @@ -183,14 +121,14 @@ def run_benchmark(scene, benchmark_args): # Append a line with all args and results in csv format with open(benchmark_args.benchmark_result_file_path, 'a') as f: - f.write(f'succeeded,{benchmark_args.mjcf},{benchmark_args.rasterizer},{benchmark_args.n_envs},{benchmark_args.n_steps},{benchmark_args.resX},{benchmark_args.resY},{benchmark_args.camera_posX},{benchmark_args.camera_posY},{benchmark_args.camera_posZ},{benchmark_args.camera_lookatX},{benchmark_args.camera_lookatY},{benchmark_args.camera_lookatZ},{benchmark_args.camera_fov},{time_taken},{time_taken_per_env},{fps},{fps_per_env}\n') + f.write(f'succeeded,{benchmark_args.mjcf},{benchmark_args.renderer_name},{benchmark_args.rasterizer},{benchmark_args.n_envs},{benchmark_args.n_steps},{benchmark_args.resX},{benchmark_args.resY},{benchmark_args.camera_posX},{benchmark_args.camera_posY},{benchmark_args.camera_posZ},{benchmark_args.camera_lookatX},{benchmark_args.camera_lookatY},{benchmark_args.camera_lookatZ},{benchmark_args.camera_fov},{time_taken},{time_taken_per_env},{fps},{fps_per_env}\n') except Exception as e: print(f"Error during benchmark: {e}") raise def main(): ######################## Parse arguments ####################### - benchmark_args = parse_args() + benchmark_args = BenchmarkArgs.parse_args() ######################## Initialize scene ####################### scene = init_gs(benchmark_args) diff --git a/examples/perf_benchmark/benchmark_plotter.py b/examples/perf_benchmark/benchmark_plotter.py index acfcbf720..525dc7794 100644 --- a/examples/perf_benchmark/benchmark_plotter.py +++ b/examples/perf_benchmark/benchmark_plotter.py @@ -6,6 +6,7 @@ import pandas as pd import matplotlib.pyplot as plt from matplotlib.ticker import ScalarFormatter +import numpy as np def generatePlotHtml(plots_dir): #Generate an html page to display all the plots @@ -16,17 +17,12 @@ def generatePlotHtml(plots_dir): print(f"No plot files found in {plots_dir}") return - # Separate regular plots from difference plots - regular_plots = [p for p in plot_files if not p.endswith('_difference.png') and not any(p.endswith(f'_difference_{ar.replace(":", "x")}.png') for ar in ["1:1", "4:3", "16:9"])] - aspect_ratio_plots = { - "1:1": [p for p in plot_files if p.endswith('_difference_1x1.png')], - "4:3": [p for p in plot_files if p.endswith('_difference_4x3.png')], - "16:9": [p for p in plot_files if p.endswith('_difference_16x9.png')] - } + # Separate regular plots from comparison charts + regular_plot_files = [p for p in plot_files if p.endswith('_plot.png') and not p.endswith('_comparison_plot.png')] # Group regular plots by MJCF file plot_groups = {} - for plot_file in regular_plots: + for plot_file in regular_plot_files: basename = os.path.basename(plot_file) mjcf_name = basename.split('_')[0] if mjcf_name not in plot_groups: @@ -35,20 +31,23 @@ def generatePlotHtml(plots_dir): # Sort plot groups by mjcf name and plot file name plot_groups = sorted(plot_groups.items(), key=lambda x: (x[0], x[1][0])) - - # Group aspect ratio plots by MJCF file - aspect_ratio_groups = {} - for aspect_ratio, plots in aspect_ratio_plots.items(): - aspect_ratio_groups[aspect_ratio] = {} - for plot_file in plots: - basename = os.path.basename(plot_file) - mjcf_name = basename.split('_')[0] - if mjcf_name not in aspect_ratio_groups[aspect_ratio]: - aspect_ratio_groups[aspect_ratio][mjcf_name] = [] - aspect_ratio_groups[aspect_ratio][mjcf_name].append(plot_file) - - # Sort each aspect ratio's plot groups - aspect_ratio_groups[aspect_ratio] = sorted(aspect_ratio_groups[aspect_ratio].items(), key=lambda x: (x[0], x[1][0])) + + # Group comparison plots by resolution + comparison_plot_files = {} + for plot_file in plot_files: + if plot_file.endswith('_comparison_plot.png'): + # Extract resolution from filename (e.g., "128x128" from "..._128x128_comparison_plot.png") + resolution = plot_file.split('_')[-3] # Get the resolution part + if resolution not in comparison_plot_files: + comparison_plot_files[resolution] = [] + comparison_plot_files[resolution].append(plot_file) + + # Sort resolutions by their dimensions + def get_resolution_dims(res): + width, height = map(int, res.split('x')) + return width * height # Sort by total pixels + + sorted_resolutions = sorted(comparison_plot_files.keys(), key=get_resolution_dims) # Create HTML file html_content = """ @@ -70,29 +69,29 @@ def generatePlotHtml(plots_dir):