From ea1b1c5ea6b1ff9fcdcf9847507aa37bd60f0e42 Mon Sep 17 00:00:00 2001 From: ramirezfranciscof Date: Mon, 21 Nov 2022 12:49:01 +0000 Subject: [PATCH 1/5] Fixed problem when non-defined monitor --- aurora/engine/submit.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aurora/engine/submit.py b/aurora/engine/submit.py index 11fc31ff..efa44324 100644 --- a/aurora/engine/submit.py +++ b/aurora/engine/submit.py @@ -16,7 +16,7 @@ TomatoMonitorData = aiida.plugins.DataFactory('calcmonitor.monitor.tomatobiologic') TomatoMonitorCalcjob = aiida.plugins.CalculationFactory('calcmonitor.calcjob_monitor') -MONITOR_CODE = load_code("monitor@localhost-verdi") +#MONITOR_CODE = load_code("monitor@localhost-verdi") GROUP_SAMPLES = load_group("BatterySamples") GROUP_METHODS = load_group("CyclingSpecs") GROUP_CALCJOBS = load_group("CalcJobs") @@ -41,7 +41,7 @@ def _find_job_remote_folder(job): return remote_folder def submit_experiment(sample, method, tomato_settings, monitor_job_settings, code_name, - sample_node_label="", method_node_label="", calcjob_node_label=""): + sample_node_label="", method_node_label="", calcjob_node_label="", monitor_code=None): """ sample : `aurora.schemas.battery.BatterySample` method : `aurora.schemas.cycling.ElectroChemSequence` @@ -98,8 +98,11 @@ def submit_experiment(sample, method, tomato_settings, monitor_job_settings, cod monitor_protocol.store() monitor_protocol.label = "" # TODO? write default name generator - e.g "monitor-rate_600-cycles_2-thr_0.80" + if monitor_code is None: + monitor_code = load_code("monitor@localhost-verdi") + monitor_builder = TomatoMonitorCalcjob.get_builder() - monitor_builder.code = MONITOR_CODE + monitor_builder.code = monitor_code monitor_builder.metadata.options.parser_name = "calcmonitor.cycler" monitor_builder.monitor_protocols = {'monitor1': monitor_protocol} From 8da3923e9d3a194aafda592410b77125c172ea1d Mon Sep 17 00:00:00 2001 From: ramirezfranciscof Date: Mon, 21 Nov 2022 12:49:23 +0000 Subject: [PATCH 2/5] Add feature to save and load protocol --- aurora/interface/cycling/custom.py | 62 ++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/aurora/interface/cycling/custom.py b/aurora/interface/cycling/custom.py index ae984278..8d68a305 100644 --- a/aurora/interface/cycling/custom.py +++ b/aurora/interface/cycling/custom.py @@ -3,13 +3,16 @@ Cycling protocol customizable by the user. TODO: Enable the user to save a customized protocol. """ - +import os +import json import logging import ipywidgets as ipw import aurora.schemas.cycling from aurora.schemas.cycling import ElectroChemPayloads, ElectroChemSequence from .technique_widget import TechniqueParametersWidget +from ipyfilechooser import FileChooser + class CyclingCustom(ipw.VBox): BOX_STYLE = {'description_width': '25%'} @@ -20,6 +23,8 @@ class CyclingCustom(ipw.VBox): BUTTON_STYLE = {'description_width': '30%'} BUTTON_LAYOUT = {'margin': '5px'} BUTTON_LAYOUT_2 = {'width': '20%', 'margin': '5px'} + #BUTTON_LAYOUT_3 = {'width': '43.5%', 'margin': '5px'} + BUTTON_LAYOUT_3 = {'width': '10%', 'margin': '5px'} GRID_LAYOUT = {"grid_template_columns": "30% 65%", 'width': '100%', 'margin': '5px'} # 'padding': '10px', 'border': 'solid 2px', 'max_height': '500px' DEFAULT_PROTOCOL = aurora.schemas.cycling.OpenCircuitVoltage _TECHNIQUES_OPTIONS = {f"{Technique.schema()['properties']['short_name']['default']} ({Technique.schema()['properties']['technique']['default']})": Technique @@ -38,7 +43,7 @@ def __init__(self, validate_callback_f): description="", layout=self.BOX_LAYOUT) self.w_button_add = ipw.Button( - description="", button_style='info', tooltip="Add step", icon='plus', + description="", button_style='success', tooltip="Add step", icon='plus', style=self.BUTTON_STYLE, layout=self.BUTTON_LAYOUT_2) self.w_button_remove = ipw.Button( description="", button_style='danger', tooltip="Remove step", icon='minus', @@ -50,6 +55,20 @@ def __init__(self, validate_callback_f): description="", button_style='', tooltip="Move step down", icon='arrow-down', style=self.BUTTON_STYLE, layout=self.BUTTON_LAYOUT_2) + self.w_button_load = ipw.Button( + description="Load", button_style='', tooltip="Load protocol", icon='', + style=self.BUTTON_STYLE, layout=self.BUTTON_LAYOUT_3) + self.w_button_save = ipw.Button( + description="Save", button_style='', tooltip="Save protocol", icon='', + style=self.BUTTON_STYLE, layout=self.BUTTON_LAYOUT_3) + home_directory = os.path.expanduser( '~' ) + self.w_filepath_explorer = FileChooser( + home_directory, layout={'width': '70%', 'margin': '5px'}, + ) + self.w_filepath_explorer.default_path = home_directory + self.w_filepath_explorer.default_filename = 'saved_protocol.json' + self.w_filepath_explorer.reset() + # initialize protocol steps list self._protocol_steps_list = ElectroChemSequence(method=[]) self.add_protocol_step() @@ -82,11 +101,14 @@ def __init__(self, validate_callback_f): super().__init__() self.children = [ self.w_header, + ipw.HBox([self.w_filepath_explorer, self.w_button_load, self.w_button_save]), self.w_protocol_label, ipw.GridBox([ ipw.VBox([ self.w_protocol_steps_list, ipw.HBox([self.w_button_add, self.w_button_remove, self.w_button_up, self.w_button_down]), + #ipw.HBox([self.w_button_load, self.w_button_save]), + #ipw.HBox([self.w_filepath_explorer]), ]), ipw.VBox([ self.w_selected_step_technique_name, @@ -106,7 +128,11 @@ def __init__(self, validate_callback_f): self.w_button_remove.on_click(self.remove_protocol_step) self.w_button_up.on_click(self.move_protocol_step_up) self.w_button_down.on_click(self.move_protocol_step_down) - + + self.w_button_load.on_click(self.procedure_load_protocol) + self.w_button_save.on_click(self.procedure_save_protocol) + + ## current step's properties: ### if technique type changes, we need to initialize a new technique from scratch the widget observer may detect a change ### even when a new step is selected therefore we check whether the new technique is the same as the one stored in @@ -211,4 +237,32 @@ def discard_current_step_properties(self, dummy=None): def callback_call(self, callback_function): "Call a callback function and this class instance to it." - return callback_function(self) \ No newline at end of file + return callback_function(self) + + def procedure_load_protocol(self, dummy=None): + """Loads the protocol from a file.""" + filepath = self.w_filepath_explorer.selected + if filepath is None: + return + + with open(filepath, 'r') as fileobj: + json_data = json.load(fileobj) + + self._protocol_steps_list = ElectroChemSequence(**json_data) + self._update_protocol_steps_list_widget_options() +# return None + + def procedure_save_protocol(self, dummy=None): + """Saves the protocol from a file.""" + #filepath = '/home/aiida/saved_protocol.json' + filepath = self.w_filepath_explorer.selected + if filepath is None: + return + + try: + json_data = json.dumps(self.protocol_steps_list.dict(), indent=2) + except Exception as err: + json_data = str(err) + + with open(filepath, 'w') as fileobj: + fileobj.write(str(json_data)) From 5ab88c26e93abd857c17d864140a438c2ef65e9a Mon Sep 17 00:00:00 2001 From: ramirezfranciscof Date: Mon, 21 Nov 2022 13:10:57 +0000 Subject: [PATCH 3/5] Sligthly cleaner review --- aurora/interface/main.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/aurora/interface/main.py b/aurora/interface/main.py index 796ae13d..c9cf7239 100644 --- a/aurora/interface/main.py +++ b/aurora/interface/main.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +import json import pandas as pd import ipywidgets as ipw from IPython.display import display @@ -152,10 +152,16 @@ def presubmission_checks_preview(self, dummy=None): if self.selected_tomato_settings is None or self.selected_monitor_job_settings is None: raise ValueError("Tomato settings were not selected!") - print(f"Battery Sample:\n {self.selected_battery_sample}") - print(f"Cycling Protocol:\n {self.selected_cycling_protocol}") - print(f"Tomato Settings:\n {self.selected_tomato_settings}") - print(f"Monitor Job Settings:\n {self.selected_monitor_job_settings}") + output_battery_sample = f'{self.selected_battery_sample}' + output_cycling_protocol = json.dumps(self.selected_cycling_protocol.dict(), indent=2) + output_tomato_settings = f'{self.selected_tomato_settings}' + output_monitor_job_settings = f'{self.selected_monitor_job_settings}' + + print(f"Battery Sample:\n"+output_battery_sample+'\n') + print(f"Cycling Protocol:\n"+output_cycling_protocol+'\n') + print(f"Tomato Settings:\n"+output_tomato_settings+'\n') + print(f"Monitor Job Settings:\n"+output_monitor_job_settings+'\n') + except ValueError as err: self.w_submit_button.disabled = True print(f"❌ {err}") From dddea4f9090b217dc485ca9c54cf514f29cec50c Mon Sep 17 00:00:00 2001 From: ramirezfranciscof Date: Sun, 4 Dec 2022 10:52:49 +0000 Subject: [PATCH 4/5] Add multiple sample run and load/save protocol --- aurora/interface/cycling/custom.py | 127 +++++--- aurora/interface/cycling/technique_widget.py | 3 +- aurora/interface/main.py | 62 ++-- aurora/interface/sample/from_id.py | 126 ++++++-- aurora/interface/sample/from_recipe.py | 22 +- aurora/interface/sample/from_specs.py | 26 +- aurora/models/__init__.py | 5 + aurora/models/battery_experiment.py | 289 +++++++++++++++++++ aurora/schemas/cycling.py | 162 ++++++++++- 9 files changed, 704 insertions(+), 118 deletions(-) create mode 100644 aurora/models/__init__.py create mode 100644 aurora/models/battery_experiment.py diff --git a/aurora/interface/cycling/custom.py b/aurora/interface/cycling/custom.py index 8d68a305..215857a1 100644 --- a/aurora/interface/cycling/custom.py +++ b/aurora/interface/cycling/custom.py @@ -30,7 +30,12 @@ class CyclingCustom(ipw.VBox): _TECHNIQUES_OPTIONS = {f"{Technique.schema()['properties']['short_name']['default']} ({Technique.schema()['properties']['technique']['default']})": Technique for Technique in ElectroChemPayloads.__args__} - def __init__(self, validate_callback_f): + def __init__(self, experiment_model, validate_callback_f): + + if experiment_model is None: + raise ValueError('An experiment model must be provided.') + self.experiment_model = experiment_model + self.experiment_model.suscribe_observer(self) if not callable(validate_callback_f): raise TypeError("validate_callback_f should be a callable function") @@ -42,6 +47,9 @@ def __init__(self, validate_callback_f): rows=10, value=None, description="", layout=self.BOX_LAYOUT) + + self._update_protocol_steps_list_widget_options() + self.w_button_add = ipw.Button( description="", button_style='success', tooltip="Add step", icon='plus', style=self.BUTTON_STYLE, layout=self.BUTTON_LAYOUT_2) @@ -70,8 +78,8 @@ def __init__(self, validate_callback_f): self.w_filepath_explorer.reset() # initialize protocol steps list - self._protocol_steps_list = ElectroChemSequence(method=[]) - self.add_protocol_step() + # self._protocol_steps_list = ElectroChemSequence(method=[]) + # self.add_protocol_step() # initialize current step properties widget self.w_selected_step_technique_name = ipw.Dropdown( @@ -124,13 +132,19 @@ def __init__(self, validate_callback_f): # setup automations ## steps list self.w_protocol_steps_list.observe(self._build_current_step_properties_widget, names='index') - self.w_button_add.on_click(self.add_protocol_step) - self.w_button_remove.on_click(self.remove_protocol_step) - self.w_button_up.on_click(self.move_protocol_step_up) - self.w_button_down.on_click(self.move_protocol_step_down) + + #self.w_button_add.on_click(self.add_protocol_step) + #self.w_button_remove.on_click(self.remove_protocol_step) + #self.w_button_up.on_click(self.move_protocol_step_up) + #self.w_button_down.on_click(self.move_protocol_step_down) + + self.w_button_add.on_click(self.w_button_add_click) + self.w_button_remove.on_click(self.w_button_remove_click) + self.w_button_up.on_click(self.w_button_up_click) + self.w_button_down.on_click(self.w_button_down_click) - self.w_button_load.on_click(self.procedure_load_protocol) - self.w_button_save.on_click(self.procedure_save_protocol) + self.w_button_load.on_click(self.button_load_protocol_click) + self.w_button_save.on_click(self.button_save_protocol_click) ## current step's properties: @@ -144,32 +158,46 @@ def __init__(self, validate_callback_f): ### validate protocol self.w_validate.on_click(lambda arg: self.callback_call(validate_callback_f)) + def update(self): + """Receive updates from the model""" + self._update_protocol_steps_list_widget_options() + self._build_current_step_properties_widget() + # Automatically done by _build_current_step_properties_widget + #self._build_technique_parameters_widgets() + @property def protocol_steps_list(self): "The list of steps composing the cycling protocol. Each step must be one of the allowed ElectroChemPayloads." - return self._protocol_steps_list + return self.experiment_model.selected_protocol + #return self._protocol_steps_list @property def selected_step_technique(self): "The step that is currently selected." - return self.protocol_steps_list.method[self.w_protocol_steps_list.index] + return self.experiment_model.selected_protocol.method[self.w_protocol_steps_list.index] + #return self.protocol_steps_list.method[self.w_protocol_steps_list.index] @selected_step_technique.setter def selected_step_technique(self, technique): - self.protocol_steps_list.method[self.w_protocol_steps_list.index] = technique + self.experiment_model.selected_protocol.method[self.w_protocol_steps_list.index] = technique + #self.protocol_steps_list.method[self.w_protocol_steps_list.index] = technique def _count_technique_occurencies(self, technique): - return [type(step) for step in self.protocol_steps_list.method].count(technique) + return [type(step) for step in self.experiment_model.selected_protocol.method].count(technique) + #return [type(step) for step in self.protocol_steps_list.method].count(technique) def _update_protocol_steps_list_widget_options(self, new_index=None): old_selected_index = self.w_protocol_steps_list.index - self.w_protocol_steps_list.options = [f"[{idx + 1}] - {step.name}" for idx, step in enumerate(self.protocol_steps_list.method)] + self.w_protocol_steps_list.options = [f"[{idx + 1}] - {step.name}" for idx, step in enumerate(self.experiment_model.selected_protocol.method)] + #self.w_protocol_steps_list.options = [f"[{idx + 1}] - {step.name}" for idx, step in enumerate(self.protocol_steps_list.method)] if new_index is not None: old_selected_index = new_index if (old_selected_index is None) or (old_selected_index < 0): self.w_protocol_steps_list.index = 0 - elif old_selected_index >= self.protocol_steps_list.n_steps: - self.w_protocol_steps_list.index = self.protocol_steps_list.n_steps - 1 + elif old_selected_index >= self.experiment_model.selected_protocol.n_steps: + self.w_protocol_steps_list.index = self.experiment_model.selected_protocol.n_steps - 1 + #elif old_selected_index >= self.protocol_steps_list.n_steps: + # self.w_protocol_steps_list.index = self.protocol_steps_list.n_steps - 1 else: self.w_protocol_steps_list.index = old_selected_index @@ -179,19 +207,24 @@ def DEFAULT_STEP_NAME(self, technique): def add_protocol_step(self, dummy=None): name = self.DEFAULT_STEP_NAME(self.DEFAULT_PROTOCOL) logging.debug(f"Adding protocol step {name}") - self.protocol_steps_list.add_step(self.DEFAULT_PROTOCOL(name=name)) - self._update_protocol_steps_list_widget_options(new_index=self.protocol_steps_list.n_steps-1) + self.experiment_model.selected_protocol.add_step(self.DEFAULT_PROTOCOL(name=name)) + self._update_protocol_steps_list_widget_options(new_index=self.experiment_model.selected_protocol.n_steps-1) + #self.protocol_steps_list.add_step(self.DEFAULT_PROTOCOL(name=name)) + #self._update_protocol_steps_list_widget_options(new_index=self.protocol_steps_list.n_steps-1) def remove_protocol_step(self, dummy=None): - self.protocol_steps_list.remove_step(self.w_protocol_steps_list.index) + self.experiment_model.selected_protocol.remove_step(self.w_protocol_steps_list.index) + #self.protocol_steps_list.remove_step(self.w_protocol_steps_list.index) self._update_protocol_steps_list_widget_options() def move_protocol_step_up(self, dummy=None): - self.protocol_steps_list.move_step_backward(self.w_protocol_steps_list.index) + self.experiment_model.selected_protocol.move_step_backward(self.w_protocol_steps_list.index) + #self.protocol_steps_list.move_step_backward(self.w_protocol_steps_list.index) self._update_protocol_steps_list_widget_options(new_index=self.w_protocol_steps_list.index - 1) def move_protocol_step_down(self, dummy=None): - moved = self.protocol_steps_list.move_step_forward(self.w_protocol_steps_list.index) + moved = self.experiment_model.selected_protocol.move_step_forward(self.w_protocol_steps_list.index) + #moved = self.protocol_steps_list.move_step_forward(self.w_protocol_steps_list.index) self._update_protocol_steps_list_widget_options(new_index=self.w_protocol_steps_list.index + 1) ## SELECTED STEP METHODS @@ -239,30 +272,48 @@ def callback_call(self, callback_function): "Call a callback function and this class instance to it." return callback_function(self) - def procedure_load_protocol(self, dummy=None): + + ################################################################################ + # NEW DESIGN + ################################################################################ + # TODO: figure out what the b param is + + def w_button_add_click(self, b): + """Click event for w_button_add""" + self.experiment_model.add_protocol_step() + number_of_steps = self.experiment_model.selected_protocol.n_steps + self._update_protocol_steps_list_widget_options(new_index = number_of_steps - 1) + + def w_button_remove_click(self, b): + """Click event for w_button_remove""" + self.experiment_model.remove_protocol_step(self.w_protocol_steps_list.index) + + def w_button_up_click(self, b): + """Click event for w_button_up""" + # TODO I need to add a guard on the index? + current_index = self.w_protocol_steps_list.index + self.experiment_model.move_protocol_step_up(current_index) + self._update_protocol_steps_list_widget_options(new_index = current_index - 1) + + def w_button_down_click(self, b): + """Click event for w_button_down""" + # TODO I need to add a guard on the index? + current_index = self.w_protocol_steps_list.index + self.experiment_model.move_protocol_step_down(current_index) + self._update_protocol_steps_list_widget_options(new_index = current_index + 1) + + def button_load_protocol_click(self, dummy=None): """Loads the protocol from a file.""" + #filepath = '/home/aiida/saved_protocol.json' filepath = self.w_filepath_explorer.selected if filepath is None: return + self.experiment_model.load_protocol(filepath) - with open(filepath, 'r') as fileobj: - json_data = json.load(fileobj) - - self._protocol_steps_list = ElectroChemSequence(**json_data) - self._update_protocol_steps_list_widget_options() -# return None - - def procedure_save_protocol(self, dummy=None): + def button_save_protocol_click(self, dummy=None): """Saves the protocol from a file.""" - #filepath = '/home/aiida/saved_protocol.json' filepath = self.w_filepath_explorer.selected if filepath is None: return + self.experiment_model.save_protocol(filepath) - try: - json_data = json.dumps(self.protocol_steps_list.dict(), indent=2) - except Exception as err: - json_data = str(err) - - with open(filepath, 'w') as fileobj: - fileobj.write(str(json_data)) diff --git a/aurora/interface/cycling/technique_widget.py b/aurora/interface/cycling/technique_widget.py index 993fa2c4..d2dde449 100644 --- a/aurora/interface/cycling/technique_widget.py +++ b/aurora/interface/cycling/technique_widget.py @@ -23,7 +23,8 @@ def build_parameter_widget(param_obj): w_check = ipw.Checkbox( value=param_obj.required or (param_obj.value is not None), disabled=param_obj.required, - description='', style=CHECKBOX_STYLE, layout=CHECKBOX_LAYOUT) + description='', style=CHECKBOX_STYLE, layout=CHECKBOX_LAYOUT + ) # read the value of parameter. If None, take use the default value param_value = param_obj.value if param_obj.value is not None else param_obj.default_value diff --git a/aurora/interface/main.py b/aurora/interface/main.py index c9cf7239..e675c3d3 100644 --- a/aurora/interface/main.py +++ b/aurora/interface/main.py @@ -8,6 +8,11 @@ from .tomato import TomatoSettings from aurora.engine import submit_experiment +from aurora.schemas.battery import BatterySample +from aurora.schemas.utils import dict_to_formatted_json + +from aurora.models import BatteryExperimentModel + CODE_NAME = "ketchup-0.2rc2" class MainPanel(ipw.VBox): @@ -51,7 +56,8 @@ def selected_recipe(self): @property def selected_cycling_protocol(self): "The Cycling Specs selected. Used by a BatteryCyclerExperiment." - return self._selected_cycling_protocol + return self.experiment_model.selected_protocol + #return self._selected_cycling_protocol @property def selected_tomato_settings(self): @@ -112,6 +118,7 @@ def sample_selection_method(self): # METHOD SELECTION ####################################################################################### def return_selected_protocol(self, cycling_widget_obj): + self.experiment_model.selected_protocol = cycling_widget_obj.protocol_steps_list self._selected_cycling_protocol = cycling_widget_obj.protocol_steps_list self.post_protocol_selection() @@ -172,16 +179,22 @@ def presubmission_checks_preview(self, dummy=None): @w_submission_output.capture() def submit_job(self, dummy=None): self.w_submit_button.disabed = True - self.process = submit_experiment( - sample=self.selected_battery_sample, - method=self.selected_cycling_protocol, - tomato_settings=self.selected_tomato_settings, - monitor_job_settings=self.selected_monitor_job_settings, - code_name=self.w_code.value, - sample_node_label="", - method_node_label="", - calcjob_node_label="" - ) + for index, battery_sample in self.experiment_model.selected_samples.iterrows(): + json_stuff = dict_to_formatted_json(battery_sample) + json_stuff['capacity'].pop('actual', None) + current_battery = BatterySample.parse_obj(json_stuff) + + self.process = submit_experiment( + sample=current_battery, + method=self.selected_cycling_protocol, + tomato_settings=self.selected_tomato_settings, + monitor_job_settings=self.selected_monitor_job_settings, + code_name=self.w_code.value, + sample_node_label="", + method_node_label="", + calcjob_node_label="" + ) + self.w_main_accordion.selected_index = None ####################################################################################### @@ -196,6 +209,7 @@ def reset_sample_selection(self, dummy=None): def reset_all_inputs(self, dummy=None): "Reset all the selected inputs." + self.experiment_model.reset_inputs() self._selected_battery_sample = None self._selected_battery_specs = None self._selected_recipe = None @@ -214,19 +228,29 @@ def reset(self, dummy=None): ####################################################################################### # MAIN ####################################################################################### - def __init__(self): + def __init__(self, experiment_model=None): + + if experiment_model is None: + experiment_model = BatteryExperimentModel() + #raise ValueError('An experiment model must be provided.') + self.experiment_model = experiment_model # initialize variables self.reset_all_inputs() # Sample selection - self.w_sample_from_id = SampleFromId(validate_callback_f=self.return_selected_sample) - self.w_sample_from_specs = SampleFromSpecs(validate_callback_f=self.return_selected_sample, recipe_callback_f=self.switch_to_recipe) - self.w_sample_from_recipe = SampleFromRecipe(validate_callback_f=self.return_selected_specs_recipe) + self.w_sample_from_id = SampleFromId(experiment_model=experiment_model, validate_callback_f=self.return_selected_sample) + self.w_sample_from_specs = SampleFromSpecs(experiment_model=experiment_model, validate_callback_f=self.return_selected_sample, recipe_callback_f=self.switch_to_recipe) + self.w_sample_from_recipe = SampleFromRecipe(experiment_model=experiment_model, validate_callback_f=self.return_selected_specs_recipe) self.w_sample_selection_tab = ipw.Tab( - children=[self.w_sample_from_id, self.w_sample_from_specs, self.w_sample_from_recipe], - selected_index=0) + children=[ + self.w_sample_from_id, + self.w_sample_from_specs, + self.w_sample_from_recipe, + ], + selected_index=0 + ) for i, title in enumerate(self._SAMPLE_INPUT_LABELS): self.w_sample_selection_tab.set_title(i, title) @@ -234,7 +258,7 @@ def __init__(self): self.w_test_sample_label = ipw.HTML("Selected sample:") self.w_test_sample_preview = ipw.Output(layout=self._SAMPLE_BOX_LAYOUT) self.w_test_standard = CyclingStandard(lambda x: x) - self.w_test_custom = CyclingCustom(validate_callback_f=self.return_selected_protocol) + self.w_test_custom = CyclingCustom(experiment_model=experiment_model, validate_callback_f=self.return_selected_protocol) self.w_test_method_tab = ipw.Tab( children=[self.w_test_standard, self.w_test_custom], selected_index=1) @@ -265,7 +289,7 @@ def __init__(self): self.w_submit_tab = ipw.VBox([ self.w_job_preview, self.w_code, - self.w_submit_button + self.w_submit_button, ]) # Reset diff --git a/aurora/interface/sample/from_id.py b/aurora/interface/sample/from_id.py index 1b3bc1f5..107ed850 100644 --- a/aurora/interface/sample/from_id.py +++ b/aurora/interface/sample/from_id.py @@ -3,34 +3,69 @@ Sample selected from a list of samples and their IDs. TODO: implement creation and labeling of sample nodes. Store them in a group, retrieve a node if it was already created. """ - +import json import ipywidgets as ipw from IPython.display import display -from aurora.query import update_available_samples, query_available_samples, write_pd_query_from_dict +#from aurora.query import update_available_samples, query_available_samples, write_pd_query_from_dict from aurora.schemas.battery import BatterySample from aurora.schemas.utils import dict_to_formatted_json, remove_empties_from_dict_decorator class SampleFromId(ipw.VBox): - BOX_LAYOUT_1 = {'width': '50%'} + BOX_LAYOUT_1 = {'width': '40%'} BOX_STYLE_1 = {'description_width': '15%'} BOX_STYLE_2 = {'description_width': 'initial'} BUTTON_STYLE = {'description_width': '30%'} BUTTON_LAYOUT = {'margin': '5px'} MAIN_LAYOUT = {'width': '100%', 'padding': '10px', 'border': 'solid blue 2px'} - def __init__(self, validate_callback_f): - + def __init__(self, experiment_model, validate_callback_f): + + self.experiment_model = experiment_model + # initialize widgets self.w_header_label = ipw.HTML(value="

Battery Selection

") + + self.w_visualize_info = ipw.Output(layout={'border': '1px solid black'}) + + self.w_update = ipw.Button( + description="Update", + button_style='', + tooltip="Update available samples", + icon='refresh', + style=self.BUTTON_STYLE, + layout=self.BUTTON_LAYOUT + ) + self.w_select = ipw.Button( + description="Select", + button_style='', + tooltip="Select chosen sample", + icon='fa-arrow-right', + style=self.BUTTON_STYLE, + layout=self.BUTTON_LAYOUT + ) + self.w_unselect = ipw.Button( + description="Unselect", + button_style='', + tooltip="Unselect chosen sample", + icon='fa-arrow-left', + style=self.BUTTON_STYLE, + layout=self.BUTTON_LAYOUT + ) + self.w_id_list = ipw.Select( rows=10, description="Battery ID:", - style=self.BOX_STYLE_1, layout=self.BOX_LAYOUT_1) - self.w_update = ipw.Button( - description="Update", - button_style='', tooltip="Update available samples", icon='refresh', - style=self.BUTTON_STYLE, layout=self.BUTTON_LAYOUT) + style=self.BOX_STYLE_1, + layout=self.BOX_LAYOUT_1 + ) + self.w_selected_list = ipw.Select( + rows=10, + description="Selected ID:", + style=self.BOX_STYLE_1, + layout=self.BOX_LAYOUT_1 + ) + self.w_sample_preview = ipw.Output() self.w_sample_node_label = ipw.Text( # TODO: this is not used yet - create a default or retrieve it from a node description="AiiDA Sample node label:", @@ -46,8 +81,12 @@ def __init__(self, validate_callback_f): self.children = [ self.w_header_label, ipw.VBox([ - ipw.HBox([self.w_id_list, - self.w_update,]), + self.w_visualize_info, + ipw.HBox([ + self.w_id_list, + ipw.VBox([self.w_update, self.w_select, self.w_unselect]), + self.w_selected_list + ]), self.w_sample_preview, self.w_sample_node_label, ], layout=self.MAIN_LAYOUT), @@ -59,12 +98,18 @@ def __init__(self, validate_callback_f): raise TypeError("validate_callback_f should be a callable function") # self.validate_callback_f = validate_callback_f self.w_id_list.value = None + self.w_selected_list.value = None self.on_update_button_clicked() # setup automations self.w_update.on_click(self.on_update_button_clicked) + self.w_select.on_click(self.on_select_button_clicked) + self.w_unselect.on_click(self.on_unselect_button_clicked) + self.w_id_list.observe(handler=self.on_battery_id_change, names='value') - self.w_validate.on_click(lambda arg: self.on_validate_button_clicked(validate_callback_f)) + self.w_validate.on_click( + lambda arg: self.on_validate_button_clicked(validate_callback_f) + ) @property @@ -75,28 +120,19 @@ def selected_sample_id(self): @remove_empties_from_dict_decorator def selected_sample_dict(self): return dict_to_formatted_json( - query_available_samples(write_pd_query_from_dict({'battery_id': self.w_id_list.value})).iloc[0]) + self.experiment_model.query_available_samples(self.experiment_model.write_pd_query_from_dict({'battery_id': self.w_id_list.value})).iloc[0]) @property def selected_sample(self): "The selected battery sample returned as a `aurora.schemas.battery.BatterySample` object." return BatterySample.parse_obj(self.selected_sample_dict) - @staticmethod - def _build_sample_id_options(): - """Returns a (option_string, battery_id) list.""" - # table = query_available_samples(project=['battery_id', 'metadata.name']).sort_values('battery_id') - table = query_available_samples().sort_values('battery_id') - def row_label(row): - # return f"<{row['battery_id']:8}> \"{row['metadata.name']}\"" - return f"{row['battery_id']:8} [{row['manufacturer'].split()[0]}] ({row['capacity.nominal']} {row['capacity.units']}) {row['metadata.name']} ({row['composition.description']})" - return [("", None)] + [(row_label(row), row['battery_id']) for index, row in table.iterrows()] def display_sample_preview(self): self.w_sample_preview.clear_output() if self.w_id_list.value is not None: with self.w_sample_preview: - display(query_available_samples(write_pd_query_from_dict({'battery_id': self.w_id_list.value}))) + display(self.experiment_model.query_available_samples(self.experiment_model.write_pd_query_from_dict({'battery_id': self.w_id_list.value}))) def update_validate_button_state(self): self.w_validate.disabled = (self.w_id_list.value is None) @@ -106,9 +142,47 @@ def on_battery_id_change(self, _ = None): self.update_validate_button_state() def on_update_button_clicked(self, _ = None): - update_available_samples() + self.experiment_model.update_available_samples() self.w_id_list.options = self._build_sample_id_options() + def on_select_button_clicked(self, _ = None): + sample_id = self.w_id_list.value + if sample_id is not None: + self.experiment_model.add_selected_sample(sample_id) + self.update_lists() + + def on_unselect_button_clicked(self, _ = None): + sample_id = self.w_selected_list.value + if sample_id is not None: + self.experiment_model.remove_selected_sample(sample_id) + self.update_lists() + def on_validate_button_clicked(self, callback_function): # call the validation callback function - return callback_function(self) \ No newline at end of file + return callback_function(self) + + def update_lists(self): + """Updates the lists.""" + self.w_id_list.options = self._build_sample_id_options() + self.w_selected_list.options = self._uptate_selected_list() + + ############################################################ + # This should go to control + ############################################################ + + #@staticmethod + def _build_sample_id_options(self): + """Returns a (option_string, battery_id) list.""" + # table = query_available_samples(project=['battery_id', 'metadata.name']).sort_values('battery_id') + table = self.experiment_model.query_available_samples().sort_values('battery_id') + def row_label(row): + # return f"<{row['battery_id']:8}> \"{row['metadata.name']}\"" + return f"{row['battery_id']:8} [{row['manufacturer'].split()[0]}] ({row['capacity.nominal']} {row['capacity.units']}) {row['metadata.name']} ({row['composition.description']})" + return [("", None)] + [(row_label(row), row['battery_id']) for index, row in table.iterrows()] + + def _uptate_selected_list(self): + """Returns a (option_string, battery_id) list.""" + table = self.experiment_model.selected_samples + def row_label(row): + return f"{row['battery_id']:8} [{row['manufacturer'].split()[0]}] ({row['capacity.nominal']} {row['capacity.units']}) {row['metadata.name']} ({row['composition.description']})" + return [("", None)] + [(row_label(row), row['battery_id']) for index, row in table.iterrows()] diff --git a/aurora/interface/sample/from_recipe.py b/aurora/interface/sample/from_recipe.py index 6627accd..392adfbf 100644 --- a/aurora/interface/sample/from_recipe.py +++ b/aurora/interface/sample/from_recipe.py @@ -20,7 +20,9 @@ class SampleFromRecipe(ipw.VBox): OUTPUT_LAYOUT = {'width': '100%', 'margin': '5px', 'padding': '5px', 'border': 'solid 2px'} #'max_height': '500px' MAIN_LAYOUT = {'width': '100%', 'padding': '10px', 'border': 'solid blue 2px'} - def __init__(self, validate_callback_f): + def __init__(self, experiment_model, validate_callback_f): + + self.experiment_model = experiment_model # initialize widgets self.w_specs_label = ipw.HTML(value="

Battery Specifications **NOT IMPLEMENTED**

") @@ -136,10 +138,10 @@ def selected_recipe_dict(self): 'metadata.creation_process': self.w_sample_metadata_creation_process.value, }) - @staticmethod - def _build_recipies_options(): + #@staticmethod + def _build_recipies_options(self): """Returns a (name, description) list.""" - dic = query_available_recipies() + dic = self.experiment_model.query_available_recipies() return [("", None)] + [(name, descr) for name, descr in dic.items()] def display_recipe_preview(self): @@ -156,12 +158,12 @@ def on_recipe_value_change(self, _=None): self.display_recipe_preview() def on_update_button_clicked(self, _=None): - update_available_specs() - update_available_recipies() - self.w_specs_manufacturer.options = query_available_specs('manufacturer') - self.w_specs_composition.options = query_available_specs('composition.description') - self.w_specs_capacity.options = query_available_specs('capacity.nominal') - self.w_specs_form_factor.options = query_available_specs('form_factor') + self.experiment_model.update_available_specs() + self.experiment_model.update_available_recipies() + self.w_specs_manufacturer.options = self.experiment_model.query_available_specs('manufacturer') + self.w_specs_composition.options = self.experiment_model.query_available_specs('composition.description') + self.w_specs_capacity.options = self.experiment_model.query_available_specs('capacity.nominal') + self.w_specs_form_factor.options = self.experiment_model.query_available_specs('form_factor') self.w_recipe_select.options = self._build_recipies_options() def on_reset_button_clicked(self, _=None): diff --git a/aurora/interface/sample/from_specs.py b/aurora/interface/sample/from_specs.py index 16759f3a..f03309f0 100644 --- a/aurora/interface/sample/from_specs.py +++ b/aurora/interface/sample/from_specs.py @@ -7,7 +7,7 @@ import ipywidgets as ipw from IPython.display import display -from aurora.query import update_available_samples, query_available_samples, update_available_specs, query_available_specs, write_pd_query_from_dict +#from aurora.query import update_available_samples, query_available_samples, update_available_specs, query_available_specs, write_pd_query_from_dict from aurora.schemas.battery import BatterySample from aurora.schemas.utils import dict_to_formatted_json, remove_empties_from_dict_decorator @@ -32,13 +32,15 @@ class SampleFromSpecs(ipw.VBox): QUERY_PRINT_COLUMNS = ['manufacturer', 'composition.description', 'capacity.nominal', 'capacity.actual', 'capacity.units', 'form_factor', 'metadata.name', 'metadata.creation_datetime'] #, 'metadata.creation_process'] - def __init__(self, validate_callback_f, recipe_callback_f): + def __init__(self, experiment_model, validate_callback_f, recipe_callback_f): if not callable(validate_callback_f): raise TypeError("validate_callback_f should be a callable function") if not callable(recipe_callback_f): raise TypeError("recipe_callback_f should be a callable function") + self.experiment_model = experiment_model + # initialize widgets self.w_specs_header = ipw.HTML(value="

Battery Specifications

") self.w_specs_manufacturer = ipw.Select( @@ -116,7 +118,7 @@ def __init__(self, validate_callback_f, recipe_callback_f): # initialize options self.on_reset_button_clicked() - update_available_specs() + self.experiment_model.update_available_specs() self._update_options() self.display_query_result() @@ -137,7 +139,7 @@ def selected_sample_id(self): @remove_empties_from_dict_decorator def selected_sample_dict(self): return dict_to_formatted_json( - query_available_samples(write_pd_query_from_dict({'battery_id': self.w_select_sample_id.value})).iloc[0]) + self.experiment_model.query_available_samples(self.experiment_model.write_pd_query_from_dict({'battery_id': self.w_select_sample_id.value})).iloc[0]) @property def selected_sample(self): @@ -165,7 +167,7 @@ def _build_sample_specs_options(self, spec_field): The current `spec_field` is removed from the query, as we want to count how many samples correspond to each available value of the `spec_field`. """ - spec_field_options_list = query_available_specs(spec_field) + spec_field_options_list = self.experiment_model.query_available_specs(spec_field) # setup sample query filter from current specs and remove current field from query sample_query_filter_dict = self.current_specs.copy() @@ -177,7 +179,7 @@ def _build_sample_specs_options(self, spec_field): print(f" {spec_field_options_list}") print(" QUERY: ", sample_query_filter_dict) - qres = query_available_samples(write_pd_query_from_dict(sample_query_filter_dict), + qres = self.experiment_model.query_available_samples(self.experiment_model.write_pd_query_from_dict(sample_query_filter_dict), project=[spec_field, 'battery_id']).sort_values('battery_id') # count values @@ -190,7 +192,7 @@ def _build_sample_specs_options(self, spec_field): def _build_sample_id_options(self): """Returns a (option_string, battery_id) list.""" - table = query_available_samples(write_pd_query_from_dict(self.current_specs)).sort_values('battery_id') + table = self.experiment_model.query_available_samples(self.experiment_model.write_pd_query_from_dict(self.current_specs)).sort_values('battery_id') return [("", None)] + [(f"<{row['battery_id']:5}> \"{row['metadata.name']}\"", row['battery_id']) for index, row in table.iterrows()] def _update_options(self): @@ -222,7 +224,7 @@ def display_query_result(self): self.w_query_result.clear_output() with self.w_query_result: # print(f'Query:\n {self.current_specs}') - query_res = query_available_samples(write_pd_query_from_dict(self.current_specs)).set_index('battery_id')[self.QUERY_PRINT_COLUMNS] + query_res = self.experiment_model.query_available_samples(self.experiment_model.write_pd_query_from_dict(self.current_specs)).set_index('battery_id')[self.QUERY_PRINT_COLUMNS] # query_res['metadata.creation_datetime'] = query_res['metadata.creation_datetime'].dt.strftime("%d-%m-%Y %H:%m") # display(query_res.style.format(formatter={'metadata.creation_datetime': lambda t: t.strftime("%d-%m-%Y")})) display(query_res) @@ -231,8 +233,8 @@ def display_sample_preview(self): self.w_sample_preview.clear_output() if self.w_select_sample_id.value is not None: with self.w_sample_preview: - # display(query_available_samples(write_pd_query_from_dict({'battery_id': self.w_select_sample_id.value}))) - print(query_available_samples(write_pd_query_from_dict({'battery_id': self.w_select_sample_id.value})).iloc[0]) + # display(self.experiment_model.query_available_samples(self.experiment_model.write_pd_query_from_dict({'battery_id': self.w_select_sample_id.value}))) + print(self.experiment_model.query_available_samples(self.experiment_model.write_pd_query_from_dict({'battery_id': self.w_select_sample_id.value})).iloc[0]) def update_validate_button_state(self): self.w_validate.disabled = (self.w_select_sample_id.value is None) @@ -248,8 +250,8 @@ def on_specs_value_change(self, _=None): self.display_query_result() def on_update_button_clicked(self, _=None): - update_available_specs() - update_available_samples() + self.experiment_model.update_available_specs() + self.experiment_model.update_available_samples() self.update_options() self.display_query_result() # notice: if the old value is not available anymore, an error might be raised! diff --git a/aurora/models/__init__.py b/aurora/models/__init__.py new file mode 100644 index 00000000..ec6bcf5d --- /dev/null +++ b/aurora/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +__all__ = ['BatteryExperimentModel'] + +from .battery_experiment import BatteryExperimentModel \ No newline at end of file diff --git a/aurora/models/battery_experiment.py b/aurora/models/battery_experiment.py new file mode 100644 index 00000000..98467b6e --- /dev/null +++ b/aurora/models/battery_experiment.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- +import json +import logging +import pandas as pd + +from pydantic import BaseModel +from typing import Dict, Generic, Sequence, TypeVar, Literal, Union + +from aurora.schemas.cycling import ElectroChemSequence, OpenCircuitVoltage +from aurora.schemas.battery import BatterySample, BatterySpecsJsonTypes, BatterySampleJsonTypes + + +AVAILABLE_SAMPLES_FILE = 'available_samples.json' + +STD_RECIPIES = { + 'Load fresh battery into cycler': ['Request the user to load a battery with the desired specs.'], + 'Synthesize - Recipe 1': ['step 1', 'step 2', 'step 3'], + 'Synthesize - Recipe 2': ['step 1', 'step 2', 'step 3'], +} + +STD_PROTOCOLS = { + "Formation cycles": { + "Procedure": [ + "6h rest", + "CCCV Charge, C/10 (CV condition: i 1: + self.selected_protocol.remove_step(protocol_index) + self.update_observers() + + def move_protocol_step_up(self, protocol_index): + self.selected_protocol.move_step_backward(protocol_index) + self.update_observers() + + def move_protocol_step_down(self, protocol_index): + moved = self.selected_protocol.move_step_forward(protocol_index) + self.update_observers() + + def load_protocol(self, filepath): + """Loads the protocol from a file.""" + if filepath is None: + return + + with open(filepath, 'r') as fileobj: + json_data = json.load(fileobj) + + self.selected_protocol = ElectroChemSequence(**json_data) + self.update_observers() + + def save_protocol(self, filepath): + """Saves the protocol from a file.""" + if filepath is None: + return + + try: + json_data = json.dumps(self.selected_protocol.dict(), indent=2) + except Exception as err: + json_data = str(err) + + with open(filepath, 'w') as fileobj: + fileobj.write(str(json_data)) diff --git a/aurora/schemas/cycling.py b/aurora/schemas/cycling.py index b8f945a1..912c36c5 100644 --- a/aurora/schemas/cycling.py +++ b/aurora/schemas/cycling.py @@ -1,10 +1,29 @@ # -*- coding: utf-8 -*- +""" +STATUS: Providing the parameters as a dict with the specific parameter dict +as a default is not allowing me to initialize the CyclingTechnique's with +a normal dictionary (the parameters dict is not being correctly converted +to the right type but to Generic[DataT] I think). + +Using TypedDict as suggested here is not working properly either: +https://stackoverflow.com/q/74643755/638366 + +I probably need to change something fundamentally with how the CyclingTechnique +and CyclingParameter interact with each other but I'm not sure what currently, +so for now I'm defining the InternalParameters as its own class inside each of +the CyclingTechnique's. + +I also need to add items and __getitem__ because some other parts of the code +expect it to behave like a dict... +""" from pydantic import (BaseModel, Extra, validator, validator, Field, NonNegativeFloat, NonNegativeInt) from typing import Dict, Generic, Sequence, TypeVar, Literal, Union +from typing_extensions import TypedDict from pydantic.generics import GenericModel DataT = TypeVar('DataT') + class CyclingParameter(GenericModel, Generic[DataT]): "Cycling parameter of type DataT" label: str # the label used in a widget @@ -42,13 +61,31 @@ class Config: "+-2.5 V", "+-5.0 V", "+-10 V", "auto", ] + +################################################################################ +# SPECIFIC TECHNIQUES +################################################################################ + class DummySequential(CyclingTechnique, extra=Extra.forbid): device: Literal["worker"] = "worker" technique: Literal["sequential"] = "sequential" short_name: Literal["DUMMY_SEQUENTIAL"] = "DUMMY_SEQUENTIAL" name = "Dummy Sequential" description = "Dummy worker - sequential numbers" - parameters: Dict[str, CyclingParameter] = { + + class InternalParameters(BaseModel): + time: CyclingParameter[NonNegativeFloat] + delay: CyclingParameter[NonNegativeFloat] + + @property + def items(self): + return dict(self._iter()).items + + def __getitem__(self, item): + return getattr(self, item) + + #parameters: Dict[str, CyclingParameter] = { + parameters: InternalParameters = InternalParameters(**{ "time": CyclingParameter[NonNegativeFloat]( label = "Time:", units = "s", @@ -61,15 +98,29 @@ class DummySequential(CyclingTechnique, extra=Extra.forbid): default_value = 1.0, required = True, ) - } + }) +################################################################################ class DummyRandom(CyclingTechnique, extra=Extra.forbid): device: Literal["worker"] = "worker" technique: Literal["random"] = "random" short_name: Literal["DUMMY_RANDOM"] = "DUMMY_RANDOM" name = "Dummy Random" description = "Dummy worker - random numbers" - parameters: Dict[str, CyclingParameter] = { + + class InternalParameters(BaseModel): + time: CyclingParameter[NonNegativeFloat] + delay: CyclingParameter[NonNegativeFloat] + + @property + def items(self): + return dict(self._iter()).items + + def __getitem__(self, item): + return getattr(self, item) + + #parameters: Dict[str, CyclingParameter] = { + parameters: InternalParameters = InternalParameters(**{ "time": CyclingParameter[NonNegativeFloat]( label = "Time:", units = "s", @@ -82,15 +133,32 @@ class DummyRandom(CyclingTechnique, extra=Extra.forbid): default_value = 1.0, required = True, ) - } + }) +################################################################################ class OpenCircuitVoltage(CyclingTechnique, extra=Extra.forbid): device: Literal["MPG2"] = "MPG2" technique: Literal["open_circuit_voltage"] = "open_circuit_voltage" short_name: Literal["OCV"] = "OCV" name = "OCV" description = "Open circuit voltage" - parameters: Dict[str, CyclingParameter] = { + + class InternalParameters(BaseModel): + time: CyclingParameter[NonNegativeFloat] + record_every_dt: CyclingParameter[NonNegativeFloat] + record_every_dE: CyclingParameter[NonNegativeFloat] + I_range: CyclingParameter[allowed_I_ranges] + E_range: CyclingParameter[allowed_E_ranges] + + @property + def items(self): + return dict(self._iter()).items + + def __getitem__(self, item): + return getattr(self, item) + + #parameters: Dict[str, CyclingParameter] = { + parameters: InternalParameters = InternalParameters(**{ "time": CyclingParameter[NonNegativeFloat]( label = "Time:", description = "The length of the OCV step", @@ -124,15 +192,40 @@ class OpenCircuitVoltage(CyclingTechnique, extra=Extra.forbid): default_value = "auto", required = True, ), - } + }) +################################################################################ class ConstantVoltage(CyclingTechnique, extra=Extra.forbid): device: Literal["MPG2"] = "MPG2" technique: Literal["constant_voltage"] = "constant_voltage" short_name: Literal["CV"] = "CV" name = "CV" description = "Controlled voltage technique, with optional current and voltage limits" - parameters: Dict[str, CyclingParameter] = { + + class InternalParameters(BaseModel): + time: CyclingParameter[NonNegativeFloat] + voltage: CyclingParameter[float] + record_every_dt: CyclingParameter[NonNegativeFloat] + record_every_dI: CyclingParameter[NonNegativeFloat] + I_range: CyclingParameter[allowed_I_ranges] + E_range: CyclingParameter[allowed_E_ranges] + n_cycles: CyclingParameter[NonNegativeInt] + is_delta: CyclingParameter[bool] + exit_on_limit: CyclingParameter[bool] + limit_voltage_max: CyclingParameter[float] + limit_voltage_min: CyclingParameter[float] + limit_current_max: CyclingParameter[Union[float, str]] + limit_current_min: CyclingParameter[Union[float, str]] + + @property + def items(self): + return dict(self._iter()).items + + def __getitem__(self, item): + return getattr(self, item) + + #parameters: Dict[str, CyclingParameter] = { + parameters: InternalParameters = InternalParameters(**{ "time": CyclingParameter[NonNegativeFloat]( label = "Time:", description = "Maximum duration of the CV step", @@ -215,15 +308,40 @@ class ConstantVoltage(CyclingTechnique, extra=Extra.forbid): units = "I", default_value = None ) - } + }) +################################################################################ class ConstantCurrent(CyclingTechnique, extra=Extra.forbid): device: Literal["MPG2"] = "MPG2" technique: Literal["constant_current"] = "constant_current" short_name: Literal["CC"] = "CC" name = "CC" description = "Controlled current technique, with optional voltage and current limits" - parameters: Dict[str, CyclingParameter] = { + + class InternalParameters(BaseModel): + time: CyclingParameter[NonNegativeFloat] + current: CyclingParameter[Union[float, str]] + record_every_dt: CyclingParameter[NonNegativeFloat] + record_every_dE: CyclingParameter[NonNegativeFloat] + I_range: CyclingParameter[allowed_I_ranges] + E_range: CyclingParameter[allowed_E_ranges] + n_cycles: CyclingParameter[NonNegativeInt] + is_delta: CyclingParameter[bool] + exit_on_limit: CyclingParameter[bool] + limit_voltage_max: CyclingParameter[float] + limit_voltage_min: CyclingParameter[float] + limit_current_max: CyclingParameter[Union[float, str]] + limit_current_min: CyclingParameter[Union[float, str]] + + @property + def items(self): + return dict(self._iter()).items + + def __getitem__(self, item): + return getattr(self, item) + + #parameters: Dict[str, CyclingParameter] = { + parameters: InternalParameters = InternalParameters(**{ "time": CyclingParameter[NonNegativeFloat]( label = "Time:", description = "Maximum duration of the CC step", @@ -306,27 +424,43 @@ class ConstantCurrent(CyclingTechnique, extra=Extra.forbid): units = "I", default_value = None ) - } + }) +################################################################################ #class SweepVoltage(CyclingTechnique, extra=Extra.forbid): # technique: Literal["sweep_voltage"] = "sweep_voltage" # short_name: Literal["LSV"] = "LSV" # name = "LSV" # description = "Controlled voltage technique, allowing linear change of voltage between pre-defined endpoints as a function of time, with optional current and voltage limits" +################################################################################ #class SweepCurrent(CyclingTechnique, extra=Extra.forbid): # technique: Literal["sweep_current"] = "sweep_current" # short_name: Literal["LSC"] = "LSC" # name = "" # description = "Controlled current technique, allowing linear change of current between pre-defined endpoints as a function of time, with optional current and voltage limits" +################################################################################ class Loop(CyclingTechnique, extra=Extra.forbid): device: Literal["MPG2"] = "MPG2" technique: Literal["loop"] = "loop" short_name: Literal["LOOP"] = "LOOP" name = "LOOP" description = "Loop technique, allowing to repeat a set of preceding techniques in a technique array" - parameters: Dict[str, CyclingParameter] = { + + class InternalParameters(BaseModel): + n_gotos: CyclingParameter[int] + goto: CyclingParameter[int] + + @property + def items(self): + return dict(self._iter()).items + + def __getitem__(self, item): + return getattr(self, item) + + #parameters: Dict[str, CyclingParameter] = { + parameters: InternalParameters = InternalParameters(**{ "n_gotos": CyclingParameter[int]( label = "Repeats", description = "Number of times the technique will jump; set to -1 for unlimited", @@ -339,7 +473,11 @@ class Loop(CyclingTechnique, extra=Extra.forbid): default_value = 1, required = True, ), - } + }) + +################################################################################ +# SEQUENCE OF TECHNIQUES +################################################################################ ElectroChemPayloads = Union[ DummySequential, From 634a37944f96f8cb0e172ebdc58e49af92a52a08 Mon Sep 17 00:00:00 2001 From: ramirezfranciscof Date: Sun, 4 Dec 2022 11:11:20 +0000 Subject: [PATCH 5/5] Add version number display --- aurora/__init__.py | 1 + aurora/interface/main.py | 3 ++- setup.cfg | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/aurora/__init__.py b/aurora/__init__.py index e69de29b..3d187266 100644 --- a/aurora/__init__.py +++ b/aurora/__init__.py @@ -0,0 +1 @@ +__version__ = "0.5.0" diff --git a/aurora/interface/main.py b/aurora/interface/main.py index e675c3d3..d1adc2c5 100644 --- a/aurora/interface/main.py +++ b/aurora/interface/main.py @@ -12,6 +12,7 @@ from aurora.schemas.utils import dict_to_formatted_json from aurora.models import BatteryExperimentModel +from aurora import __version__ CODE_NAME = "ketchup-0.2rc2" @@ -21,7 +22,7 @@ class MainPanel(ipw.VBox): _SAMPLE_INPUT_LABELS = ['Select from ID', 'Select from Specs', 'Make from Recipe'] _SAMPLE_INPUT_METHODS = ['id', 'specs', 'recipe'] _METHOD_LABELS = ['Standardized', 'Customized'] - w_header = ipw.HTML(value="

Aurora

") + w_header = ipw.HTML(value=f"

Aurora

\n Version {__version__}") _SAMPLE_BOX_LAYOUT = {'width': '90%', 'border': 'solid blue 2px', 'align_content': 'center', 'margin': '5px', 'padding': '5px'} _SUBMISSION_INPUT_LAYOUT = {'width': '90%', 'border': 'solid blue 1px', 'margin': '5px', 'padding': '5px', 'max_height': '500px', 'overflow': 'scroll'} _SUBMISSION_OUTPUT_LAYOUT = {'width': '90%', 'border': 'solid red 1px', 'margin': '5px', 'padding': '5px', 'max_height': '500px', 'overflow': 'scroll'} diff --git a/setup.cfg b/setup.cfg index 720b6ec4..fb515b79 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,8 @@ title = Aurora [metadata] name = aurora -version = 0.4.0 +version = attr: aurora.__version__ +#version = 0.4.0 author = Loris Ercole author_email = loris.ercole@epfl.ch description = An AiiDAlab application for the Aurora BIG-MAP Stakeholder initiative.