Skip to content

Commit 758c02f

Browse files
committed
Add jaccard similarity, cli args
1 parent 6a51a2b commit 758c02f

1 file changed

Lines changed: 70 additions & 25 deletions

File tree

  • codeclash/analysis/code_evolve

codeclash/analysis/code_evolve/main.py

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Code evolution and consistency analysis across tournament games."""
22

3+
import argparse
34
import difflib
45
import json
56
from concurrent.futures import ProcessPoolExecutor, as_completed
@@ -16,7 +17,6 @@
1617
from codeclash.constants import LOCAL_LOG_DIR
1718
from codeclash.games import ARENAS
1819

19-
DATA_CACHE = Path("assets/code_evolve_cache.jsonl")
2020
MODELS_PATH = Path("configs/models.yaml")
2121
TARGET_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+
84110
def _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

96122
def 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

361394
if __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

Comments
 (0)