diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index e9ef4e20..83c51b9a 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -14,7 +14,7 @@ permissions: jobs: build: - if: ${{ github.repository == 'slaclab/Badger' }} + if: ${{ github.repository == 'YektaY/Badger' }} runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -35,6 +35,10 @@ jobs: miniforge-version: latest activate-environment: badger-env environment-file: environment-dev.yml + - name: Install xopt + shell: bash -el {0} + run: | + pip install --user xopt - name: Install python packages shell: bash -el {0} run: | @@ -42,7 +46,8 @@ jobs: mamba install flake8 zipp mamba install --file requirements.txt --file windows-dev-requirements.txt else - mamba install flake8 zipp $(cat requirements.txt dev-requirements.txt) + mamba install flake8 zipp + mamba install --file requirements.txt fi - name: Install pyqt5 shell: bash -el {0} @@ -58,7 +63,12 @@ jobs: sudo /sbin/start-stop-daemon --start --pidfile /tmp/custom_herbstluftwm_99.pid --make-pidfile --background --exec /usr/bin/herbstluftwm sleep 1 fi + - name: Install Badger + shell: bash -el {0} + run: | + pip install . - name: Test with pytest shell: bash -el {0} run: | - python run_tests.py + python run_tests.py + diff --git a/requirements.txt b/requirements.txt index d1b0e161..11cabb68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,6 @@ qdarkstyle pillow requests tqdm -xopt>=2.0.0 pytest -pytest-qt \ No newline at end of file +pytest-qt +pytest-mock \ No newline at end of file diff --git a/src/badger/core.py b/src/badger/core.py index 55805aa2..e079e076 100644 --- a/src/badger/core.py +++ b/src/badger/core.py @@ -164,3 +164,5 @@ def run_routine( except Exception as e: opt_logger.update(Events.OPTIMIZATION_END, solution_meta) raise e + + diff --git a/src/badger/core_subprocess.py b/src/badger/core_subprocess.py new file mode 100644 index 00000000..75d39e60 --- /dev/null +++ b/src/badger/core_subprocess.py @@ -0,0 +1,217 @@ +import typing +import time +from pandas import concat, DataFrame +import logging +from badger.errors import ( + BadgerRunTerminatedError, +) +from badger.routine import Routine +from badger.logger import _get_default_logger +from badger.logger.event import Events +from badger.utils import ( + curr_ts_to_str, + dump_state, +) +#from db import list_routine, load_routine, remove_routine, get_runs_by_routine, get_runs +from multiprocessing import Queue, Process, Event + +''' +def build_routine(routine_data): + routine, timestamp = load_routine(routine_data) + return routine +''' + +def check_run_status(self, routine, stop_process, pause_process, termination_condition = None): + """ + check for termination condition + + - checks for internal triggers (max eval, max time) and external triggers + + """ + # Check if termination condition has been satisfied + if termination_condition: + tc_config = termination_condition + idx = tc_config['tc_idx'] + if idx == 0: + max_eval = tc_config['max_eval'] + if len(routine.data) >= max_eval: + stop_process.is_set() + + elif idx == 1: + max_time = tc_config['max_time'] + dt = time.time() - self.start_time # need to pipe time? + if dt >= max_time: + stop_process.is_set() + + # External triggers + if stop_process.is_set(): + raise BadgerRunTerminatedError + elif pause_process.is_set(): + pause_process.wait() + else: + return 0 # continue to run + +def convert_to_solution(result: DataFrame, routine: Routine): + vocs = routine.vocs + try: + best_idx, _ = vocs.select_best(routine.sorted_data, n=1) + if best_idx != len(routine.data) - 1: + is_optimal = False + else: + is_optimal = True + except NotImplementedError: + is_optimal = False # disable the optimal highlight for MO problems + + vars = list(result[vocs.variable_names].to_numpy()[0]) + objs = list(result[vocs.objective_names].to_numpy()[0]) + cons = list(result[vocs.constraint_names].to_numpy()[0]) + stas = list(result[vocs.observable_names].to_numpy()[0]) + + solution = (vars, objs, cons, stas, is_optimal, + vocs.variable_names, + vocs.objective_names, + vocs.constraint_names, + vocs.observable_names) + + return solution + +def run_routine_subprocess(queue, evaluate_queue, stop_process, pause_process) -> None: + """ + Run the provided routine object using Xopt. This method is run as a subproccess + + Parameters + ---------- + queue : + + stop_process : + + pause_process : + """ + #logger = logging.getLogger() + #handler = logging.FileHandler('subprocess.log') + #formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + #handler.setFormatter(formatter) + #logger.addHandler(handler) + #logger.setLevel(logging.DEBUG) + + try: + args = queue.get(timeout=1) + except Exception as e: + print(f"Error in subprocess: {type(e).__name__}, {str(e)}") + + # set required arguments + routine = args['routine'] + print(type(routine)) + #logger.info(f"type {type(routine)}") + #logger.info(f"data {routine}") + + # set optional arguments + try: + evaluate = args['evaluate'] + except KeyError: + evaluate = None + + try: + save_states = args['save_states'] + except KeyError: + save_states = None + + try: + dump_file_callback = args['dump_file_callback'] + except KeyError: + dump_file_callback = None + + try: + verbose = args['verbose'] + except KeyError: + verbose = 2 + + try: + termination_condition = args['termination_condition'] + except KeyError: + termination_condition = None + + environment = routine.environment + initial_points = routine.initial_points + + # Log the optimization progress in terminal + opt_logger = _get_default_logger(verbose) + + # Save system states if applicable + states = environment.get_system_states() + if save_states and (states is not None): + queue.put(states) # might need to change queue here + + # Optimization starts + print('') + solution_meta = (None, None, None, None, None, + routine.vocs.variable_names, + routine.vocs.objective_names, + routine.vocs.constraint_names, + routine.vocs.observable_names) + opt_logger.update(Events.OPTIMIZATION_START, solution_meta) + + # evaluate initial points: + # Nikita: more care about the setting var logic, + # wait or consider timeout/retry + # TODO: need to evaluate a single point at the time + + print("reached here") + + for _, ele in initial_points.iterrows(): + result = routine.evaluate_data(ele.to_dict()) + solution = convert_to_solution(result, routine) + opt_logger.update(Events.OPTIMIZATION_STEP, solution) + if evaluate: + queue.put(result) + + # Prepare for dumping file + if dump_file_callback: + combined_results = None + ts_start = curr_ts_to_str() + dump_file = dump_file_callback() + if not dump_file: + dump_file = f"xopt_states_{ts_start}.yaml" + + print("reached optimization") + # perform optimization + try: + while True: + + if stop_process.is_set(): + raise BadgerRunTerminatedError + elif pause_process.is_set(): + pause_process.wait() + + # generate points to observe + candidates = routine.generator.generate(1)[0] + candidates = DataFrame(candidates, index=[0]) + + # generate_callback(generator, candidates) + # generate_callback(candidates) + + if stop_process.is_set(): + raise BadgerRunTerminatedError + elif pause_process.is_set(): + pause_process.wait() + + # if still active evaluate the points and add to generator + # check active_callback evaluate point + result = routine.evaluate_data(candidates) + solution = convert_to_solution(result, routine) + opt_logger.update(Events.OPTIMIZATION_STEP, solution) + if evaluate: + queue.put(result) + + # Dump Xopt state after each step + if dump_file_callback: + if combined_results is not None: + combined_results = concat([combined_results, result], + axis=0).reset_index(drop=True) + else: + combined_results = result + + dump_state(dump_file, routine.generator, combined_results) + except Exception as e: + opt_logger.update(Events.OPTIMIZATION_END, solution_meta) + raise e diff --git a/src/badger/db.py b/src/badger/db.py index b0613ea2..48d46e95 100644 --- a/src/badger/db.py +++ b/src/badger/db.py @@ -244,6 +244,9 @@ def get_runs_by_routine(routine: str): @maybe_create_runs_db def get_runs(): + """ + FINE + """ db_run = os.path.join(BADGER_DB_ROOT, 'runs.db') con = sqlite3.connect(db_run) @@ -261,6 +264,9 @@ def get_runs(): @maybe_create_runs_db def remove_run_by_filename(name): + """ + FINE + """ db_run = os.path.join(BADGER_DB_ROOT, 'runs.db') con = sqlite3.connect(db_run) @@ -274,6 +280,9 @@ def remove_run_by_filename(name): @maybe_create_runs_db def remove_run_by_id(rid): + """ + FINE + """ db_run = os.path.join(BADGER_DB_ROOT, 'runs.db') con = sqlite3.connect(db_run) @@ -286,6 +295,9 @@ def remove_run_by_id(rid): def import_routines(filename): + """ + FINE + """ con = sqlite3.connect(filename) cur = con.cursor() @@ -316,6 +328,9 @@ def import_routines(filename): def export_routines(filename, routine_name_list): + """ + FINE + """ con = sqlite3.connect(filename) cur = con.cursor() diff --git a/src/badger/factory.py b/src/badger/factory.py index b1250232..7af01e15 100644 --- a/src/badger/factory.py +++ b/src/badger/factory.py @@ -10,7 +10,7 @@ import os import importlib import yaml -from xopt.generators import generators, get_generator, try_load_all_generators +from xopt.generators import generators, get_generator import logging logger = logging.getLogger(__name__) @@ -189,7 +189,12 @@ def get_env(name): def list_generators(): - try_load_all_generators() + try: + from xopt.generators import try_load_all_generators + + try_load_all_generators() + except ImportError: # this API changed somehow + pass # there is nothing we can do... generator_names = list(generators.keys()) # Filter the names generator_names = [n for n in generator_names if n not in ALGO_EXCLUDED] diff --git a/src/badger/gui/default/components/routine_runner.py b/src/badger/gui/default/components/routine_runner.py index eea7f998..1f0a15cb 100644 --- a/src/badger/gui/default/components/routine_runner.py +++ b/src/badger/gui/default/components/routine_runner.py @@ -1,13 +1,15 @@ import logging - logger = logging.getLogger(__name__) + import time +from multiprocessing import Queue, Process, Event, Manager, Pipe from pandas import DataFrame -from PyQt5.QtCore import pyqtSignal, QObject, QRunnable +from PyQt5.QtCore import pyqtSignal, QObject, QRunnable, QTimer from ....core import run_routine, Routine +from ....core_subprocess import run_routine_subprocess +import yaml from ....errors import BadgerRunTerminatedError - class BadgerRoutineSignals(QObject): env_ready = pyqtSignal(list) finished = pyqtSignal() @@ -156,3 +158,125 @@ def ctrl_routine(self, pause): def stop_routine(self): self.is_killed = True + + +class BadgerRoutineSubprocess(): + """ + launches suprocess to run routine using code in core.py + """ + + def __init__(self, routine: Routine, save: bool, verbose=2, use_full_ts=False): + """ + Parameters + ---------- + routine: Routine + Defined routine for runner + + save: bool + Flag to enable saving to database + + verbose: int, default: 2 + Verbostiy level (higher is more output) + + use_full_ts: bool + If true use full time stamp info when dumping to database + """ + super().__init__() + + # Signals should belong to instance rather than class + # Since there could be multiple runners running in parallel + self.signals = BadgerRoutineSignals() + + self.routine = routine + self.run_filename = None + self.states = None # system states to be saved at start of a run + self.save = save + self.verbose = verbose + self.use_full_ts = use_full_ts + self.termination_condition = None # additional option to control the optimization flow + self.start_time = None # track the time cost of the run + self.last_dump_time = None # track the time the run data got dumped + + self.data_queue = None + self.stop_event = None + self.pause_event = None + self.routine_process = None + self.is_killed = False + + def set_termination_condition(self, termination_condition): + self.termination_condition = termination_condition + + def run(self) -> None: + self.start_time = time.time() + self.last_dump_time = None # reset the timer + + try: + self.save_init_vars() + self.stop_event = Event() + self.pause_event = Event() + self.data_queue = Queue() + self.evaluate_queue = Queue() + + arg_dict = { + 'routine': self.routine, + 'termination_condition': self.termination_condition} + + self.routine_process = Process(target=run_routine_subprocess, + args=(self.data_queue, self.evaluate_queue, self.stop_event, self.pause_event,)) + self.routine_process.start() + + print("about to") + self.data_queue.put(arg_dict) + print("done") + + self.setup_timer() + #self.routine.data = None # reset data + + except BadgerRunTerminatedError as e: + self.signals.finished.emit() + self.signals.info.emit(str(e)) + except Exception as e: + print(e) + self.signals.finished.emit() + self.signals.error.emit(e) + + + def setup_timer(self): + self.timer = QTimer() + self.timer.timeout.connect(self.check_queue) + + def check_queue(self): + if not self.evaluate_queue.empty(): + results = self.evaluate_queue.get() + self.after_evaluate() + + def after_evaluate(self): + self.signals.progress.emit() + time.sleep(0.1) + + def save_init_vars(self): + var_names = self.routine.vocs.variable_names + var_dict = self.routine.environment._get_variables(var_names) + init_vars = list(var_dict.values()) + self.signals.env_ready.emit(init_vars) + + def stop_routine(self): + self.stop_event.set() + self.routine_process.join(timeout=2) # hmm 0.7 seconds + + if self.routine_process.is_alive(): + print('hard stop') + self.routine_process.terminate() + + self.timer.stop() + print('Killed') + self.is_killed = True + + def ctrl_routine(self, pause): + if pause: + self.pause_event.set() + else: + self.pause_event.clear() + + + diff --git a/src/badger/gui/default/components/run_monitor.py b/src/badger/gui/default/components/run_monitor.py index ff8082aa..6355a114 100644 --- a/src/badger/gui/default/components/run_monitor.py +++ b/src/badger/gui/default/components/run_monitor.py @@ -12,7 +12,7 @@ from xopt import VOCS from .extensions_palette import ExtensionsPalette -from .routine_runner import BadgerRoutineRunner +from .routine_runner import BadgerRoutineRunner, BadgerRoutineSubprocess from ..utils import create_button from ..windows.terminition_condition_dialog import BadgerTerminationConditionDialog from ....routine import Routine @@ -20,6 +20,8 @@ from ....logbook import send_to_logbook, BADGER_LOGBOOK_ROOT from ....archive import archive_run, BADGER_ARCHIVE_ROOT +from multiprocessing import shared_memory + # disable chained assignment warning from pydantic pd.options.mode.chained_assignment = None # default='warn' @@ -541,9 +543,14 @@ def _configure_plot(self, plot_object, inspector, names): def init_routine_runner(self): self.reset_routine_runner() - self.routine_runner = routine_runner = BadgerRoutineRunner( - self.routine, False - ) + test = True + + if test: + print(self.routine) + self.routine_runner = routine_runner = BadgerRoutineSubprocess(self.routine, False) + else: + self.routine_runner = routine_runner = BadgerRoutineRunner(self.routine, False) + routine_runner.signals.env_ready.connect(self.env_ready) routine_runner.signals.finished.connect(self.routine_finished) routine_runner.signals.progress.connect(self.update) @@ -562,12 +569,13 @@ def reset_routine_runner(self): def start(self, use_termination_condition=False): self.sig_new_run.emit() self.init_plots(self.routine) + print(type(self.routine), "routine type", self.routine) self.init_routine_runner() if use_termination_condition: self.routine_runner.set_termination_condition(self.termination_condition) self.running = True # if a routine runner is working - self.thread_pool.start(self.routine_runner) - + #self.thread_pool.start(self.routine_runner) + self.routine_runner.run() self.btn_stop.setStyleSheet(stylesheet_stop) self.btn_stop.setPopupMode(QToolButton.DelayedPopup) self.run_action.setText('Stop') @@ -994,6 +1002,7 @@ def delete_run(self): self.sig_del.emit() def set_run_action(self): + print("i am here!") if self.btn_stop.defaultAction() is not self.run_action: self.btn_stop.setDefaultAction(self.run_action) diff --git a/src/badger/gui/default/pages/home_page.py b/src/badger/gui/default/pages/home_page.py index 4ca67a9b..3a5c235c 100644 --- a/src/badger/gui/default/pages/home_page.py +++ b/src/badger/gui/default/pages/home_page.py @@ -21,7 +21,7 @@ from ....archive import load_run, delete_run from ....utils import get_header, strtobool from ....settings import read_value - +from typing import List stylesheet = ''' QPushButton:hover:pressed @@ -39,6 +39,9 @@ ''' class BadgerHomePage(QWidget): + """ + The BadgerHomePage class is for initalizing the UI elements in the home page. + """ sig_routine_activated = pyqtSignal(bool) def __init__(self): @@ -54,6 +57,9 @@ def __init__(self): self.load_all_runs() def init_ui(self): + """ + Initalizes the UI elements. + """ icon_ref = resources.files(__package__) / '../images/add.png' with resources.as_file(icon_ref) as icon_path: self.icon_add = QIcon(str(icon_path)) @@ -64,17 +70,12 @@ def init_ui(self): with resources.as_file(icon_ref) as icon_path: self.icon_export = QIcon(str(icon_path)) - # cool_font = QFont() - # cool_font.setWeight(QFont.DemiBold) - # cool_font.setPixelSize(13) - # Set up the layout vbox = QVBoxLayout(self) vbox.setContentsMargins(0, 0, 0, 0) splitter = QSplitter(Qt.Horizontal) splitter.setStretchFactor(0, 0) splitter.setStretchFactor(1, 1) - # splitter.setSizes([100, 200]) vbox.addWidget(splitter, 1) # Routine panel @@ -86,7 +87,6 @@ def init_ui(self): panel_search = QWidget() hbox_search = QHBoxLayout(panel_search) hbox_search.setContentsMargins(0, 0, 0, 0) - # hbox_search.setSpacing(8) self.sbar = sbar = search_bar() sbar.setFixedHeight(36) @@ -98,7 +98,6 @@ def init_ui(self): btn_new.setIcon(self.icon_add) btn_new.setToolTip('Create new routine') hbox_search.addWidget(sbar) - # hbox_search.addSpacing(4) hbox_search.addWidget(btn_new) vbox_routine.addWidget(panel_search) @@ -205,6 +204,9 @@ def init_ui(self): vbox_view.addWidget(status_bar) def config_logic(self): + """ + + """ self.colors = ['c', 'g', 'm', 'y', 'b', 'r', 'w'] self.symbols = ['o', 't', 't1', 's', 'p', 'h', 'd'] @@ -243,13 +245,22 @@ def config_logic(self): self.btn_import.clicked.connect(self.import_routines) def go_search(self): + """ + FINE + """ self.sbar.setFocus() def load_all_runs(self): + """ + FINE + """ runs = get_runs() self.cb_history.updateItems(runs) def create_new_routine(self): + """ + Fine + """ self.splitter_state = self.splitter_run.saveState() self.routine_editor.set_routine(None) self.tab_state = self.tabs.currentIndex() @@ -259,6 +270,9 @@ def create_new_routine(self): self.toggle_lock(True, 0) def select_routine(self, routine_item: QListWidgetItem): + """ + TODO: CHANGE + """ if self.prev_routine_item: try: self.routine_list.itemWidget(self.prev_routine_item).deactivate() @@ -289,12 +303,13 @@ def select_routine(self, routine_item: QListWidgetItem): if not runs: # auto plot will not be triggered self.run_monitor.init_plots(routine) - self.routine_list.itemWidget(routine_item).activate() - def build_routine_list(self, - routines: list[str], - timestamps: list[str], - descriptions: list[str]): + routines: List[str], + timestamps: List[str], + descriptions: List[str]): + """ + FINE? + """ try: selected_routine = self.prev_routine_item.routine_name except Exception: @@ -314,6 +329,9 @@ def build_routine_list(self, self.prev_routine_item = item def get_current_routines(self): + """ + FINE + """ keyword = self.sbar.text() tag_obj = self.filter_box.cb_obj.currentText() tag_reg = self.filter_box.cb_reg.currentText() @@ -330,11 +348,17 @@ def get_current_routines(self): return routines, timestamps, descriptions def refresh_routine_list(self): + """ + Fine + """ routines, timestamps, descriptions = self.get_current_routines() self.build_routine_list(routines, timestamps, descriptions) def go_run(self, i: int): + """ + TODO: NEEDS CHANGING + """ if self.cb_history.itemText(0) == 'Optimization in progress...': return # if self.cb_history.currentText() == 'Optimization in progress...': @@ -367,18 +391,33 @@ def go_run(self, i: int): self.status_bar.set_summary(f'current routine: {self.current_routine.name}') def go_prev_run(self): + """ + Fine + """ self.cb_history.selectPreviousItem() def go_next_run(self): + """ + Fine + """ self.cb_history.selectNextItem() def inspect_solution(self, idx): + """ + Fine + """ self.run_table.selectRow(idx) - def solution_selected(self, r, c): + def solution_selected(self, r): + """ + Fine + """ self.run_monitor.jump_to_solution(r) def table_selection_changed(self): + """ + Fine + """ indices = self.run_table.selectedIndexes() if len(indices) == 1: # let other method handles it return @@ -398,6 +437,9 @@ def table_selection_changed(self): self.run_monitor.jump_to_solution(row) def toggle_lock(self, lock, lock_tab=1): + """ + Fine + """ if lock: self.panel_routine.setDisabled(True) self.history_nav_bar.setDisabled(True) @@ -409,6 +451,9 @@ def toggle_lock(self, lock, lock_tab=1): self.tabs.setTabEnabled(1, True) def new_run(self): + """ + Fine + """ self.cb_history.insertItem(0, 'Optimization in progress...') self.cb_history.setCurrentIndex(0) @@ -416,6 +461,9 @@ def new_run(self): reset_table(self.run_table, header) def run_name(self, name): + """ + TODO: Might need changing + """ if self.prev_routine_item: runs = get_runs_by_routine(self.current_routine.name) else: @@ -423,6 +471,9 @@ def run_name(self, name): self.cb_history.updateItems(runs) def progress(self, solution: DataFrame): + """ + TODO: NEEDS CHANGING + """ vocs = self.current_routine.vocs vars = list(solution[vocs.variable_names].to_numpy()[0]) objs = list(solution[vocs.objective_names].to_numpy()[0]) @@ -488,6 +539,9 @@ def routine_deleted(self, name=None): self.refresh_routine_list() def routine_description_updated(self, name, descr): + """ + FINE + """ for i in range(self.routine_list.count()): item = self.routine_list.item(i) if item is not None: @@ -497,6 +551,9 @@ def routine_description_updated(self, name, descr): break def export_routines(self): + """ + Fine + """ options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog filename, _ = QFileDialog.getSaveFileName(self, 'Export Badger routines', '', 'Database Files (*.db)', options=options) @@ -518,6 +575,9 @@ def export_routines(self): QMessageBox.critical(self, 'Export failed!', f'Export failed: {str(e)}') def import_routines(self): + """ + FINE + """ options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog filename, _ = QFileDialog.getOpenFileName(self, 'Import Badger routines', diff --git a/src/badger/routine.py b/src/badger/routine.py index 76d1ffa9..d6816734 100644 --- a/src/badger/routine.py +++ b/src/badger/routine.py @@ -1,3 +1,4 @@ + import json from copy import deepcopy from typing import Optional, List, Any @@ -6,16 +7,12 @@ from pandas import DataFrame from pydantic import ConfigDict, Field, model_validator, field_validator, \ ValidationInfo, SerializeAsAny -from xopt import Xopt, VOCS, Evaluator -from xopt.generators import get_generator - -from badger.environment import Environment, instantiate_env -from badger.factory import get_env +from xopt import Xopt, Evaluator from badger.utils import curr_ts +from badger.environment import Environment, instantiate_env class Routine(Xopt): - name: str description: Optional[str] = Field(None) environment: SerializeAsAny[Environment] @@ -26,68 +23,58 @@ class Routine(Xopt): model_config = ConfigDict(arbitrary_types_allowed=True) + @model_validator(mode="before") @classmethod def validate_model(cls, data: Any): - if isinstance(data, dict): - # validate vocs - if isinstance(data["vocs"], dict): - data["vocs"] = VOCS(**data["vocs"]) - - # validate generator - if isinstance(data["generator"], dict): - name = data["generator"].pop("name") - generator_class = get_generator(name) - data["generator"] = generator_class.model_validate( - {**data["generator"], "vocs": data["vocs"]} - ) - elif isinstance(data["generator"], str): - generator_class = get_generator(data["generator"]) - - data["generator"] = generator_class.model_validate( - {"vocs": data["vocs"]} - ) - - # validate data (if it exists - if "data" in data: - if isinstance(data["data"], dict): - try: - data["data"] = pd.DataFrame(data["data"]) - except IndexError: - data["data"] = pd.DataFrame(data["data"], index=[0]) - - data["generator"].add_data(data["data"]) - - # instantiate env - if isinstance(data["environment"], dict): - # TODO: Actually we need this interface info, but - # should be put somewhere else (in parallel with env?) + from badger.factory import get_env + + if not isinstance(data, dict): + return data + + # validate vocs + data = super().validate_model(data) + + # validate data (if it exists + if "data" in data: + if isinstance(data["data"], dict): try: - del data["environment"]["interface"] - except KeyError: # no interface at all, which is good - pass - name = data["environment"].pop("name") - env_class, configs_env = get_env(name) - configs_env["params"] |= data["environment"] - data["environment"] = instantiate_env(env_class, configs_env) - else: # should be an instantiated env already - pass + data["data"] = pd.DataFrame(data["data"]) + except IndexError: + data["data"] = pd.DataFrame(data["data"], index=[0]) + + data["generator"].add_data(data["data"]) - # create evaluator - env = data["environment"] + # instantiate env + if isinstance(data["environment"], dict): + # TODO: Actually we need this interface info, but + # should be put somewhere else (in parallel with env?) + try: + del data["environment"]["interface"] + except KeyError: # no interface at all, which is good + pass + name = data["environment"].pop("name") + env_class, configs_env = get_env(name) + configs_env["params"] |= data["environment"] + data["environment"] = instantiate_env(env_class, configs_env) + else: # should be an instantiated env already + pass + + # create evaluator + env = data["environment"] - def evaluate_point(point: dict): - # sanitize inputs - point = pd.Series(point).explode().to_dict() - env._set_variables(point) - obs = env._get_observables(data["vocs"].output_names) + def evaluate_point(point: dict): + # sanitize inputs + point = pd.Series(point).explode().to_dict() + env._set_variables(point) + obs = env._get_observables(data["vocs"].output_names) - ts = curr_ts() - obs['timestamp'] = ts.timestamp() + ts = curr_ts() + obs['timestamp'] = ts.timestamp() - return obs + return obs - data["evaluator"] = Evaluator(function=evaluate_point) + data["evaluator"] = Evaluator(function=evaluate_point) return data @@ -139,3 +126,12 @@ def json(self, **kwargs) -> str: pass return json.dumps(dict_result) + + def __getstate__(self): + return self.name + + def __setstate__(self, data): + from .db import load_routine + print("called set", data) + routine,_ = load_routine(data) + self.__dict__.update(routine) diff --git a/src/badger/tests/test_cli_basic.py b/src/badger/tests/test_cli_basic.py index 2201d007..ec82c078 100644 --- a/src/badger/tests/test_cli_basic.py +++ b/src/badger/tests/test_cli_basic.py @@ -18,16 +18,19 @@ def test_cli_main(): assert exitcode == 0 # Check output lines - outlines = out.split('\n') - assert len(outlines) == 8 + outlines = out.splitlines() + assert len(outlines) == 7 # Check name assert outlines[0] == 'name: Badger the optimizer' # Check version version = metadata.version('badger-opt') - assert outlines[1] == f'version: {version}' - + try: # yaml encoding number-like string differently + _ = float(version) + assert outlines[1] == f"version: '{version}'" + except ValueError: + assert outlines[1] == f'version: {version}' def test_list_algo(): command = ['badger', 'generator'] @@ -36,7 +39,7 @@ def test_list_algo(): assert exitcode == 0 # Check output lines - outlines = out.split('\n') + outlines = out.splitlines() assert '- upper_confidence_bound' in outlines diff --git a/src/badger/tests/test_run_monitor.py b/src/badger/tests/test_run_monitor.py index e4a5b252..27b5de52 100644 --- a/src/badger/tests/test_run_monitor.py +++ b/src/badger/tests/test_run_monitor.py @@ -2,10 +2,10 @@ import numpy as np from unittest.mock import patch -from PyQt5.QtCore import QPointF, Qt +from PyQt5.QtCore import QPointF, Qt, QPoint from PyQt5.QtGui import QMouseEvent from PyQt5.QtTest import QSignalSpy, QTest -from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtWidgets import QMessageBox, QMainWindow def create_test_run_monitor(add_data=True): @@ -64,7 +64,7 @@ def test_run_monitor(qtbot): spy = QSignalSpy(monitor.routine_runner.signals.progress) assert spy.isValid() QTest.mouseClick(monitor.btn_stop, Qt.MouseButton.LeftButton) - time.sleep(1) + qtbot.wait(1000) QTest.mouseClick(monitor.btn_stop, Qt.MouseButton.LeftButton) @@ -112,7 +112,9 @@ def test_click_graph(qtbot, mocker): mock_event._scenePos = QPointF(350, 240) orginal_value = monitor.inspector_variable.value() + monitor.on_mouse_click(mock_event) + qtbot.wait(1000) new_variable_value = monitor.inspector_variable.value() assert new_variable_value != orginal_value @@ -165,8 +167,7 @@ def test_x_axis_specification(qtbot, mocker): assert plot_con_axis_time.label.toPlainText().strip() == "time (s)" mock_event = mocker.MagicMock(spec=QMouseEvent) - mock_event._scenePos = QPointF(350, 240) - + mock_event._scenePos = QPointF(550, 250) monitor.on_mouse_click(mock_event) # Check type of value @@ -186,18 +187,28 @@ def test_x_axis_specification(qtbot, mocker): def test_y_axis_specification(qtbot): - monitor = create_test_run_monitor() + monitor = create_test_run_monitor(add_data=False) + monitor.termination_condition = { + "tc_idx": 0, + "max_eval": 10, + } + monitor.start(True) + + # Wait until the run is done + while monitor.running: + qtbot.wait(100) + select_x_plot_y_axis_spy = QSignalSpy(monitor.cb_plot_y.currentIndexChanged) index = monitor.inspector_variable.value() - + monitor.check_relative.setChecked(False) # check raw - non relative monitor.cb_plot_y.setCurrentIndex(0) assert len(select_x_plot_y_axis_spy) == 0 # since 0 is the default value raw_value = monitor.curves_variable["x0"].getData()[1][index] - assert raw_value == 0.5 - + assert raw_value == 0.5 + # relative monitor.check_relative.setChecked(True) @@ -210,14 +221,15 @@ def test_y_axis_specification(qtbot): assert len(select_x_plot_y_axis_spy) == 1 normalized_relative_value = monitor.curves_variable["x0"].getData()[1][index] - assert normalized_relative_value == 0.0 + assert normalized_relative_value == 0.0 - # raw normalized + # raw normalized monitor.check_relative.setChecked(False) normalized_raw_value = monitor.curves_variable["x0"].getData()[1][index] assert normalized_raw_value == 0.75 + def test_pause_play(qtbot): monitor = create_test_run_monitor(add_data=False) diff --git a/windows-dev-requirements.txt b/windows-dev-requirements.txt index e9fd20a1..642120a8 100644 --- a/windows-dev-requirements.txt +++ b/windows-dev-requirements.txt @@ -1,6 +1,4 @@ pytorch codecov -pytest>=3.6 -pytest-qt pytest-cov pytest-timeout