Skip to content

Commit 56da917

Browse files
Add AlgoTune tasks for OpenEvolve
1 parent bc66c5b commit 56da917

File tree

469 files changed

+75210
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

469 files changed

+75210
-0
lines changed

examples/algotune/AlgoTuneTasks/__init__.py

Whitespace-only changes.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import hmac
2+
import logging
3+
import os
4+
from typing import Any
5+
6+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
7+
8+
from AlgoTuneTasks.base import register_task, Task
9+
10+
11+
# Define standard key sizes and nonce size
12+
AES_KEY_SIZES = [16, 24, 32] # AES-128, AES-192, AES-256
13+
GCM_NONCE_SIZE = 12 # Recommended nonce size for GCM
14+
GCM_TAG_SIZE = 16 # Standard tag size for GCM
15+
16+
17+
@register_task("aes_gcm_encryption")
18+
class AesGcmEncryption(Task):
19+
"""
20+
AesGcmEncryption Task:
21+
22+
Encrypt plaintext using AES-GCM from the `cryptography` library.
23+
The difficulty scales with the size of the plaintext.
24+
"""
25+
26+
DEFAULT_KEY_SIZE = 16 # Default to AES-128
27+
DEFAULT_PLAINTEXT_MULTIPLIER = 1024 # Bytes of plaintext per unit of n
28+
29+
def __init__(self, **kwargs):
30+
super().__init__(**kwargs)
31+
32+
def generate_problem(self, n: int, random_seed: int = 1) -> dict[str, Any]:
33+
"""
34+
Generate inputs for the AES-GCM encryption task.
35+
36+
Args:
37+
n (int): Scaling parameter, determines the plaintext size.
38+
random_seed (int): Seed for reproducibility (used for key, nonce, plaintext).
39+
40+
Returns:
41+
dict: A dictionary containing the problem parameters (key, nonce, plaintext, associated_data).
42+
"""
43+
# Use n to scale the plaintext size
44+
plaintext_size = max(1, n * self.DEFAULT_PLAINTEXT_MULTIPLIER) # Ensure at least 1 byte
45+
46+
# Use fixed key size and nonce size for simplicity in scaling
47+
key_size = self.DEFAULT_KEY_SIZE
48+
nonce_size = GCM_NONCE_SIZE
49+
50+
logging.debug(
51+
f"Generating AES-GCM problem with n={n} (plaintext_size={plaintext_size}), "
52+
f"key_size={key_size}, nonce_size={nonce_size}, random_seed={random_seed}"
53+
)
54+
55+
if key_size not in AES_KEY_SIZES:
56+
raise ValueError(f"Unsupported key size: {key_size}. Must be one of {AES_KEY_SIZES}.")
57+
58+
# Use os.urandom for cryptographic randomness.
59+
# While random_seed is an input, for crypto tasks, using truly random
60+
# keys/nonces per problem instance is generally better practice,
61+
# even if it makes the benchmark non-deterministic w.r.t random_seed.
62+
# If strict determinism based on random_seed is required, we'd need
63+
# to use a seeded PRNG like random.getrandbits, but that's less secure.
64+
key = os.urandom(key_size)
65+
nonce = os.urandom(nonce_size)
66+
plaintext = os.urandom(plaintext_size)
67+
# Generate some associated data for testing, can be empty
68+
associated_data = os.urandom(32) if n % 2 == 0 else b"" # Example: include AAD for even n
69+
70+
return {
71+
"key": key,
72+
"nonce": nonce,
73+
"plaintext": plaintext,
74+
"associated_data": associated_data,
75+
}
76+
77+
def solve(self, problem: dict[str, Any]) -> dict[str, bytes]:
78+
"""
79+
Encrypt the plaintext using AES-GCM from the `cryptography` library.
80+
81+
Args:
82+
problem (dict): The problem dictionary generated by `generate_problem`.
83+
84+
Returns:
85+
dict: A dictionary containing 'ciphertext' and 'tag'.
86+
"""
87+
key = problem["key"]
88+
nonce = problem["nonce"]
89+
plaintext = problem["plaintext"]
90+
associated_data = problem["associated_data"]
91+
92+
try:
93+
# Validate key size based on provided key length
94+
if len(key) not in AES_KEY_SIZES:
95+
raise ValueError(f"Invalid key size: {len(key)}. Must be one of {AES_KEY_SIZES}.")
96+
97+
aesgcm = AESGCM(key)
98+
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
99+
100+
# GCM ciphertext includes the tag appended at the end. We need to split them.
101+
# The tag length is fixed (usually 16 bytes / 128 bits).
102+
if len(ciphertext) < GCM_TAG_SIZE:
103+
raise ValueError("Encrypted output is shorter than the expected tag size.")
104+
105+
actual_ciphertext = ciphertext[:-GCM_TAG_SIZE]
106+
tag = ciphertext[-GCM_TAG_SIZE:]
107+
108+
return {"ciphertext": actual_ciphertext, "tag": tag}
109+
110+
except Exception as e:
111+
logging.error(f"Error during AES-GCM encryption in solve: {e}")
112+
raise # Re-raise exception
113+
114+
def is_solution(self, problem: dict[str, Any], solution: dict[str, bytes] | Any) -> bool:
115+
"""
116+
Verify the provided solution by comparing its ciphertext and tag
117+
against the result obtained from calling the task's own solve() method.
118+
119+
Args:
120+
problem (dict): The problem dictionary.
121+
solution (dict): The proposed solution dictionary with 'ciphertext' and 'tag'.
122+
123+
Returns:
124+
bool: True if the solution matches the result from self.solve().
125+
"""
126+
if not isinstance(solution, dict) or "ciphertext" not in solution or "tag" not in solution:
127+
logging.error(
128+
f"Invalid solution format. Expected dict with 'ciphertext' and 'tag'. Got: {type(solution)}"
129+
)
130+
return False
131+
132+
try:
133+
# Get the correct result by calling the solve method
134+
reference_result = self.solve(problem)
135+
reference_ciphertext = reference_result["ciphertext"]
136+
reference_tag = reference_result["tag"]
137+
except Exception as e:
138+
# If solve itself fails, we cannot verify the solution
139+
logging.error(f"Failed to generate reference solution in is_solution: {e}")
140+
return False
141+
142+
solution_ciphertext = solution["ciphertext"]
143+
solution_tag = solution["tag"]
144+
145+
# Ensure types are bytes before comparison
146+
if not isinstance(solution_ciphertext, bytes) or not isinstance(solution_tag, bytes):
147+
logging.error("Solution 'ciphertext' or 'tag' is not bytes.")
148+
return False
149+
150+
# Constant-time comparison for security
151+
ciphertext_match = hmac.compare_digest(reference_ciphertext, solution_ciphertext)
152+
tag_match = hmac.compare_digest(reference_tag, solution_tag)
153+
154+
return ciphertext_match and tag_match
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
AesGcmEncryption Task:
2+
3+
Task Description:
4+
Encrypt a given plaintext using AES (Advanced Encryption Standard) in GCM (Galois/Counter Mode) with a provided key, nonce (Initialization Vector - IV), and optional associated data (AAD). This task uses `cryptography.hazmat.primitives.ciphers.aead.AESGCM`. AES-GCM provides both confidentiality and data authenticity. The primary computational cost scales with the length of the plaintext.
5+
6+
Input:
7+
A dictionary with keys:
8+
- "key": A bytes object representing the AES key (e.g., 16, 24, or 32 bytes for AES-128, AES-192, AES-256).
9+
- "nonce": A bytes object representing the nonce (IV). For GCM, 12 bytes is commonly recommended. Must be unique for each encryption with the same key.
10+
- "plaintext": A bytes object representing the data to encrypt. The size of this data will scale with the problem size 'n'.
11+
- "associated_data": A bytes object representing additional data to authenticate but not encrypt (optional, can be `None` or empty bytes `b''`).
12+
13+
Example input:
14+
{
15+
"key": b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10', # 16 bytes key for AES-128
16+
"nonce": b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b', # 12 bytes nonce
17+
"plaintext": b'data to encrypt' * 100, # Example scaled plaintext
18+
"associated_data": b'metadata'
19+
}
20+
21+
Output:
22+
A dictionary containing:
23+
- "ciphertext": A bytes object representing the encrypted data.
24+
- "tag": A bytes object representing the GCM authentication tag (typically 16 bytes).
25+
26+
Example output:
27+
# The actual output depends on the exact inputs (key, nonce, plaintext, aad).
28+
# This is a conceptual placeholder.
29+
{
30+
"ciphertext": b'\xencrypted...\data',
31+
"tag": b'\xauthentication-tag' # 16 bytes
32+
}
33+
34+
Category: cryptography
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import logging
2+
import random
3+
from typing import Any
4+
5+
import numpy as np
6+
import scipy.ndimage
7+
8+
from AlgoTuneTasks.base import register_task, Task
9+
10+
11+
@register_task("affine_transform_2d")
12+
class AffineTransform2D(Task):
13+
def __init__(self, **kwargs):
14+
"""
15+
Initialize the AffineTransform2D task.
16+
17+
Performs an affine transformation on a 2D image using scipy.ndimage.affine_transform.
18+
The transformation is defined by a 2x3 matrix. Uses cubic spline interpolation
19+
(order=3) and 'constant' mode for handling boundaries.
20+
"""
21+
super().__init__(**kwargs)
22+
self.order = 3
23+
self.mode = "constant" # Or 'nearest', 'reflect', 'mirror', 'wrap'
24+
25+
def generate_problem(self, n: int, random_seed: int = 1) -> dict[str, Any]:
26+
"""
27+
Generates a 2D affine transformation problem.
28+
29+
Creates a random 2D array (image) of size n x n and a 2x3 affine
30+
transformation matrix.
31+
32+
:param n: The side dimension of the square input image.
33+
:param random_seed: Seed for reproducibility.
34+
:return: A dictionary representing the problem with keys:
35+
"image": Input image as a numpy array (n x n).
36+
"matrix": The 2x3 affine transformation matrix (numpy array).
37+
"""
38+
logging.debug(
39+
f"Generating 2D Affine Transform problem with n={n}, random_seed={random_seed}"
40+
)
41+
random.seed(random_seed)
42+
np.random.seed(random_seed)
43+
44+
# Generate random n x n image
45+
image = np.random.rand(n, n) * 255.0 # Example: 0-255 range image
46+
47+
# Generate a random affine matrix (e.g., combining rotation, scale, shear, translation)
48+
angle = np.random.uniform(-np.pi / 6, np.pi / 6) # +/- 30 degrees
49+
scale = np.random.uniform(0.8, 1.2, size=2)
50+
shear = np.random.uniform(-0.2, 0.2)
51+
translation = np.random.uniform(-n * 0.1, n * 0.1, size=2) # Translate up to 10%
52+
53+
# Rotation matrix
54+
cos_a, sin_a = np.cos(angle), np.sin(angle)
55+
R = np.array([[cos_a, -sin_a], [sin_a, cos_a]])
56+
57+
# Scale matrix
58+
S = np.array([[scale[0], 0], [0, scale[1]]])
59+
60+
# Shear matrix
61+
H = np.array([[1, shear], [0, 1]])
62+
63+
# Combine transformations (excluding translation for the 2x2 part)
64+
M_2x2 = R @ S @ H
65+
66+
# Create the 2x3 matrix [ M_2x2 | translation ]
67+
matrix = np.hstack((M_2x2, translation[:, np.newaxis]))
68+
69+
problem = {"image": image, "matrix": matrix}
70+
logging.debug(f"Generated 2D Affine Transform problem for image shape ({n},{n})")
71+
return problem
72+
73+
def solve(self, problem: dict[str, Any]) -> dict[str, list[list[float]]]:
74+
"""
75+
Solves the 2D affine transformation problem using scipy.ndimage.affine_transform.
76+
77+
:param problem: A dictionary representing the problem.
78+
:return: A dictionary with key "transformed_image":
79+
"transformed_image": The transformed image as a list of lists.
80+
"""
81+
image = problem["image"]
82+
matrix = problem["matrix"]
83+
84+
# Perform affine transformation
85+
try:
86+
# output_shape can be specified, default is same as input
87+
transformed_image = scipy.ndimage.affine_transform(
88+
image, matrix, order=self.order, mode=self.mode
89+
)
90+
except Exception as e:
91+
logging.error(f"scipy.ndimage.affine_transform failed: {e}")
92+
# Return an empty list to indicate failure? Adjust based on benchmark policy.
93+
return {"transformed_image": []}
94+
95+
solution = {"transformed_image": transformed_image}
96+
return solution
97+
98+
def is_solution(self, problem: dict[str, Any], solution: dict[str, list[list[float]]]) -> bool:
99+
"""
100+
Check if the provided affine transformation solution is valid.
101+
102+
Checks structure, dimensions, finite values, and numerical closeness to
103+
the reference scipy.ndimage.affine_transform output.
104+
105+
:param problem: The problem definition dictionary.
106+
:param solution: The proposed solution dictionary.
107+
:return: True if the solution is valid, False otherwise.
108+
"""
109+
if not all(k in problem for k in ["image", "matrix"]):
110+
logging.error("Problem dictionary missing 'image' or 'matrix'.")
111+
return False
112+
image = problem["image"]
113+
matrix = problem["matrix"]
114+
115+
if not isinstance(solution, dict) or "transformed_image" not in solution:
116+
logging.error("Solution format invalid: missing 'transformed_image' key.")
117+
return False
118+
119+
proposed_list = solution["transformed_image"]
120+
121+
# Handle potential failure case from solve()
122+
if proposed_list == []:
123+
logging.warning("Proposed solution is empty list (potential failure).")
124+
# Check if reference solver also fails/produces empty-like result
125+
try:
126+
ref_output = scipy.ndimage.affine_transform(
127+
image, matrix, order=self.order, mode=self.mode
128+
)
129+
if ref_output.size == 0: # Check if reference is also effectively empty
130+
logging.info(
131+
"Reference solver also produced empty result. Accepting empty solution."
132+
)
133+
return True
134+
else:
135+
logging.error("Reference solver succeeded, but proposed solution was empty.")
136+
return False
137+
except Exception:
138+
logging.info("Reference solver also failed. Accepting empty solution.")
139+
return True # Both failed, likely invalid input
140+
141+
if not isinstance(proposed_list, list):
142+
logging.error("'transformed_image' is not a list.")
143+
return False
144+
145+
try:
146+
proposed_array = np.asarray(proposed_list, dtype=float)
147+
except ValueError:
148+
logging.error("Could not convert 'transformed_image' list to numpy float array.")
149+
return False
150+
151+
# Expected output shape is usually same as input for affine_transform unless specified
152+
if proposed_array.shape != image.shape:
153+
logging.error(f"Output shape {proposed_array.shape} != input shape {image.shape}.")
154+
# This might be acceptable if output_shape was used, but base case expects same shape.
155+
# Adjust if the task allows different output shapes.
156+
return False # Assuming same shape output for now
157+
158+
if not np.all(np.isfinite(proposed_array)):
159+
logging.error("Proposed 'transformed_image' contains non-finite values.")
160+
return False
161+
162+
# Re-compute reference solution
163+
try:
164+
ref_array = scipy.ndimage.affine_transform(
165+
image, matrix, order=self.order, mode=self.mode
166+
)
167+
except Exception as e:
168+
logging.error(f"Error computing reference solution: {e}")
169+
return False # Cannot verify if reference fails
170+
171+
# Compare results
172+
rtol = 1e-5
173+
atol = 1e-7 # Slightly tighter atol for image data often in 0-255 range
174+
is_close = np.allclose(proposed_array, ref_array, rtol=rtol, atol=atol)
175+
176+
if not is_close:
177+
abs_diff = np.abs(proposed_array - ref_array)
178+
max_abs_err = np.max(abs_diff) if abs_diff.size > 0 else 0
179+
logging.error(
180+
f"Solution verification failed: Output mismatch. "
181+
f"Max absolute error: {max_abs_err:.3f} (rtol={rtol}, atol={atol})"
182+
)
183+
return False
184+
185+
logging.debug("Solution verification successful.")
186+
return True

0 commit comments

Comments
 (0)