2024-06-14 12:59:44 +08:00
from __future__ import annotations
2024-06-28 11:45:23 +08:00
import pickle
import subprocess
import uuid
2024-06-14 12:59:44 +08:00
from pathlib import Path
2024-06-28 11:45:23 +08:00
from typing import Tuple , Union
2024-06-14 12:59:44 +08:00
2024-06-28 11:45:23 +08:00
import pandas as pd
from filelock import FileLock
2024-06-14 12:59:44 +08:00
2024-09-12 00:30:16 +08:00
from rdagent.app.kaggle.conf import KAGGLE_IMPLEMENT_SETTING
2024-07-05 17:42:00 +08:00
from rdagent.components.coder.factor_coder.config import FACTOR_IMPLEMENT_SETTINGS
2024-07-26 12:12:16 +08:00
from rdagent.core.exception import CodeFormatError , CustomRuntimeError , NoOutputError
2024-07-17 15:00:13 +08:00
from rdagent.core.experiment import Experiment , FBWorkspace , Task
2024-07-16 20:35:42 +08:00
from rdagent.log import rdagent_logger as logger
2024-06-28 11:45:23 +08:00
from rdagent.oai.llm_utils import md5_hash
2024-05-21 22:48:41 +08:00
2024-07-02 17:58:37 +08:00
class FactorTask ( Task ):
# TODO: generalized the attributes into the Task
2024-06-21 16:41:34 +08:00
# - factor_* -> *
2024-05-21 22:48:41 +08:00
def __init__ (
self ,
factor_name ,
factor_description ,
factor_formulation ,
2024-09-11 15:26:52 +08:00
* args ,
2024-05-21 22:48:41 +08:00
variables : dict = {},
2024-06-14 12:59:44 +08:00
resource : str = None ,
2024-08-07 19:01:46 +08:00
factor_implementation : bool = False ,
2024-09-11 15:26:52 +08:00
** kwargs ,
2024-05-21 22:48:41 +08:00
) -> None :
self . factor_name = factor_name
self . factor_description = factor_description
self . factor_formulation = factor_formulation
self . variables = variables
2024-06-14 12:59:44 +08:00
self . factor_resources = resource
2024-08-07 19:01:46 +08:00
self . factor_implementation = factor_implementation
2024-09-11 15:26:52 +08:00
super () . __init__ ( * args , ** kwargs )
2024-05-21 22:48:41 +08:00
2024-07-15 08:28:34 +00:00
def get_task_information ( self ):
2024-05-21 22:48:41 +08:00
return f """factor_name: { self . factor_name }
factor_description: { self . factor_description }
factor_formulation: { self . factor_formulation }
2024-06-28 11:45:23 +08:00
variables: { str ( self . variables ) } """
2024-05-21 22:48:41 +08:00
2024-08-07 19:01:46 +08:00
def get_task_information_and_implementation_result ( self ):
return {
"factor_name" : self . factor_name ,
"factor_description" : self . factor_description ,
"factor_formulation" : self . factor_formulation ,
"variables" : str ( self . variables ),
"factor_implementation" : str ( self . factor_implementation ),
}
2024-05-21 22:48:41 +08:00
@staticmethod
def from_dict ( dict ):
2024-07-02 17:58:37 +08:00
return FactorTask ( ** dict )
2024-05-21 22:48:41 +08:00
def __repr__ ( self ) -> str :
return f "< { self . __class__ . __name__ } [ { self . factor_name } ]>"
2024-07-17 15:00:13 +08:00
class FactorFBWorkspace ( FBWorkspace ):
2024-05-21 22:48:41 +08:00
"""
This class is used to implement a factor by writing the code to a file.
Input data and output factor value are also written to files.
"""
# TODO: (Xiao) think raising errors may get better information for processing
FB_FROM_CACHE = "The factor value has been executed and stored in the instance variable."
FB_EXEC_SUCCESS = "Execution succeeded without error."
FB_CODE_NOT_SET = "code is not set."
FB_EXECUTION_SUCCEEDED = "Execution succeeded without error."
FB_OUTPUT_FILE_NOT_FOUND = " \n Expected output file not found."
FB_OUTPUT_FILE_FOUND = " \n Expected output file found."
def __init__ (
self ,
2024-07-10 15:45:43 +08:00
* args ,
2024-09-11 15:26:52 +08:00
executed_factor_value_dataframe : pd . DataFrame = None ,
raise_exception : bool = False ,
2024-07-10 15:45:43 +08:00
** kwargs ,
2024-05-21 22:48:41 +08:00
) -> None :
2024-07-10 15:45:43 +08:00
super () . __init__ ( * args , ** kwargs )
2024-05-21 22:48:41 +08:00
self . executed_factor_value_dataframe = executed_factor_value_dataframe
self . raise_exception = raise_exception
@staticmethod
def link_data_to_workspace ( data_path : Path , workspace_path : Path ):
data_path = Path ( data_path )
workspace_path = Path ( workspace_path )
for data_file_path in data_path . iterdir ():
workspace_data_file_path = workspace_path / data_file_path . name
if workspace_data_file_path . exists ():
workspace_data_file_path . unlink ()
subprocess . run (
[ "ln" , "-s" , data_file_path , workspace_data_file_path ],
check = False ,
)
2024-07-15 10:23:32 +00:00
def execute ( self , store_result : bool = False , data_type : str = "Debug" ) -> Tuple [ str , pd . DataFrame ]:
2024-05-21 22:48:41 +08:00
"""
execute the implementation and get the factor value by the following steps:
1. make the directory in workspace path
2. write the code to the file in the workspace path
3. link all the source data to the workspace path folder
2024-09-11 15:26:52 +08:00
if call_factor_py is True:
4. execute the code
else:
4. generate a script from template to import the factor.py dump get the factor value to result.h5
2024-05-21 22:48:41 +08:00
5. read the factor value from the output file in the workspace path folder
returns the execution feedback as a string and the factor value as a pandas dataframe
parameters:
store_result: if True, store the factor value in the instance variable, this feature is to be used in the gt implementation to avoid multiple execution on the same gt implementation
"""
2024-07-17 15:00:13 +08:00
super () . execute ()
2024-07-10 15:45:43 +08:00
if self . code_dict is None or "factor.py" not in self . code_dict :
2024-05-21 22:48:41 +08:00
if self . raise_exception :
2024-07-18 22:36:04 +08:00
raise CodeFormatError ( self . FB_CODE_NOT_SET )
2024-05-21 22:48:41 +08:00
else :
2024-07-18 17:00:41 +08:00
return self . FB_CODE_NOT_SET , None
2024-05-21 22:48:41 +08:00
with FileLock ( self . workspace_path / "execution.lock" ):
2024-06-18 11:50:03 +08:00
if FACTOR_IMPLEMENT_SETTINGS . enable_execution_cache :
2024-07-15 10:23:32 +00:00
# NOTE: cache the result for the same code and same data type
target_file_name = md5_hash ( data_type + self . code_dict [ "factor.py" ])
2024-07-17 15:00:13 +08:00
cache_file_path = Path ( FACTOR_IMPLEMENT_SETTINGS . cache_location ) / f " { target_file_name } .pkl"
Path ( FACTOR_IMPLEMENT_SETTINGS . cache_location ) . mkdir ( exist_ok = True , parents = True )
2024-05-21 22:48:41 +08:00
if cache_file_path . exists () and not self . raise_exception :
cached_res = pickle . load ( open ( cache_file_path , "rb" ))
if store_result and cached_res [ 1 ] is not None :
self . executed_factor_value_dataframe = cached_res [ 1 ]
return cached_res
if self . executed_factor_value_dataframe is not None :
return self . FB_FROM_CACHE , self . executed_factor_value_dataframe
2024-09-11 15:26:52 +08:00
if self . target_task . version == 1 :
source_data_path = (
Path (
FACTOR_IMPLEMENT_SETTINGS . data_folder_debug ,
)
if data_type == "Debug"
else Path (
FACTOR_IMPLEMENT_SETTINGS . data_folder ,
)
2024-07-15 10:23:32 +00:00
)
2024-09-11 15:26:52 +08:00
elif self . target_task . version == 2 :
# TODO you can change the name of the data folder for a better understanding
2024-09-12 00:30:16 +08:00
source_data_path = Path ( FACTOR_IMPLEMENT_SETTINGS . data_folder ) / KAGGLE_IMPLEMENT_SETTING . competition
2024-07-10 15:45:43 +08:00
2024-06-27 09:39:17 +01:00
source_data_path . mkdir ( exist_ok = True , parents = True )
2024-07-10 15:45:43 +08:00
code_path = self . workspace_path / f "factor.py"
2024-05-21 22:48:41 +08:00
self . link_data_to_workspace ( source_data_path , self . workspace_path )
execution_feedback = self . FB_EXECUTION_SUCCEEDED
2024-07-18 15:01:07 +08:00
execution_success = False
2024-09-11 15:26:52 +08:00
if self . target_task . version == 1 :
execution_code_path = code_path
elif self . target_task . version == 2 :
execution_code_path = self . workspace_path / f " { uuid . uuid4 () } .py"
execution_code_path . write_text (( Path ( __file__ ) . parent / "factor_execution_template.txt" ) . read_text ())
2024-05-21 22:48:41 +08:00
try :
subprocess . check_output (
2024-09-11 15:26:52 +08:00
f " { FACTOR_IMPLEMENT_SETTINGS . python_bin } { execution_code_path } " ,
2024-05-21 22:48:41 +08:00
shell = True ,
cwd = self . workspace_path ,
stderr = subprocess . STDOUT ,
2024-06-18 11:50:03 +08:00
timeout = FACTOR_IMPLEMENT_SETTINGS . file_based_execution_timeout ,
2024-05-21 22:48:41 +08:00
)
2024-07-18 15:01:07 +08:00
execution_success = True
2024-05-21 22:48:41 +08:00
except subprocess . CalledProcessError as e :
import site
execution_feedback = (
e . output . decode ()
2024-09-11 15:26:52 +08:00
. replace ( str ( execution_code_path . parent . absolute ()), r "/path/to" )
2024-05-21 22:48:41 +08:00
. replace ( str ( site . getsitepackages ()[ 0 ]), r "/path/to/site-packages" )
)
if len ( execution_feedback ) > 2000 :
execution_feedback = (
execution_feedback [: 1000 ] + "....hidden long error message...." + execution_feedback [ - 1000 :]
)
if self . raise_exception :
2024-07-18 22:36:04 +08:00
raise CustomRuntimeError ( execution_feedback )
2024-05-21 22:48:41 +08:00
except subprocess . TimeoutExpired :
2024-06-18 11:50:03 +08:00
execution_feedback += f "Execution timeout error and the timeout is set to { FACTOR_IMPLEMENT_SETTINGS . file_based_execution_timeout } seconds."
2024-05-21 22:48:41 +08:00
if self . raise_exception :
2024-07-18 22:36:04 +08:00
raise CustomRuntimeError ( execution_feedback )
2024-05-21 22:48:41 +08:00
workspace_output_file_path = self . workspace_path / "result.h5"
2024-07-18 15:01:07 +08:00
if workspace_output_file_path . exists () and execution_success :
2024-05-21 22:48:41 +08:00
try :
executed_factor_value_dataframe = pd . read_hdf ( workspace_output_file_path )
execution_feedback += self . FB_OUTPUT_FILE_FOUND
except Exception as e :
execution_feedback += f "Error found when reading hdf file: { e } " [: 1000 ]
executed_factor_value_dataframe = None
2024-07-18 15:01:07 +08:00
else :
execution_feedback += self . FB_OUTPUT_FILE_NOT_FOUND
executed_factor_value_dataframe = None
if self . raise_exception :
2024-07-22 12:58:09 +08:00
raise NoOutputError ( execution_feedback )
2024-05-21 22:48:41 +08:00
if store_result and executed_factor_value_dataframe is not None :
self . executed_factor_value_dataframe = executed_factor_value_dataframe
2024-06-18 11:50:03 +08:00
if FACTOR_IMPLEMENT_SETTINGS . enable_execution_cache :
2024-05-21 22:48:41 +08:00
pickle . dump (
( execution_feedback , executed_factor_value_dataframe ),
open ( cache_file_path , "wb" ),
)
return execution_feedback , executed_factor_value_dataframe
def __str__ ( self ) -> str :
# NOTE:
# If the code cache works, the workspace will be None.
return f "File Factor[ { self . target_task . factor_name } ]: { self . workspace_path } "
def __repr__ ( self ) -> str :
return self . __str__ ()
@staticmethod
2024-07-02 17:58:37 +08:00
def from_folder ( task : FactorTask , path : Union [ str , Path ], ** kwargs ):
2024-05-21 22:48:41 +08:00
path = Path ( path )
2024-07-10 15:45:43 +08:00
code_dict = {}
for file_path in path . iterdir ():
if file_path . suffix == ".py" :
code_dict [ file_path . name ] = file_path . read_text ()
2024-07-17 15:00:13 +08:00
return FactorFBWorkspace ( target_task = task , code_dict = code_dict , ** kwargs )
2024-07-02 17:58:37 +08:00
2024-07-17 15:00:13 +08:00
FactorExperiment = Experiment
2024-09-12 00:30:16 +08:00
FeatureExperiment = Experiment