llm4ad 1.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm4ad/__init__.py +6 -0
- llm4ad/base/__init__.py +9 -0
- llm4ad/base/code.py +270 -0
- llm4ad/base/evaluate.py +285 -0
- llm4ad/base/modify_code.py +402 -0
- llm4ad/base/sample.py +234 -0
- llm4ad/gui.py +141 -0
- llm4ad/method/__init__.py +57 -0
- llm4ad/method/eoh/__init__.py +2 -0
- llm4ad/method/eoh/eoh.py +305 -0
- llm4ad/method/eoh/population.py +83 -0
- llm4ad/method/eoh/profiler.py +161 -0
- llm4ad/method/eoh/prompt.py +120 -0
- llm4ad/method/eoh/resume.py +104 -0
- llm4ad/method/eoh/sampler.py +29 -0
- llm4ad/method/funsearch/__init__.py +3 -0
- llm4ad/method/funsearch/config.py +23 -0
- llm4ad/method/funsearch/funsearch.py +195 -0
- llm4ad/method/funsearch/profiler.py +135 -0
- llm4ad/method/funsearch/programs_database.py +305 -0
- llm4ad/method/funsearch/resume.py +111 -0
- llm4ad/method/hillclimb/__init__.py +2 -0
- llm4ad/method/hillclimb/hillclimb.py +189 -0
- llm4ad/method/hillclimb/profiler.py +8 -0
- llm4ad/method/hillclimb/resume.py +83 -0
- llm4ad/method/randsample/__init__.py +2 -0
- llm4ad/method/randsample/profiler.py +8 -0
- llm4ad/method/randsample/randsample.py +181 -0
- llm4ad/method/randsample/resume.py +73 -0
- llm4ad/task/__init__.py +29 -0
- llm4ad/task/machine_learning/__init__.py +0 -0
- llm4ad/task/machine_learning/acrobot/__init__.py +2 -0
- llm4ad/task/machine_learning/acrobot/evaluation.py +65 -0
- llm4ad/task/machine_learning/acrobot/template.py +35 -0
- llm4ad/task/machine_learning/acrobot/test.py +46 -0
- llm4ad/task/machine_learning/car_mountain/__init__.py +2 -0
- llm4ad/task/machine_learning/car_mountain/evaluation.py +57 -0
- llm4ad/task/machine_learning/car_mountain/q-learning.py +123 -0
- llm4ad/task/machine_learning/car_mountain/template.py +22 -0
- llm4ad/task/machine_learning/car_mountain/test.py +163 -0
- llm4ad/task/machine_learning/moon_lander/__init__.py +2 -0
- llm4ad/task/machine_learning/moon_lander/evaluation.py +84 -0
- llm4ad/task/machine_learning/moon_lander/template.py +32 -0
- llm4ad/task/machine_learning/moon_lander/test.py +51 -0
- llm4ad/task/optimization/__init__.py +30 -0
- llm4ad/task/optimization/admissible_set/__init__.py +2 -0
- llm4ad/task/optimization/admissible_set/evaluation.py +155 -0
- llm4ad/task/optimization/admissible_set/template.py +22 -0
- llm4ad/task/optimization/cvrp_construct/__init__.py +2 -0
- llm4ad/task/optimization/cvrp_construct/evaluation.py +152 -0
- llm4ad/task/optimization/cvrp_construct/get_instance.py +50 -0
- llm4ad/task/optimization/cvrp_construct/template.py +22 -0
- llm4ad/task/optimization/online_bin_packing/__init__.py +2 -0
- llm4ad/task/optimization/online_bin_packing/evaluation.py +91 -0
- llm4ad/task/optimization/online_bin_packing/generate_weibull_instances.py +36 -0
- llm4ad/task/optimization/online_bin_packing/template.py +15 -0
- llm4ad/task/optimization/ovrp_construct/__init__.py +2 -0
- llm4ad/task/optimization/ovrp_construct/evaluation.py +124 -0
- llm4ad/task/optimization/ovrp_construct/get_instance.py +50 -0
- llm4ad/task/optimization/ovrp_construct/template.py +22 -0
- llm4ad/task/optimization/tsp_construct/__init__.py +2 -0
- llm4ad/task/optimization/tsp_construct/evaluation.py +148 -0
- llm4ad/task/optimization/tsp_construct/get_instance.py +15 -0
- llm4ad/task/optimization/tsp_construct/template.py +24 -0
- llm4ad/task/optimization/vrptw_construct/__init__.py +2 -0
- llm4ad/task/optimization/vrptw_construct/evaluation.py +174 -0
- llm4ad/task/optimization/vrptw_construct/get_instance.py +65 -0
- llm4ad/task/optimization/vrptw_construct/template.py +23 -0
- llm4ad/task/science_discovery/__init__.py +0 -0
- llm4ad/task/science_discovery/bactgrow/__init__.py +2 -0
- llm4ad/task/science_discovery/bactgrow/evaluation.py +87 -0
- llm4ad/task/science_discovery/bactgrow/template.py +20 -0
- llm4ad/task/science_discovery/bactgrow/test_id.py +17502 -0
- llm4ad/task/science_discovery/bactgrow/test_odd.py +105002 -0
- llm4ad/task/science_discovery/bactgrow/train.py +52502 -0
- llm4ad/task/science_discovery/ode_1d/__init__.py +2 -0
- llm4ad/task/science_discovery/ode_1d/evaluation.py +152 -0
- llm4ad/task/science_discovery/ode_1d/strogatz_equations.py +224 -0
- llm4ad/task/science_discovery/ode_1d/strogatz_extended.py +34001 -0
- llm4ad/task/science_discovery/ode_1d/template.py +22 -0
- llm4ad/task/science_discovery/oscillator1/__init__.py +2 -0
- llm4ad/task/science_discovery/oscillator1/evaluation.py +85 -0
- llm4ad/task/science_discovery/oscillator1/template.py +19 -0
- llm4ad/task/science_discovery/oscillator1/test_id.py +50002 -0
- llm4ad/task/science_discovery/oscillator1/test_odd.py +50002 -0
- llm4ad/task/science_discovery/oscillator1/train.py +50002 -0
- llm4ad/task/science_discovery/oscillator2/__init__.py +2 -0
- llm4ad/task/science_discovery/oscillator2/evaluation.py +87 -0
- llm4ad/task/science_discovery/oscillator2/template.py +20 -0
- llm4ad/task/science_discovery/oscillator2/test_id.py +60002 -0
- llm4ad/task/science_discovery/oscillator2/test_odd.py +60002 -0
- llm4ad/task/science_discovery/oscillator2/train.py +60002 -0
- llm4ad/task/science_discovery/stresstrain/__init__.py +2 -0
- llm4ad/task/science_discovery/stresstrain/evaluation.py +85 -0
- llm4ad/task/science_discovery/stresstrain/template.py +19 -0
- llm4ad/task/science_discovery/stresstrain/test_id.py +7212 -0
- llm4ad/task/science_discovery/stresstrain/test_odd.py +3692 -0
- llm4ad/task/science_discovery/stresstrain/train.py +10807 -0
- llm4ad/tools/__init__.py +3 -0
- llm4ad/tools/llm/__init__.py +32 -0
- llm4ad/tools/llm/llm_api_https.py +73 -0
- llm4ad/tools/llm/llm_api_openai.py +21 -0
- llm4ad/tools/log_utils.py +730 -0
- llm4ad/tools/profiler/__init__.py +40 -0
- llm4ad/tools/profiler/profile.py +208 -0
- llm4ad/tools/profiler/tensorboard_profiler.py +85 -0
- llm4ad/tools/profiler/wandb_profiler.py +100 -0
- llm4ad-1.0.dist-info/LICENSE +21 -0
- llm4ad-1.0.dist-info/METADATA +19 -0
- llm4ad-1.0.dist-info/RECORD +112 -0
- llm4ad-1.0.dist-info/WHEEL +6 -0
- llm4ad-1.0.dist-info/top_level.txt +1 -0
llm4ad/__init__.py
ADDED
llm4ad/base/__init__.py
ADDED
llm4ad/base/code.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file implements 2 classes representing unities of code:
|
|
3
|
+
|
|
4
|
+
- Function, containing all the information we need about functions: name, args,
|
|
5
|
+
body and optionally a return type and a docstring.
|
|
6
|
+
|
|
7
|
+
- Program, which contains a code preface (which could be imports, global
|
|
8
|
+
variables and classes, ...) and a list of Functions.
|
|
9
|
+
|
|
10
|
+
- For example, a function is shown below,
|
|
11
|
+
which is an un-executable program because 'np' is not defined, and 'WEIGHT' is not defined.
|
|
12
|
+
--------------------------------------------------------------------------------------------
|
|
13
|
+
def func(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
14
|
+
b = b + WEIGHT
|
|
15
|
+
return a + b
|
|
16
|
+
--------------------------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
- A program is an executable object as shown below.
|
|
19
|
+
--------------------------------------------------------------------------------------------
|
|
20
|
+
import numpy as np
|
|
21
|
+
WEIGHT = 10
|
|
22
|
+
|
|
23
|
+
def func(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
24
|
+
b = b + WEIGHT
|
|
25
|
+
return a + b
|
|
26
|
+
--------------------------------------------------------------------------------------------
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import ast
|
|
31
|
+
import copy
|
|
32
|
+
import dataclasses
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclasses.dataclass
|
|
37
|
+
class Function:
|
|
38
|
+
"""A parsed Python function."""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
args: str
|
|
42
|
+
body: str
|
|
43
|
+
return_type: str | None = None
|
|
44
|
+
docstring: str | None = None
|
|
45
|
+
score: Any | None = None
|
|
46
|
+
evaluate_time: float | None = None
|
|
47
|
+
sample_time: float | None = None
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
return_type = f' -> {self.return_type}' if self.return_type else ''
|
|
51
|
+
|
|
52
|
+
function = f'def {self.name}({self.args}){return_type}:\n'
|
|
53
|
+
if self.docstring:
|
|
54
|
+
# self.docstring is already indented on every line except the first one.
|
|
55
|
+
# Here, we assume the indentation is always four spaces.
|
|
56
|
+
new_line = '\n' if self.body else ''
|
|
57
|
+
function += f' """{self.docstring}"""{new_line}'
|
|
58
|
+
# self.body is already indented.
|
|
59
|
+
function += self.body + '\n\n'
|
|
60
|
+
return function
|
|
61
|
+
|
|
62
|
+
def __setattr__(self, name: str, value: str) -> None:
|
|
63
|
+
# Ensure there aren't leading & trailing new lines in `body`.
|
|
64
|
+
if name == 'body':
|
|
65
|
+
value = value.strip('\n')
|
|
66
|
+
# Ensure there aren't leading & trailing quotes in `docstring``.
|
|
67
|
+
if name == 'docstring' and value is not None:
|
|
68
|
+
if '"""' in value:
|
|
69
|
+
value = value.strip()
|
|
70
|
+
value = value.replace('"""', '')
|
|
71
|
+
super().__setattr__(name, value)
|
|
72
|
+
|
|
73
|
+
def __eq__(self, other: Function):
|
|
74
|
+
assert isinstance(other, Function)
|
|
75
|
+
return (self.name == other.name and
|
|
76
|
+
self.args == other.args and
|
|
77
|
+
self.return_type == other.return_type and
|
|
78
|
+
self.body == other.body)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclasses.dataclass(frozen=True)
|
|
82
|
+
class Program:
|
|
83
|
+
"""A parsed Python program."""
|
|
84
|
+
|
|
85
|
+
# `preface` is everything from the beginning of the code till the first
|
|
86
|
+
# function is found.
|
|
87
|
+
preface: str
|
|
88
|
+
functions: list[Function]
|
|
89
|
+
|
|
90
|
+
def __str__(self) -> str:
|
|
91
|
+
program = f'{self.preface}\n' if self.preface else ''
|
|
92
|
+
program += '\n'.join([str(f) for f in self.functions])
|
|
93
|
+
return program
|
|
94
|
+
|
|
95
|
+
def find_function_index(self, function_name: str) -> int:
|
|
96
|
+
"""Returns the index of input function name."""
|
|
97
|
+
function_names = [f.name for f in self.functions]
|
|
98
|
+
count = function_names.count(function_name)
|
|
99
|
+
if count == 0:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f'function {function_name} does not exist in program:\n{str(self)}'
|
|
102
|
+
)
|
|
103
|
+
if count > 1:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f'function {function_name} exists more than once in program:\n'
|
|
106
|
+
f'{str(self)}'
|
|
107
|
+
)
|
|
108
|
+
index = function_names.index(function_name)
|
|
109
|
+
return index
|
|
110
|
+
|
|
111
|
+
def get_function(self, function_name: str) -> Function:
|
|
112
|
+
index = self.find_function_index(function_name)
|
|
113
|
+
return self.functions[index]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _ProgramVisitor(ast.NodeVisitor):
|
|
117
|
+
"""Parses code to collect all required information to produce a `Program`.
|
|
118
|
+
Note that we do not store function decorators.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, sourcecode: str):
|
|
122
|
+
self._codelines: list[str] = sourcecode.splitlines()
|
|
123
|
+
self._preface: str = ''
|
|
124
|
+
self._functions: list[Function] = []
|
|
125
|
+
self._current_function: str | None = None
|
|
126
|
+
|
|
127
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
128
|
+
"""Collects all information about the function being parsed.
|
|
129
|
+
"""
|
|
130
|
+
# We only care about first level functions.
|
|
131
|
+
if node.col_offset == 0:
|
|
132
|
+
self._current_function = node.name
|
|
133
|
+
# --------------------------------------------------------------------------------------------
|
|
134
|
+
# RZ: Here is an issue: some cases were not taken into consideration.
|
|
135
|
+
# For instance, if the first parsed function has decorators,
|
|
136
|
+
# these decorators will also be retained in the preceding code snippet.
|
|
137
|
+
# However, in the algorithm's flow, these decorators are invalid,
|
|
138
|
+
# leading to subsequent evaluations reporting errors.
|
|
139
|
+
# --------------------------------------------------------------------------------------------
|
|
140
|
+
# if not self._functions:
|
|
141
|
+
# self._preface = '\n'.join(self._codelines[:node.lineno - 1])
|
|
142
|
+
# --------------------------------------------------------------------------------------------
|
|
143
|
+
# Fix bugs: if the first function is decorated, find the smallest line_no of the decorator,
|
|
144
|
+
# FIX bugs: and retrain the code above it as 'preface'.
|
|
145
|
+
# --------------------------------------------------------------------------------------------
|
|
146
|
+
if not self._functions:
|
|
147
|
+
has_decorators = bool(node.decorator_list)
|
|
148
|
+
if has_decorators:
|
|
149
|
+
# find the minimum line number and retain the code above
|
|
150
|
+
decorator_start_line = min(decorator.lineno for decorator in node.decorator_list)
|
|
151
|
+
self._preface = '\n'.join(self._codelines[:decorator_start_line - 1])
|
|
152
|
+
else:
|
|
153
|
+
self._preface = '\n'.join(self._codelines[:node.lineno - 1])
|
|
154
|
+
# --------------------------------------------------------------------------------------------
|
|
155
|
+
function_end_line = node.end_lineno
|
|
156
|
+
body_start_line = node.body[0].lineno - 1
|
|
157
|
+
# Extract the docstring.
|
|
158
|
+
docstring = None
|
|
159
|
+
if isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Str):
|
|
160
|
+
docstring = f' """{ast.literal_eval(ast.unparse(node.body[0]))}"""'
|
|
161
|
+
if len(node.body) > 1:
|
|
162
|
+
# ----------------------------------------------------------------------------
|
|
163
|
+
# RZ: If the next line of the docstring is also comment that starts with "#"
|
|
164
|
+
# We should preserve it.
|
|
165
|
+
# ----------------------------------------------------------------------------
|
|
166
|
+
# body_start_line = node.body[1].lineno - 1
|
|
167
|
+
# ----------------------------------------------------------------------------
|
|
168
|
+
# Fix bugs: If we find the docstring, the function body start with
|
|
169
|
+
# Fix bugs: body[0].end_lineno, where body[0] refers to the docstring node.
|
|
170
|
+
# ----------------------------------------------------------------------------
|
|
171
|
+
body_start_line = node.body[0].end_lineno
|
|
172
|
+
# ----------------------------------------------------------------------------
|
|
173
|
+
else:
|
|
174
|
+
body_start_line = function_end_line
|
|
175
|
+
|
|
176
|
+
self._functions.append(Function(
|
|
177
|
+
name=node.name,
|
|
178
|
+
args=ast.unparse(node.args),
|
|
179
|
+
return_type=ast.unparse(node.returns) if node.returns else None,
|
|
180
|
+
docstring=docstring,
|
|
181
|
+
body='\n'.join(self._codelines[body_start_line:function_end_line]),
|
|
182
|
+
))
|
|
183
|
+
self.generic_visit(node)
|
|
184
|
+
|
|
185
|
+
def return_program(self) -> Program:
|
|
186
|
+
return Program(preface=self._preface, functions=self._functions)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TextFunctionProgramConverter:
|
|
190
|
+
"""Convert text to Program instance and Function instance,
|
|
191
|
+
Convert Program instance to Function instance, and Function instance to Program instance.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def text_to_program(cls, program_str: str) -> Program | None:
|
|
196
|
+
"""Returns Program object by parsing input text using Python AST.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
# We assume that the program is composed of some preface (e.g. imports,
|
|
200
|
+
# classes, assignments, ...) followed by a sequence of functions.
|
|
201
|
+
tree = ast.parse(program_str)
|
|
202
|
+
visitor = _ProgramVisitor(program_str)
|
|
203
|
+
visitor.visit(tree)
|
|
204
|
+
return visitor.return_program()
|
|
205
|
+
except:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def text_to_function(cls, program_str: str) -> Function | None:
|
|
210
|
+
"""Returns Function object by parsing input text using Python AST.
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
program = cls.text_to_program(program_str)
|
|
214
|
+
if len(program.functions) != 1:
|
|
215
|
+
raise ValueError(f'Only one function expected, got {len(program.functions)}'
|
|
216
|
+
f':\n{program.functions}')
|
|
217
|
+
return program.functions[0]
|
|
218
|
+
except ValueError as value_err:
|
|
219
|
+
raise value_err
|
|
220
|
+
except:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def function_to_program(cls, function: str | Function, template_program: str | Program) -> Program | None:
|
|
225
|
+
try:
|
|
226
|
+
# convert function to Function instance
|
|
227
|
+
if isinstance(function, str):
|
|
228
|
+
function = cls.text_to_function(function)
|
|
229
|
+
else:
|
|
230
|
+
function = copy.deepcopy(function)
|
|
231
|
+
|
|
232
|
+
# convert template_program to Program instance
|
|
233
|
+
if isinstance(template_program, str):
|
|
234
|
+
template_program = cls.text_to_program(template_program)
|
|
235
|
+
else:
|
|
236
|
+
template_program = copy.deepcopy(template_program)
|
|
237
|
+
|
|
238
|
+
# assert that a program have one function
|
|
239
|
+
if len(template_program.functions) != 1:
|
|
240
|
+
raise ValueError(f'Only one function expected, got {len(template_program.functions)}'
|
|
241
|
+
f':\n{template_program.functions}')
|
|
242
|
+
|
|
243
|
+
# replace the function body with the new function body
|
|
244
|
+
template_program.functions[0].body = function.body
|
|
245
|
+
return template_program
|
|
246
|
+
except ValueError as value_err:
|
|
247
|
+
raise value_err
|
|
248
|
+
except:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def program_to_function(cls, program: str | Program) -> Function | None:
|
|
253
|
+
try:
|
|
254
|
+
# convert program to Program instance
|
|
255
|
+
if isinstance(program, str):
|
|
256
|
+
program = cls.text_to_program(program)
|
|
257
|
+
else:
|
|
258
|
+
program = copy.deepcopy(program)
|
|
259
|
+
|
|
260
|
+
# assert that a program have one function
|
|
261
|
+
if len(program.functions) != 1:
|
|
262
|
+
raise ValueError(f'Only one function expected, got {len(program.functions)}'
|
|
263
|
+
f':\n{program.functions}')
|
|
264
|
+
|
|
265
|
+
# return the function
|
|
266
|
+
return program.functions[0]
|
|
267
|
+
except ValueError as value_err:
|
|
268
|
+
raise value_err
|
|
269
|
+
except:
|
|
270
|
+
return None
|
llm4ad/base/evaluate.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import multiprocessing
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .code import TextFunctionProgramConverter, Program
|
|
10
|
+
from .modify_code import ModifyCode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Evaluation(ABC):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
template_program: str,
|
|
17
|
+
task_description: str = '',
|
|
18
|
+
use_numba_accelerate: bool = False,
|
|
19
|
+
use_protected_div: bool = False,
|
|
20
|
+
protected_div_delta: float = 1e-5,
|
|
21
|
+
random_seed: int | None = None,
|
|
22
|
+
timeout_seconds: int | float = None,
|
|
23
|
+
*,
|
|
24
|
+
exec_code: bool = True,
|
|
25
|
+
safe_evaluate: bool = True,
|
|
26
|
+
daemon_eval_process: bool = False,
|
|
27
|
+
**kwargs
|
|
28
|
+
):
|
|
29
|
+
"""Evaluator for executing generated code.
|
|
30
|
+
Args:
|
|
31
|
+
task_description: a brief description of the algorithm design task and specification.
|
|
32
|
+
template_program: the seed program (in str) as the initial function of the run.
|
|
33
|
+
the template_program should be executable, i.e., incorporating package import, and function definition, and function body.
|
|
34
|
+
The function body can be 'pass' in EoH as EoH does not request the template_program tobe valid.
|
|
35
|
+
use_numba_accelerate: Wrap the function with '@numba.jit(nopython=True)'.
|
|
36
|
+
use_protected_div : Modify 'a / b' => 'a / (b + delta)'.
|
|
37
|
+
protected_div_delta : Delta value in protected div.
|
|
38
|
+
random_seed : If is not None, set random seed in the first line of the function body.
|
|
39
|
+
timeout_seconds : Terminate the evaluation after timeout seconds.
|
|
40
|
+
exec_code : Using 'exec()' to compile the code and provide the callable function.
|
|
41
|
+
If is set to 'False', the 'callable_func' argument in 'self.evaluate_program' is always 'None'.
|
|
42
|
+
If is set to 'True', the user should provide the score of the program based on 'program_str' argument in 'self.evaluate_program'.
|
|
43
|
+
safe_evaluate : Evaluate in safe mode using a new process. If is set to False,
|
|
44
|
+
the evaluation will not be terminated after timeout seconds. The user should consider how to
|
|
45
|
+
terminate evaluating in time.
|
|
46
|
+
daemon_eval_process : Set the evaluate process as a daemon process. If set to True,
|
|
47
|
+
you can not set new processes in the evaluator. Which means in self.evaluate_program(),
|
|
48
|
+
you can not create new processes.
|
|
49
|
+
|
|
50
|
+
-Assume that: use_numba_accelerate=True, self.use_protected_div=True, and self.random_seed=2024.
|
|
51
|
+
-The original function:
|
|
52
|
+
--------------------------------------------------------------------------------
|
|
53
|
+
import numpy as np
|
|
54
|
+
|
|
55
|
+
def f(a, b):
|
|
56
|
+
a = np.random.random()
|
|
57
|
+
return a / b
|
|
58
|
+
--------------------------------------------------------------------------------
|
|
59
|
+
-The modified function will be:
|
|
60
|
+
--------------------------------------------------------------------------------
|
|
61
|
+
import numpy as np
|
|
62
|
+
import numba
|
|
63
|
+
|
|
64
|
+
@numba.jit(nopython=True)
|
|
65
|
+
def f():
|
|
66
|
+
np.random.seed(2024)
|
|
67
|
+
a = np.random.random()
|
|
68
|
+
return _protected_div(a, b)
|
|
69
|
+
|
|
70
|
+
def _protected_div(a, b, delta=1e-5):
|
|
71
|
+
return a / (b + delta)
|
|
72
|
+
--------------------------------------------------------------------------------
|
|
73
|
+
As shown above, the 'import numba', 'numba.jit()' decorator, and '_protected_dev' will be added by this function.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
self.template_program = template_program
|
|
77
|
+
self.task_description = task_description
|
|
78
|
+
self.use_numba_accelerate = use_numba_accelerate
|
|
79
|
+
self.use_protected_div = use_protected_div
|
|
80
|
+
self.protected_div_delta = protected_div_delta
|
|
81
|
+
self.random_seed = random_seed
|
|
82
|
+
self.timeout_seconds = timeout_seconds
|
|
83
|
+
self.exec_code = exec_code
|
|
84
|
+
self.safe_evaluate = safe_evaluate
|
|
85
|
+
self.daemon_eval_process = daemon_eval_process
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None:
|
|
89
|
+
"""Evaluate a given function. You can use compiled function (function_callable),
|
|
90
|
+
as well as the original function strings for evaluation.
|
|
91
|
+
Args:
|
|
92
|
+
program_str: The function in string. You can ignore this argument when implementation. (See below).
|
|
93
|
+
callable_func: The callable heuristic function. You can call it using `callable_func(args, kwargs)`.
|
|
94
|
+
Return:
|
|
95
|
+
Returns the fitness value.
|
|
96
|
+
|
|
97
|
+
Assume that: self.use_numba_accelerate = True, self.use_protected_div = True,
|
|
98
|
+
and self.random_seed = 2024, the argument 'function_str' will be something like below:
|
|
99
|
+
--------------------------------------------------------------------------------
|
|
100
|
+
import numpy as np
|
|
101
|
+
import numba
|
|
102
|
+
|
|
103
|
+
@numba.jit(nopython=True)
|
|
104
|
+
def f(a, b):
|
|
105
|
+
np.random.seed(2024)
|
|
106
|
+
a = a + np.random.random()
|
|
107
|
+
return _protected_div(a, b)
|
|
108
|
+
|
|
109
|
+
def _protected_div(a, b, delta=1e-5):
|
|
110
|
+
return a / (b + delta)
|
|
111
|
+
--------------------------------------------------------------------------------
|
|
112
|
+
As shown above, the 'import numba', 'numba.jit()' decorator,
|
|
113
|
+
and '_protected_dev' will be added by this function.
|
|
114
|
+
"""
|
|
115
|
+
raise NotImplementedError('Must provide a evaluator for a function.')
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class SecureEvaluator:
|
|
119
|
+
def __init__(self, evaluator: Evaluation, debug_mode=False, *, fork_proc: str | bool = 'auto', **kwargs):
|
|
120
|
+
assert fork_proc in [True, False, 'auto', 'default']
|
|
121
|
+
self._evaluator = evaluator
|
|
122
|
+
self._debug_mode = debug_mode
|
|
123
|
+
|
|
124
|
+
if self._evaluator.safe_evaluate:
|
|
125
|
+
if fork_proc == 'auto':
|
|
126
|
+
# force MacOS and Linux use 'fork' to generate new process
|
|
127
|
+
if sys.platform.startswith('darwin') or sys.platform.startswith('linux'):
|
|
128
|
+
multiprocessing.set_start_method('fork', force=True)
|
|
129
|
+
elif fork_proc is True:
|
|
130
|
+
multiprocessing.set_start_method('fork', force=True)
|
|
131
|
+
elif fork_proc is False:
|
|
132
|
+
multiprocessing.set_start_method('spawn', force=True)
|
|
133
|
+
|
|
134
|
+
def _modify_program_code(self, program_str: str) -> str:
|
|
135
|
+
function_name = TextFunctionProgramConverter.text_to_function(program_str).name
|
|
136
|
+
if self._evaluator.use_numba_accelerate:
|
|
137
|
+
program_str = ModifyCode.add_numba_decorator(
|
|
138
|
+
program_str, function_name=function_name
|
|
139
|
+
)
|
|
140
|
+
if self._evaluator.use_protected_div:
|
|
141
|
+
program_str = ModifyCode.replace_div_with_protected_div(
|
|
142
|
+
program_str, self._evaluator.protected_div_delta, self._evaluator.use_numba_accelerate
|
|
143
|
+
)
|
|
144
|
+
if self._evaluator.random_seed is not None:
|
|
145
|
+
program_str = ModifyCode.add_numpy_random_seed_to_func(
|
|
146
|
+
program_str, function_name, self._evaluator.random_seed
|
|
147
|
+
)
|
|
148
|
+
return program_str
|
|
149
|
+
|
|
150
|
+
def evaluate_program(self, program: str | Program):
|
|
151
|
+
try:
|
|
152
|
+
program_str = str(program)
|
|
153
|
+
# record function name BEFORE modifying program code
|
|
154
|
+
function_name = TextFunctionProgramConverter.text_to_function(program_str).name
|
|
155
|
+
|
|
156
|
+
program_str = self._modify_program_code(program_str)
|
|
157
|
+
if self._debug_mode:
|
|
158
|
+
print(f'DEBUG: evaluated program:\n{program_str}\n')
|
|
159
|
+
|
|
160
|
+
# safe evaluate
|
|
161
|
+
if self._evaluator.safe_evaluate:
|
|
162
|
+
result_queue = multiprocessing.Queue()
|
|
163
|
+
process = multiprocessing.Process(
|
|
164
|
+
target=self._evaluate_in_safe_process,
|
|
165
|
+
args=(program_str, function_name, result_queue),
|
|
166
|
+
daemon=self._evaluator.daemon_eval_process
|
|
167
|
+
)
|
|
168
|
+
process.start()
|
|
169
|
+
|
|
170
|
+
if self._evaluator.timeout_seconds is not None:
|
|
171
|
+
try:
|
|
172
|
+
# get the result in timeout seconds
|
|
173
|
+
result = result_queue.get(timeout=self._evaluator.timeout_seconds)
|
|
174
|
+
# after getting the result, terminate/kill the process
|
|
175
|
+
process.terminate()
|
|
176
|
+
process.join()
|
|
177
|
+
except:
|
|
178
|
+
# timeout
|
|
179
|
+
if self._debug_mode:
|
|
180
|
+
print(f'DEBUG: the evaluation time exceeds {self._evaluator.timeout_seconds}s.')
|
|
181
|
+
process.terminate()
|
|
182
|
+
process.join()
|
|
183
|
+
result = None
|
|
184
|
+
else:
|
|
185
|
+
result = result_queue.get()
|
|
186
|
+
process.terminate()
|
|
187
|
+
process.join()
|
|
188
|
+
return result
|
|
189
|
+
else:
|
|
190
|
+
return self._evaluate(program_str, function_name)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
if self._debug_mode:
|
|
193
|
+
print(e)
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def evaluate_program_record_time(self, program: str | Program):
|
|
197
|
+
evaluate_start = time.time()
|
|
198
|
+
try:
|
|
199
|
+
program_str = str(program)
|
|
200
|
+
# record function name BEFORE modifying program code
|
|
201
|
+
function_name = TextFunctionProgramConverter.text_to_function(program_str).name
|
|
202
|
+
|
|
203
|
+
program_str = self._modify_program_code(program_str)
|
|
204
|
+
if self._debug_mode:
|
|
205
|
+
print(f'DEBUG: evaluated program:\n{program_str}\n')
|
|
206
|
+
|
|
207
|
+
# safe evaluate
|
|
208
|
+
if self._evaluator.safe_evaluate:
|
|
209
|
+
result_queue = multiprocessing.Queue()
|
|
210
|
+
process = multiprocessing.Process(
|
|
211
|
+
target=self._evaluate_in_safe_process,
|
|
212
|
+
args=(program_str, function_name, result_queue),
|
|
213
|
+
daemon=self._evaluator.daemon_eval_process
|
|
214
|
+
)
|
|
215
|
+
process.start()
|
|
216
|
+
|
|
217
|
+
if self._evaluator.timeout_seconds is not None:
|
|
218
|
+
try:
|
|
219
|
+
# get the result in timeout seconds
|
|
220
|
+
result = result_queue.get(timeout=self._evaluator.timeout_seconds)
|
|
221
|
+
# after getting the result, terminate/kill the process
|
|
222
|
+
process.terminate()
|
|
223
|
+
process.join()
|
|
224
|
+
except:
|
|
225
|
+
# timeout
|
|
226
|
+
if self._debug_mode:
|
|
227
|
+
print(f'DEBUG: the evaluation time exceeds {self._evaluator.timeout_seconds}s.')
|
|
228
|
+
process.terminate()
|
|
229
|
+
process.join()
|
|
230
|
+
result = None
|
|
231
|
+
else:
|
|
232
|
+
result = result_queue.get()
|
|
233
|
+
process.terminate()
|
|
234
|
+
process.join()
|
|
235
|
+
# return evaluate result and evaluate time
|
|
236
|
+
return result, time.time() - evaluate_start
|
|
237
|
+
else:
|
|
238
|
+
start = time.time()
|
|
239
|
+
score = self._evaluate(program_str, function_name)
|
|
240
|
+
return score, time.time() - start
|
|
241
|
+
except Exception as e:
|
|
242
|
+
if self._debug_mode:
|
|
243
|
+
print(e)
|
|
244
|
+
# return None and evaluate time
|
|
245
|
+
return None, time.time() - evaluate_start
|
|
246
|
+
|
|
247
|
+
def _evaluate_in_safe_process(self, program_str: str, function_name, result_queue: multiprocessing.Queue):
|
|
248
|
+
try:
|
|
249
|
+
if self._evaluator.exec_code:
|
|
250
|
+
# compile the program, and maps the global func/var/class name to its address
|
|
251
|
+
all_globals_namespace = {}
|
|
252
|
+
# execute the program, map func/var/class to global namespace
|
|
253
|
+
exec(program_str, all_globals_namespace)
|
|
254
|
+
# get the pointer of 'function_to_run'
|
|
255
|
+
program_callable = all_globals_namespace[function_name]
|
|
256
|
+
else:
|
|
257
|
+
program_callable = None
|
|
258
|
+
|
|
259
|
+
# get evaluate result
|
|
260
|
+
res = self._evaluator.evaluate_program(program_str, program_callable)
|
|
261
|
+
result_queue.put(res)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
if self._debug_mode:
|
|
264
|
+
print(e)
|
|
265
|
+
result_queue.put(None)
|
|
266
|
+
|
|
267
|
+
def _evaluate(self, program_str: str, function_name):
|
|
268
|
+
try:
|
|
269
|
+
if self._evaluator.exec_code:
|
|
270
|
+
# compile the program, and maps the global func/var/class name to its address
|
|
271
|
+
all_globals_namespace = {}
|
|
272
|
+
# execute the program, map func/var/class to global namespace
|
|
273
|
+
exec(program_str, all_globals_namespace)
|
|
274
|
+
# get the pointer of 'function_to_run'
|
|
275
|
+
program_callable = all_globals_namespace[function_name]
|
|
276
|
+
else:
|
|
277
|
+
program_callable = None
|
|
278
|
+
|
|
279
|
+
# get evaluate result
|
|
280
|
+
res = self._evaluator.evaluate_program(program_str, program_callable)
|
|
281
|
+
return res
|
|
282
|
+
except Exception as e:
|
|
283
|
+
if self._debug_mode:
|
|
284
|
+
print(e)
|
|
285
|
+
return None
|