diff --git a/.gitignore b/.gitignore index b2c38a81..a1dd635e 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,8 @@ git_ignore_folder/ # DB files *.db + +# Docker +env_factor/ +env_tpl/ +mlruns/ \ No newline at end of file diff --git a/rdagent/app/qlib_rd_loop/conf.py b/rdagent/app/qlib_rd_loop/conf.py index a97f76ff..f09ea203 100644 --- a/rdagent/app/qlib_rd_loop/conf.py +++ b/rdagent/app/qlib_rd_loop/conf.py @@ -1,4 +1,5 @@ from pydantic_settings import BaseSettings +from pathlib import Path class PropSetting(BaseSettings): @@ -22,6 +23,7 @@ class PropSetting(BaseSettings): qlib_model_summarizer: str = "rdagent.scenarios.qlib.task_generator.feedback.QlibModelHypothesisExperiment2Feedback" evolving_n: int = 10 - - + + py_bin: str = "/usr/bin/python" + PROP_SETTING = PropSetting() diff --git a/rdagent/app/qlib_rd_loop/factor.py b/rdagent/app/qlib_rd_loop/factor.py index cfabe90a..0f5f5de8 100644 --- a/rdagent/app/qlib_rd_loop/factor.py +++ b/rdagent/app/qlib_rd_loop/factor.py @@ -27,7 +27,7 @@ hypothesis2experiment: Hypothesis2Experiment = import_class(PROP_SETTING.qlib_fa qlib_factor_coder: TaskGenerator = import_class(PROP_SETTING.qlib_factor_coder)(scen) qlib_factor_runner: TaskGenerator = import_class(PROP_SETTING.qlib_factor_runner)(scen) -qlib_factor_summarizer: HypothesisExperiment2Feedback = import_class(PROP_SETTING.qlib_factor_summarizer)() +qlib_factor_summarizer: HypothesisExperiment2Feedback = import_class(PROP_SETTING.qlib_factor_summarizer)(scen) trace = Trace(scen=scen) @@ -39,3 +39,20 @@ for _ in range(PROP_SETTING.evolving_n): feedback = qlib_factor_summarizer.generateFeedback(exp, hypothesis, trace) trace.hist.append((hypothesis, exp, feedback)) + + +""" +trace = Trace(scen=scen) +# for _ in range(PROP_SETTING.evolving_n): +for _ in range(1): + hypothesis = hypothesis_gen.gen(trace) + exp = hypothesis2experiment.convert(hypothesis, trace) + # exp = qlib_factor_coder.generate(exp) + import pickle + file_path = '/home/finco/v-yuanteli/RD-Agent/git_ignore_folder/factor_data_output/exp_new.pkl' + with open(file_path, 'rb') as file: + exp = pickle.load(file) + exp = qlib_factor_runner.generate(exp) + feedback = qlib_factor_summarizer.generateFeedback(exp, hypothesis, trace) + # trace.hist.append((hypothesis, exp, feedback)) +""" \ No newline at end of file diff --git a/rdagent/core/experiment.py b/rdagent/core/experiment.py index 42125da7..e0cb6dfc 100644 --- a/rdagent/core/experiment.py +++ b/rdagent/core/experiment.py @@ -18,6 +18,10 @@ ASpecificTask = TypeVar("ASpecificTask", bound=Task) class Implementation(ABC, Generic[ASpecificTask]): + # TODO: workspace; + # - code or data(optional) + # - Execute logic + # - `env is not included`. It is a underlying infra def __init__(self, target_task: ASpecificTask) -> None: self.target_task = target_task @@ -85,6 +89,7 @@ class FBImplementation(Implementation): typical usage of `*args, **kwargs`: Different methods shares the same data. The data are passed by the arguments. """ + # TODO: model and factor prepare; def inject_code(self, **files: str): """ @@ -112,12 +117,14 @@ class Experiment(ABC, Generic[ASpecificTask, ASpecificImp]): """ The experiment is a sequence of tasks and the implementations of the tasks after generated by the TaskGenerator. """ + result_ws: Optional[FBImplementation] def __init__(self, sub_tasks: Sequence[ASpecificTask]) -> None: self.sub_tasks = sub_tasks self.sub_implementations: Sequence[ASpecificImp] = [None for _ in self.sub_tasks] self.based_experiments: Sequence[Experiment] = [] self.result: object = None # The result of the experiment, can be different types in different scenarios. + self.result_ws = None TaskOrExperiment = TypeVar("TaskOrExperiment", Task, Experiment) diff --git a/rdagent/core/proposal.py b/rdagent/core/proposal.py index 93e20453..30370d21 100644 --- a/rdagent/core/proposal.py +++ b/rdagent/core/proposal.py @@ -92,9 +92,12 @@ class Hypothesis2Experiment(ABC, Generic[ASpecificExp]): class HypothesisExperiment2Feedback: """ "Generated feedbacks on the hypothesis from **Executed** Implementations of different tasks & their comparisons with previous performances""" - def generateFeedback(self, ti: Experiment, hypothesis: Hypothesis, trace: Trace) -> HypothesisFeedback: + def __init__(self, scen: Scenario): + self.scen = scen + + def generateFeedback(self, exp: Experiment, hypothesis: Hypothesis, trace: Trace) -> HypothesisFeedback: """ - The `ti` should be executed and the results should be included, as well as the comparison between previous results (done by LLM). + The `exp` should be executed and the results should be included, as well as the comparison between previous results (done by LLM). For example: `mlflow` of Qlib will be included. """ raise NotImplementedError("generateFeedback method is not implemented.") diff --git a/rdagent/scenarios/qlib/prompts.yaml b/rdagent/scenarios/qlib/prompts.yaml index a10ef598..7481c734 100644 --- a/rdagent/scenarios/qlib/prompts.yaml +++ b/rdagent/scenarios/qlib/prompts.yaml @@ -47,4 +47,36 @@ model_experiment_output_format: |- "model_type": "type of model 1, Tabular or TimesSeries" } # Don't add ellipsis (...) or any filler text that might cause JSON parsing errors here! - } \ No newline at end of file + } + +data_feedback_generation: + system: |- + You are a professional result analysis assistant on data driven R&D. + The task is described in the following scenario: + {{ scenario }} + You will receive a hypothesis, multiple tasks with their factors, and some results. + Your feedback should specify whether the current result supports or refutes the hypothesis, compare it with previous results, and suggest improvements or new directions. + Please provide detailed and constructive feedback for the future exploration. + Please respond in JSON format, and example JSON Structure for Result Analysis: + { + "Observations": "Your overall observations here", + "Feedback for Hypothesis": "Observations related to the hypothesis", + "New Hypothesis": "Put your new hypothesis here.", + "Reasoning": "Provide reasoning for the hypothesis here.", + "Replace Best Result": "yes or no" + } + user: |- + Target hypothesis: + {{hypothesis}} + Tasks and Factors: + {{task_details}} + Current Result: + {{current_result}} + SOTA Result: + {{sota_result}} + Analyze the current result in the context of its ability to: + 1. Support or refute the hypothesis. + 2. Show improvement or deterioration compared to the last experiment. + 3. Demonstrate positive or negative effects when compared to Alpha158. + + Provide detailed feedback and recommend whether to replace the best result if the new factor proves superior. diff --git a/rdagent/scenarios/qlib/task_generator/data.py b/rdagent/scenarios/qlib/task_generator/data.py index abc63a85..ab72dc08 100644 --- a/rdagent/scenarios/qlib/task_generator/data.py +++ b/rdagent/scenarios/qlib/task_generator/data.py @@ -1,6 +1,28 @@ +from pathlib import Path +import shutil +from typing import List +import pandas as pd +import pickle +from rdagent.app.qlib_rd_loop.conf import PROP_SETTING from rdagent.core.task_generator import TaskGenerator +from rdagent.utils.env import QTDockerEnv, LocalConf, LocalEnv from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment +from rdagent.core.log import RDAgentLog +DIRNAME = Path(__file__).absolute().resolve().parent +DIRNAME_local = Path.cwd() +logger = RDAgentLog() + +# class QlibFactorExpWorkspace: + +# def prepare(): +# # create a folder; +# # copy template +# # place data inside the folder `combined_factors` +# # +# def execute(): +# de = DockerEnv() +# de.run(local_path=self.ws_path, entry="qrun conf.yaml") class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): """ @@ -13,6 +35,183 @@ class QlibFactorRunner(TaskGenerator[QlibFactorExperiment]): - TODO: implement a qlib handler """ + + def FetchAlpha158ResultFromDocker(self): + """ + Run Docker to get alpha158 result. + + This method prepares the Qlib Docker environment, executes the necessary commands to + run the backtest, and fetches the results stored in a pickle file. + + Returns: + Any: The alpha158 result. If successful, returns a pandas DataFrame. Otherwise, returns None. + """ + # Initialize and prepare the Qlib Docker environment + qtde = QTDockerEnv() + qtde.prepare() + + # Clean up any previous run artifacts by deleting the mlruns directory + result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="rm -r mlruns", env={"PYTHONPATH": "./"}) + + # Run the Qlib backtest using the configuration file conf.yaml + result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="qrun conf.yaml", env={"PYTHONPATH": "./"}) + + # Execute a Python script to extract the experiment results + result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="python read_exp_res.py") + + pkl_path = DIRNAME / 'env_factor/qlib_res.pkl' + + if not pkl_path.exists(): + logger.error(f"File {pkl_path} does not exist.") + return None + + with open(pkl_path, 'rb') as f: + result = pickle.load(f) + + # Check if the loaded result is a pandas DataFrame and not empty + if isinstance(result, pd.DataFrame): + if not result.empty: + logger.info("Successfully retrieved alpha158 result.") + return result + else: + logger.error("Result DataFrame is empty.") + return None + else: + logger.error("Data format error.") + return None + def generate(self, exp: QlibFactorExperiment) -> QlibFactorExperiment: - return exp # TODO IMPLEMENT THIS + """ + Generate the experiment by processing and combining factor data, + then passing the combined data to Docker for backtest results. + """ + + SOTA_factor = self.process_factor_data(exp.based_experiments) + + if exp.based_experiments[-1].result is None: + exp.based_experiments[-1].result = self.FetchAlpha158ResultFromDocker() + + # Process the new factors data + new_factors = self.process_factor_data(exp) + + # Combine the SOTA factor and new factors if SOTA factor exists + if SOTA_factor is not None: + combined_factors = pd.concat([SOTA_factor, new_factors], axis=1).dropna() + else: + combined_factors = new_factors + + # Sort and nest the combined factors under 'feature' + combined_factors = combined_factors.sort_index() + new_columns = pd.MultiIndex.from_product([['feature'], combined_factors.columns]) + combined_factors.columns = new_columns + + # Save the combined factors to a pickle file + combined_factors_path = DIRNAME / 'env_factor/combined_factors_df.pkl' + with open(combined_factors_path, 'wb') as f: + pickle.dump(combined_factors, f) + + """ Docker run + # Call Docker, pass the combined factors to Docker, and generate backtest results + qtde = QTDockerEnv() + qtde.prepare() + + # Run the Docker command + result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="rm -r mlruns", env={"PYTHONPATH": "./"}) + # Run the Qlib backtest + result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="qrun conf_combined.yaml", env={"PYTHONPATH": "./"}) + + result = qtde.run(local_path=str(DIRNAME / "env_factor"), entry="python read_exp_res.py") + + pkl_path = DIRNAME / 'env_factor/qlib_res.pkl' + + if not pkl_path.exists(): + logger.error(f"File {pkl_path} does not exist.") + return None + + with open(pkl_path, 'rb') as f: + result = pickle.load(f) + """ + + # Local run + # Clean up any previous run artifacts by deleting the mlruns directory + mlruns_path = DIRNAME_local / 'mlruns' / '1' + if mlruns_path.exists() and mlruns_path.is_dir(): + shutil.rmtree(mlruns_path) + + # Prepare local Qlib environment + local_conf = LocalConf( + py_bin=PROP_SETTING.py_bin, + default_entry="qrun conf_combined.yaml", + ) + qle = LocalEnv(conf=local_conf) + qle.prepare() + conf_path = str(DIRNAME / "env_factor" / "conf_combined.yaml") + qle.run(entry="qrun " + conf_path) + + # Verify if the new folder is created + mlrun_p = DIRNAME_local / 'mlruns' / '1' + assert mlrun_p.exists(), f"Expected output file {mlrun_p} not found" + + # Locate the newly generated folder in mlruns/1/ + new_folders = [folder for folder in mlrun_p.iterdir() if folder.is_dir()] + if not new_folders: + raise FileNotFoundError("No new folders found in 'mlruns/1/'.") + + new_folder = new_folders[0] # Assuming there's only one new folder + pickle_file = new_folder / 'artifacts' / 'portfolio_analysis' / 'port_analysis_1day.pkl' + assert pickle_file.exists(), f"Expected pickle file {pickle_file} not found" + + with open(pickle_file, 'rb') as f: + result = pickle.load(f) + + exp.result = result + + # Check if the result is valid and is a DataFrame + if isinstance(result, pd.DataFrame): + if not result.empty: + logger.info("Successfully retrieved experiment result.") + return exp + else: + logger.error("Result DataFrame is empty.") + return None + else: + logger.error("Data format error.") + return None + + def process_factor_data(self, exp_or_list: List[QlibFactorExperiment] | QlibFactorExperiment) -> pd.DataFrame: + """ + Process and combine factor data from experiment implementations. + + Args: + exp (ASpecificExp): The experiment containing factor data. + + Returns: + pd.DataFrame: Combined factor data without NaN values. + """ + if isinstance(exp_or_list, QlibFactorExperiment): + exp_or_list = [exp_or_list] + factor_dfs = [] + + # Collect all exp's dataframes + for exp in exp_or_list: + # Iterate over sub-implementations and execute them to get each factor data + for implementation in exp.sub_implementations: + message, df = implementation.execute() + + # Check if factor generation was successful + if 'Execution succeeded without error.\nExpected output file found.' in message: + factor_dfs.append(df) + + # Combine all successful factor data + if factor_dfs: + combined_factors = pd.concat(factor_dfs, axis=1) + + # Remove rows with NaN values + combined_factors = combined_factors.dropna() + + # print(combined_factors) + return combined_factors + else: + logger.error("No valid factor data found to merge.") + return pd.DataFrame() # Return an empty DataFrame if no valid data diff --git a/rdagent/scenarios/qlib/task_generator/feedback.py b/rdagent/scenarios/qlib/task_generator/feedback.py index 0c6a09dd..fee41d26 100644 --- a/rdagent/scenarios/qlib/task_generator/feedback.py +++ b/rdagent/scenarios/qlib/task_generator/feedback.py @@ -1,10 +1,95 @@ # TODO: # Implement to feedback. +from pathlib import Path + +from jinja2 import Environment, StrictUndefined +from rdagent.core.prompts import Prompts from rdagent.core.proposal import HypothesisExperiment2Feedback +from rdagent.core.proposal import Trace +from rdagent.core.experiment import Experiment +from rdagent.core.proposal import Hypothesis, HypothesisFeedback +from rdagent.oai.llm_utils import APIBackend +from rdagent.utils.env import QTDockerEnv +from rdagent.core.log import RDAgentLog +import json +import pandas as pd +import pickle - -class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): ... - +feedback_prompts = Prompts(file_path=Path(__file__).parent.parent / "prompts.yaml") +DIRNAME = Path(__file__).absolute().resolve().parent +logger = RDAgentLog() class QlibModelHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): ... + +class QlibFactorHypothesisExperiment2Feedback(HypothesisExperiment2Feedback): + def generateFeedback(self, exp: Experiment, hypothesis: Hypothesis, trace: Trace) -> HypothesisFeedback: + """ + Generate feedback for the given experiment and hypothesis. + + Args: + exp (QlibFactorExperiment): The experiment to generate feedback for. + hypothesis (QlibFactorHypothesis): The hypothesis to generate feedback for. + trace (Trace): The trace of the experiment. + + Returns: + Any: The feedback generated for the given experiment and hypothesis. + """ + logger.info("Generating feedback...") + hypothesis_text = hypothesis.hypothesis + current_result = exp.result + tasks_factors = [task.get_factor_information() for task in exp.sub_tasks] + + sota_result = exp.based_experiments[-1].result + + # Generate the system prompt + sys_prompt = Environment(undefined=StrictUndefined).from_string(feedback_prompts["data_feedback_generation"]["system"]).render(scenario=self.scen.get_scenario_all_desc()) + + + # Prepare task details + task_details = "\n".join([f"Task: {factor_name}, Factor: {factor_description}" for factor_name, factor_description in tasks_factors]) + + # Generate the user prompt + usr_prompt = Environment(undefined=StrictUndefined).from_string(feedback_prompts["data_feedback_generation"]["user"]).format( + hypothesis_text=hypothesis_text, + task_details=task_details, + current_result=current_result, + sota_result=sota_result + ) + + # Call the APIBackend to generate the response for hypothesis feedback + response = APIBackend().build_messages_and_create_chat_completion( + user_prompt=usr_prompt, + system_prompt=sys_prompt, + json_mode=True, + ) + + # Parse the JSON response to extract the feedback + response_json = json.loads(response) + + # Extract fields from JSON response + observations = response_json.get("Observations", "No observations provided") + hypothesis_evaluation = response_json.get("Feedback for Hypothesis", "No feedback provided") + new_hypothesis = response_json.get("New Hypothesis", "No new hypothesis provided") + reason = response_json.get("Reasoning", "No reasoning provided") + decision = response_json.get("Replace Best Result", "no").lower() == "yes" + + # Create HypothesisFeedback object + hypothesis_feedback = HypothesisFeedback( + observations=observations, + hypothesis_evaluation=hypothesis_evaluation, + new_hypothesis=new_hypothesis, + reason=reason, + decision=decision + ) + + logger.info( + "Generated Hypothesis Feedback:\n" + f"Observations: {observations}\n" + f"Feedback for Hypothesis: {hypothesis_evaluation}\n" + f"New Hypothesis: {new_hypothesis}\n" + f"Reason: {reason}\n" + f"Replace Best Result: {'Yes' if decision else 'No'}" + ) + + return hypothesis_feedback diff --git a/test/utils/README.md b/test/utils/README.md new file mode 100644 index 00000000..23a36a14 --- /dev/null +++ b/test/utils/README.md @@ -0,0 +1,116 @@ +# 🐳 Run Docker & Qlib +--- + +## 📄 Description +This guide explains how to run the Qlib Docker test file located at `test/utils/test_env.py` in the RD-Agent repository. + +--- + +## 🚀 Running Instructions + +### 1. Install the required Python libraries +- Ensure that the `docker` Python library is installed: + ```sh + pip install docker + ``` + +### 2. Run the test script +- Execute the test script to verify the Docker environment setup: + ```sh + python test/utils/test_env.py + ``` + +### Troubleshooting +- **PermissionError: [Errno 13] Permission denied.** + > This error occurs when the current user does not have the necessary permissions to access the Docker socket. To resolve this issue, follow these steps: + +1. **Add the current user to the `docker` group** +Docker requires root or `docker` group user permissions to access the Docker socket. Add the current user to the `docker` group: + ```sh + sudo usermod -aG docker $USER + ``` + +2. **Refresh group changes** +To apply the group changes, log out and log back in, or use the following command: + ```sh + newgrp docker + ``` + +3. **Verify Docker access** +Run the following command to ensure that Docker can be accessed: + ```sh + docker run hello-world + ``` + +4. **Rerun the test script** + After completing these steps, rerun the test script: + ```sh + python test/utils/test_env.py + ``` +--- +## 🛠️ Detailed Qlib Docker Function Framework + +Here, we provide an overview of the specific functions within the Qlib Docker framework, their purposes, and examples of how to call them. + +### QTDockerEnv Class in `env.py` + +The `QTDockerEnv` class is responsible for setting up and running Docker environments for Qlib experiments. + +#### Methods: + +1. **prepare()** + - **Purpose**: Prepares the Docker environment for running experiments. This includes building the Docker image if necessary. + - **Example**: + ```python + qtde = QTDockerEnv() + qtde.prepare() + ``` + +2. **run(local_path: str, entry: str) -> str** + - **Purpose**: Runs a specified entry point (e.g., a configuration file) in the prepared Docker environment. + - **Parameters**: + - `local_path`: Path to the local directory to mount into the Docker container. + - `entry`: Command or entry point to run inside the Docker container. + - **Returns**: The stdout output from the Docker container. + - **Example**: + ```python + result = qtde.run(local_path="/path/to/env_tpl", entry="qrun conf.yaml") + ``` +--- +### 📊 Expected Output + +Upon successful execution, the test script will produce analysis results of benchmark returns and various risk metrics. The expected output should be similar to: + +``` +'The following are analysis results of benchmark return (1 day).' +risk +mean 0.000477 +std 0.012295 +annualized_return 0.113561 +information_ratio 0.598699 +max_drawdown -0.370479 + +'The following are analysis results of the excess return without cost (1 day).' +risk +mean 0.000530 +std 0.005718 +annualized_return 0.126029 +information_ratio 1.428574 +max_drawdown -0.072310 + +'The following are analysis results of the excess return with cost (1 day).' +risk +mean 0.000339 +std 0.005717 +annualized_return 0.080654 +information_ratio 0.914486 +max_drawdown -0.086083 + +'The following are analysis results of indicators (1 day).' +value +ffr 1.0 +pa 0.0 +pos 0.0 +``` + +By following these steps and using the provided functions, you should be able to run the Qlib Docker tests and obtain the expected analysis results. \ No newline at end of file diff --git a/test/utils/test_env.py b/test/utils/test_env.py index 2f41764f..f108d320 100644 --- a/test/utils/test_env.py +++ b/test/utils/test_env.py @@ -23,18 +23,17 @@ class EnvUtils(unittest.TestCase): # NOTE: Since I don't know the exact environment in which it will be used, here's just an example. # NOTE: Because you need to download the data during the prepare process. So you need to have pyqlib in your environment. - # def test_local(self): - # local_conf = LocalConf( - # py_bin="/home/v-linlanglv/miniconda3/envs/RD-Agent-310/bin", - # default_entry="qrun conf.yaml", - # ) - # qle = LocalEnv(conf=local_conf) - # qle.prepare() - # exe_path = str(DIRNAME / "env_tpl") - # conf_path = str(DIRNAME / "env_tpl" / "conf.yaml") - # qle.run(entry="qrun " + conf_path, local_path=exe_path) - # mlrun_p = DIRNAME / "env_tpl" / "mlruns" - # self.assertTrue(mlrun_p.exists(), f"Expected output file {mlrun_p} not found") + def test_local(self): + local_conf = LocalConf( + py_bin="/home/v-linlanglv/miniconda3/envs/RD-Agent-310/bin", + default_entry="qrun conf.yaml", + ) + qle = LocalEnv(conf=local_conf) + qle.prepare() + conf_path = str(DIRNAME / "env_tpl" / "conf.yaml") + qle.run(entry="qrun " + conf_path) + mlrun_p = DIRNAME / "env_tpl" / "mlruns" + self.assertTrue(mlrun_p.exists(), f"Expected output file {mlrun_p} not found") def test_docker(self): """ @@ -51,7 +50,8 @@ class EnvUtils(unittest.TestCase): self.assertTrue(mlrun_p.exists(), f"Expected output file {mlrun_p} not found") # read experiment - result = qtde.run(local_path=str(DIRNAME / "env_tpl"), entry="python read_exp.py") + result = qtde.run(local_path=str(DIRNAME / "env_tpl"), entry="python read_exp_res.py") + print("here") print(result) diff --git a/test/utils/test_env2.py b/test/utils/test_env2.py new file mode 100644 index 00000000..543e89a0 --- /dev/null +++ b/test/utils/test_env2.py @@ -0,0 +1,38 @@ +import os +import sys +import unittest +from pathlib import Path +sys.path.append(str(Path(__file__).resolve().parent.parent)) +from rdagent.utils.env import QTDockerEnv, LocalEnv, LocalConf +import shutil + + +DIRNAME = Path(__file__).absolute().resolve().parent + + +class EnvUtils(unittest.TestCase): + def setUp(self): + pass + + def test_docker(self): + """ + We will mount `env_tpl` into the docker image. + And run the docker image with `qrun conf.yaml` + """ + qtde = QTDockerEnv() + qtde.prepare() + qtde.prepare() # you can prepare for multiple times. It is expected to handle it correctly + # the stdout are returned as result + result = qtde.run(local_path=str(DIRNAME / "env_tpl"), entry="qrun conf2.yaml") + + mlrun_p = DIRNAME / "env_tpl" / "mlruns" + self.assertTrue(mlrun_p.exists(), f"Expected output file {mlrun_p} not found") + + # read experiment + result = qtde.run(local_path=str(DIRNAME / "env_tpl"), entry="python read_exp_res.py") + print("here") + # print(result) + + +if __name__ == "__main__": + unittest.main()