11"""Code evolution and consistency analysis across tournament games."""
22
3+ import argparse
34import difflib
45import json
56from concurrent .futures import ProcessPoolExecutor , as_completed
1617from codeclash .constants import LOCAL_LOG_DIR
1718from codeclash .games import ARENAS
1819
19- DATA_CACHE = Path ("assets/code_evolve_cache.jsonl" )
2020MODELS_PATH = Path ("configs/models.yaml" )
2121TARGET_ROUNDS = [1 , 5 , 10 , 15 ]
2222
@@ -73,28 +73,54 @@ def find_max_round_for_player(log_folder: Path, player_name: str) -> int:
7373 return max (rounds ) if rounds else 0
7474
7575
76- def compute_code_similarity (diff1 : PatchSet , diff2 : PatchSet ) -> float :
76+ def _compute_code_sim_difflib (diff1 : PatchSet , diff2 : PatchSet ) -> float :
7777 """Compute similarity score between two diffs using edit distance (0.0 = different, 1.0 = identical)."""
7878 diff1_str = "\n " .join (str (f ) for f in diff1 )
7979 diff2_str = "\n " .join (str (f ) for f in diff2 )
8080 seq_matcher = difflib .SequenceMatcher (None , diff1_str , diff2_str , autojunk = False )
8181 return seq_matcher .ratio ()
8282
8383
84+ def compute_code_sim_jaccard (diff1 : PatchSet , diff2 : PatchSet ) -> float :
85+ """Jaccard similarity on line-level tokens."""
86+
87+ def get_lines (patch ):
88+ return {str (f ).splitlines () for f in patch }
89+
90+ lines1 = get_lines (diff1 )
91+ lines2 = get_lines (diff2 )
92+
93+ if not lines1 and not lines2 :
94+ return 1.0
95+ if not lines1 or not lines2 :
96+ return 0.0
97+
98+ intersection = len (lines1 & lines2 )
99+ union = len (lines1 | lines2 )
100+ return intersection / union
101+
102+
103+ def compute_code_similarity (diff1 : PatchSet , diff2 : PatchSet , similarity : str = "difflib" ) -> float :
104+ return {
105+ "difflib" : _compute_code_sim_difflib ,
106+ "jaccard" : compute_code_sim_jaccard ,
107+ }[similarity ](diff1 , diff2 )
108+
109+
84110def _compute_similarity_row (args ):
85111 """Helper for parallel similarity computation."""
86- i , patch_i , patches = args
112+ i , patch_i , patches , similarity = args
87113 row = np .zeros (len (patches ))
88114 for j , patch_j in enumerate (patches ):
89115 if i != j :
90- row [j ] = compute_code_similarity (patch_i , patch_j )
116+ row [j ] = compute_code_similarity (patch_i , patch_j , similarity )
91117 else :
92118 row [j ] = 1.0
93119 return i , row
94120
95121
96122def compute_round_consistency (
97- model : str , opponent : str , arena : str , round_num : int , n_workers : int = 4
123+ model : str , opponent : str , arena : str , round_num : int , n_workers : int = 4 , similarity : str = "difflib"
98124) -> tuple [np .ndarray , np .ndarray ]:
99125 """
100126 Compute pairwise similarity between a model's solutions at a specific round across multiple games.
@@ -110,7 +136,7 @@ def compute_round_consistency(
110136 similarity_matrix = np .zeros ((n , n ))
111137
112138 with ProcessPoolExecutor (max_workers = n_workers ) as executor :
113- tasks = [(i , patch_list [i ], patch_list ) for i in range (n )]
139+ tasks = [(i , patch_list [i ], patch_list , similarity ) for i in range (n )]
114140 futures = {executor .submit (_compute_similarity_row , task ): task for task in tasks }
115141
116142 for future in tqdm (as_completed (futures ), total = n , desc = "Computing similarities" ):
@@ -127,14 +153,16 @@ def tag_to_str(tag: dict) -> str:
127153 return f"{ tag ['model_a' ]} __vs__{ tag ['model_b' ]} __in__{ tag ['arena' ]} __r{ tag ['round' ]} "
128154
129155
130- def collect_data ():
156+ def collect_data (
157+ data_cache : Path = Path ("assets/code_evolve_cache_BattleSnake_difflib.jsonl" ),
158+ arena : str = "BattleSnake" ,
159+ similarity : str = "difflib" ,
160+ ):
131161 """Run code evolution analyses."""
132- if not DATA_CACHE .parent .exists ():
133- DATA_CACHE .parent .mkdir (parents = True , exist_ok = True )
134162 mode , to_skip = "w" , []
135- if DATA_CACHE .exists ():
163+ if data_cache .exists ():
136164 mode = "a"
137- with open (DATA_CACHE ) as f :
165+ with open (data_cache ) as f :
138166 for line in f :
139167 entry = json .loads (line )
140168 to_skip .append (
@@ -147,16 +175,19 @@ def collect_data():
147175 }
148176 )
149177 )
178+ print (f"Found cache file, skipping { len (to_skip )} entries." )
150179
151180 with open (MODELS_PATH ) as f :
152181 models = [x ["model_name" ].rsplit ("/" )[- 1 ] for x in yaml .safe_load (f )]
153- arena = "BattleSnake"
154- with open (DATA_CACHE , mode ) as f :
182+
183+ with open (data_cache , mode ) as f :
155184 for i in range (0 , len (models )):
156185 for j in range (0 , len (models )):
157186 if i == j :
158187 continue
159188 for round in TARGET_ROUNDS :
189+ if round != 1 :
190+ continue
160191 if (
161192 tag_to_str (
162193 {
@@ -170,7 +201,9 @@ def collect_data():
170201 ):
171202 continue
172203 try :
173- sim_matrix , _ = compute_round_consistency (models [i ], models [j ], arena , round )
204+ sim_matrix , _ = compute_round_consistency (
205+ models [i ], models [j ], arena , round , similarity = similarity
206+ )
174207 except Exception as e :
175208 print (
176209 f"Error computing consistency for { models [i ]} vs { models [j ]} in { arena } at round { round } : { e } "
@@ -196,10 +229,10 @@ def collect_data():
196229# =============================================c
197230
198231
199- def load_cached_results () -> list [dict ]:
232+ def load_cached_results (data_cache : Path ) -> list [dict ]:
200233 """Load all cached results from the data file."""
201234 results = []
202- with open (DATA_CACHE ) as f :
235+ with open (data_cache ) as f :
203236 for line in f :
204237 results .append (json .loads (line ))
205238 return results
@@ -264,15 +297,15 @@ def compute_opponent_effect_matrix(results: list[dict], target_round: int) -> tu
264297 return sorted (opponent_matrix .keys ()), opponent_matrix
265298
266299
267- def plot_opponent_effect_heatmap (target_round : int , output_path : str = None ):
300+ def plot_opponent_effect_heatmap (data_cache : str , target_round : int , output_path : str = None ):
268301 """
269302 Plot heatmap showing how model consistency varies by opponent.
270303 Answers questions 2a (round 1) and 2b (round 15).
271304 """
272305 if output_path is None :
273306 output_path = f"assets/heatmap_code_evolution_per_opponent_r{ target_round } .png"
274307
275- results = load_cached_results ()
308+ results = load_cached_results (data_cache )
276309 models , opponent_matrix = compute_opponent_effect_matrix (results , target_round )
277310
278311 # Get all unique opponents
@@ -291,7 +324,7 @@ def plot_opponent_effect_heatmap(target_round: int, output_path: str = None):
291324 matrix [i , j ] = opponent_matrix [model ][opponent ]
292325
293326 # Create heatmap with blue-white-red colormap like win_rates
294- FONT_BOLD .set_size (16 )
327+ FONT_BOLD .set_size (14 )
295328 _ , ax = plt .subplots (figsize = (6 , 6 ))
296329 cmap = mcolors .LinearSegmentedColormap .from_list ("br" , ["#3498db" , "#ffffff" , "#e74c3c" ])
297330 masked = np .ma .masked_where (np .isnan (matrix ), matrix )
@@ -328,12 +361,12 @@ def plot_opponent_effect_heatmap(target_round: int, output_path: str = None):
328361 print (f"Saved heatmap to { output_path } " )
329362
330363
331- def plot_consistency_over_rounds (output_path : str = "assets/line_chart_code_evolution.png" ):
364+ def plot_consistency_over_rounds (data_cache : str , output_path : str = "assets/line_chart_code_evolution.png" ):
332365 """
333366 Plot line graph: x-axis = round, y-axis = code similarity, one line per model.
334367 Answers questions 1a (early round consistency) and 1b (evolution over time).
335368 """
336- results = load_cached_results ()
369+ results = load_cached_results (data_cache )
337370 model_consistency = compute_model_consistency_over_rounds (results )
338371
339372 plt .figure (figsize = (6 , 6 ))
@@ -359,12 +392,24 @@ def plot_consistency_over_rounds(output_path: str = "assets/line_chart_code_evol
359392
360393
361394if __name__ == "__main__" :
395+ parser = argparse .ArgumentParser (description = "Code Evolution and Consistency Analysis" )
396+ parser .add_argument ("-a" , "--arena" , type = str , default = "BattleSnake" , help = "Arena name to analyze" )
397+ parser .add_argument (
398+ "-s" , "--similarity" , type = str , default = "difflib" , help = "Similarity function to use (difflib or jaccard)"
399+ )
400+ args = parser .parse_args ()
401+
402+ data_cache = Path (f"assets/code_evolve_cache_{ args .arena } _{ args .similarity } .jsonl" )
362403 # Run data collection
363- # collect_data()
404+ collect_data (
405+ data_cache = data_cache ,
406+ arena = args .arena ,
407+ similarity = args .similarity ,
408+ )
364409
365410 # Questions 1a/1b: Consistency over rounds
366- plot_consistency_over_rounds () # Questions 1a and 1b
411+ plot_consistency_over_rounds (data_cache ) # Questions 1a and 1b
367412
368413 # Questions 2a/2b: Opponent effect
369- plot_opponent_effect_heatmap (target_round = 1 ) # Question 2a
370- plot_opponent_effect_heatmap (target_round = 15 ) # Question 2b
414+ plot_opponent_effect_heatmap (data_cache , target_round = 1 ) # Question 2a
415+ plot_opponent_effect_heatmap (data_cache , target_round = 15 ) # Question 2b
0 commit comments