From 0f6fdd6d34f19bac546d6f8e326ddeed5af3723e Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 6 Jun 2023 17:50:32 -0500 Subject: [PATCH 001/125] temp save --- dashapp.py | 249 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 dashapp.py diff --git a/dashapp.py b/dashapp.py new file mode 100644 index 0000000..79bd847 --- /dev/null +++ b/dashapp.py @@ -0,0 +1,249 @@ +# import dash +# import dash_html_components as html +# import dash_core_components as dcc +# from dash.dependencies import Input, Output, State +# from pymongo import MongoClient + +# # MongoDB connection setup +# client = MongoClient('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority') +# db = client['diaogroup'] +# collection = db['recipes'] + +# app = dash.Dash(__name__) + +# app.layout = html.Div([ +# dcc.Input(id='document-name', type='text', placeholder='Enter document name'), +# dcc.Textarea(id='yaml-editor', style={'width': '100%', 'height': '400px'}), +# html.Button('Save Document', id='save-button', n_clicks=0), +# html.Div(id='save-message'), +# html.Hr(), +# dcc.Dropdown( +# id='document-dropdown', +# options=[], +# value='', +# placeholder='Select a document' +# ), +# html.Div(id='yaml-output') +# ]) + + +# @app.callback(Output('document-dropdown', 'options'), Output('document-dropdown', 'value'), +# [Input('document-dropdown', 'value'), Input('save-button', 'n_clicks')], +# [State('document-name', 'value'), State('yaml-editor', 'value')]) +# def update_document_dropdown(selected_document, save_clicks, document_name, yaml_content): +# # Fetch the list of documents from the MongoDB collection +# documents = collection.find({}, {"_id": 0, "name": 1}) +# options = [{'label': doc['name'], 'value': doc['name']} for doc in documents] + +# if selected_document not in [doc['value'] for doc in options]: +# selected_document = options[0]['value'] + +# if save_clicks > 0: +# # Insert the new document into the collection +# new_document = {'name': document_name, 'content': yaml_content} +# collection.insert_one(new_document) + +# return options, selected_document + + +# @app.callback(Output('yaml-editor', 'value'), Output('yaml-output', 'children'), +# [Input('document-dropdown', 'value')]) +# def update_yaml_editor(selected_document): +# # Fetch the selected document from the MongoDB collection +# document = collection.find_one({'name': selected_document}) + +# if document: +# # Extract the YAML content from the document +# yaml_content = document.get('content', '') + +# # Update the YAML output +# yaml_output = html.Pre(yaml_content) + +# return yaml_content, yaml_output + +# return '', '' + + +# @app.callback(Output('save-message', 'children'), +# [Input('save-button', 'n_clicks')], +# [State('document-name', 'value'), State('yaml-editor', 'value')]) +# def save_document(n_clicks, document_name, yaml_content): +# if n_clicks > 0: +# # Insert the new document into the collection +# new_document = {'name': document_name, 'content': yaml_content} +# collection.insert_one(new_document) +# return html.Div('Document saved successfully.') + +# return '' + + +# if __name__ == '__main__': +# app.run_server(debug=True) + + + + + +import dash +import dash_core_components as dcc +import dash_html_components as html +from dash import dash_table +from dash.dependencies import Input, Output, State +from pymongo import MongoClient +import json +from bson import ObjectId + +# MongoDB connection +client = MongoClient('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority') +db = client["diaogroup"] +collection = db["recipes2"] +# Initialize the Dash app +app = dash.Dash(__name__) + +# Define the layout +app.layout = html.Div( + children=[ + html.H1("CRUD App with MongoDB"), + html.Div( + children=[ + html.Div( + children=[ + html.H3("Create Document"), + dcc.Input(id="create-id-input", type="text", placeholder="Enter document ID"), + dcc.Input(id="create-input", type="text", placeholder="Enter document data"), + html.Button("Create", id="create-button", n_clicks=0), + ], + className="crud-item", + ), + html.Div( + children=[ + html.H3("Existing Documents"), + html.Button("Refresh", id="refresh-button", n_clicks=0), + dash_table.DataTable( + id="documents-table", + columns=[ + {"name": "ID", "id": "identifier"}, + {"name": "Data", "id": "data"}, + ], + data=[], + editable=False, + row_selectable="single", + style_cell={"textAlign": "left"}, + style_data={"whiteSpace": "normal", "height": "auto"}, + ), + ], + className="crud-item", + ), + html.Div( + children=[ + html.H3("Selected Document"), + html.Div(id="selected-document-output"), + html.Button("Read", id="read-button", n_clicks=0), + html.Button("Update", id="update-button", n_clicks=0), + html.Button("Delete", id="delete-button", n_clicks=0), + ], + className="crud-item", + ), + ], + className="crud-container", + ), + ] +) +# Update the documents table with existing documents +@app.callback( + Output("documents-table", "data"), + Output("selected-document-output", "children"), + Input("create-button", "n_clicks"), + Input("read-button", "n_clicks"), + Input("update-button", "n_clicks"), + Input("delete-button", "n_clicks"), + Input("refresh-button", "n_clicks"), + State("create-id-input", "value"), + State("create-input", "value"), + State("documents-table", "selected_rows"), + State("documents-table", "data"), +) +def update_documents_table( + create_n_clicks, + read_n_clicks, + update_n_clicks, + delete_n_clicks, + refresh_n_clicks, + id_input_value, + input_value, + selected_rows, + documents, +): + ctx = dash.callback_context + triggered_button_id = ctx.triggered[0]["prop_id"].split(".")[0] + + if triggered_button_id == "create-button" and create_n_clicks > 0 and id_input_value and input_value: + document = {"identifier": id_input_value, "data": input_value} + collection.insert_one(document) + + if triggered_button_id == "refresh-button" and refresh_n_clicks > 0: + documents = list(collection.find()) + + # Convert ObjectId values to strings + for document in documents: + document["_id"] = str(document["_id"]) + + data = [] + if documents: + data = json.loads(json.dumps(documents)) + + selected_document_output = "" + if selected_rows: + selected_document = documents[selected_rows[0]] + selected_document_output = html.Div( + [ + html.H4("Selected Document"), + html.P(f"ID: {selected_document['identifier']}"), + html.P(f"Data: {selected_document['data']}"), + ] + ) + + if triggered_button_id == "read-button" and read_n_clicks > 0 and selected_rows: + selected_document = documents[selected_rows[0]] + selected_document_output = html.Div( + [ + html.H4("Selected Document"), + html.P(f"ID: {selected_document['identifier']}"), + html.P(f"Data: {selected_document['data']}"), + html.P(f"Data3: {selected_document.get('data3', '')}"), + ] + ) + + if triggered_button_id == "update-button" and update_n_clicks > 0 and selected_rows: + selected_document = documents[selected_rows[0]] + # Implement your update logic here + # For example, update the 'data' field with the new value + # new_data_value = "New Value" + # collection.update_one( + # {"identifier": selected_document["identifier"]}, + # {"$set": {"data": new_data_value}}, + # ) + selected_document_output = html.Div( + [ + html.H4("Selected Document"), + html.P(f"ID: {selected_document['identifier']}"), + html.P(f"Data: {selected_document['data']}"), + ] + ) + + if triggered_button_id == "delete-button" and delete_n_clicks > 0 and selected_rows: + selected_document = documents[selected_rows[0]] + collection.delete_one({"identifier": selected_document["identifier"]}) + documents = list(collection.find()) + # Convert ObjectId values to strings + for document in documents: + document["_id"] = str(document["_id"]) + data = json.loads(json.dumps(documents)) + selected_document_output = "" + + return data, selected_document_output + + + +if __name__ == "__main__": + app.run_server(debug=True) From a89821714560fd1b5d8f301863c78c37dd9865ae Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 9 Jun 2023 13:29:06 -0500 Subject: [PATCH 002/125] devices showing up in table --- command_sequence.py | 14 ++ devices/device.py | 11 ++ devices/dummy_heater.py | 7 + devices/dummy_meter.py | 36 ++++++ devices/dummy_motor.py | 7 + recipe_tool.py | 19 +-- temp1.py | 275 ++++++++++++++++++++++++++++++++++++++++ util.py | 47 +++++++ 8 files changed, 402 insertions(+), 14 deletions(-) create mode 100644 temp1.py create mode 100644 util.py diff --git a/command_sequence.py b/command_sequence.py index 6218bec..cc03138 100644 --- a/command_sequence.py +++ b/command_sequence.py @@ -8,6 +8,7 @@ from commands.command import Command from devices.device import Device from commands.utility_commands import LoopStartCommand, LoopEndCommand +import util # Representer.add_representer(ABCMeta, Representer.represent_name) @@ -531,3 +532,16 @@ def remove_all_loop_commands(self): # if inner broke, then break outer loop to start over # https://stackoverflow.com/questions/189645/how-to-break-out-of-multiple-loops break + + def get_clean_device_list(self): + """Returns a list for use with the dashboard.""" + device_list = self.get_device_names_classes().copy() + device_list_ret = [] + for index, device in enumerate(device_list): + device_list_temp = [] + device_list_temp.append(device_list[index][1]) + device_list_temp.append(util.device_to_dict(self.device_list[index])) + # device_list[index].append(util.device_to_dict(self.device_list[index])) + device_list_ret.append(device_list_temp) + return device_list_ret + \ No newline at end of file diff --git a/devices/device.py b/devices/device.py index 38865c7..9868253 100644 --- a/devices/device.py +++ b/devices/device.py @@ -94,6 +94,17 @@ def deinitialize(self) -> Tuple[bool, str]: """ pass + # @abstractmethod TODO uncomment and implement method in all devices + def get_args(self) -> dict: + """The get_args abstract method that all devices should implement. Method should return a dict with only the arguments needed to initialize the device. + + Returns + ------- + dict + Returns a dict containing the arguments needed to initialize the device. + """ + pass + class SerialDevice(Device): """A Device that uses serial communication.""" diff --git a/devices/dummy_heater.py b/devices/dummy_heater.py index e93b79d..28dc7e6 100644 --- a/devices/dummy_heater.py +++ b/devices/dummy_heater.py @@ -17,6 +17,13 @@ def __init__(self, name: str, heat_rate: float = 20.0): self._temperature = random.uniform(self.min_temperature, self.max_temperature) self._hardware_interval = 0.05 + def get_args(self) -> dict: + args_dict = { + "name": self._name, + "heat_rate": self._heat_rate, + } + return args_dict + @property def temperature(self) -> float: return self._temperature diff --git a/devices/dummy_meter.py b/devices/dummy_meter.py index 129ef67..bfb2387 100644 --- a/devices/dummy_meter.py +++ b/devices/dummy_meter.py @@ -44,6 +44,42 @@ def __init__( self.b2 = b2 self.noise_width = noise_width + def __init__( + self, + name: str, + heater: dict, + motor: dict, + a1: List[float], + b1: List[float], + a2: List[float], + b2: List[float], + noise_width: float = 0.0 + ): + super().__init__(name) + # All arguments except 'name' are only for emulation purposes + self.heater = DummyHeater(**heater) + self.motor = DummyMotor(**motor) + self.x1_range = (self.heater.min_temperature, self.heater.max_temperature) + self.x2_range = (self.motor.motor.min_speed, self.motor.motor.max_speed) + self.a1 = a1 + self.b1 = b1 + self.a2 = a2 + self.b2 = b2 + self.noise_width = noise_width + + def get_args(self) -> dict: + args_dict = { + "name": self._name, + "heater": self.heater.get_args(), + "motor": self.motor.get_args(), + "a1": self.a1, + "b1": self.b1, + "a2": self.a2, + "b2": self.b2, + "noise_width": self.noise_width, + } + return args_dict + def initialize(self): self._is_initialized = True return (True, "Initialized DummyMeter") diff --git a/devices/dummy_motor.py b/devices/dummy_motor.py index 432187e..ebe3e33 100644 --- a/devices/dummy_motor.py +++ b/devices/dummy_motor.py @@ -12,6 +12,13 @@ def __init__(self, name: str, speed: float = 20.0): # Using composition instead of multiple inheritance self.motor = DummyMotorSource(speed) + def get_args(self) -> dict: + args_dict = { + "name": self._name, + "speed": self.motor.speed, + } + return args_dict + @property def position(self) -> float: return self.motor.position diff --git a/recipe_tool.py b/recipe_tool.py index 25adac5..b28ab42 100644 --- a/recipe_tool.py +++ b/recipe_tool.py @@ -23,11 +23,13 @@ from devices.heating_stage import HeatingStage from devices.multi_stepper import MultiStepper from devices.newport_esp301 import NewportESP301 -from devices.stellarnet_spectrometer import StellarNetSpectrometer -from devices.ximea_camera import XimeaCamera +# from devices.stellarnet_spectrometer import StellarNetSpectrometer +# from devices.ximea_camera import XimeaCamera from devices.dummy_heater import DummyHeater from devices.dummy_motor import DummyMotor +import project_const + # TODO # Clean up comments @@ -36,18 +38,7 @@ # add compatibility with composite and utility commands (try to avoid coding speciific class dependencies) #================ Constants ============================= -named_devices = { - "PrintingStage": HeatingStage, - "AnnealingStage": HeatingStage, - "MultiStepper1": MultiStepper, - "PrinterMotorX": NewportESP301, - "Spectrometer": StellarNetSpectrometer, - "SampleCamera": XimeaCamera, - "DummyHeater1": DummyHeater, - "DummyHeater2": DummyHeater, - "DummyMotor1": DummyMotor, - "DummyMotor2": DummyMotor, - } +named_devices = project_const.named_devices command_directory = "commands/" load_directory = "recipes/user_recipes/" save_directory = "recipes/user_recipes/" diff --git a/temp1.py b/temp1.py new file mode 100644 index 0000000..26706a5 --- /dev/null +++ b/temp1.py @@ -0,0 +1,275 @@ +from command_sequence import CommandSequence +import project_const + + +com = CommandSequence() + + +print() +print() + +com.load_from_yaml("e1.yaml") +# print(com.device_list[0].__dict__) + + +# print(com.device_list[0].__dict__) +# print(com.device_list[1]) +# print(type(com.command_list[0][0].__dict__)) + + +# print(com.get_device_names_classes()) +print(com.get_clean_device_list()) +# quit() +# print(com.get_command_names()) + +# temp = { +# "_name": "heater1", +# "_is_initialized": False, +# "_heat_rate": 20.0, +# "min_heat_rate": 1.0, +# "max_heat_rate": 50.0, +# "min_temperature": 25.0, +# "max_temperature": 100.0, +# "_temperature": 95.71927572168929, +# "_hardware_interval": 0.05, +# # "name": "heater1", +# } +# print("hello") +# print(temp) + +import util + +# out = util.dict_to_device(com.device_list[1].__dict__, com.get_device_names_classes()[1][1]) +# out = util.dict_to_device(com.device_list[0], com.get_device_names_classes()[0][1]) + +out = util.device_to_dict(com.device_list[0]) + + +print("\n\nresult:\n\n") +print(out) +# quit() + + +# device_cls = project_const.named_devices[com.get_device_names_classes()[0][1]] +# arg_dict = temp +# com.add_device(device_cls(**arg_dict)) +# print(com.add_device(device_cls(**arg_dict))) + +# quit() + +import dash +import dash_core_components as dcc +import dash_html_components as html +import dash_table +from dash.dependencies import Input, Output, State +import random + +app = dash.Dash(__name__) + +# Sample data lists +data_list1 = [ + {"Name": "John", "Value": 25}, + {"Name": "Amy", "Value": 31}, + {"Name": "David", "Value": 28}, +] +data_list2 = [ + {"Name": "Apple", "Value": 10}, + {"Name": "Banana", "Value": 5}, + {"Name": "Orange", "Value": 8}, +] +data_list3 = [ + {"Name": "Red", "Value": 15}, + {"Name": "Green", "Value": 20}, + {"Name": "Blue", "Value": 12}, +] + +app.layout = html.Div( + [ + html.H1("Dash Tables"), + html.Div( + [ + html.Div( + [ + html.H2("Devices"), + html.Button("Refresh", id="refresh-button1", n_clicks=0), + html.Div(id="table-container1"), + ], + className="table-container", + ), + html.Div( + [ + html.H2("Table 2"), + html.Button("Refresh", id="refresh-button2", n_clicks=0), + html.Div(id="table-container2"), + ], + className="table-container", + ), + html.Div( + [ + html.H2("Table 3"), + html.Button("Refresh", id="refresh-button3", n_clicks=0), + html.Div(id="table-container3"), + ], + className="table-container", + ), + ], + className="tables-container", + ), + ], + className="main-container", +) + +data_list4 = com.device_list +dl5_og = [ + { + "_name": "heater1", + "_is_initialized": False, + "_heat_rate": 20.0, + "min_heat_rate": 1.0, + "max_heat_rate": 50.0, + "min_temperature": 25.0, + "max_temperature": 100.0, + "_temperature": 95.71927572168929, + "_hardware_interval": 0.05, + }, + { + "_name": "motor1", + "_is_initialized": False, + "motor": { + "_speed": 20.0, + "min_speed": 1.0, + "max_speed": 50.0, + "min_position": 0.0, + "max_position": 100.0, + "_position": 37.44120879993549, + "_hardware_interval": 0.05, + }, + }, + { + "_name": "motor2", + "_is_initialized": False, + "motor": { + "_speed": 20.0, + "min_speed": 1.0, + "max_speed": 50.0, + "min_position": 0.0, + "max_position": 100.0, + "_position": 82.48283286198111, + "_hardware_interval": 0.05, + }, + }, +] +# dl5 = [] +# for list in data_list4: +# dl5.append(list.__dict__) + +print() +print() +print() +# print(dl5[2]['motor'].__dict__) + + +@app.callback( + Output("table-container1", "children"), + [Input("refresh-button1", "n_clicks")], + [State("table-container1", "children")], +) +def update_table1(n_clicks, table): + dl5 = dl5_og.copy() + dl5_props_temp = {} + count = 0 + for list in dl5: + dl5_props_temp.clear() + if len(list) > 1: + for prop in list: + if prop != "_name": + dl5_props_temp[prop] = list[prop] + # print(prop) + # print("h") + dl5_temp_list = list + dl5[count] = {} + dl5[count]["_name"] = dl5_temp_list["_name"] + # dl5[count]["_is_initialized"] = dl5_temp_list["_is_initialized"] + dl5[count]["props"] = str(dl5_props_temp) + # print(str(dl5_props_temp)) + count += 1 + + # table_data1 = dl5 + table_data1 = com.get_clean_device_list().copy() + + table_data1_new = [] + for index, list in enumerate(table_data1): + # table_data1[index][1].update({"device_type": table_data1[index][0]}) + # del table_data1[index][0] + table_data1_new.append({"device_type": table_data1[index][0], "props": str(table_data1[index][1])}) + + table_data1 = table_data1_new + # print(table_data1) + table = dash_table.DataTable( + data=table_data1, + columns=[ + {"name": "Type", "id": "device_type"}, + # {"name": "Initialized", "id": "_is_initialized"}, + {"name": "Properties", "id": "props"}, + ], + # style_data_conditional=[ + # {"if": {"column_id": "_name"}, "width": "250px", 'overflow': 'hidden', 'textOverflow': 'ellipsis'}, + # # {"if": {"column_id": "_is_initialized"}, "width": "20%"}, + # # {"if": {"column_id": "props"}, "width": "100px", 'overflow': 'hidden', 'textOverflow': 'ellipsis'}, + # ], + # style_cell={"textAlign": "left", "padding": "5px"}, + style_cell={ + "overflow": "hidden", + "textOverflow": "ellipsis", + "maxWidth": 0, + "textAlign": "left", + "padding": "5px", + }, + style_cell_conditional=[ + {"if": {"column_id": "_name"}, "width": "20%"}, + {"if": {"column_id": "props"}, "width": "80%"}, + ], + tooltip_data=[ + { + column: {"value": str(value), "type": "markdown"} + for column, value in row.items() + } + for row in table_data1 + ], + tooltip_duration=None, + editable = True, + ) + print("done") + return table + + +# @app.callback( +# Output("table-container2", "children"), +# [Input("refresh-button2", "n_clicks")], +# [State("table-container2", "children")], +# ) +# def update_table2(n_clicks, table): +# table_data2 = data_list2 +# table = dash_table.DataTable( +# data=table_data2, +# columns=[{"name": "Name", "id": "Name"}, {"name": "Value", "id": "Value"}], +# ) +# return table + + +# @app.callback( +# Output("table-container3", "children"), +# [Input("refresh-button3", "n_clicks")], +# [State("table-container3", "children")], +# ) +# def update_table3(n_clicks, table): +# table_data3 = data_list3 +# table = dash_table.DataTable( +# data=table_data3, +# columns=[{"name": "Name", "id": "Name"}, {"name": "Value", "id": "Value"}], +# ) +# return table + + +if __name__ == "__main__": + app.run_server(debug=True) diff --git a/util.py b/util.py new file mode 100644 index 0000000..da402f9 --- /dev/null +++ b/util.py @@ -0,0 +1,47 @@ +from commands.command import Command +from commands.utility_commands import LoopStartCommand, LoopEndCommand +from devices.heating_stage import HeatingStage +from devices.multi_stepper import MultiStepper +from devices.newport_esp301 import NewportESP301 +# from devices.stellarnet_spectrometer import StellarNetSpectrometer +# from devices.ximea_camera import XimeaCamera +from devices.dummy_heater import DummyHeater +from devices.dummy_motor import DummyMotor +from devices.device import Device + + + +named_devices = { + "PrintingStage": HeatingStage, + "AnnealingStage": HeatingStage, + "MultiStepper1": MultiStepper, + "PrinterMotorX": NewportESP301, + # "Spectrometer": StellarNetSpectrometer, + # "SampleCamera": XimeaCamera, + "DummyHeater": DummyHeater, + "DummyHeater1": DummyHeater, + "DummyHeater2": DummyHeater, + "DummyMotor": DummyMotor, + "DummyMotor1": DummyMotor, + "DummyMotor2": DummyMotor, + } +command_directory = "commands/" +approved_devices = list(named_devices.keys()) + +device_init_args = { + "DummyHeater": ["name", "heat_rate"], + "DummyMotor": ["name", "speed"], +} + +def dict_to_device(device: Device, type: str): + device_cls = named_devices[type] + arg_dict = device.get_args() + + # for attr in device_init_args[type]: + # arg_dict[attr] = dict["_"+attr] + + # print(arg_dict) + return device_cls(**arg_dict) + +def device_to_dict(device: Device): + return device.get_args() \ No newline at end of file From 415e519ba42421ee9974d3119a0b041433e3a6eb Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 13 Jun 2023 17:35:04 -0500 Subject: [PATCH 003/125] progress june 13 --- commands/command.py | 10 ++ devices/device.py | 10 +- devices/dummy_heater.py | 2 +- devices/dummy_motor_source.py | 3 +- temp1.py | 216 +++++++++++++++++++++++++++++----- util.py | 25 ++-- 6 files changed, 227 insertions(+), 39 deletions(-) diff --git a/commands/command.py b/commands/command.py index 9a917cd..c83f2b2 100644 --- a/commands/command.py +++ b/commands/command.py @@ -101,6 +101,16 @@ def result(self): # """ # return self._result_message + def get_init_args(self) -> dict: + """Get the command's initialization arguments. + + Returns + ------- + dict + Returns a dictionary of the command's initialization arguments. + """ + return self._params + # store info like name and description in here too? or leave separate? Leave separate, semantically i think it makes sense # to retrive non-result info from the command instead of the commandresult object, potentially avoid conflicting info too # Mainly adding to future proof in case more info needs to be retrieved after execution diff --git a/devices/device.py b/devices/device.py index 9868253..05dec45 100644 --- a/devices/device.py +++ b/devices/device.py @@ -6,6 +6,7 @@ import serial except ImportError: pass +import json # Decorator to check if is initialized, optional custom message on fail @@ -95,7 +96,7 @@ def deinitialize(self) -> Tuple[bool, str]: pass # @abstractmethod TODO uncomment and implement method in all devices - def get_args(self) -> dict: + def get_init_args(self) -> dict: """The get_args abstract method that all devices should implement. Method should return a dict with only the arguments needed to initialize the device. Returns @@ -302,4 +303,9 @@ def parse_equal_sign(text: str) -> Tuple[bool, str]: last_token = text.split("=")[-1] return (True, last_token.strip()) else: - return (False, "") \ No newline at end of file + return (False, "") + + +class MiscDeviceClass(): + def exists(): + return True \ No newline at end of file diff --git a/devices/dummy_heater.py b/devices/dummy_heater.py index 28dc7e6..2d99f0f 100644 --- a/devices/dummy_heater.py +++ b/devices/dummy_heater.py @@ -17,7 +17,7 @@ def __init__(self, name: str, heat_rate: float = 20.0): self._temperature = random.uniform(self.min_temperature, self.max_temperature) self._hardware_interval = 0.05 - def get_args(self) -> dict: + def get_init_args(self) -> dict: args_dict = { "name": self._name, "heat_rate": self._heat_rate, diff --git a/devices/dummy_motor_source.py b/devices/dummy_motor_source.py index 0aac591..6e0caea 100644 --- a/devices/dummy_motor_source.py +++ b/devices/dummy_motor_source.py @@ -1,7 +1,8 @@ import threading import random +from devices.device import MiscDeviceClass -class DummyMotorSource(): +class DummyMotorSource(MiscDeviceClass): def __init__(self, speed: float = 20.0): self._speed = speed diff --git a/temp1.py b/temp1.py index 26706a5..cbfadb3 100644 --- a/temp1.py +++ b/temp1.py @@ -1,5 +1,8 @@ from command_sequence import CommandSequence import project_const +import json +from devices.device import Device +from commands.command import Command, CompositeCommand com = CommandSequence() @@ -18,7 +21,7 @@ # print(com.get_device_names_classes()) -print(com.get_clean_device_list()) +# print(com.get_clean_device_list()) # quit() # print(com.get_command_names()) @@ -45,8 +48,8 @@ out = util.device_to_dict(com.device_list[0]) -print("\n\nresult:\n\n") -print(out) +# print("\n\nresult:\n\n") +# print(out) # quit() @@ -58,13 +61,14 @@ # quit() import dash -import dash_core_components as dcc -import dash_html_components as html -import dash_table +from dash import dcc +from dash import html +from dash import dash_table from dash.dependencies import Input, Output, State import random +import dash_bootstrap_components as dbc -app = dash.Dash(__name__) +app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) # Sample data lists data_list1 = [ @@ -83,30 +87,56 @@ {"Name": "Blue", "Value": 12}, ] + app.layout = html.Div( [ - html.H1("Dash Tables"), + html.H1("Edit Recipe"), html.Div( [ html.Div( [ html.H2("Devices"), - html.Button("Refresh", id="refresh-button1", n_clicks=0), + dbc.Button("Refresh", id="refresh-button1", n_clicks=0), + dbc.Button("Open editor", id="open-editor"), + dbc.Button("Add device", id="add-device-button"), + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Editor"), close_button=False + ), + dbc.ModalBody("this will be the editor"), + dbc.ModalFooter(dbc.Button("Save", id="save-editor")), + ], + id="editor-modal", + keyboard=False, + backdrop="static", + ), html.Div(id="table-container1"), ], className="table-container", ), html.Div( [ - html.H2("Table 2"), - html.Button("Refresh", id="refresh-button2", n_clicks=0), + html.H2("Commands"), + dbc.Button("Refresh", id="refresh-button2", n_clicks=0), + dbc.Button("Add command", id="add-command-button"), html.Div(id="table-container2"), + dbc.Accordion( + [ + dbc.AccordionItem( + "item1", title="Item 1", item_id="item1" + ) + ], + id="commands-accordion", + # start_collapsed=True, + style={"display": "none"}, + ), ], className="table-container", ), html.Div( [ - html.H2("Table 3"), + html.H2("Command Iterations"), html.Button("Refresh", id="refresh-button3", n_clicks=0), html.Div(id="table-container3"), ], @@ -196,15 +226,17 @@ def update_table1(n_clicks, table): # table_data1 = dl5 table_data1 = com.get_clean_device_list().copy() - + table_data1_new = [] for index, list in enumerate(table_data1): # table_data1[index][1].update({"device_type": table_data1[index][0]}) # del table_data1[index][0] - table_data1_new.append({"device_type": table_data1[index][0], "props": str(table_data1[index][1])}) - + table_data1_new.append( + {"device_type": table_data1[index][0], "props": str(table_data1[index][1])} + ) + table_data1 = table_data1_new - # print(table_data1) + print(table_data1) table = dash_table.DataTable( data=table_data1, columns=[ @@ -237,24 +269,152 @@ def update_table1(n_clicks, table): for row in table_data1 ], tooltip_duration=None, - editable = True, + # editable = True, ) print("done") return table +@app.callback( + Output("editor-modal", "is_open"), + [Input("open-editor", "n_clicks"), Input("save-editor", "n_clicks")], + [State("editor-modal", "is_open")], +) +def toggle_editor_modal(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + + # @app.callback( -# Output("table-container2", "children"), -# [Input("refresh-button2", "n_clicks")], -# [State("table-container2", "children")], +# Output("commands-accordion", "children"), +# Input("add-command-button", "n_clicks"), +# State("commands-accordion", "children"), +# prevent_initial_call=True, +# allow_duplicate=True # ) -# def update_table2(n_clicks, table): -# table_data2 = data_list2 -# table = dash_table.DataTable( -# data=table_data2, -# columns=[{"name": "Name", "id": "Name"}, {"name": "Value", "id": "Value"}], -# ) -# return table +# def add_command_accordian(n_clicks, children): +# children=[ +# dbc.AccordionItem( +# "new item", title="new title", item_id="new") +# ] + +# return children + + +@app.callback( + Output("commands-accordion", "children"), + Input("refresh-button2", "n_clicks"), + State("commands-accordion", "children"), + # allow_duplicate=True +) +def load_commands_accordion(n_clicks, children): + children = [] + print() + command_list = com.get_unlooped_command_list().copy() + # print(command_list) + # for command in command_list: + # if isinstance(command, CompositeCommand): + # for sub_command in command._command_list: + # sub_command._receiver = sub_command._receiver._name + # sub_command = sub_command.__dict__ + # else: + # # print(command.__dict__) + # command._receiver = command._receiver._name + command_params = [] + for command in command_list: + # if isinstance(command, CompositeCommand): + # print(type(command).__name__) + temp_dict_command_params = {"command":type(command).__name__} + temp_dict_command_params.update({"params":command.get_init_args()}) + command_params.append(temp_dict_command_params) + # print(command._params) + # else: + # command_params.append(command._params) + # print(command_params) + # print(com.get_command_names()) + for index, command in enumerate(command_params): + # print(command) + children.append( + dbc.AccordionItem( + dcc.Markdown( + children=[ + "**Command Object:**", + "```json", + json.dumps(command, indent=4, cls=util.Encoder), + # str(command.__dict__), + "```" + ], + ), + # str(command.__dict__), + title=command['command'], + item_id=command['command']+str(index), + ) + ) + return children + + +@app.callback( + Output("table-container2", "children"), + [Input("refresh-button2", "n_clicks")], + [State("table-container2", "children")], +) +def update_table2(n_clicks, table): + command_list = com.get_unlooped_command_list().copy() + # print(command_list) + # for command in command_list: + # if isinstance(command, CompositeCommand): + # for sub_command in command._command_list: + # sub_command._receiver = sub_command._receiver._name + # sub_command = sub_command.__dict__ + # else: + # # print(command.__dict__) + # command._receiver = command._receiver._name + command_params = [] + for command in command_list: + # if isinstance(command, CompositeCommand): + # print(type(command).__name__) + temp_dict_command_params = {"command":type(command).__name__} + temp_dict_command_params.update({"params":str(command.get_init_args())}) + command_params.append((temp_dict_command_params)) + # print(command._params) + # else: + # command_params.append(command._params) + # print(command_params) + + print(command_params) + table_data2 = command_params + # print(com.get_unlooped_command_list().copy()[3].__dict__) + # add_command_accordian(0, to_add=[dbc.AccordionItem("new new", title="new new", item_id="new new")]) + + table = dash_table.DataTable( + data=table_data2, + columns=[ + {"name": "Command", "id": "command"}, + {"name": "Parameters", "id": "params"}, + ], + style_cell={ + "overflow": "hidden", + "textOverflow": "ellipsis", + "maxWidth": 0, + "textAlign": "left", + "padding": "5px", + }, + style_cell_conditional=[ + {"if": {"column_id": "command"}, "width": "20%"}, + {"if": {"column_id": "params"}, "width": "80%"}, + ], + # tooltip_data=[ + # { + # column: {"value": str(value), "type": "markdown"} + # for column, value in row.items() + # } + # for row in table_data2 + # ], + # tooltip_duration=None, + # editable = True, + ) + return table # @app.callback( @@ -273,3 +433,5 @@ def update_table1(n_clicks, table): if __name__ == "__main__": app.run_server(debug=True) + + diff --git a/util.py b/util.py index da402f9..2d3956f 100644 --- a/util.py +++ b/util.py @@ -7,8 +7,9 @@ # from devices.ximea_camera import XimeaCamera from devices.dummy_heater import DummyHeater from devices.dummy_motor import DummyMotor -from devices.device import Device - +from devices.device import Device, MiscDeviceClass +import json +import numpy as np named_devices = { @@ -28,14 +29,14 @@ command_directory = "commands/" approved_devices = list(named_devices.keys()) -device_init_args = { - "DummyHeater": ["name", "heat_rate"], - "DummyMotor": ["name", "speed"], -} +# device_init_args = { +# "DummyHeater": ["name", "heat_rate"], +# "DummyMotor": ["name", "speed"], +# } def dict_to_device(device: Device, type: str): device_cls = named_devices[type] - arg_dict = device.get_args() + arg_dict = device.get_init_args() # for attr in device_init_args[type]: # arg_dict[attr] = dict["_"+attr] @@ -44,4 +45,12 @@ def dict_to_device(device: Device, type: str): return device_cls(**arg_dict) def device_to_dict(device: Device): - return device.get_args() \ No newline at end of file + return device.get_init_args() + +class Encoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Device) or isinstance(obj, Command) or isinstance(obj, MiscDeviceClass): + return obj.__dict__ + elif isinstance(obj, np.ndarray): + return obj.tolist() + return super().default(obj) \ No newline at end of file From b152a86787bbbdaa782908da51f3a0309640c870 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 15 Jun 2023 14:37:48 -0500 Subject: [PATCH 004/125] progress june 15 midday --- commands/dummy_motor_commands.py | 2 + devices/device.py | 12 + devices/dummy_heater.py | 5 + devices/dummy_motor.py | 8 +- temp1.py | 485 ++++++++++++++++++++----------- util.py | 4 + 6 files changed, 349 insertions(+), 167 deletions(-) diff --git a/commands/dummy_motor_commands.py b/commands/dummy_motor_commands.py index ff34a9c..53c3b8e 100644 --- a/commands/dummy_motor_commands.py +++ b/commands/dummy_motor_commands.py @@ -74,6 +74,8 @@ def __init__(self, receiver: DummyMotor, speed: float, position: float, **kwargs self.add_command(DummyMotorSetSpeed(receiver, speed)) self.add_command(DummyMotorMoveAbsolute(receiver, position)) self.add_command(DummyMotorSetSpeed(receiver, original_speed)) + self._params['speed'] = speed + self._params["position"] = position class DummyMotorMultiMoveAbsolute(CompositeCommand): """Move a list of motors to a list of position at a particular speed""" diff --git a/devices/device.py b/devices/device.py index 05dec45..83cec59 100644 --- a/devices/device.py +++ b/devices/device.py @@ -106,6 +106,18 @@ def get_init_args(self) -> dict: """ pass + # @abstractmethod # TODO uncomment and implement method in all devices + def update_init_args(self, args_dict: dict): + """The update_init_args abstract method that all devices should implement. Method should update the arguments needed to initialize the device. + + Parameters + ---------- + + args_dict : dict + A dict containing the arguments needed to initialize the device. + """ + pass + class SerialDevice(Device): """A Device that uses serial communication.""" diff --git a/devices/dummy_heater.py b/devices/dummy_heater.py index 2d99f0f..c904001 100644 --- a/devices/dummy_heater.py +++ b/devices/dummy_heater.py @@ -23,6 +23,11 @@ def get_init_args(self) -> dict: "heat_rate": self._heat_rate, } return args_dict + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._heat_rate = args_dict["heat_rate"] + @property def temperature(self) -> float: diff --git a/devices/dummy_motor.py b/devices/dummy_motor.py index ebe3e33..fd08eaa 100644 --- a/devices/dummy_motor.py +++ b/devices/dummy_motor.py @@ -12,12 +12,16 @@ def __init__(self, name: str, speed: float = 20.0): # Using composition instead of multiple inheritance self.motor = DummyMotorSource(speed) - def get_args(self) -> dict: + def get_init_args(self) -> dict: args_dict = { "name": self._name, - "speed": self.motor.speed, + "speed": self.motor._speed, } return args_dict + + def update_init_args(self, args_dict: dict): + self.motor._speed = args_dict["speed"] + self._name = args_dict["name"] @property def position(self) -> float: diff --git a/temp1.py b/temp1.py index cbfadb3..eee0a9f 100644 --- a/temp1.py +++ b/temp1.py @@ -8,57 +8,16 @@ com = CommandSequence() -print() -print() com.load_from_yaml("e1.yaml") -# print(com.device_list[0].__dict__) - - -# print(com.device_list[0].__dict__) -# print(com.device_list[1]) -# print(type(com.command_list[0][0].__dict__)) - - -# print(com.get_device_names_classes()) -# print(com.get_clean_device_list()) -# quit() -# print(com.get_command_names()) - -# temp = { -# "_name": "heater1", -# "_is_initialized": False, -# "_heat_rate": 20.0, -# "min_heat_rate": 1.0, -# "max_heat_rate": 50.0, -# "min_temperature": 25.0, -# "max_temperature": 100.0, -# "_temperature": 95.71927572168929, -# "_hardware_interval": 0.05, -# # "name": "heater1", -# } -# print("hello") -# print(temp) - -import util - -# out = util.dict_to_device(com.device_list[1].__dict__, com.get_device_names_classes()[1][1]) -# out = util.dict_to_device(com.device_list[0], com.get_device_names_classes()[0][1]) - -out = util.device_to_dict(com.device_list[0]) -# print("\n\nresult:\n\n") -# print(out) -# quit() +import util +import ctypes -# device_cls = project_const.named_devices[com.get_device_names_classes()[0][1]] -# arg_dict = temp -# com.add_device(device_cls(**arg_dict)) -# print(com.add_device(device_cls(**arg_dict))) -# quit() +out = util.device_to_dict(com.device_list[0]) import dash from dash import dcc @@ -70,25 +29,24 @@ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) -# Sample data lists -data_list1 = [ - {"Name": "John", "Value": 25}, - {"Name": "Amy", "Value": 31}, - {"Name": "David", "Value": 28}, -] -data_list2 = [ - {"Name": "Apple", "Value": 10}, - {"Name": "Banana", "Value": 5}, - {"Name": "Orange", "Value": 8}, -] -data_list3 = [ - {"Name": "Red", "Value": 15}, - {"Name": "Green", "Value": 20}, - {"Name": "Blue", "Value": 12}, -] - - -app.layout = html.Div( +navbar = dbc.NavbarSimple( + children = [ + dbc.NavItem(dbc.NavLink("Home", href="/")), + dbc.NavItem(dbc.NavLink("Edit Recipe", href="/edit-recipe")), + ], + brand = "AAMP", + brand_href = "/", + color="primary", + dark=True +) + +home_layout = html.Div( + [ + html.H1("Home"), + ] +) + +edit_recipe_layout = html.Div( [ html.H1("Edit Recipe"), html.Div( @@ -97,21 +55,45 @@ [ html.H2("Devices"), dbc.Button("Refresh", id="refresh-button1", n_clicks=0), - dbc.Button("Open editor", id="open-editor"), dbc.Button("Add device", id="add-device-button"), + dbc.Button("Edit", id="edit-device-button"), dbc.Modal( [ dbc.ModalHeader( dbc.ModalTitle("Editor"), close_button=False ), - dbc.ModalBody("this will be the editor"), - dbc.ModalFooter(dbc.Button("Save", id="save-editor")), + dbc.ModalBody( + [ + dcc.Textarea( + id="device-json-editor", + style={ + "width": "100%", + "height": "200px", + "fontFamily": "monospace", + "backgroundColor": "#f5f5f5", + "border": "1px solid #ccc", + "padding": "10px", + "color": "#333", + }, + ), + html.Div( + id="edit-device-error", + style={"color": "red"}, + ), + ] + ), + dbc.ModalFooter( + dbc.Button("Save", id="save-device-editor") + ), ], - id="editor-modal", + id="device-editor-modal", keyboard=False, backdrop="static", ), - html.Div(id="table-container1"), + html.Div( + children=[dash_table.DataTable(id="devices-table")], + id="devices-table-div", + ), ], className="table-container", ), @@ -120,7 +102,44 @@ html.H2("Commands"), dbc.Button("Refresh", id="refresh-button2", n_clicks=0), dbc.Button("Add command", id="add-command-button"), - html.Div(id="table-container2"), + dbc.Button("Edit", id="edit-command-button"), + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Editor"), close_button=False + ), + dbc.ModalBody( + [ + dcc.Textarea( + id="command-json-editor", + style={ + "width": "100%", + "height": "200px", + "fontFamily": "monospace", + "backgroundColor": "#f5f5f5", + "border": "1px solid #ccc", + "padding": "10px", + "color": "#333", + }, + ), + html.Div( + id="edit-command-error", + style={"color": "red"}, + ), + ] + ), + dbc.ModalFooter( + dbc.Button("Save", id="save-command-editor") + ), + ], + id="command-editor-modal", + keyboard=False, + backdrop="static", + ), + html.Div( + children=[dash_table.DataTable(id="commands-table")], + id="commands-table-div", + ), dbc.Accordion( [ dbc.AccordionItem( @@ -149,46 +168,20 @@ className="main-container", ) +app.layout = html.Div([dcc.Location(id="url", refresh=False), navbar, html.Div(id="page-content")]) + +@app.callback(Output("page-content", "children"), [Input("url", "pathname")]) +def display_page(pathname): + if pathname == "/": + return home_layout + elif pathname == "/edit-recipe": + return edit_recipe_layout + else: + return html.Div("404") + + data_list4 = com.device_list -dl5_og = [ - { - "_name": "heater1", - "_is_initialized": False, - "_heat_rate": 20.0, - "min_heat_rate": 1.0, - "max_heat_rate": 50.0, - "min_temperature": 25.0, - "max_temperature": 100.0, - "_temperature": 95.71927572168929, - "_hardware_interval": 0.05, - }, - { - "_name": "motor1", - "_is_initialized": False, - "motor": { - "_speed": 20.0, - "min_speed": 1.0, - "max_speed": 50.0, - "min_position": 0.0, - "max_position": 100.0, - "_position": 37.44120879993549, - "_hardware_interval": 0.05, - }, - }, - { - "_name": "motor2", - "_is_initialized": False, - "motor": { - "_speed": 20.0, - "min_speed": 1.0, - "max_speed": 50.0, - "min_position": 0.0, - "max_position": 100.0, - "_position": 82.48283286198111, - "_hardware_interval": 0.05, - }, - }, -] + # dl5 = [] # for list in data_list4: # dl5.append(list.__dict__) @@ -200,49 +193,51 @@ @app.callback( - Output("table-container1", "children"), - [Input("refresh-button1", "n_clicks")], - [State("table-container1", "children")], + Output("devices-table-div", "children"), + [Input("refresh-button1", "n_clicks"), Input('devices-table', 'data')], + [State("devices-table-div", "children")], ) -def update_table1(n_clicks, table): - dl5 = dl5_og.copy() - dl5_props_temp = {} - count = 0 - for list in dl5: - dl5_props_temp.clear() - if len(list) > 1: - for prop in list: - if prop != "_name": - dl5_props_temp[prop] = list[prop] - # print(prop) - # print("h") - dl5_temp_list = list - dl5[count] = {} - dl5[count]["_name"] = dl5_temp_list["_name"] - # dl5[count]["_is_initialized"] = dl5_temp_list["_is_initialized"] - dl5[count]["props"] = str(dl5_props_temp) - # print(str(dl5_props_temp)) - count += 1 +def update_table1(n_clicks, data, table): + # dl5 = dl5_og.copy() + # dl5_props_temp = {} + # count = 0 + # for list in dl5: + # dl5_props_temp.clear() + # if len(list) > 1: + # for prop in list: + # if prop != "_name": + # dl5_props_temp[prop] = list[prop] + # # print(prop) + # # print("h") + # dl5_temp_list = list + # dl5[count] = {} + # dl5[count]["_name"] = dl5_temp_list["_name"] + # # dl5[count]["_is_initialized"] = dl5_temp_list["_is_initialized"] + # dl5[count]["props"] = str(dl5_props_temp) + # # print(str(dl5_props_temp)) + # count += 1 # table_data1 = dl5 table_data1 = com.get_clean_device_list().copy() - + # print(com.device_list[1].get_init_args()) table_data1_new = [] for index, list in enumerate(table_data1): # table_data1[index][1].update({"device_type": table_data1[index][0]}) # del table_data1[index][0] table_data1_new.append( - {"device_type": table_data1[index][0], "props": str(table_data1[index][1])} + {"index": index,"device_type": table_data1[index][0], "params": str(table_data1[index][1])} ) table_data1 = table_data1_new - print(table_data1) + table = dash_table.DataTable( + id="devices-table", data=table_data1, columns=[ + {"name": "Index", "id": "index"}, {"name": "Type", "id": "device_type"}, # {"name": "Initialized", "id": "_is_initialized"}, - {"name": "Properties", "id": "props"}, + {"name": "Parameters", "id": "params"}, ], # style_data_conditional=[ # {"if": {"column_id": "_name"}, "width": "250px", 'overflow': 'hidden', 'textOverflow': 'ellipsis'}, @@ -258,17 +253,18 @@ def update_table1(n_clicks, table): "padding": "5px", }, style_cell_conditional=[ - {"if": {"column_id": "_name"}, "width": "20%"}, - {"if": {"column_id": "props"}, "width": "80%"}, - ], - tooltip_data=[ - { - column: {"value": str(value), "type": "markdown"} - for column, value in row.items() - } - for row in table_data1 + {"if": {"column_id": "index"}, "width": "5%"}, + {"if": {"column_id": "device_type"}, "width": "20%"}, + {"if": {"column_id": "params"}, "width": "70%"}, ], - tooltip_duration=None, + # tooltip_data=[ + # { + # column: {"value": str(value), "type": "markdown"} + # for column, value in row.items() + # } + # for row in table_data1 + # ], + # tooltip_duration=None, # editable = True, ) print("done") @@ -276,16 +272,148 @@ def update_table1(n_clicks, table): @app.callback( - Output("editor-modal", "is_open"), - [Input("open-editor", "n_clicks"), Input("save-editor", "n_clicks")], - [State("editor-modal", "is_open")], + Output("device-editor-modal", "is_open"), + [Input("edit-device-button", "n_clicks"), Input("save-device-editor", "n_clicks")], + [State("device-editor-modal", "is_open")], ) -def toggle_editor_modal(n1, n2, is_open): +def toggle_device_editor_modal(n1, n2, is_open): if n1 or n2: return not is_open return is_open +@app.callback( + Output("command-editor-modal", "is_open"), + [ + Input("edit-command-button", "n_clicks"), + Input("save-command-editor", "n_clicks"), + ], + [State("command-editor-modal", "is_open")], +) +def toggle_command_editor_modal(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + + +@app.callback( + Output("commands-table", "data"), + Input("save-command-editor", "n_clicks"), + [ + State("commands-table", "active_cell"), + State("commands-table", "data"), + State("command-json-editor", "value"), + ], + prevent_initial_call=True, +) +def save_command(n_clicks, active_cell, data, value): + if active_cell is not None and data[active_cell["row"]]["params"] != str( + json.loads(value) + ): + # data[active_cell['row']]['params'] = str(json.loads(value)) + # com.command_list[data[active_cell['row']]['index']] + # print(data[active_cell['row']]['params']) + # print((eval(value))) + + com.command_list[data[active_cell["row"]]["index"]][0]._params = eval(value) + # print((com.command_list[data[active_cell['row']]['index']][0]._params)) + # print(com.get_unlooped_command_list()[active_cell['row']]) + return None + return data + + +@app.callback( + Output("devices-table", "data"), + Input("save-device-editor", "n_clicks"), + [ + State("devices-table", "active_cell"), + State("devices-table", "data"), + State("device-json-editor", "value"), + ], + prevent_initial_call=True, +) +def save_device(n_clicks, active_cell, data, value): + if active_cell is not None and data[active_cell["row"]]['params'] != str(json.loads(value)): + # data_row = data[active_cell["row"]] + params = eval(value) + # print(com.device_by_name[params['name']]) + # init_str = data_row['device_type'] + '(' + # for key, value2 in params.items(): + # if isinstance(value2, str): + # init_str += key + '=' + "'" + str(value2) + "'" + ',' + # else: + # init_str += key + '=' + str(value2) + ',' + # init_str = init_str[:-1] + ')' + com.device_by_name[params['name']].update_init_args(params) + return None + return data + +@app.callback( + Output("command-json-editor", "value"), + [Input("command-editor-modal", "is_open")], + [State("commands-table", "active_cell"), State("commands-table", "data")], + prevent_initial_call=True, +) +def fill_command_json_editor(is_open, active_cell, data): + if active_cell is not None and is_open: + # print(active_cell) + # print(eval(data[active_cell['row']]['params'])) + return json.dumps(eval(data[active_cell["row"]]["params"]), indent=4) + + return "" + +@app.callback( + Output("device-json-editor", "value"), + [Input("device-editor-modal", "is_open")], + [State("devices-table", "active_cell"), State("devices-table", "data")], + prevent_initial_call=True, +) +def fill_device_json_editor(is_open, active_cell, data): + if active_cell is not None and is_open: + return json.dumps(eval(data[active_cell['row']]['params']), indent=4) + return "" + +@app.callback( + [ + Output("save-command-editor", "disabled"), + Output("edit-command-error", "children"), + ], + Input("command-json-editor", "value"), + State("command-editor-modal", "is_open"), + prevent_initial_call=True, +) +def enable_save_command_button(value, is_open): + if not is_open: + return False, "" + try: + parsed_json = json.loads(value) + # print(type(parsed_json)) + # print(parsed_json) + if parsed_json["delay"] < 0: + return True, "Delay must be greater than or equal to 0" + return False, "" + except Exception as e: + if type(e) == json.decoder.JSONDecodeError: + return True, "Invalid JSON" + return True, str(type(e)) + +@app.callback( + [Output('save-device-editor', 'disabled'),Output('edit-device-error', 'children')], + Input('device-json-editor', 'value'), + State('device-editor-modal', 'is_open'), + prevent_initial_call=True +) +def enable_save_device_button(value, is_open): + if not is_open: + return False, "" + try: + parsed_json = json.loads(value) + return False, "" + except Exception as e: + if type(e) == json.decoder.JSONDecodeError: + return True, "Invalid JSON" + return True, str(type(e)) + # @app.callback( # Output("commands-accordion", "children"), # Input("add-command-button", "n_clicks"), @@ -325,8 +453,8 @@ def load_commands_accordion(n_clicks, children): for command in command_list: # if isinstance(command, CompositeCommand): # print(type(command).__name__) - temp_dict_command_params = {"command":type(command).__name__} - temp_dict_command_params.update({"params":command.get_init_args()}) + temp_dict_command_params = {"command": type(command).__name__} + temp_dict_command_params.update({"params": command.get_init_args()}) command_params.append(temp_dict_command_params) # print(command._params) # else: @@ -343,24 +471,48 @@ def load_commands_accordion(n_clicks, children): "```json", json.dumps(command, indent=4, cls=util.Encoder), # str(command.__dict__), - "```" + "```", ], ), # str(command.__dict__), - title=command['command'], - item_id=command['command']+str(index), + title=command["command"], + item_id=command["command"] + str(index), ) ) return children @app.callback( - Output("table-container2", "children"), - [Input("refresh-button2", "n_clicks")], - [State("table-container2", "children")], + Output("edit-command-button", "disabled"), + Input("commands-table", "active_cell"), ) -def update_table2(n_clicks, table): - command_list = com.get_unlooped_command_list().copy() +def edit_command_button(table_div_children): + active_cell = table_div_children + # print(active_cell) + # if active_cell is not None and active_cell["column_id"] == "params": + if active_cell is not None: + return False + else: + return True + +@app.callback( + Output("edit-device-button", 'disabled'), + Input("devices-table", "active_cell"), +) +def edit_device_button(table_div_children): + active_cell = table_div_children + if active_cell is not None: + return False + else: + return True + +@app.callback( + Output("commands-table-div", "children"), + [Input("refresh-button2", "n_clicks"), Input("commands-table", "data")], + [State("commands-table-div", "children")], +) +def update_table2(n_clicks, data, table): + command_list = com.command_list.copy() # print(command_list) # for command in command_list: # if isinstance(command, CompositeCommand): @@ -371,25 +523,29 @@ def update_table2(n_clicks, table): # # print(command.__dict__) # command._receiver = command._receiver._name command_params = [] - for command in command_list: + for index, command in enumerate(command_list): # if isinstance(command, CompositeCommand): # print(type(command).__name__) - temp_dict_command_params = {"command":type(command).__name__} - temp_dict_command_params.update({"params":str(command.get_init_args())}) + temp_dict_command_params = {"command": type(command[0]).__name__} + temp_dict_command_params.update( + {"params": str(command[0].get_init_args()), "index": index} + ) command_params.append((temp_dict_command_params)) # print(command._params) # else: # command_params.append(command._params) # print(command_params) - print(command_params) + # print(command_params) table_data2 = command_params # print(com.get_unlooped_command_list().copy()[3].__dict__) # add_command_accordian(0, to_add=[dbc.AccordionItem("new new", title="new new", item_id="new new")]) table = dash_table.DataTable( + id="commands-table", data=table_data2, columns=[ + {"name": "Index", "id": "index"}, {"name": "Command", "id": "command"}, {"name": "Parameters", "id": "params"}, ], @@ -401,8 +557,9 @@ def update_table2(n_clicks, table): "padding": "5px", }, style_cell_conditional=[ + {"if": {"column_id": "index"}, "width": "5%"}, {"if": {"column_id": "command"}, "width": "20%"}, - {"if": {"column_id": "params"}, "width": "80%"}, + {"if": {"column_id": "params"}, "width": "70%"}, ], # tooltip_data=[ # { @@ -433,5 +590,3 @@ def update_table2(n_clicks, table): if __name__ == "__main__": app.run_server(debug=True) - - diff --git a/util.py b/util.py index 2d3956f..e89967f 100644 --- a/util.py +++ b/util.py @@ -44,6 +44,10 @@ def dict_to_device(device: Device, type: str): # print(arg_dict) return device_cls(**arg_dict) +def str_to_device(device_str: str): + print(device_str) + return eval(device_str) + def device_to_dict(device: Device): return device.get_init_args() From 64ceee22f878f6b3d6540001230838d60d7031ca Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 15 Jun 2023 14:42:07 -0500 Subject: [PATCH 005/125] added requirements and lts150 --- commands/linear_stage_150_commands.py | 78 ++++++++++++++++ devices/linear_stage_150.py | 128 ++++++++++++++++++++++++++ requirements.txt | 33 +++++++ 3 files changed, 239 insertions(+) create mode 100644 commands/linear_stage_150_commands.py create mode 100644 devices/linear_stage_150.py create mode 100644 requirements.txt diff --git a/commands/linear_stage_150_commands.py b/commands/linear_stage_150_commands.py new file mode 100644 index 0000000..c78c6b9 --- /dev/null +++ b/commands/linear_stage_150_commands.py @@ -0,0 +1,78 @@ +from devices.device import Device +from .command import Command, CommandResult +from devices.linear_stage_150 import LinearStage150 + + +class LinearStage150ParentCommand(Command): + """Parent class for all LinearStage150 commands.""" + receiver_cls = LinearStage150 + + def __init__(self, receiver: LinearStage150, **kwargs): + super().__init__(receiver, **kwargs) + +class LinearStage150Connect(LinearStage150ParentCommand): + """Open a serial port for the linear stage.""" + + def __init__(self, receiver: LinearStage150, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.start_serial()) + +class LinearStage150Initialize(LinearStage150ParentCommand): + """Initialize the linear stage by homing it.""" + + def __init__(self, receiver: LinearStage150, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + +class LinearStage150Deinitialize(LinearStage150ParentCommand): + """Deinitialize the linear stage.""" + + def __init__(self, receiver: LinearStage150, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize()) + +class LinearStage150EnableMotor(LinearStage150ParentCommand): + """Enable the linear stage motor.""" + + def __init__(self, receiver: LinearStage150, **kwargs): + super().__init__(receiver, **kwargs) + self._params['state'] = True + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_enabled_state(self._params['state'])) + +class LinearStage150DisableMotor(LinearStage150ParentCommand): + """Disable the linear stage motor.""" + + def __init__(self, receiver: LinearStage150, **kwargs): + super().__init__(receiver, **kwargs) + self._params['state'] = False + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_enabled_state(self._params['state'])) + +class LinearStage150MoveAbsolute(LinearStage150ParentCommand): + """Move the linear stage to an absolute position.""" + + def __init__(self, receiver: LinearStage150, position: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params['position'] = position + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_absolute(self._params['position'])) + +class LinearStage150MoveRelative(LinearStage150ParentCommand): + """Move the linear stage by a relative distance.""" + + def __init__(self, receiver: LinearStage150, distance: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params['distance'] = distance + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_relative(self._params['distance'])) \ No newline at end of file diff --git a/devices/linear_stage_150.py b/devices/linear_stage_150.py new file mode 100644 index 0000000..2e1a778 --- /dev/null +++ b/devices/linear_stage_150.py @@ -0,0 +1,128 @@ +from typing import Optional, Tuple +from struct import pack,unpack +import time +from .device import SerialDevice, check_serial, check_initialized + +class LinearStage150(SerialDevice): + def __init__(self, name: str, port: str = '\'/dev/cu.URT0\'', baudrate: int = 115200, timeout: float | None = 0.1, destination: int = 0x50, source: int = 0x01, channel: int = 1): + super().__init__(name, port, baudrate, timeout) + self._destination = destination + self._source = source + self._channel = channel + # print("dest: "+str(self._destination)) + + @check_serial + def initialize(self) -> Tuple[bool, str]: + self._is_initialized = False + + #lts150: initialize, home it (if needed) + + #Home Stage; MGMSG_MOT_MOVE_HOME + self.ser.write(pack(' Tuple[bool, str]: + + #i dont think this is needed: lts150: deinitialize + # if reset_init_flag: //used in other devices + self._is_initialized = False + return (True, "Successfully deinitialized LTS150.") + # return super().deinitialize() + + @check_serial + # @check_initialized + def get_enabled_state(self) -> bool: + self._is_enabled = False + + #TODO: lts150 get enabled state, MGMSG_MOD_GET_CHANENABLESTATE + # self.ser.write(pack(' Tuple[bool, str]: + if state: + self.ser.write(pack(' float: + self._position = 0.0 + Device_Unit_SF = 409600 + # MGMSG_MOT_GET_POSCOUNTER + self.ser.write(pack(' Tuple[bool, str]: + if position > 150: + return (False, "Position " + str(position) + " is out of range.") + + Device_Unit_SF = 409600 + dUnitpos = int(Device_Unit_SF*position) + self.ser.write(pack(' Tuple[bool, str]: + if distance + self.get_position() > 150: + return (False, "Position " + str(distance + self.get_position()) + " is out of range.") + + Device_Unit_SF = 409600 + dUnitpos = int(Device_Unit_SF*distance) + self.ser.write(pack(' Date: Fri, 16 Jun 2023 12:52:31 -0500 Subject: [PATCH 006/125] june 16 3 pages --- devices/linear_stage_150.py | 21 ++++++ mongodb_helper.py | 6 +- temp1.py | 128 ++++++++++++++++++++++++++---------- 3 files changed, 119 insertions(+), 36 deletions(-) diff --git a/devices/linear_stage_150.py b/devices/linear_stage_150.py index 2e1a778..ce76d59 100644 --- a/devices/linear_stage_150.py +++ b/devices/linear_stage_150.py @@ -11,6 +11,27 @@ def __init__(self, name: str, port: str = '\'/dev/cu.URT0\'', baudrate: int = 11 self._channel = channel # print("dest: "+str(self._destination)) + def get_init_args(self) -> dict: + args_dict = { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "destination": self._destination, + "source": self._source, + "channel": self._channel, + } + return args_dict + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._destination = args_dict["destination"] + self._source = args_dict["source"] + self._channel = args_dict["channel"] + @check_serial def initialize(self) -> Tuple[bool, str]: self._is_initialized = False diff --git a/mongodb_helper.py b/mongodb_helper.py index 84b012b..d0aa3ba 100644 --- a/mongodb_helper.py +++ b/mongodb_helper.py @@ -59,9 +59,11 @@ def insert_yaml_file(self, collection, file_path): str: The inserted document ID. """ with open(file_path, 'r') as file: - yaml_data = yaml.safe_load(file) + # yaml_data = yaml.load(file, Loader=yaml.Loader) + yaml_data = file.read() + doc = {'yaml_data': yaml_data, 'file_name': file_path} - return str(self.db[collection].insert_one(yaml_data).inserted_id) + return str(self.db[collection].insert_one(doc).inserted_id) def update_yaml_file(self, collection, file_id, updated_data): """ diff --git a/temp1.py b/temp1.py index eee0a9f..90ad256 100644 --- a/temp1.py +++ b/temp1.py @@ -1,4 +1,5 @@ from command_sequence import CommandSequence +from command_invoker import CommandInvoker import project_const import json from devices.device import Device @@ -8,13 +9,17 @@ com = CommandSequence() - -com.load_from_yaml("e1.yaml") - +com.load_from_yaml("to_load.yaml") import util import ctypes +from mongodb_helper import MongoDBHelper + +mongo = MongoDBHelper( + "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "diaogroup", +) out = util.device_to_dict(com.device_list[0]) @@ -30,22 +35,59 @@ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) navbar = dbc.NavbarSimple( - children = [ + children=[ dbc.NavItem(dbc.NavLink("Home", href="/")), dbc.NavItem(dbc.NavLink("Edit Recipe", href="/edit-recipe")), + dbc.NavItem(dbc.NavLink("Execute Recipe", href="/execute-recipe")), ], - brand = "AAMP", - brand_href = "/", + brand="AAMP", + brand_href="/", color="primary", - dark=True + dark=True, ) home_layout = html.Div( [ html.H1("Home"), + dcc.Input(id="filename-input", type="text", placeholder="Enter filename name"), + dbc.Button("Load", id="filename-input-button", n_clicks=0), + html.Div(id="home-output"), ] ) +execute_recipe_layout = html.Div( + [html.H1("Execute Recipe"), dbc.Button("Execute", id="execute-button", n_clicks=0), html.Div(id="execute-recipe-output")] +) + +@app.callback( + Output("execute-recipe-output", "children"), + [Input("execute-button", "n_clicks")], + prevent_initial_call=True, +) +def execute_recipe(n_clicks): + invoker = CommandInvoker(com,False,None,False) + return html.Div(str(invoker.invoke_commands())) + + +@app.callback( + Output("url", "pathname"), + [Input("filename-input-button", "n_clicks")], + [State("filename-input", "value")], +) +def get_document_from_db(n_clicks, filename): + if filename is not None and filename != "": + # Extract the YAML content from the document + document = mongo.find_documents("recipes", {"file_name": filename})[0] + yaml_content = document.get("yaml_data", "") + # Update the YAML output + with open("to_load.yaml", "w") as file: + file.write(yaml_content) + com.load_from_yaml("to_load.yaml") + return "/edit-recipe" + + return "/" + + edit_recipe_layout = html.Div( [ html.H1("Edit Recipe"), @@ -168,7 +210,10 @@ className="main-container", ) -app.layout = html.Div([dcc.Location(id="url", refresh=False), navbar, html.Div(id="page-content")]) +app.layout = html.Div( + [dcc.Location(id="url", refresh=False), navbar, html.Div(id="page-content")] +) + @app.callback(Output("page-content", "children"), [Input("url", "pathname")]) def display_page(pathname): @@ -176,9 +221,11 @@ def display_page(pathname): return home_layout elif pathname == "/edit-recipe": return edit_recipe_layout + elif pathname == "/execute-recipe": + return execute_recipe_layout else: return html.Div("404") - + data_list4 = com.device_list @@ -194,7 +241,7 @@ def display_page(pathname): @app.callback( Output("devices-table-div", "children"), - [Input("refresh-button1", "n_clicks"), Input('devices-table', 'data')], + [Input("refresh-button1", "n_clicks"), Input("devices-table", "data")], [State("devices-table-div", "children")], ) def update_table1(n_clicks, data, table): @@ -225,11 +272,15 @@ def update_table1(n_clicks, data, table): # table_data1[index][1].update({"device_type": table_data1[index][0]}) # del table_data1[index][0] table_data1_new.append( - {"index": index,"device_type": table_data1[index][0], "params": str(table_data1[index][1])} + { + "index": index, + "device_type": table_data1[index][0], + "params": str(table_data1[index][1]), + } ) table_data1 = table_data1_new - + table = dash_table.DataTable( id="devices-table", data=table_data1, @@ -323,17 +374,19 @@ def save_command(n_clicks, active_cell, data, value): @app.callback( - Output("devices-table", "data"), - Input("save-device-editor", "n_clicks"), - [ - State("devices-table", "active_cell"), - State("devices-table", "data"), - State("device-json-editor", "value"), - ], - prevent_initial_call=True, + Output("devices-table", "data"), + Input("save-device-editor", "n_clicks"), + [ + State("devices-table", "active_cell"), + State("devices-table", "data"), + State("device-json-editor", "value"), + ], + prevent_initial_call=True, ) def save_device(n_clicks, active_cell, data, value): - if active_cell is not None and data[active_cell["row"]]['params'] != str(json.loads(value)): + if active_cell is not None and data[active_cell["row"]]["params"] != str( + json.loads(value) + ): # data_row = data[active_cell["row"]] params = eval(value) # print(com.device_by_name[params['name']]) @@ -344,10 +397,11 @@ def save_device(n_clicks, active_cell, data, value): # else: # init_str += key + '=' + str(value2) + ',' # init_str = init_str[:-1] + ')' - com.device_by_name[params['name']].update_init_args(params) + com.device_by_name[params["name"]].update_init_args(params) return None return data + @app.callback( Output("command-json-editor", "value"), [Input("command-editor-modal", "is_open")], @@ -362,17 +416,19 @@ def fill_command_json_editor(is_open, active_cell, data): return "" + @app.callback( - Output("device-json-editor", "value"), - [Input("device-editor-modal", "is_open")], - [State("devices-table", "active_cell"), State("devices-table", "data")], - prevent_initial_call=True, + Output("device-json-editor", "value"), + [Input("device-editor-modal", "is_open")], + [State("devices-table", "active_cell"), State("devices-table", "data")], + prevent_initial_call=True, ) def fill_device_json_editor(is_open, active_cell, data): if active_cell is not None and is_open: - return json.dumps(eval(data[active_cell['row']]['params']), indent=4) + return json.dumps(eval(data[active_cell["row"]]["params"]), indent=4) return "" + @app.callback( [ Output("save-command-editor", "disabled"), @@ -394,14 +450,15 @@ def enable_save_command_button(value, is_open): return False, "" except Exception as e: if type(e) == json.decoder.JSONDecodeError: - return True, "Invalid JSON" + return True, "Invalid JSON" return True, str(type(e)) + @app.callback( - [Output('save-device-editor', 'disabled'),Output('edit-device-error', 'children')], - Input('device-json-editor', 'value'), - State('device-editor-modal', 'is_open'), - prevent_initial_call=True + [Output("save-device-editor", "disabled"), Output("edit-device-error", "children")], + Input("device-json-editor", "value"), + State("device-editor-modal", "is_open"), + prevent_initial_call=True, ) def enable_save_device_button(value, is_open): if not is_open: @@ -414,6 +471,7 @@ def enable_save_device_button(value, is_open): return True, "Invalid JSON" return True, str(type(e)) + # @app.callback( # Output("commands-accordion", "children"), # Input("add-command-button", "n_clicks"), @@ -495,9 +553,10 @@ def edit_command_button(table_div_children): else: return True + @app.callback( - Output("edit-device-button", 'disabled'), - Input("devices-table", "active_cell"), + Output("edit-device-button", "disabled"), + Input("devices-table", "active_cell"), ) def edit_device_button(table_div_children): active_cell = table_div_children @@ -506,6 +565,7 @@ def edit_device_button(table_div_children): else: return True + @app.callback( Output("commands-table-div", "children"), [Input("refresh-button2", "n_clicks"), Input("commands-table", "data")], From d8537f876ef8c32de1ede9d1ea088e2d054736d5 Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 20 Jun 2023 15:00:18 -0500 Subject: [PATCH 007/125] june 20 prog --- recipe_tool.py | 4 +- temp1.py | 155 +++++++++++++++++++++++++++++++++++++++++++++---- util.py | 1 - 3 files changed, 147 insertions(+), 13 deletions(-) diff --git a/recipe_tool.py b/recipe_tool.py index b28ab42..e49a96c 100644 --- a/recipe_tool.py +++ b/recipe_tool.py @@ -28,7 +28,7 @@ from devices.dummy_heater import DummyHeater from devices.dummy_motor import DummyMotor -import project_const +import util # TODO @@ -38,7 +38,7 @@ # add compatibility with composite and utility commands (try to avoid coding speciific class dependencies) #================ Constants ============================= -named_devices = project_const.named_devices +named_devices = util.named_devices command_directory = "commands/" load_directory = "recipes/user_recipes/" save_directory = "recipes/user_recipes/" diff --git a/temp1.py b/temp1.py index 90ad256..8d755a3 100644 --- a/temp1.py +++ b/temp1.py @@ -4,6 +4,7 @@ import json from devices.device import Device from commands.command import Command, CompositeCommand +from devices.device import Device, SerialDevice com = CommandSequence() @@ -15,6 +16,7 @@ import util import ctypes from mongodb_helper import MongoDBHelper +import pandas as pd mongo = MongoDBHelper( "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", @@ -52,22 +54,77 @@ dcc.Input(id="filename-input", type="text", placeholder="Enter filename name"), dbc.Button("Load", id="filename-input-button", n_clicks=0), html.Div(id="home-output"), + dbc.Button("Refresh List", id='home-refresh-list-button', n_clicks=0), + dash_table.DataTable( + id='home-recipes-list-table', + columns=[ + {'name': 'File Name', 'id': 'file_name'}, + # {'name':'YAML', 'id':'yaml_data'} + ], + data=[], + style_table={'width': '300px'}, + style_cell={'textAlign': 'left'} + ), ] ) +@app.callback( + Output("home-recipes-list-table", "data"), + [ Input("home-refresh-list-button", "n_clicks")], + # prevent_initial_call=True, +) +def fetch_recipe_list(n_clicks): + docs = mongo.find_documents('recipes', {}) + docs = mongo.db['recipes'].find({}, {'_id': 0, 'file_name': 1}) + # print(pd.DataFrame(docs).to_dict('records')) + print('recipe list refresh done') + return pd.DataFrame(docs).to_dict('records') + +@app.callback( + Output('filename-input', 'value'), + Input('home-recipes-list-table', 'active_cell'), + State('home-recipes-list-table', 'data'), + prevent_initial_call=True, +) +def fill_filename_input(active_cell, data): + if active_cell is not None: + return data[active_cell['row']]['file_name'] + return "" + + execute_recipe_layout = html.Div( - [html.H1("Execute Recipe"), dbc.Button("Execute", id="execute-button", n_clicks=0), html.Div(id="execute-recipe-output")] + [ + html.H1("Execute Recipe"), + dbc.Button("Execute", id="execute-button", n_clicks=0), + html.Div(id="execute-recipe-output"), + dcc.Interval(id='update-interval', interval=1000, n_intervals=0), + dcc.Textarea( + id="console-output", readOnly=True, style={"width": "100%", "height": 200} + ), + ] ) + @app.callback( Output("execute-recipe-output", "children"), [Input("execute-button", "n_clicks")], prevent_initial_call=True, ) def execute_recipe(n_clicks): - invoker = CommandInvoker(com,False,None,False) + invoker = CommandInvoker(com, False, None, False) return html.Div(str(invoker.invoke_commands())) +# import sys +# from io import StringIO +# stringio = StringIO() +# sys.stdout = stringio +# @app.callback(Output('console-output', 'value'), [Input('update-interval', 'n_intervals')]) +# def show_console_output(n): +# # Retrieve console output from StringIO object +# stringio.seek(0) +# console_output = stringio.read() +# return console_output + @app.callback( Output("url", "pathname"), @@ -122,6 +179,9 @@ def get_document_from_db(n_clicks, filename): id="edit-device-error", style={"color": "red"}, ), + html.Div( + id='edit-device-serial-ports-info', + ), ] ), dbc.ModalFooter( @@ -132,6 +192,47 @@ def get_document_from_db(n_clicks, filename): keyboard=False, backdrop="static", ), + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Add Device") + ), + dbc.ModalBody( + [ + dcc.Dropdown( + id='add-device-dropdown', + options=[], + value = None, + ), + dcc.Textarea( + id="add-device-json-editor", + style={ + "width": "100%", + "height": "200px", + "fontFamily": "monospace", + "backgroundColor": "#f5f5f5", + "border": "1px solid #ccc", + "padding": "10px", + "color": "#333", + }, + ), + html.Div( + id="add-device-error", + style={"color": "red"}, + ), + html.Div( + id='add-device-serial-ports-info', + ), + ] + ), + dbc.ModalFooter( + dbc.Button("Add", id="add-device-editor") + ), + ], + id="device-add-modal", + keyboard=False, + backdrop="static", + ), html.Div( children=[dash_table.DataTable(id="devices-table")], id="devices-table-div", @@ -217,6 +318,7 @@ def get_document_from_db(n_clicks, filename): @app.callback(Output("page-content", "children"), [Input("url", "pathname")]) def display_page(pathname): + print("\n\nrefreshing to " + pathname) if pathname == "/": return home_layout elif pathname == "/edit-recipe": @@ -233,9 +335,6 @@ def display_page(pathname): # for list in data_list4: # dl5.append(list.__dict__) -print() -print() -print() # print(dl5[2]['motor'].__dict__) @@ -318,7 +417,7 @@ def update_table1(n_clicks, data, table): # tooltip_duration=None, # editable = True, ) - print("done") + print("devices table refresh done") return table @@ -332,6 +431,16 @@ def toggle_device_editor_modal(n1, n2, is_open): return not is_open return is_open +@app.callback( + Output("device-add-modal", "is_open"), + [Input("add-device-button", "n_clicks"), Input("add-device-editor", "n_clicks")], + [State("device-add-modal", "is_open")], +) +def toggle_device_add_modal(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + @app.callback( Output("command-editor-modal", "is_open"), @@ -416,17 +525,42 @@ def fill_command_json_editor(is_open, active_cell, data): return "" +@app.callback( + [Output("add-device-json-editor", "value"),Output('add-device-dropdown', 'options')], + [Input("device-add-modal", "is_open")], + [State("devices-table", "active_cell"), State("devices-table", "data")], + prevent_initial_call=True, +) +def fill_device_add_modal(is_open, active_cell, data): + return '',util.approved_devices + +try: + import serial.tools.list_ports +except ImportError: + _has_serial = False +else: + _has_serial = True @app.callback( - Output("device-json-editor", "value"), + [Output("device-json-editor", "value"), Output('edit-device-serial-ports-info', 'children')], [Input("device-editor-modal", "is_open")], [State("devices-table", "active_cell"), State("devices-table", "data")], prevent_initial_call=True, ) def fill_device_json_editor(is_open, active_cell, data): if active_cell is not None and is_open: - return json.dumps(eval(data[active_cell["row"]]["params"]), indent=4) - return "" + if _has_serial and isinstance(com.device_by_name[eval(data[active_cell["row"]]["params"])["name"]], SerialDevice): + ports = serial.tools.list_ports.comports() + str_ports = "" + for port, desc, hwid in sorted(ports): + str_ports += f"{port}: {desc} [{hwid}]\n" + lines = str_ports.splitlines() + device_port_html = [html.Div(['COM Port Info:'], style={'font-weight': 'bold'})] + device_port_html.append(html.Div([html.Div(line) for line in lines])) + else: + device_port_html = "" + return json.dumps(eval(data[active_cell["row"]]["params"]), indent=4), device_port_html + return "","" @app.callback( @@ -496,7 +630,7 @@ def enable_save_device_button(value, is_open): ) def load_commands_accordion(n_clicks, children): children = [] - print() + # print() command_list = com.get_unlooped_command_list().copy() # print(command_list) # for command in command_list: @@ -631,6 +765,7 @@ def update_table2(n_clicks, data, table): # tooltip_duration=None, # editable = True, ) + print("commands table refresh done") return table diff --git a/util.py b/util.py index e89967f..ddfb2b1 100644 --- a/util.py +++ b/util.py @@ -19,7 +19,6 @@ "PrinterMotorX": NewportESP301, # "Spectrometer": StellarNetSpectrometer, # "SampleCamera": XimeaCamera, - "DummyHeater": DummyHeater, "DummyHeater1": DummyHeater, "DummyHeater2": DummyHeater, "DummyMotor": DummyMotor, From c5e16be3914eb148dbb33ddce3d294a80d29d894 Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 26 Jun 2023 09:45:28 -0500 Subject: [PATCH 008/125] prog --- temp1.py | 142 ++++++++++++++++++++++++++++++++++++++------------- to_load.yaml | 48 +++++++++++++++++ 2 files changed, 154 insertions(+), 36 deletions(-) create mode 100644 to_load.yaml diff --git a/temp1.py b/temp1.py index 8d755a3..9194b8a 100644 --- a/temp1.py +++ b/temp1.py @@ -1,6 +1,5 @@ from command_sequence import CommandSequence from command_invoker import CommandInvoker -import project_const import json from devices.device import Device from commands.command import Command, CompositeCommand @@ -24,7 +23,7 @@ ) -out = util.device_to_dict(com.device_list[0]) +# out = util.device_to_dict(com.device_list[0]) import dash from dash import dcc @@ -54,41 +53,43 @@ dcc.Input(id="filename-input", type="text", placeholder="Enter filename name"), dbc.Button("Load", id="filename-input-button", n_clicks=0), html.Div(id="home-output"), - dbc.Button("Refresh List", id='home-refresh-list-button', n_clicks=0), + dbc.Button("Refresh List", id="home-refresh-list-button", n_clicks=0), dash_table.DataTable( - id='home-recipes-list-table', + id="home-recipes-list-table", columns=[ - {'name': 'File Name', 'id': 'file_name'}, - # {'name':'YAML', 'id':'yaml_data'} - ], + {"name": "File Name", "id": "file_name"}, + # {'name':'YAML', 'id':'yaml_data'} + ], data=[], - style_table={'width': '300px'}, - style_cell={'textAlign': 'left'} + style_table={"width": "300px"}, + style_cell={"textAlign": "left"}, ), ] ) + @app.callback( Output("home-recipes-list-table", "data"), - [ Input("home-refresh-list-button", "n_clicks")], + [Input("home-refresh-list-button", "n_clicks")], # prevent_initial_call=True, ) def fetch_recipe_list(n_clicks): - docs = mongo.find_documents('recipes', {}) - docs = mongo.db['recipes'].find({}, {'_id': 0, 'file_name': 1}) + docs = mongo.find_documents("recipes", {}) + docs = mongo.db["recipes"].find({}, {"_id": 0, "file_name": 1}) # print(pd.DataFrame(docs).to_dict('records')) - print('recipe list refresh done') - return pd.DataFrame(docs).to_dict('records') + print("recipe list refresh done") + return pd.DataFrame(docs).to_dict("records") + @app.callback( - Output('filename-input', 'value'), - Input('home-recipes-list-table', 'active_cell'), - State('home-recipes-list-table', 'data'), + Output("filename-input", "value"), + Input("home-recipes-list-table", "active_cell"), + State("home-recipes-list-table", "data"), prevent_initial_call=True, ) def fill_filename_input(active_cell, data): if active_cell is not None: - return data[active_cell['row']]['file_name'] + return data[active_cell["row"]]["file_name"] return "" @@ -97,7 +98,7 @@ def fill_filename_input(active_cell, data): html.H1("Execute Recipe"), dbc.Button("Execute", id="execute-button", n_clicks=0), html.Div(id="execute-recipe-output"), - dcc.Interval(id='update-interval', interval=1000, n_intervals=0), + dcc.Interval(id="update-interval", interval=1000, n_intervals=0), dcc.Textarea( id="console-output", readOnly=True, style={"width": "100%", "height": 200} ), @@ -114,6 +115,7 @@ def execute_recipe(n_clicks): invoker = CommandInvoker(com, False, None, False) return html.Div(str(invoker.invoke_commands())) + # import sys # from io import StringIO # stringio = StringIO() @@ -180,7 +182,7 @@ def get_document_from_db(n_clicks, filename): style={"color": "red"}, ), html.Div( - id='edit-device-serial-ports-info', + id="edit-device-serial-ports-info", ), ] ), @@ -194,15 +196,13 @@ def get_document_from_db(n_clicks, filename): ), dbc.Modal( [ - dbc.ModalHeader( - dbc.ModalTitle("Add Device") - ), + dbc.ModalHeader(dbc.ModalTitle("Add Device")), dbc.ModalBody( [ dcc.Dropdown( - id='add-device-dropdown', + id="add-device-dropdown", options=[], - value = None, + value=None, ), dcc.Textarea( id="add-device-json-editor", @@ -221,7 +221,7 @@ def get_document_from_db(n_clicks, filename): style={"color": "red"}, ), html.Div( - id='add-device-serial-ports-info', + id="add-device-serial-ports-info", ), ] ), @@ -431,6 +431,7 @@ def toggle_device_editor_modal(n1, n2, is_open): return not is_open return is_open + @app.callback( Output("device-add-modal", "is_open"), [Input("add-device-button", "n_clicks"), Input("add-device-editor", "n_clicks")], @@ -525,14 +526,38 @@ def fill_command_json_editor(is_open, active_cell, data): return "" + @app.callback( - [Output("add-device-json-editor", "value"),Output('add-device-dropdown', 'options')], - [Input("device-add-modal", "is_open")], - [State("devices-table", "active_cell"), State("devices-table", "data")], - prevent_initial_call=True, + [ + # Output("add-device-json-editor", "value"), + Output("add-device-dropdown", "options"), + ], + [Input("device-add-modal", "is_open")], + [State("devices-table", "active_cell"), State("devices-table", "data")], + prevent_initial_call=True, ) def fill_device_add_modal(is_open, active_cell, data): - return '',util.approved_devices + return [util.approved_devices] + +import inspect + +@app.callback( + [Output('add-device-json-editor', 'value')], + [Input('add-device-dropdown', 'value'), Input('device-add-modal', 'is_open')], + prevent_initial_call=True +) +def fill_device_add_json_editor(value, is_open): + if not is_open or value is None: + return [""] + args_list = inspect.getfullargspec(util.named_devices[value].__init__).args + args_dict = {} + for arg in args_list: + if arg != 'self' and arg != 'name': + args_dict[arg] = None + if arg == 'name': + args_dict[arg] = value + return [(json.dumps(args_dict, indent=4))] + try: import serial.tools.list_ports @@ -541,26 +566,38 @@ def fill_device_add_modal(is_open, active_cell, data): else: _has_serial = True + @app.callback( - [Output("device-json-editor", "value"), Output('edit-device-serial-ports-info', 'children')], + [ + Output("device-json-editor", "value"), + Output("edit-device-serial-ports-info", "children"), + ], [Input("device-editor-modal", "is_open")], [State("devices-table", "active_cell"), State("devices-table", "data")], prevent_initial_call=True, ) def fill_device_json_editor(is_open, active_cell, data): if active_cell is not None and is_open: - if _has_serial and isinstance(com.device_by_name[eval(data[active_cell["row"]]["params"])["name"]], SerialDevice): + if _has_serial and isinstance( + com.device_by_name[eval(data[active_cell["row"]]["params"])["name"]], + SerialDevice, + ): ports = serial.tools.list_ports.comports() str_ports = "" for port, desc, hwid in sorted(ports): str_ports += f"{port}: {desc} [{hwid}]\n" lines = str_ports.splitlines() - device_port_html = [html.Div(['COM Port Info:'], style={'font-weight': 'bold'})] + device_port_html = [ + html.Div(["COM Port Info:"], style={"font-weight": "bold"}) + ] device_port_html.append(html.Div([html.Div(line) for line in lines])) else: device_port_html = "" - return json.dumps(eval(data[active_cell["row"]]["params"]), indent=4), device_port_html - return "","" + return ( + json.dumps(eval(data[active_cell["row"]]["params"]), indent=4), + device_port_html, + ) + return "", "" @app.callback( @@ -605,6 +642,39 @@ def enable_save_device_button(value, is_open): return True, "Invalid JSON" return True, str(type(e)) +import typing + +@app.callback( + [Output("add-device-editor", "disabled"), Output("add-device-error", "children")], + [ Input("add-device-json-editor", "value"), Input('add-device-dropdown', 'value')], + State("device-add-modal", "is_open"), + prevent_initial_call=True, +) +def enable_add_device_button(value, device_type, is_open): + if value == "": + return True, "No device selected" + if not is_open: + return False, "" + try: + sig = inspect.signature(util.named_devices[device_type].__init__) + args = {} + for param in sig.parameters.values(): + arg_type = param.annotation + args[param.name] = typing.get_args(arg_type)[0] if typing.get_origin(arg_type) is typing.Union else arg_type + parsed_json = json.loads(value) + for key in parsed_json: + print('\n'+key) + print('input: '+str(type((parsed_json[key]))) + ', expected: '+ str(args[key])) + if type((parsed_json[key])) != args[key]: + return True, f"Invalid type for {key}. Expected {str(args[key])}" + # if not isinstance(parsed_json[key], args[key]): + # return True, f"Invalid type for {key}. Expected {str(args[key])}" + return False, "" + except Exception as e: + if type(e) == json.decoder.JSONDecodeError: + return True, "Invalid JSON" + return True, str(type(e)) + # @app.callback( # Output("commands-accordion", "children"), diff --git a/to_load.yaml b/to_load.yaml new file mode 100644 index 0000000..53fc9c5 --- /dev/null +++ b/to_load.yaml @@ -0,0 +1,48 @@ +- - &id001 !!python/object:devices.linear_stage_150.LinearStage150 + _name: LinearStage150 + _is_initialized: false + _port: /dev/cu.URT0 + _baudrate: 115200 + _timeout: 0.1 + _destination: 0x50 + _source: 0x01 + _channel: 1 + ser: !!python/object:serial.serialposix.Serial + is_open: false + portstr: null + name: null + _port: null + _baudrate: 9600 + _bytesize: 8 + _parity: N + _stopbits: 1 + _timeout: null + _write_timeout: null + _xonxoff: false + _rtscts: false + _dsrdtr: false + _inter_byte_timeout: null + _rs485_mode: null + _rts_state: true + _dtr_state: true + _break_state: false + _exclusive: null +- - - !!python/object:commands.linear_stage_150_commands.LinearStage150Connect + _receiver: *id001 + _params: + receiver_name: LinearStage150 + delay: 0.0 + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null + _name: LinearStage150Connect receiver_name=LinearStage150 + - - !!python/object:commands.linear_stage_150_commands.LinearStage150EnableMotor + _receiver: *id001 + _params: + receiver_name: LinearStage150 + delay: 0.0 + state: true + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null +- ALL From 3fd428b9c615c9907db8b52e7abef46367fc0600 Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 26 Jun 2023 09:46:30 -0500 Subject: [PATCH 009/125] renamed temp1 to app --- temp1.py => app.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename temp1.py => app.py (100%) diff --git a/temp1.py b/app.py similarity index 100% rename from temp1.py rename to app.py From bdc91f7faff8c03eb6483249b59717a5e5ac44d7 Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 26 Jun 2023 14:12:32 -0500 Subject: [PATCH 010/125] refactored and cleaned code --- app.py | 291 ++++++--------------------------- db/gridfs.py | 27 +++ db/validation/devices.py | 326 +++++++++++++++++++++++++++++++++++++ db/validation/films.py | 77 +++++++++ db/validation/solutions.py | 67 ++++++++ pages/data.py | 15 ++ pages/edit-recipe.py | 172 +++++++++++++++++++ pages/execute-recipe.py | 46 ++++++ pages/home.py | 25 +++ requirements.txt | 33 +++- 10 files changed, 838 insertions(+), 241 deletions(-) create mode 100644 db/gridfs.py create mode 100644 db/validation/devices.py create mode 100644 db/validation/films.py create mode 100644 db/validation/solutions.py create mode 100644 pages/data.py create mode 100644 pages/edit-recipe.py create mode 100644 pages/execute-recipe.py create mode 100644 pages/home.py diff --git a/app.py b/app.py index 9194b8a..b7a3178 100644 --- a/app.py +++ b/app.py @@ -33,13 +33,16 @@ import random import dash_bootstrap_components as dbc -app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) + +app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP], use_pages=True) +server = app.server navbar = dbc.NavbarSimple( children=[ dbc.NavItem(dbc.NavLink("Home", href="/")), dbc.NavItem(dbc.NavLink("Edit Recipe", href="/edit-recipe")), dbc.NavItem(dbc.NavLink("Execute Recipe", href="/execute-recipe")), + dbc.NavItem(dbc.NavLink("Data", href="/data")), ], brand="AAMP", brand_href="/", @@ -47,25 +50,7 @@ dark=True, ) -home_layout = html.Div( - [ - html.H1("Home"), - dcc.Input(id="filename-input", type="text", placeholder="Enter filename name"), - dbc.Button("Load", id="filename-input-button", n_clicks=0), - html.Div(id="home-output"), - dbc.Button("Refresh List", id="home-refresh-list-button", n_clicks=0), - dash_table.DataTable( - id="home-recipes-list-table", - columns=[ - {"name": "File Name", "id": "file_name"}, - # {'name':'YAML', 'id':'yaml_data'} - ], - data=[], - style_table={"width": "300px"}, - style_cell={"textAlign": "left"}, - ), - ] -) + @app.callback( @@ -93,17 +78,32 @@ def fill_filename_input(active_cell, data): return "" -execute_recipe_layout = html.Div( - [ - html.H1("Execute Recipe"), - dbc.Button("Execute", id="execute-button", n_clicks=0), - html.Div(id="execute-recipe-output"), - dcc.Interval(id="update-interval", interval=1000, n_intervals=0), - dcc.Textarea( - id="console-output", readOnly=True, style={"width": "100%", "height": 200} - ), - ] + + +@app.callback( + Output('data-output', 'value'), + Input('load-data-button', 'n_clicks'), + prevent_initial_call=True, ) +def load_data(n): + val = "" + docs = mongo.db['recipes'].find() + for doc in docs: + val += str(doc) + + return val + +import sys + +@app.callback( + Output('console-output', 'readOnly'), + Input('stop-button', 'n_clicks'), + prevent_initial_call=True, +) +def stop_execution(n): + print('stop') + + return True @app.callback( @@ -147,186 +147,28 @@ def get_document_from_db(n_clicks, filename): return "/" -edit_recipe_layout = html.Div( - [ - html.H1("Edit Recipe"), - html.Div( - [ - html.Div( - [ - html.H2("Devices"), - dbc.Button("Refresh", id="refresh-button1", n_clicks=0), - dbc.Button("Add device", id="add-device-button"), - dbc.Button("Edit", id="edit-device-button"), - dbc.Modal( - [ - dbc.ModalHeader( - dbc.ModalTitle("Editor"), close_button=False - ), - dbc.ModalBody( - [ - dcc.Textarea( - id="device-json-editor", - style={ - "width": "100%", - "height": "200px", - "fontFamily": "monospace", - "backgroundColor": "#f5f5f5", - "border": "1px solid #ccc", - "padding": "10px", - "color": "#333", - }, - ), - html.Div( - id="edit-device-error", - style={"color": "red"}, - ), - html.Div( - id="edit-device-serial-ports-info", - ), - ] - ), - dbc.ModalFooter( - dbc.Button("Save", id="save-device-editor") - ), - ], - id="device-editor-modal", - keyboard=False, - backdrop="static", - ), - dbc.Modal( - [ - dbc.ModalHeader(dbc.ModalTitle("Add Device")), - dbc.ModalBody( - [ - dcc.Dropdown( - id="add-device-dropdown", - options=[], - value=None, - ), - dcc.Textarea( - id="add-device-json-editor", - style={ - "width": "100%", - "height": "200px", - "fontFamily": "monospace", - "backgroundColor": "#f5f5f5", - "border": "1px solid #ccc", - "padding": "10px", - "color": "#333", - }, - ), - html.Div( - id="add-device-error", - style={"color": "red"}, - ), - html.Div( - id="add-device-serial-ports-info", - ), - ] - ), - dbc.ModalFooter( - dbc.Button("Add", id="add-device-editor") - ), - ], - id="device-add-modal", - keyboard=False, - backdrop="static", - ), - html.Div( - children=[dash_table.DataTable(id="devices-table")], - id="devices-table-div", - ), - ], - className="table-container", - ), - html.Div( - [ - html.H2("Commands"), - dbc.Button("Refresh", id="refresh-button2", n_clicks=0), - dbc.Button("Add command", id="add-command-button"), - dbc.Button("Edit", id="edit-command-button"), - dbc.Modal( - [ - dbc.ModalHeader( - dbc.ModalTitle("Editor"), close_button=False - ), - dbc.ModalBody( - [ - dcc.Textarea( - id="command-json-editor", - style={ - "width": "100%", - "height": "200px", - "fontFamily": "monospace", - "backgroundColor": "#f5f5f5", - "border": "1px solid #ccc", - "padding": "10px", - "color": "#333", - }, - ), - html.Div( - id="edit-command-error", - style={"color": "red"}, - ), - ] - ), - dbc.ModalFooter( - dbc.Button("Save", id="save-command-editor") - ), - ], - id="command-editor-modal", - keyboard=False, - backdrop="static", - ), - html.Div( - children=[dash_table.DataTable(id="commands-table")], - id="commands-table-div", - ), - dbc.Accordion( - [ - dbc.AccordionItem( - "item1", title="Item 1", item_id="item1" - ) - ], - id="commands-accordion", - # start_collapsed=True, - style={"display": "none"}, - ), - ], - className="table-container", - ), - html.Div( - [ - html.H2("Command Iterations"), - html.Button("Refresh", id="refresh-button3", n_clicks=0), - html.Div(id="table-container3"), - ], - className="table-container", - ), - ], - className="tables-container", - ), - ], - className="main-container", -) -app.layout = html.Div( - [dcc.Location(id="url", refresh=False), navbar, html.Div(id="page-content")] -) +# app.layout = html.Div( +# [, , html.Div(id="page-content")] +# ) + +app.layout = html.Div([dcc.Location(id="url"), navbar, dash.page_container]) -@app.callback(Output("page-content", "children"), [Input("url", "pathname")]) -def display_page(pathname): - print("\n\nrefreshing to " + pathname) - if pathname == "/": - return home_layout - elif pathname == "/edit-recipe": - return edit_recipe_layout - elif pathname == "/execute-recipe": - return execute_recipe_layout - else: - return html.Div("404") + +# @app.callback(Output("page-content", "children"), [Input("url", "pathname")]) +# def display_page(pathname): +# print("\n\nrefreshing to " + pathname) +# if pathname == "/": +# return home_layout +# elif pathname == "/edit-recipe": +# return edit_recipe_layout +# elif pathname == "/execute-recipe": +# return execute_recipe_layout +# elif pathname == "/data": +# return data_layout +# else: +# return html.Div("404") data_list4 = com.device_list @@ -344,24 +186,7 @@ def display_page(pathname): [State("devices-table-div", "children")], ) def update_table1(n_clicks, data, table): - # dl5 = dl5_og.copy() - # dl5_props_temp = {} - # count = 0 - # for list in dl5: - # dl5_props_temp.clear() - # if len(list) > 1: - # for prop in list: - # if prop != "_name": - # dl5_props_temp[prop] = list[prop] - # # print(prop) - # # print("h") - # dl5_temp_list = list - # dl5[count] = {} - # dl5[count]["_name"] = dl5_temp_list["_name"] - # # dl5[count]["_is_initialized"] = dl5_temp_list["_is_initialized"] - # dl5[count]["props"] = str(dl5_props_temp) - # # print(str(dl5_props_temp)) - # count += 1 + # table_data1 = dl5 table_data1 = com.get_clean_device_list().copy() @@ -389,12 +214,7 @@ def update_table1(n_clicks, data, table): # {"name": "Initialized", "id": "_is_initialized"}, {"name": "Parameters", "id": "params"}, ], - # style_data_conditional=[ - # {"if": {"column_id": "_name"}, "width": "250px", 'overflow': 'hidden', 'textOverflow': 'ellipsis'}, - # # {"if": {"column_id": "_is_initialized"}, "width": "20%"}, - # # {"if": {"column_id": "props"}, "width": "100px", 'overflow': 'hidden', 'textOverflow': 'ellipsis'}, - # ], - # style_cell={"textAlign": "left", "padding": "5px"}, + style_cell={ "overflow": "hidden", "textOverflow": "ellipsis", @@ -499,14 +319,6 @@ def save_device(n_clicks, active_cell, data, value): ): # data_row = data[active_cell["row"]] params = eval(value) - # print(com.device_by_name[params['name']]) - # init_str = data_row['device_type'] + '(' - # for key, value2 in params.items(): - # if isinstance(value2, str): - # init_str += key + '=' + "'" + str(value2) + "'" + ',' - # else: - # init_str += key + '=' + str(value2) + ',' - # init_str = init_str[:-1] + ')' com.device_by_name[params["name"]].update_init_args(params) return None return data @@ -529,7 +341,6 @@ def fill_command_json_editor(is_open, active_cell, data): @app.callback( [ - # Output("add-device-json-editor", "value"), Output("add-device-dropdown", "options"), ], [Input("device-add-modal", "is_open")], diff --git a/db/gridfs.py b/db/gridfs.py new file mode 100644 index 0000000..19650a5 --- /dev/null +++ b/db/gridfs.py @@ -0,0 +1,27 @@ +from mongodb_helper import MongoDBHelper +from gridfs import GridFS +from bson import ObjectId + + +mongo = MongoDBHelper( + "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "diaogroup", +) + +db = mongo.db + +fs = GridFS(db, collection="recipes") + +# Open and store the CSV file using GridFS +with open("data.csv", "rb") as file: + file_id = fs.put(file, filename="data.csv") + # create ObjectId(file_id) in document to point to csv + +# Retrieve the CSV file from GridFS +gridfs_file = fs.find_one({"filename": "data.csv"}) + +# Read the CSV data from the file +csv_data = gridfs_file.read() + +# Print the CSV data +print(csv_data.decode()) diff --git a/db/validation/devices.py b/db/validation/devices.py new file mode 100644 index 0000000..7c27f6f --- /dev/null +++ b/db/validation/devices.py @@ -0,0 +1,326 @@ +from mongodb_helper import MongoDBHelper + +mongo = MongoDBHelper( + "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "diaogroup", +) + +db = mongo.db + +device_dict = { + "device": { + "electrode_etl": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + "active_layer": { + "donor": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + "acceptor": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + }, + "electrode_htl": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + }, + "result": { + "jv_curve": ["object_id", "null"], + "t80": ["float", "null"], + }, +} + +device_dict_schema = { + "bsonType": "object", + "title": "Device Object Validation", + "required": ["device", "result"], + "properties": { + "device": { + "bsonType": "object", + "title": "Device Validation", + "required": ["electrode_etl", "active_layer", "electrode_htl"], + "properties": { + "electrode_etl": { + "bsonType": "object", + "title": "Electrode ETL Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": ["solvent", "concentration", "printing_speed", "printing_temperature", "additive"], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": ["molecule", "concentration"], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "active_layer": { + "bsonType": "object", + "title": "Active Layer Validation", + "required": ["donor", "acceptor"], + "properties": { + "donor": { + "bsonType": "object", + "title": "Donor Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": ["solvent", "concentration", "printing_speed", "printing_temperature", "additive"], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": ["molecule", "concentration"], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "acceptor": { + "bsonType": "object", + "title": "Acceptor Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": ["solvent", "concentration", "printing_speed", "printing_temperature", "additive"], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": ["molecule", "concentration"], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "electrode_htl": { + "bsonType": "object", + "title": "Electrode HTL Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": ["solvent", "concentration", "printing_speed", "printing_temperature", "additive"], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": ["molecule", "concentration"], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "result": { + "bsonType": "object", + "title": "Result Object Validation", + "required": ["jv_curve", "t80"], + "properties": { + "jv_curve": { + "bsonType": ["objectId", "null"], + "description": "'jv_curve' must be an objectId and is required", + }, + "t80": { + "bsonType": ["double", "null"], + "description": "'t80' must be a double and is required", + }, + }, + }, + }, +} + + +collection_name = "devices" +collection_options = {"validator": {"$jsonSchema": device_dict_schema}} +db.create_collection(collection_name, **collection_options) +quit() diff --git a/db/validation/films.py b/db/validation/films.py new file mode 100644 index 0000000..b956e1a --- /dev/null +++ b/db/validation/films.py @@ -0,0 +1,77 @@ +from mongodb_helper import MongoDBHelper + + +mongo = MongoDBHelper( + "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "diaogroup", +) + +db = mongo.db + + +film_dict = { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + }, + "result": { + "uv_vis": ["object_id", "null"], + "t80": ["float", "null"], + }, +} + + +film_dict_schema = { + "bsonType": "object", + "title": "Film Object Validation", + "required": ["metadata", "result"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Object Validation", + "required": ["solvent", "concentration", "printing_speed", "printing_temperature"], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'printing_speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'printing_temperature' must be a double and is required", + }, + }, + }, + "result": { + "bsonType": "object", + "title": "Result Object Validation", + "required": ["uv_vis", "t80"], + "properties": { + "uv_vis": { + "bsonType": ["objectId", "null"], + "description": "'uv_vis' must be an objectId and is required", + }, + "t80": { + "bsonType": ["double", "null"], + "description": "'t80' must be a double and is required", + }, + }, + }, + }, +} + + + +collection_name = "film" +collection_options = {"validator": {"$jsonSchema": film_dict_schema}} +db.create_collection(collection_name, **collection_options) +quit() \ No newline at end of file diff --git a/db/validation/solutions.py b/db/validation/solutions.py new file mode 100644 index 0000000..9d36f23 --- /dev/null +++ b/db/validation/solutions.py @@ -0,0 +1,67 @@ +from mongodb_helper import MongoDBHelper + +mongo = MongoDBHelper( + "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "diaogroup", +) + +db = mongo.db + + +solution_dict = { + "metadata": { + "solvent": "string", + "concentration": 0.3, + }, + "result": { + "uv_vis": None, + "t80": 0.3, + }, +} + + + +json_schema = { + "bsonType": "object", + "title": "Solution Object Validation", + "required": ["metadata", "result"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Object Validation", + "required": ["solvent", "concentration"], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + }, + }, + "result": { + "bsonType": "object", + "title": "Result Object Validation", + "required": ["uv_vis", "t80"], + "properties": { + "uv_vis": { + "bsonType": ["objectId", "null"], + "description": "'uv_vis' must be an objectId and is required", + }, + "t80": { + "bsonType": ["double", "null"], + "description": "'t80' must be a double and is required", + }, + }, + }, + }, +} + + + +collection_name = "solutions" +collection_options = {"validator": {"$jsonSchema": json_schema}} +db.create_collection(collection_name, **collection_options) +quit() diff --git a/pages/data.py b/pages/data.py new file mode 100644 index 0000000..b289bbd --- /dev/null +++ b/pages/data.py @@ -0,0 +1,15 @@ +from dash import Dash, html, dcc, dash_table +import dash_bootstrap_components as dbc +import dash + +dash.register_page(__name__, "/data") + +layout = html.Div( + [ + html.H1("Data"), + dbc.Button("Load data",id='load-data-button', n_clicks=0), + dcc.Textarea( + id="data-output", readOnly=True, style={"width": "100%", "height": 400} + ) + ] +) \ No newline at end of file diff --git a/pages/edit-recipe.py b/pages/edit-recipe.py new file mode 100644 index 0000000..48fa503 --- /dev/null +++ b/pages/edit-recipe.py @@ -0,0 +1,172 @@ +from dash import Dash, html, dcc, dash_table +import dash_bootstrap_components as dbc +import dash + +dash.register_page(__name__, "/edit-recipe") + +layout = html.Div( + [ + html.H1("Edit Recipe"), + html.Div( + [ + html.Div( + [ + html.H2("Devices"), + dbc.Button("Refresh", id="refresh-button1", n_clicks=0), + dbc.Button("Add device", id="add-device-button"), + dbc.Button("Edit", id="edit-device-button"), + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Editor"), close_button=False + ), + dbc.ModalBody( + [ + dcc.Textarea( + id="device-json-editor", + style={ + "width": "100%", + "height": "200px", + "fontFamily": "monospace", + "backgroundColor": "#f5f5f5", + "border": "1px solid #ccc", + "padding": "10px", + "color": "#333", + }, + ), + html.Div( + id="edit-device-error", + style={"color": "red"}, + ), + html.Div( + id="edit-device-serial-ports-info", + ), + ] + ), + dbc.ModalFooter( + dbc.Button("Save", id="save-device-editor") + ), + ], + id="device-editor-modal", + keyboard=False, + backdrop="static", + ), + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Add Device")), + dbc.ModalBody( + [ + dcc.Dropdown( + id="add-device-dropdown", + options=[], + value=None, + ), + dcc.Textarea( + id="add-device-json-editor", + style={ + "width": "100%", + "height": "200px", + "fontFamily": "monospace", + "backgroundColor": "#f5f5f5", + "border": "1px solid #ccc", + "padding": "10px", + "color": "#333", + }, + ), + html.Div( + id="add-device-error", + style={"color": "red"}, + ), + html.Div( + id="add-device-serial-ports-info", + ), + ] + ), + dbc.ModalFooter( + dbc.Button("Add", id="add-device-editor") + ), + ], + id="device-add-modal", + keyboard=False, + backdrop="static", + ), + html.Div( + children=[dash_table.DataTable(id="devices-table")], + id="devices-table-div", + ), + ], + className="table-container", + ), + html.Div( + [ + html.H2("Commands"), + dbc.Button("Refresh", id="refresh-button2", n_clicks=0), + dbc.Button("Add command", id="add-command-button"), + dbc.Button("Edit", id="edit-command-button"), + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Editor"), close_button=False + ), + dbc.ModalBody( + [ + dcc.Textarea( + id="command-json-editor", + style={ + "width": "100%", + "height": "200px", + "fontFamily": "monospace", + "backgroundColor": "#f5f5f5", + "border": "1px solid #ccc", + "padding": "10px", + "color": "#333", + }, + ), + html.Div( + id="edit-command-error", + style={"color": "red"}, + ), + ] + ), + dbc.ModalFooter( + dbc.Button("Save", id="save-command-editor") + ), + ], + id="command-editor-modal", + keyboard=False, + backdrop="static", + ), + html.Div( + children=[dash_table.DataTable(id="commands-table")], + id="commands-table-div", + ), + dbc.Accordion( + [ + dbc.AccordionItem( + "item1", title="Item 1", item_id="item1" + ) + ], + id="commands-accordion", + # start_collapsed=True, + style={"display": "none"}, + ), + ], + className="table-container", + ), + html.Div( + [ + html.H2("Command Iterations"), + html.Button("Refresh", id="refresh-button3", n_clicks=0), + html.Div(id="table-container3"), + ], + className="table-container", + ), + ], + className="tables-container", + ), + ], + className="main-container", +) + + + diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py new file mode 100644 index 0000000..3aa6395 --- /dev/null +++ b/pages/execute-recipe.py @@ -0,0 +1,46 @@ +from dash import Dash, html, dcc, dash_table, Input, Output, callback +import dash_bootstrap_components as dbc +import dash +import logging +from dash_dangerously_set_inner_html import DangerouslySetInnerHTML + + +dash.register_page(__name__, "/execute-recipe") + +layout = html.Div( + [ + html.H1("Execute Recipe"), + dbc.Button("Execute", id="execute-button", n_clicks=0), + dbc.Button("Stop", id="stop-button", n_clicks=0), + html.Div(id="execute-recipe-output"), + dcc.Interval(id="update-interval", interval=1000, n_intervals=0), + dcc.Textarea( + id="console-output", readOnly=True, style={"width": "100%", "height": 0} + ), + dcc.Interval(id='interval1', interval=500, n_intervals=0), + html.H1(id='div-out', children='Log'), + # html.Iframe(id='console-out',srcDoc='',style={'width': '100%','height':400}), + html.Div(id='console-out2') + ] +) + +class DashLoggerHandler(logging.StreamHandler): + def __init__(self): + logging.StreamHandler.__init__(self) + self.queue = [] + + def emit(self, record): + msg = self.format(record) + self.queue.append(msg) + + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +dashLoggerHandler = DashLoggerHandler() +logger.addHandler(dashLoggerHandler) + +@callback( + Output('console-out2', 'children'), + Input('interval1', 'n_intervals')) +def update_output(n): + return DangerouslySetInnerHTML(('\n'.join(dashLoggerHandler.queue)).replace('\n', '
')) \ No newline at end of file diff --git a/pages/home.py b/pages/home.py new file mode 100644 index 0000000..8e1abec --- /dev/null +++ b/pages/home.py @@ -0,0 +1,25 @@ +from dash import Dash, html, dcc, dash_table +import dash_bootstrap_components as dbc +import dash + +dash.register_page(__name__, "/") + +layout = html.Div( + [ + html.H1("Home"), + dcc.Input(id="filename-input", type="text", placeholder="Enter filename name"), + dbc.Button("Load", id="filename-input-button", n_clicks=0), + html.Div(id="home-output"), + dbc.Button("Refresh List", id="home-refresh-list-button", n_clicks=0), + dash_table.DataTable( + id="home-recipes-list-table", + columns=[ + {"name": "File Name", "id": "file_name"}, + # {'name':'YAML', 'id':'yaml_data'} + ], + data=[], + style_table={"width": "300px"}, + style_cell={"textAlign": "left"}, + ), + ] +) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 65c7b60..346f333 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,33 +1,64 @@ +aiohttp==3.8.4 +aiosignal==1.3.1 +ansible-core==2.15.0 +async-timeout==4.0.2 +attrs==23.1.0 +certifi==2023.5.7 +cffi==1.15.1 +charset-normalizer==3.1.0 +click==8.1.3 colorama==0.4.6 contourpy==1.0.7 +cryptography==41.0.1 cycler==0.11.0 +dash==2.10.2 +dash-bootstrap-components==1.4.1 +dash-core-components==2.0.0 +dash-dangerously-set-inner-html==0.0.2 +dash-html-components==2.0.0 +dash-table==5.0.0 dnspython==2.3.0 +Flask==2.2.5 fonttools==4.39.4 +frozenlist==1.3.3 +idna==3.4 install==1.3.5 +itsdangerous==2.1.2 Jinja2==3.1.2 joblib==1.2.0 kiwisolver==1.4.4 MarkupSafe==2.1.2 matplotlib==3.7.1 +mfc==0.4.0 +multidict==6.0.4 numpy==1.24.3 packaging==23.1 pandas==2.0.2 pdoc==13.1.1 Pillow==9.5.0 +plotly==5.14.1 prompt-toolkit==3.0.38 pyaml==23.5.9 +pycparser==2.21 Pygments==2.15.1 pymongo==4.3.3 pyparsing==3.0.9 +pyperclip==1.8.2 pyserial==3.5 python-dateutil==2.8.2 pytz==2023.3 PyYAML==6.0 questionary==1.10.0 +requests==2.31.0 +resolvelib==1.0.1 scikit-learn==1.2.2 scikit-optimize==0.9.0 scipy==1.10.1 six==1.16.0 +tenacity==8.2.2 threadpoolctl==3.1.0 tzdata==2023.3 -wcwidth==0.2.6 \ No newline at end of file +urllib3==2.0.3 +wcwidth==0.2.6 +Werkzeug==2.2.3 +yarl==1.9.2 From cec4baca91f4a179f5b6a8a1f9afacb9331fe0f3 Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 27 Jun 2023 12:31:21 -0500 Subject: [PATCH 011/125] added python editor --- app.py | 109 +++++++++++++++++++++++++++++------ db/validation/devices.py | 70 +++++++++++++++++++++-- pages/data.py | 6 +- pages/execute-recipe.py | 32 +++-------- pages/python-edit-recipe.py | 111 ++++++++++++++++++++++++++++++++++++ 5 files changed, 278 insertions(+), 50 deletions(-) create mode 100644 pages/python-edit-recipe.py diff --git a/app.py b/app.py index b7a3178..9839beb 100644 --- a/app.py +++ b/app.py @@ -4,11 +4,11 @@ from devices.device import Device from commands.command import Command, CompositeCommand from devices.device import Device, SerialDevice +import threading - +print('reset complete') com = CommandSequence() - com.load_from_yaml("to_load.yaml") @@ -42,6 +42,7 @@ dbc.NavItem(dbc.NavLink("Home", href="/")), dbc.NavItem(dbc.NavLink("Edit Recipe", href="/edit-recipe")), dbc.NavItem(dbc.NavLink("Execute Recipe", href="/execute-recipe")), + dbc.NavItem(dbc.NavLink('Edit Recipe (PY)', href='/python-edit-recipe')), dbc.NavItem(dbc.NavLink("Data", href="/data")), ], brand="AAMP", @@ -78,32 +79,41 @@ def fill_filename_input(active_cell, data): return "" +from dash_dangerously_set_inner_html import DangerouslySetInnerHTML @app.callback( - Output('data-output', 'value'), + Output('data-output-div', 'children'), Input('load-data-button', 'n_clicks'), prevent_initial_call=True, ) def load_data(n): - val = "" + val = '' docs = mongo.db['recipes'].find() for doc in docs: val += str(doc) + val += '

' + # nval = val. + return DangerouslySetInnerHTML(val) - return val +import os, signal -import sys +def kill_execution(): + os.kill(os.getpid(), signal.SIGINT) @app.callback( - Output('console-output', 'readOnly'), + Output('hidden-div', 'children'), Input('stop-button', 'n_clicks'), prevent_initial_call=True, ) def stop_execution(n): - print('stop') - - return True + print('stopping') + # kill_execution() + # print('stop done') + return [] + +import logging +from dash_dangerously_set_inner_html import DangerouslySetInnerHTML @app.callback( @@ -112,10 +122,42 @@ def stop_execution(n): prevent_initial_call=True, ) def execute_recipe(n_clicks): + invoker = CommandInvoker(com, False, None, False) - return html.Div(str(invoker.invoke_commands())) + invoker.invoke_commands() + return "done" + +class DashLoggerHandler(logging.StreamHandler): + def __init__(self): + logging.StreamHandler.__init__(self) + self.queue = [] + + def emit(self, record): + msg = self.format(record) + self.queue.append(msg) +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +dashLoggerHandler = DashLoggerHandler() +logger.addHandler(dashLoggerHandler) + +@app.callback( + Output('console-out2', 'children') , + Input('interval1', 'n_intervals') +) +def update_output(n): + return DangerouslySetInnerHTML(('\n'.join(dashLoggerHandler.queue)).replace('\n', '
')) + +@app.callback( + Output('console-out2', 'children', allow_duplicate=True), + Input('reset-button', 'n_clicks'), + prevent_initial_call=True, +) +def reset_console(n): + dashLoggerHandler.queue = [] + return [] + # import sys # from io import StringIO # stringio = StringIO() @@ -137,17 +179,50 @@ def get_document_from_db(n_clicks, filename): if filename is not None and filename != "": # Extract the YAML content from the document document = mongo.find_documents("recipes", {"file_name": filename})[0] - yaml_content = document.get("yaml_data", "") - # Update the YAML output - with open("to_load.yaml", "w") as file: - file.write(yaml_content) - com.load_from_yaml("to_load.yaml") + if document.get('dash_friendly', '') == False or document.get('python_code', '') == '': + yaml_content = document.get("yaml_data", "") + # Update the YAML output + with open("to_load.yaml", "w") as file: + file.write(yaml_content) + com.load_from_yaml("to_load.yaml") + else: + exec(document.get('python_code', '')) + com.load_from_yaml('to_save.yaml') + com.document = document + # com.python_code = document.get("python_code", "") + return "/edit-recipe" return "/" +@app.callback( + Output('ace-recipe-editor', 'value'), + [Input('refresh-button-ace', 'n_clicks'), Input('url', 'pathname')], + prevent_initial_call=True, +) +def fill_ace_editor(n, url): + if url == '/python-edit-recipe': + return com.document.get("python_code", "") + return "" - +@app.callback( + [Output('ace-recipe-editor', 'value', allow_duplicate=True), Output('ace-editor-alert', 'is_open'), Output('ace-editor-alert', 'children'), Output('ace-editor-alert', 'color'), Output('ace-editor-alert', 'duration')], + Input('execute-and-save-button', 'n_clicks'), + [State('ace-recipe-editor', 'value'), State('ace-editor-alert', 'is_open')], + prevent_initial_call=True, +) +def execute_and_save(n, value, is_open): + if value is not None and value != "": + try: + exec(value) + doc_id = com.document.get('_id', '') + (mongo.update_yaml_file('recipes', doc_id,{'python_code': value})) + com.load_from_yaml('to_save.yaml') + com.document = mongo.find_documents("recipes", {"_id": doc_id})[0] + return [value, True, "Saved!", 'success', 1500] + except Exception as e: + return [value, True, str(e), 'danger', 5000] + return [value, False, 'No code to execute', 'warning', 1000] # app.layout = html.Div( # [, , html.Div(id="page-content")] diff --git a/db/validation/devices.py b/db/validation/devices.py index 7c27f6f..1775a1a 100644 --- a/db/validation/devices.py +++ b/db/validation/devices.py @@ -23,6 +23,20 @@ } } }, + "etl": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, "active_layer": { "donor": { "material": { @@ -53,6 +67,20 @@ } }, }, + "htl": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, "electrode_htl": { "material": { "metadata": { @@ -97,7 +125,13 @@ "metadata": { "bsonType": "object", "title": "Metadata Validation", - "required": ["solvent", "concentration", "printing_speed", "printing_temperature", "additive"], + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], "properties": { "solvent": { "bsonType": "string", @@ -154,7 +188,13 @@ "metadata": { "bsonType": "object", "title": "Metadata Validation", - "required": ["solvent", "concentration", "printing_speed", "printing_temperature", "additive"], + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], "properties": { "solvent": { "bsonType": "string", @@ -175,7 +215,10 @@ "additive": { "bsonType": "object", "title": "Additive Validation", - "required": ["molecule", "concentration"], + "required": [ + "molecule", + "concentration", + ], "properties": { "molecule": { "bsonType": "string", @@ -206,7 +249,13 @@ "metadata": { "bsonType": "object", "title": "Metadata Validation", - "required": ["solvent", "concentration", "printing_speed", "printing_temperature", "additive"], + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], "properties": { "solvent": { "bsonType": "string", @@ -227,7 +276,10 @@ "additive": { "bsonType": "object", "title": "Additive Validation", - "required": ["molecule", "concentration"], + "required": [ + "molecule", + "concentration", + ], "properties": { "molecule": { "bsonType": "string", @@ -260,7 +312,13 @@ "metadata": { "bsonType": "object", "title": "Metadata Validation", - "required": ["solvent", "concentration", "printing_speed", "printing_temperature", "additive"], + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], "properties": { "solvent": { "bsonType": "string", diff --git a/pages/data.py b/pages/data.py index b289bbd..c6c03f6 100644 --- a/pages/data.py +++ b/pages/data.py @@ -1,6 +1,7 @@ from dash import Dash, html, dcc, dash_table import dash_bootstrap_components as dbc import dash +# from dash_dangerously_set_inner_html import DangerouslySetInnerHTML dash.register_page(__name__, "/data") @@ -9,7 +10,8 @@ html.H1("Data"), dbc.Button("Load data",id='load-data-button', n_clicks=0), dcc.Textarea( - id="data-output", readOnly=True, style={"width": "100%", "height": 400} - ) + id="data-output", readOnly=True, style={"width": "100%", "height": 0} + ), + html.Div(id='data-output-div') ] ) \ No newline at end of file diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py index 3aa6395..22c404f 100644 --- a/pages/execute-recipe.py +++ b/pages/execute-recipe.py @@ -12,35 +12,17 @@ html.H1("Execute Recipe"), dbc.Button("Execute", id="execute-button", n_clicks=0), dbc.Button("Stop", id="stop-button", n_clicks=0), + dbc.Button("Reset", id="reset-button", n_clicks=0), html.Div(id="execute-recipe-output"), dcc.Interval(id="update-interval", interval=1000, n_intervals=0), - dcc.Textarea( - id="console-output", readOnly=True, style={"width": "100%", "height": 0} - ), + # dcc.Textarea( + # id="console-output", readOnly=True, style={"width": "100%", "height": 0} + # ), dcc.Interval(id='interval1', interval=500, n_intervals=0), - html.H1(id='div-out', children='Log'), + html.Div(id='hidden-div', style={'display':'none'}), + html.H4(id='div-out', children='Log'), # html.Iframe(id='console-out',srcDoc='',style={'width': '100%','height':400}), - html.Div(id='console-out2') + html.Div(id='console-out2'), ] ) -class DashLoggerHandler(logging.StreamHandler): - def __init__(self): - logging.StreamHandler.__init__(self) - self.queue = [] - - def emit(self, record): - msg = self.format(record) - self.queue.append(msg) - - -logger = logging.getLogger() -logger.setLevel(logging.DEBUG) -dashLoggerHandler = DashLoggerHandler() -logger.addHandler(dashLoggerHandler) - -@callback( - Output('console-out2', 'children'), - Input('interval1', 'n_intervals')) -def update_output(n): - return DangerouslySetInnerHTML(('\n'.join(dashLoggerHandler.queue)).replace('\n', '
')) \ No newline at end of file diff --git a/pages/python-edit-recipe.py b/pages/python-edit-recipe.py new file mode 100644 index 0000000..bf83cbc --- /dev/null +++ b/pages/python-edit-recipe.py @@ -0,0 +1,111 @@ +from dash import Dash, html, dcc, dash_table +import dash_bootstrap_components as dbc +import dash +import dash_ace + +dash.register_page(__name__, "/python-edit-recipe") + +layout = html.Div( + [ + html.H1("Edit Recipe"), + html.Div( + [ + html.Div( + [ + html.H2("Devices"), + dbc.Alert("Alert", id="ace-editor-alert", is_open=False, duration=500), + dbc.Button("Fill editor", id="refresh-button-ace", n_clicks=0), + dbc.Button("Add device", id="add-device-button"), + dbc.Button("Add command", id="add-command-button"), + dbc.Button('Execute and save yaml', id='execute-and-save-button', n_clicks=0), + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Add Device")), + dbc.ModalBody( + [ + dcc.Dropdown( + id="add-device-dropdown", + options=[], + value=None, + ), + + ] + ), + dbc.ModalFooter( + dbc.Button("Add", id="add-device-editor") + ), + ], + id="device-add-modal", + keyboard=False, + backdrop="static", + ), + dash_ace.DashAceEditor(id='ace-recipe-editor', mode = 'python', enableBasicAutocompletion=True, enableLiveAutocompletion=True, theme='github') + ], + className="table-container", + ), + # html.Div( + # [ + # html.H2("Commands"), + # dbc.Button("Refresh", id="refresh-button2", n_clicks=0), + + # dbc.Button("Edit", id="edit-command-button"), + # dbc.Modal( + # [ + # dbc.ModalHeader( + # dbc.ModalTitle("Editor"), close_button=False + # ), + # dbc.ModalBody( + # [ + # dcc.Textarea( + # id="command-json-editor", + # style={ + # "width": "100%", + # "height": "200px", + # "fontFamily": "monospace", + # "backgroundColor": "#f5f5f5", + # "border": "1px solid #ccc", + # "padding": "10px", + # "color": "#333", + # }, + # ), + # html.Div( + # id="edit-command-error", + # style={"color": "red"}, + # ), + # ] + # ), + # dbc.ModalFooter( + # dbc.Button("Save", id="save-command-editor") + # ), + # ], + # id="command-editor-modal", + # keyboard=False, + # backdrop="static", + # ), + # html.Div( + # children=[dash_table.DataTable(id="commands-table")], + # id="commands-table-div", + # ), + # dbc.Accordion( + # [ + # dbc.AccordionItem( + # "item1", title="Item 1", item_id="item1" + # ) + # ], + # id="commands-accordion", + # # start_collapsed=True, + # style={"display": "none"}, + # ), + # ], + # className="table-container", + # ), + + ], + className="tables-container", + ), + ], + className="main-container", +) + + + From 7222d56c42f8b73722baca08a7e64ad6aa34e681 Mon Sep 17 00:00:00 2001 From: Piyush Date: Wed, 28 Jun 2023 10:49:57 -0500 Subject: [PATCH 012/125] py editor and refactor --- app.py | 388 +++++++++++++++++++++++------------- pages/data.py | 14 +- pages/edit-recipe.py | 23 +-- pages/execute-recipe.py | 11 +- pages/python-edit-recipe.py | 37 ++-- util.py | 36 +++- 6 files changed, 325 insertions(+), 184 deletions(-) diff --git a/app.py b/app.py index 9839beb..4ccecc2 100644 --- a/app.py +++ b/app.py @@ -5,18 +5,36 @@ from commands.command import Command, CompositeCommand from devices.device import Device, SerialDevice import threading +import util +from mongodb_helper import MongoDBHelper +import pandas as pd +import dash +from dash import dcc +from dash import html +from dash import dash_table +from dash.dependencies import Input, Output, State +import random +import dash_bootstrap_components as dbc +from dash_dangerously_set_inner_html import DangerouslySetInnerHTML +import os, signal +import logging +import inspect + +try: + import serial.tools.list_ports +except ImportError: + _has_serial = False +else: + _has_serial = True +import typing + -print('reset complete') +print("\nreset complete") com = CommandSequence() com.load_from_yaml("to_load.yaml") -import util -import ctypes -from mongodb_helper import MongoDBHelper -import pandas as pd - mongo = MongoDBHelper( "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", "diaogroup", @@ -25,16 +43,13 @@ # out = util.device_to_dict(com.device_list[0]) -import dash -from dash import dcc -from dash import html -from dash import dash_table -from dash.dependencies import Input, Output, State -import random -import dash_bootstrap_components as dbc - -app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP], use_pages=True) +app = dash.Dash( + __name__, + external_stylesheets=[dbc.themes.BOOTSTRAP], + use_pages=True, + prevent_initial_callbacks="initial_duplicate", +) server = app.server navbar = dbc.NavbarSimple( @@ -42,7 +57,7 @@ dbc.NavItem(dbc.NavLink("Home", href="/")), dbc.NavItem(dbc.NavLink("Edit Recipe", href="/edit-recipe")), dbc.NavItem(dbc.NavLink("Execute Recipe", href="/execute-recipe")), - dbc.NavItem(dbc.NavLink('Edit Recipe (PY)', href='/python-edit-recipe')), + dbc.NavItem(dbc.NavLink("Edit Recipe (PY)", href="/python-edit-recipe")), dbc.NavItem(dbc.NavLink("Data", href="/data")), ], brand="AAMP", @@ -52,8 +67,6 @@ ) - - @app.callback( Output("home-recipes-list-table", "data"), [Input("home-refresh-list-button", "n_clicks")], @@ -71,50 +84,46 @@ def fetch_recipe_list(n_clicks): Output("filename-input", "value"), Input("home-recipes-list-table", "active_cell"), State("home-recipes-list-table", "data"), - prevent_initial_call=True, + # prevent_initial_call=True, ) def fill_filename_input(active_cell, data): if active_cell is not None: return data[active_cell["row"]]["file_name"] + if hasattr(com, "document"): + return com.document["file_name"] return "" -from dash_dangerously_set_inner_html import DangerouslySetInnerHTML - - @app.callback( - Output('data-output-div', 'children'), - Input('load-data-button', 'n_clicks'), - prevent_initial_call=True, + Output("data-output-div", "children"), + Input("load-data-button", "n_clicks"), + prevent_initial_call=True, ) def load_data(n): - val = '' - docs = mongo.db['recipes'].find() + val = "" + docs = mongo.db["recipes"].find() for doc in docs: val += str(doc) - val += '

' + val += "

" # nval = val. return DangerouslySetInnerHTML(val) -import os, signal def kill_execution(): os.kill(os.getpid(), signal.SIGINT) + @app.callback( - Output('hidden-div', 'children'), - Input('stop-button', 'n_clicks'), - prevent_initial_call=True, + Output("hidden-div", "children"), + Input("stop-button", "n_clicks"), + prevent_initial_call=True, ) def stop_execution(n): - print('stopping') + print("stopping") # kill_execution() # print('stop done') return [] -import logging -from dash_dangerously_set_inner_html import DangerouslySetInnerHTML - @app.callback( Output("execute-recipe-output", "children"), @@ -122,11 +131,11 @@ def stop_execution(n): prevent_initial_call=True, ) def execute_recipe(n_clicks): - invoker = CommandInvoker(com, False, None, False) invoker.invoke_commands() return "done" + class DashLoggerHandler(logging.StreamHandler): def __init__(self): logging.StreamHandler.__init__(self) @@ -142,22 +151,24 @@ def emit(self, record): dashLoggerHandler = DashLoggerHandler() logger.addHandler(dashLoggerHandler) -@app.callback( - Output('console-out2', 'children') , - Input('interval1', 'n_intervals') -) + +@app.callback(Output("console-out2", "children"), Input("interval1", "n_intervals")) def update_output(n): - return DangerouslySetInnerHTML(('\n'.join(dashLoggerHandler.queue)).replace('\n', '
')) + return DangerouslySetInnerHTML( + ("\n".join(dashLoggerHandler.queue)).replace("\n", "
") + ) + @app.callback( - Output('console-out2', 'children', allow_duplicate=True), - Input('reset-button', 'n_clicks'), - prevent_initial_call=True, + Output("console-out2", "children", allow_duplicate=True), + Input("reset-button", "n_clicks"), + prevent_initial_call=True, ) def reset_console(n): dashLoggerHandler.queue = [] return [] + # import sys # from io import StringIO # stringio = StringIO() @@ -179,50 +190,81 @@ def get_document_from_db(n_clicks, filename): if filename is not None and filename != "": # Extract the YAML content from the document document = mongo.find_documents("recipes", {"file_name": filename})[0] - if document.get('dash_friendly', '') == False or document.get('python_code', '') == '': + if ( + document.get("dash_friendly", "") == False + or document.get("python_code", "") == "" + ): yaml_content = document.get("yaml_data", "") # Update the YAML output with open("to_load.yaml", "w") as file: file.write(yaml_content) com.load_from_yaml("to_load.yaml") + com.document = document + return "/python-edit-recipe" else: - exec(document.get('python_code', '')) - com.load_from_yaml('to_save.yaml') - com.document = document + exec(document.get("python_code", "")) + com.load_from_yaml("to_save.yaml") + com.document = document + # com.python_code = document.get("python_code", "") return "/edit-recipe" return "/" + @app.callback( - Output('ace-recipe-editor', 'value'), - [Input('refresh-button-ace', 'n_clicks'), Input('url', 'pathname')], - prevent_initial_call=True, + [ + Output("ace-recipe-editor", "value", allow_duplicate=True), + Output("ace-editor-alert", "is_open"), + Output("ace-editor-alert", "children"), + Output("ace-editor-alert", "color"), + Output("ace-editor-alert", "duration"), + ], + [Input("url", "pathname"), Input("refresh-button-ace", "n_clicks")], ) -def fill_ace_editor(n, url): - if url == '/python-edit-recipe': - return com.document.get("python_code", "") - return "" +def fill_ace_editor(url, n): + if url == "/python-edit-recipe": + if hasattr(com, "document"): + python_code = com.document.get("python_code", "") + if python_code is not None and python_code != "": + return [python_code, True, "Loaded!", "success", 1500] + else: + return ["", True, "No code available", "warning", 1000] + else: + return ["", True, "No code available", "warning", 1000] + else: + return ["", False, "", "success", 0] + @app.callback( - [Output('ace-recipe-editor', 'value', allow_duplicate=True), Output('ace-editor-alert', 'is_open'), Output('ace-editor-alert', 'children'), Output('ace-editor-alert', 'color'), Output('ace-editor-alert', 'duration')], - Input('execute-and-save-button', 'n_clicks'), - [State('ace-recipe-editor', 'value'), State('ace-editor-alert', 'is_open')], + [ + Output("ace-recipe-editor", "value", allow_duplicate=True), + Output("ace-editor-alert", "is_open", allow_duplicate=True), + Output("ace-editor-alert", "children", allow_duplicate=True), + Output("ace-editor-alert", "color", allow_duplicate=True), + Output("ace-editor-alert", "duration", allow_duplicate=True), + ], + Input("execute-and-save-button", "n_clicks"), + [State("ace-recipe-editor", "value"), State("ace-editor-alert", "is_open")], prevent_initial_call=True, ) def execute_and_save(n, value, is_open): if value is not None and value != "": try: exec(value) - doc_id = com.document.get('_id', '') - (mongo.update_yaml_file('recipes', doc_id,{'python_code': value})) - com.load_from_yaml('to_save.yaml') + doc_id = com.document.get("_id", "") + (mongo.update_yaml_file("recipes", doc_id, {"python_code": value})) + com.load_from_yaml("to_save.yaml") com.document = mongo.find_documents("recipes", {"_id": doc_id})[0] - return [value, True, "Saved!", 'success', 1500] + return [value, True, "Saved!", "success", 1500] except Exception as e: - return [value, True, str(e), 'danger', 5000] - return [value, False, 'No code to execute', 'warning', 1000] + return [value, True, str(e), "danger", 5000] + elif n: + return ["", True, "No code to execute", "warning", 1000] + + return ["", False, "", "success", 1500] + # app.layout = html.Div( # [, , html.Div(id="page-content")] @@ -246,7 +288,7 @@ def execute_and_save(n, value, is_open): # return html.Div("404") -data_list4 = com.device_list +# data_list4 = com.device_list # dl5 = [] # for list in data_list4: @@ -261,8 +303,6 @@ def execute_and_save(n, value, is_open): [State("devices-table-div", "children")], ) def update_table1(n_clicks, data, table): - - # table_data1 = dl5 table_data1 = com.get_clean_device_list().copy() # print(com.device_list[1].get_init_args()) @@ -289,7 +329,6 @@ def update_table1(n_clicks, data, table): # {"name": "Initialized", "id": "_is_initialized"}, {"name": "Parameters", "id": "params"}, ], - style_cell={ "overflow": "hidden", "textOverflow": "ellipsis", @@ -338,6 +377,20 @@ def toggle_device_add_modal(n1, n2, is_open): return is_open +@app.callback( + Output("device-add-modal-ace", "is_open"), + [ + Input("add-device-button-ace", "n_clicks"), + Input("add-device-editor-ace", "n_clicks"), + ], + [State("device-add-modal-ace", "is_open")], +) +def toggle_device_add_modal_ace(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + + @app.callback( Output("command-editor-modal", "is_open"), [ @@ -425,12 +478,23 @@ def fill_command_json_editor(is_open, active_cell, data): def fill_device_add_modal(is_open, active_cell, data): return [util.approved_devices] -import inspect @app.callback( - [Output('add-device-json-editor', 'value')], - [Input('add-device-dropdown', 'value'), Input('device-add-modal', 'is_open')], - prevent_initial_call=True + [ + Output("add-device-dropdown-ace", "options"), + Output("add-device-dropdown-ace", "value"), + ], + [Input("device-add-modal-ace", "is_open")], + prevent_initial_call=True, +) +def fill_device_add_modal_ace(is_open): + return util.devices_ref_keys, "" + + +@app.callback( + [Output("add-device-json-editor", "value")], + [Input("add-device-dropdown", "value"), Input("device-add-modal", "is_open")], + prevent_initial_call=True, ) def fill_device_add_json_editor(value, is_open): if not is_open or value is None: @@ -438,21 +502,13 @@ def fill_device_add_json_editor(value, is_open): args_list = inspect.getfullargspec(util.named_devices[value].__init__).args args_dict = {} for arg in args_list: - if arg != 'self' and arg != 'name': + if arg != "self" and arg != "name": args_dict[arg] = None - if arg == 'name': + if arg == "name": args_dict[arg] = value return [(json.dumps(args_dict, indent=4))] -try: - import serial.tools.list_ports -except ImportError: - _has_serial = False -else: - _has_serial = True - - @app.callback( [ Output("device-json-editor", "value"), @@ -528,11 +584,10 @@ def enable_save_device_button(value, is_open): return True, "Invalid JSON" return True, str(type(e)) -import typing @app.callback( [Output("add-device-editor", "disabled"), Output("add-device-error", "children")], - [ Input("add-device-json-editor", "value"), Input('add-device-dropdown', 'value')], + [Input("add-device-json-editor", "value"), Input("add-device-dropdown", "value")], State("device-add-modal", "is_open"), prevent_initial_call=True, ) @@ -546,11 +601,20 @@ def enable_add_device_button(value, device_type, is_open): args = {} for param in sig.parameters.values(): arg_type = param.annotation - args[param.name] = typing.get_args(arg_type)[0] if typing.get_origin(arg_type) is typing.Union else arg_type + args[param.name] = ( + typing.get_args(arg_type)[0] + if typing.get_origin(arg_type) is typing.Union + else arg_type + ) parsed_json = json.loads(value) for key in parsed_json: - print('\n'+key) - print('input: '+str(type((parsed_json[key]))) + ', expected: '+ str(args[key])) + print("\n" + key) + print( + "input: " + + str(type((parsed_json[key]))) + + ", expected: " + + str(args[key]) + ) if type((parsed_json[key])) != args[key]: return True, f"Invalid type for {key}. Expected {str(args[key])}" # if not isinstance(parsed_json[key], args[key]): @@ -562,6 +626,54 @@ def enable_add_device_button(value, device_type, is_open): return True, str(type(e)) +@app.callback( + Output("add-device-editor-ace", "disabled"), + [ + Input("add-device-dropdown-ace", "value"), + Input("device-add-modal-ace", "is_open"), + ], + State("device-add-modal-ace", "is_open"), + prevent_initial_call=True, +) +def enable_add_device_button_ace(value, is_openInp, is_open): + if value == "" or value is None: + return True + return False + + +@app.callback( + [ + Output("ace-recipe-editor", "value"), + Output("ace-editor-alert", "is_open", allow_duplicate=True), + Output("ace-editor-alert", "children", allow_duplicate=True), + Output("ace-editor-alert", "color", allow_duplicate=True), + Output("ace-editor-alert", "duration", allow_duplicate=True), + ], + Input("add-device-editor-ace", "n_clicks"), + [State("ace-recipe-editor", "value"), State("add-device-dropdown-ace", "value")], + prevent_initial_call=True, +) +def add_device_to_recipe_ace(n_clicks, value, device_type): + if value == "" or value is None: + return ["", True, "No code in editor", "warning", 3000] + try: + import_line = util.devices_ref[device_type]["import"] + init_line = util.devices_ref[device_type]["init"] + if import_line not in value: + value = import_line + "\n" + value + value.replace("seq", "") + value = value.replace( + "##################################################\n##### Add commands to the command sequence", + "seq.add_device(" + + init_line + + ")\n\n##################################################\n##### Add commands to the command sequence", + ) + return [value, True, "Device added successfully", "success", 3000] + except Exception as e: + print(e) + return [value, True, "Error adding device: " + str(e), "danger", 3000] + + # @app.callback( # Output("commands-accordion", "children"), # Input("add-command-button", "n_clicks"), @@ -578,56 +690,56 @@ def enable_add_device_button(value, device_type, is_open): # return children -@app.callback( - Output("commands-accordion", "children"), - Input("refresh-button2", "n_clicks"), - State("commands-accordion", "children"), - # allow_duplicate=True -) -def load_commands_accordion(n_clicks, children): - children = [] - # print() - command_list = com.get_unlooped_command_list().copy() - # print(command_list) - # for command in command_list: - # if isinstance(command, CompositeCommand): - # for sub_command in command._command_list: - # sub_command._receiver = sub_command._receiver._name - # sub_command = sub_command.__dict__ - # else: - # # print(command.__dict__) - # command._receiver = command._receiver._name - command_params = [] - for command in command_list: - # if isinstance(command, CompositeCommand): - # print(type(command).__name__) - temp_dict_command_params = {"command": type(command).__name__} - temp_dict_command_params.update({"params": command.get_init_args()}) - command_params.append(temp_dict_command_params) - # print(command._params) - # else: - # command_params.append(command._params) - # print(command_params) - # print(com.get_command_names()) - for index, command in enumerate(command_params): - # print(command) - children.append( - dbc.AccordionItem( - dcc.Markdown( - children=[ - "**Command Object:**", - "```json", - json.dumps(command, indent=4, cls=util.Encoder), - # str(command.__dict__), - "```", - ], - ), - # str(command.__dict__), - title=command["command"], - item_id=command["command"] + str(index), - ) - ) - return children +# @app.callback( +# Output("commands-accordion", "children"), +# Input("refresh-button2", "n_clicks"), +# State("commands-accordion", "children"), +# # allow_duplicate=True +# ) +# def load_commands_accordion(n_clicks, children): +# children = [] +# # print() +# command_list = com.get_unlooped_command_list().copy() +# # print(command_list) +# # for command in command_list: +# # if isinstance(command, CompositeCommand): +# # for sub_command in command._command_list: +# # sub_command._receiver = sub_command._receiver._name +# # sub_command = sub_command.__dict__ +# # else: +# # # print(command.__dict__) +# # command._receiver = command._receiver._name +# command_params = [] +# for command in command_list: +# # if isinstance(command, CompositeCommand): +# # print(type(command).__name__) +# temp_dict_command_params = {"command": type(command).__name__} +# temp_dict_command_params.update({"params": command.get_init_args()}) +# command_params.append(temp_dict_command_params) +# # print(command._params) +# # else: +# # command_params.append(command._params) +# # print(command_params) +# # print(com.get_command_names()) +# for index, command in enumerate(command_params): +# # print(command) +# children.append( +# dbc.AccordionItem( +# dcc.Markdown( +# children=[ +# "**Command Object:**", +# "```json", +# json.dumps(command, indent=4, cls=util.Encoder), +# # str(command.__dict__), +# "```", +# ], +# ), +# # str(command.__dict__), +# title=command["command"], +# item_id=command["command"] + str(index), +# ) +# ) +# return children @app.callback( diff --git a/pages/data.py b/pages/data.py index c6c03f6..39a94e1 100644 --- a/pages/data.py +++ b/pages/data.py @@ -1,17 +1,17 @@ from dash import Dash, html, dcc, dash_table import dash_bootstrap_components as dbc import dash -# from dash_dangerously_set_inner_html import DangerouslySetInnerHTML + dash.register_page(__name__, "/data") layout = html.Div( [ html.H1("Data"), - dbc.Button("Load data",id='load-data-button', n_clicks=0), - dcc.Textarea( - id="data-output", readOnly=True, style={"width": "100%", "height": 0} - ), - html.Div(id='data-output-div') + dbc.Button("Load data", id="load-data-button", n_clicks=0), + # dcc.Textarea( + # id="data-output", readOnly=True, style={"width": "100%", "height": 0} + # ), + html.Div(id="data-output-div"), ] -) \ No newline at end of file +) diff --git a/pages/edit-recipe.py b/pages/edit-recipe.py index 48fa503..fc4bf6e 100644 --- a/pages/edit-recipe.py +++ b/pages/edit-recipe.py @@ -140,16 +140,16 @@ children=[dash_table.DataTable(id="commands-table")], id="commands-table-div", ), - dbc.Accordion( - [ - dbc.AccordionItem( - "item1", title="Item 1", item_id="item1" - ) - ], - id="commands-accordion", - # start_collapsed=True, - style={"display": "none"}, - ), + # dbc.Accordion( + # [ + # dbc.AccordionItem( + # "item1", title="Item 1", item_id="item1" + # ) + # ], + # id="commands-accordion", + # # start_collapsed=True, + # style={"display": "none"}, + # ), ], className="table-container", ), @@ -167,6 +167,3 @@ ], className="main-container", ) - - - diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py index 22c404f..323359d 100644 --- a/pages/execute-recipe.py +++ b/pages/execute-recipe.py @@ -18,11 +18,10 @@ # dcc.Textarea( # id="console-output", readOnly=True, style={"width": "100%", "height": 0} # ), - dcc.Interval(id='interval1', interval=500, n_intervals=0), - html.Div(id='hidden-div', style={'display':'none'}), - html.H4(id='div-out', children='Log'), - # html.Iframe(id='console-out',srcDoc='',style={'width': '100%','height':400}), - html.Div(id='console-out2'), + dcc.Interval(id="interval1", interval=500, n_intervals=0), + html.Div(id="hidden-div", style={"display": "none"}), + html.H4(id="div-out", children="Log"), + # html.Iframe(id='console-out',srcDoc='',style={'width': '100%','height':400}), + html.Div(id="console-out2"), ] ) - diff --git a/pages/python-edit-recipe.py b/pages/python-edit-recipe.py index bf83cbc..b6d22b2 100644 --- a/pages/python-edit-recipe.py +++ b/pages/python-edit-recipe.py @@ -7,39 +7,51 @@ layout = html.Div( [ - html.H1("Edit Recipe"), + html.H1("Edit Recipe in Python"), html.Div( [ html.Div( [ - html.H2("Devices"), - dbc.Alert("Alert", id="ace-editor-alert", is_open=False, duration=500), + dbc.Alert( + "Alert", id="ace-editor-alert", is_open=False, duration=500 + ), dbc.Button("Fill editor", id="refresh-button-ace", n_clicks=0), - dbc.Button("Add device", id="add-device-button"), + dbc.Button("Add device", id="add-device-button-ace"), dbc.Button("Add command", id="add-command-button"), - dbc.Button('Execute and save yaml', id='execute-and-save-button', n_clicks=0), + dbc.Button( + "Execute and save yaml", + id="execute-and-save-button", + n_clicks=0, + ), dbc.Modal( [ dbc.ModalHeader(dbc.ModalTitle("Add Device")), dbc.ModalBody( [ dcc.Dropdown( - id="add-device-dropdown", + id="add-device-dropdown-ace", options=[], value=None, ), - ] ), dbc.ModalFooter( - dbc.Button("Add", id="add-device-editor") + dbc.Button("Add", id="add-device-editor-ace") ), ], - id="device-add-modal", + id="device-add-modal-ace", keyboard=False, backdrop="static", ), - dash_ace.DashAceEditor(id='ace-recipe-editor', mode = 'python', enableBasicAutocompletion=True, enableLiveAutocompletion=True, theme='github') + dash_ace.DashAceEditor( + id="ace-recipe-editor", + mode="python", + enableBasicAutocompletion=True, + enableLiveAutocompletion=True, + theme="github", + wrapEnabled=True, + style={"width": "100%", "height": "550px"}, + ), ], className="table-container", ), @@ -47,7 +59,6 @@ # [ # html.H2("Commands"), # dbc.Button("Refresh", id="refresh-button2", n_clicks=0), - # dbc.Button("Edit", id="edit-command-button"), # dbc.Modal( # [ @@ -99,13 +110,9 @@ # ], # className="table-container", # ), - ], className="tables-container", ), ], className="main-container", ) - - - diff --git a/util.py b/util.py index ddfb2b1..485e2fa 100644 --- a/util.py +++ b/util.py @@ -3,10 +3,12 @@ from devices.heating_stage import HeatingStage from devices.multi_stepper import MultiStepper from devices.newport_esp301 import NewportESP301 + # from devices.stellarnet_spectrometer import StellarNetSpectrometer # from devices.ximea_camera import XimeaCamera from devices.dummy_heater import DummyHeater from devices.dummy_motor import DummyMotor +from devices.linear_stage_150 import LinearStage150 from devices.device import Device, MiscDeviceClass import json import numpy as np @@ -24,7 +26,7 @@ "DummyMotor": DummyMotor, "DummyMotor1": DummyMotor, "DummyMotor2": DummyMotor, - } +} command_directory = "commands/" approved_devices = list(named_devices.keys()) @@ -33,27 +35,51 @@ # "DummyMotor": ["name", "speed"], # } + def dict_to_device(device: Device, type: str): device_cls = named_devices[type] arg_dict = device.get_init_args() - + # for attr in device_init_args[type]: # arg_dict[attr] = dict["_"+attr] - + # print(arg_dict) return device_cls(**arg_dict) + def str_to_device(device_str: str): print(device_str) return eval(device_str) + def device_to_dict(device: Device): return device.get_init_args() + class Encoder(json.JSONEncoder): def default(self, obj): - if isinstance(obj, Device) or isinstance(obj, Command) or isinstance(obj, MiscDeviceClass): + if ( + isinstance(obj, Device) + or isinstance(obj, Command) + or isinstance(obj, MiscDeviceClass) + ): return obj.__dict__ elif isinstance(obj, np.ndarray): return obj.tolist() - return super().default(obj) \ No newline at end of file + return super().default(obj) + + +devices_ref = { + "PrintingStage": {"obj": HeatingStage}, + "AnnealingStage": {"obj": HeatingStage}, + "MultiStepper": {"obj": MultiStepper}, + "PrinterMotorX": {"obj": NewportESP301}, + # "Spectrometer": {"obj": StellarNetSpectrometer}, + # "SampleCamera": {"obj": XimeaCamera}, + "DummyHeater": {"obj": DummyHeater}, + "DummyMotor": {"obj": DummyMotor}, + 'LinearStage150': {'obj': LinearStage150, + 'import': 'from devices.linear_stage_150 import LinearStage150', + 'init': 'LinearStage150(name=\'LinearStage150\', port=\'/dev/cu.URT0\', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)'}, +} +devices_ref_keys = list(devices_ref.keys()) \ No newline at end of file From e883b5946be56b8a5ccde4633b8fcabead142521 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 29 Jun 2023 17:38:49 -0500 Subject: [PATCH 013/125] prog --- app.py | 284 ++++++++++++++++++++++++------------ blank.yaml | 3 + command_invoker.py | 30 +++- pages/execute-recipe.py | 44 ++++-- pages/home.py | 99 +++++++++++-- pages/python-edit-recipe.py | 63 +++++++- pages/view-recipe.py | 205 ++++++++++++++++++++++++++ to_load.yaml | 27 +--- to_save.yaml | 120 +++++++++++++++ util.py | 19 ++- 10 files changed, 745 insertions(+), 149 deletions(-) create mode 100644 blank.yaml create mode 100644 pages/view-recipe.py create mode 100644 to_save.yaml diff --git a/app.py b/app.py index 4ccecc2..a9d4621 100644 --- a/app.py +++ b/app.py @@ -16,7 +16,7 @@ import random import dash_bootstrap_components as dbc from dash_dangerously_set_inner_html import DangerouslySetInnerHTML -import os, signal +import os, signal, sys import logging import inspect @@ -31,8 +31,10 @@ print("\nreset complete") com = CommandSequence() - -com.load_from_yaml("to_load.yaml") +invoker = CommandInvoker(com, log_to_file=True, log_filename='mylog.log') +invoker.clear_log_file() +invoker.invoking = False +com.load_from_yaml("blank.yaml") mongo = MongoDBHelper( @@ -55,9 +57,9 @@ navbar = dbc.NavbarSimple( children=[ dbc.NavItem(dbc.NavLink("Home", href="/")), - dbc.NavItem(dbc.NavLink("Edit Recipe", href="/edit-recipe")), - dbc.NavItem(dbc.NavLink("Execute Recipe", href="/execute-recipe")), + dbc.NavItem(dbc.NavLink("View Recipe", href="/view-recipe")), dbc.NavItem(dbc.NavLink("Edit Recipe (PY)", href="/python-edit-recipe")), + dbc.NavItem(dbc.NavLink("Execute Recipe", href="/execute-recipe")), dbc.NavItem(dbc.NavLink("Data", href="/data")), ], brand="AAMP", @@ -76,7 +78,7 @@ def fetch_recipe_list(n_clicks): docs = mongo.find_documents("recipes", {}) docs = mongo.db["recipes"].find({}, {"_id": 0, "file_name": 1}) # print(pd.DataFrame(docs).to_dict('records')) - print("recipe list refresh done") + print("fetch_recipe_list") return pd.DataFrame(docs).to_dict("records") @@ -88,8 +90,10 @@ def fetch_recipe_list(n_clicks): ) def fill_filename_input(active_cell, data): if active_cell is not None: + print('fill_filename_input') return data[active_cell["row"]]["file_name"] if hasattr(com, "document"): + print('fill_filename_input') return com.document["file_name"] return "" @@ -100,6 +104,7 @@ def fill_filename_input(active_cell, data): prevent_initial_call=True, ) def load_data(n): + print('load_data') val = "" docs = mongo.db["recipes"].find() for doc in docs: @@ -119,21 +124,24 @@ def kill_execution(): prevent_initial_call=True, ) def stop_execution(n): - print("stopping") + print("stop_execution") # kill_execution() # print('stop done') return [] + @app.callback( Output("execute-recipe-output", "children"), [Input("execute-button", "n_clicks")], prevent_initial_call=True, ) def execute_recipe(n_clicks): - invoker = CommandInvoker(com, False, None, False) + print('execute_recipe') + invoker.invoking = True invoker.invoke_commands() - return "done" + invoker.invoking = False + return ["done"] class DashLoggerHandler(logging.StreamHandler): @@ -146,17 +154,34 @@ def emit(self, record): self.queue.append(msg) -logger = logging.getLogger() -logger.setLevel(logging.DEBUG) -dashLoggerHandler = DashLoggerHandler() -logger.addHandler(dashLoggerHandler) - +# logger = logging.getLogger() +# logger.setLevel(logging.DEBUG) +# dashLoggerHandler = DashLoggerHandler() +# logger.addHandler(dashLoggerHandler) -@app.callback(Output("console-out2", "children"), Input("interval1", "n_intervals")) -def update_output(n): - return DangerouslySetInnerHTML( - ("\n".join(dashLoggerHandler.queue)).replace("\n", "
") - ) +# logger = logging.getLogger(invoker.log.name) +# logger.setLevel(logging.DEBUG) +# from io import StringIO +# log_capture = StringIO() + +# # Create a stream handler and set its stream to the log_capture object +# stream_handler = logging.StreamHandler(log_capture) +# logger.addHandler(stream_handler) +# log_messages = [] + +@app.callback(Output("console-out2", "children"), + Input("interval1", "n_intervals"), + State('url', 'pathname')) +def update_output(n, url): + # print(invoker.invoking) + if url == '/execute-recipe': + log_string = '' + log_list = invoker.get_log_messages() + for msg in log_list: + log_string += msg + # log_string += '
' + return html.Pre(log_string) + return [] @app.callback( @@ -165,10 +190,14 @@ def update_output(n): prevent_initial_call=True, ) def reset_console(n): - dashLoggerHandler.queue = [] + print('reset_console') + invoker.clear_log_file() + # dashLoggerHandler.queue = [] return [] + + # import sys # from io import StringIO # stringio = StringIO() @@ -182,14 +211,23 @@ def reset_console(n): @app.callback( - Output("url", "pathname"), + [ + Output('home-load-file-alert', 'is_open'), + Output('home-load-file-alert', 'children'), + Output('home-load-file-alert', 'color'), + Output('home-load-file-alert', 'duration'),], [Input("filename-input-button", "n_clicks")], [State("filename-input", "value")], + prevent_initial_call=True, ) def get_document_from_db(n_clicks, filename): + print('get_document_from_db') if filename is not None and filename != "": # Extract the YAML content from the document document = mongo.find_documents("recipes", {"file_name": filename})[0] + if os.name == 'posix': + if 'posix_friendly' in document and not document['posix_friendly']: + return [True, "This recipe is not compatible with your system (POSIX compatability error)", "danger", 8000] if ( document.get("dash_friendly", "") == False or document.get("python_code", "") == "" @@ -200,7 +238,7 @@ def get_document_from_db(n_clicks, filename): file.write(yaml_content) com.load_from_yaml("to_load.yaml") com.document = document - return "/python-edit-recipe" + return [True, "Recipe loaded", "success", 10000] else: exec(document.get("python_code", "")) com.load_from_yaml("to_save.yaml") @@ -208,9 +246,9 @@ def get_document_from_db(n_clicks, filename): # com.python_code = document.get("python_code", "") - return "/edit-recipe" + return [True, "Recipe loaded", "success", 10000] - return "/" + return [True, "No recipe selected", "warning", 3000] @app.callback( @@ -221,20 +259,23 @@ def get_document_from_db(n_clicks, filename): Output("ace-editor-alert", "color"), Output("ace-editor-alert", "duration"), ], - [Input("url", "pathname"), Input("refresh-button-ace", "n_clicks")], + [Input("refresh-button-ace", "n_clicks")], + State("url", "pathname") ) -def fill_ace_editor(url, n): +def fill_ace_editor(n, url): if url == "/python-edit-recipe": if hasattr(com, "document"): python_code = com.document.get("python_code", "") if python_code is not None and python_code != "": - return [python_code, True, "Loaded!", "success", 1500] + print('fill_ace_editor') + return [str(python_code), True, "Loaded!", "success", 1500] else: + print('fill_ace_editor') return ["", True, "No code available", "warning", 1000] else: return ["", True, "No code available", "warning", 1000] else: - return ["", False, "", "success", 0] + return ["", False, "na", "success", 0] @app.callback( @@ -246,10 +287,11 @@ def fill_ace_editor(url, n): Output("ace-editor-alert", "duration", allow_duplicate=True), ], Input("execute-and-save-button", "n_clicks"), - [State("ace-recipe-editor", "value"), State("ace-editor-alert", "is_open")], + [State("ace-recipe-editor", "value")], prevent_initial_call=True, ) -def execute_and_save(n, value, is_open): +def execute_and_save(n, value): + print('execute_and_save') if value is not None and value != "": try: exec(value) @@ -257,13 +299,13 @@ def execute_and_save(n, value, is_open): (mongo.update_yaml_file("recipes", doc_id, {"python_code": value})) com.load_from_yaml("to_save.yaml") com.document = mongo.find_documents("recipes", {"_id": doc_id})[0] - return [value, True, "Saved!", "success", 1500] + return [str(value), True, "Saved!", "success", 1500] except Exception as e: - return [value, True, str(e), "danger", 5000] - elif n: + return [str(value), True, str(e), "danger", 5000] + else: return ["", True, "No code to execute", "warning", 1000] - return ["", False, "", "success", 1500] + # return ["", False, "", "success", 1500] # app.layout = html.Div( @@ -302,7 +344,8 @@ def execute_and_save(n, value, is_open): [Input("refresh-button1", "n_clicks"), Input("devices-table", "data")], [State("devices-table-div", "children")], ) -def update_table1(n_clicks, data, table): +def update_device_table(n_clicks, data, table): + print('update_device_table') # table_data1 = dl5 table_data1 = com.get_clean_device_list().copy() # print(com.device_list[1].get_init_args()) @@ -351,59 +394,10 @@ def update_table1(n_clicks, data, table): # tooltip_duration=None, # editable = True, ) - print("devices table refresh done") + # print("devices table refresh done") return table -@app.callback( - Output("device-editor-modal", "is_open"), - [Input("edit-device-button", "n_clicks"), Input("save-device-editor", "n_clicks")], - [State("device-editor-modal", "is_open")], -) -def toggle_device_editor_modal(n1, n2, is_open): - if n1 or n2: - return not is_open - return is_open - - -@app.callback( - Output("device-add-modal", "is_open"), - [Input("add-device-button", "n_clicks"), Input("add-device-editor", "n_clicks")], - [State("device-add-modal", "is_open")], -) -def toggle_device_add_modal(n1, n2, is_open): - if n1 or n2: - return not is_open - return is_open - - -@app.callback( - Output("device-add-modal-ace", "is_open"), - [ - Input("add-device-button-ace", "n_clicks"), - Input("add-device-editor-ace", "n_clicks"), - ], - [State("device-add-modal-ace", "is_open")], -) -def toggle_device_add_modal_ace(n1, n2, is_open): - if n1 or n2: - return not is_open - return is_open - - -@app.callback( - Output("command-editor-modal", "is_open"), - [ - Input("edit-command-button", "n_clicks"), - Input("save-command-editor", "n_clicks"), - ], - [State("command-editor-modal", "is_open")], -) -def toggle_command_editor_modal(n1, n2, is_open): - if n1 or n2: - return not is_open - return is_open - @app.callback( Output("commands-table", "data"), @@ -416,6 +410,7 @@ def toggle_command_editor_modal(n1, n2, is_open): prevent_initial_call=True, ) def save_command(n_clicks, active_cell, data, value): + print('save_command') if active_cell is not None and data[active_cell["row"]]["params"] != str( json.loads(value) ): @@ -442,6 +437,7 @@ def save_command(n_clicks, active_cell, data, value): prevent_initial_call=True, ) def save_device(n_clicks, active_cell, data, value): + print('save_device') if active_cell is not None and data[active_cell["row"]]["params"] != str( json.loads(value) ): @@ -462,6 +458,7 @@ def fill_command_json_editor(is_open, active_cell, data): if active_cell is not None and is_open: # print(active_cell) # print(eval(data[active_cell['row']]['params'])) + print('fill_command_json_editor') return json.dumps(eval(data[active_cell["row"]]["params"]), indent=4) return "" @@ -476,6 +473,7 @@ def fill_command_json_editor(is_open, active_cell, data): prevent_initial_call=True, ) def fill_device_add_modal(is_open, active_cell, data): + print('fill_device_add_modal') return [util.approved_devices] @@ -488,7 +486,40 @@ def fill_device_add_modal(is_open, active_cell, data): prevent_initial_call=True, ) def fill_device_add_modal_ace(is_open): - return util.devices_ref_keys, "" + if is_open: + print('fill_device_add_modal_ace') + return list(util.devices_ref.keys()), "" + return [], "" + + +@app.callback( + [ + Output("add-command-device-dropdown-ace", "options"), + Output("add-command-device-dropdown-ace", "value"), + ], + [Input("command-add-modal-ace", "is_open")], + prevent_initial_call=True, +) +def fill_command_device_add_modal_ace(is_open): + if is_open: + print('fill_command_device_add_modal_ace') + return list(util.devices_ref.keys()), "" + return [], "" + + +@app.callback( + [ + Output("add-command-command-dropdown-ace", "options"), + Output("add-command-command-dropdown-ace", "value"), + ], + [Input("add-command-device-dropdown-ace", "value")], + prevent_initial_call=True, +) +def fill_command_add_modal_ace(device): + if device is not None and device != "": + print('fill_command_add_modal_ace') + return list(util.devices_ref[device]["commands"].keys()), "" + return [], "" @app.callback( @@ -497,6 +528,7 @@ def fill_device_add_modal_ace(is_open): prevent_initial_call=True, ) def fill_device_add_json_editor(value, is_open): + print('fill_device_add_json_editor') if not is_open or value is None: return [""] args_list = inspect.getfullargspec(util.named_devices[value].__init__).args @@ -519,6 +551,7 @@ def fill_device_add_json_editor(value, is_open): prevent_initial_call=True, ) def fill_device_json_editor(is_open, active_cell, data): + print('fill_device_json_editor') if active_cell is not None and is_open: if _has_serial and isinstance( com.device_by_name[eval(data[active_cell["row"]]["params"])["name"]], @@ -560,6 +593,7 @@ def enable_save_command_button(value, is_open): # print(parsed_json) if parsed_json["delay"] < 0: return True, "Delay must be greater than or equal to 0" + print('enable_save_command_button') return False, "" except Exception as e: if type(e) == json.decoder.JSONDecodeError: @@ -578,6 +612,7 @@ def enable_save_device_button(value, is_open): return False, "" try: parsed_json = json.loads(value) + print('enable_save_device_button') return False, "" except Exception as e: if type(e) == json.decoder.JSONDecodeError: @@ -619,6 +654,7 @@ def enable_add_device_button(value, device_type, is_open): return True, f"Invalid type for {key}. Expected {str(args[key])}" # if not isinstance(parsed_json[key], args[key]): # return True, f"Invalid type for {key}. Expected {str(args[key])}" + print('enable_add_device_button') return False, "" except Exception as e: if type(e) == json.decoder.JSONDecodeError: @@ -638,6 +674,23 @@ def enable_add_device_button(value, device_type, is_open): def enable_add_device_button_ace(value, is_openInp, is_open): if value == "" or value is None: return True + print('enable_add_device_button_ace') + return False + + +@app.callback( + Output("add-command-editor-ace", "disabled"), + [ + Input("add-command-command-dropdown-ace", "value"), + Input("command-add-modal-ace", "is_open"), + ], + State("command-add-modal-ace", "is_open"), + prevent_initial_call=True, +) +def enable_add_command_button_ace(value, is_openInp, is_open): + if value == "" or value is None: + return True + print('enable_add_command_button_ace') return False @@ -654,24 +707,60 @@ def enable_add_device_button_ace(value, is_openInp, is_open): prevent_initial_call=True, ) def add_device_to_recipe_ace(n_clicks, value, device_type): + print('add_device_to_recipe_ace') if value == "" or value is None: return ["", True, "No code in editor", "warning", 3000] try: - import_line = util.devices_ref[device_type]["import"] + value = str(value) + import_line = util.devices_ref[device_type]["import_device"] init_line = util.devices_ref[device_type]["init"] if import_line not in value: value = import_line + "\n" + value - value.replace("seq", "") value = value.replace( "##################################################\n##### Add commands to the command sequence", "seq.add_device(" + init_line + ")\n\n##################################################\n##### Add commands to the command sequence", ) - return [value, True, "Device added successfully", "success", 3000] + return [str(value), True, "Device added successfully", "success", 3000] + except Exception as e: + print(e) + return [str(value), True, "Error adding device: " + str(e), "danger", 6000] + + +@app.callback( + [ + Output("ace-recipe-editor", "value", allow_duplicate=True), + Output("ace-editor-alert", "is_open", allow_duplicate=True), + Output("ace-editor-alert", "children", allow_duplicate=True), + Output("ace-editor-alert", "color", allow_duplicate=True), + Output("ace-editor-alert", "duration", allow_duplicate=True), + ], + Input("add-command-editor-ace", "n_clicks"), + [State("ace-recipe-editor", "value"), State("add-command-command-dropdown-ace", "value"), State("add-command-device-dropdown-ace", "value")], + prevent_initial_call=True, +) +def add_commands_to_recipe_ace(n_clicks, value, command, device_type): + print('add_commands_to_recipe_ace') + og_value = str(value) + if value == "" or value is None: + return ["", True, "No code in editor", "warning", 3000] + try: + value = str(value) + command_line = util.devices_ref[device_type]['commands'][command] + import_line = util.devices_ref[device_type]["import_commands"] + import_device_line = util.devices_ref[device_type]["import_device"] + if import_device_line not in value: + raise Exception('Device (or its import \''+import_device_line+'\') not found in recipe') + if import_line not in value: + value = import_line + "\n" + value + if "\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)" not in value: + raise Exception('Code is not in valid format') + value = value.replace("\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)", "\nseq.add_command("+command_line+")\n\n\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)") + return [str(value), True, "Command added successfully", "success", 3000] except Exception as e: print(e) - return [value, True, "Error adding device: " + str(e), "danger", 3000] + return [og_value, True, str("Error adding command: " + str(e)), "danger", 6000] # @app.callback( @@ -747,6 +836,7 @@ def add_device_to_recipe_ace(n_clicks, value, device_type): Input("commands-table", "active_cell"), ) def edit_command_button(table_div_children): + print('edit_command_button') active_cell = table_div_children # print(active_cell) # if active_cell is not None and active_cell["column_id"] == "params": @@ -761,6 +851,7 @@ def edit_command_button(table_div_children): Input("devices-table", "active_cell"), ) def edit_device_button(table_div_children): + print('edit_device_button') active_cell = table_div_children if active_cell is not None: return False @@ -773,7 +864,8 @@ def edit_device_button(table_div_children): [Input("refresh-button2", "n_clicks"), Input("commands-table", "data")], [State("commands-table-div", "children")], ) -def update_table2(n_clicks, data, table): +def update_commands_table(n_clicks, data, table): + print('update_commands_table') command_list = com.command_list.copy() # print(command_list) # for command in command_list: @@ -833,7 +925,7 @@ def update_table2(n_clicks, data, table): # tooltip_duration=None, # editable = True, ) - print("commands table refresh done") + # print("commands table refresh done") return table @@ -850,6 +942,14 @@ def update_table2(n_clicks, data, table): # ) # return table +@app.server.errorhandler(Exception) +def handle_exception(e): + # Print the error to the console + print("Callback Error:", str(e)) + # Optionally, you can log the error to a file or perform other error handling actions + + # Return a custom error message to display in the app + return "An error occurred. Please try again later." if __name__ == "__main__": app.run_server(debug=True) diff --git a/blank.yaml b/blank.yaml new file mode 100644 index 0000000..c4e5ad8 --- /dev/null +++ b/blank.yaml @@ -0,0 +1,3 @@ +- [] +- [] +- ALL diff --git a/command_invoker.py b/command_invoker.py index 74a7d34..29c8c0e 100644 --- a/command_invoker.py +++ b/command_invoker.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple, Union +from typing import Optional, Tuple, Union, List from datetime import datetime import logging import time @@ -231,9 +231,33 @@ def log_command_names_descriptions(self, unloop: bool = False): self.log.info("(Number of iterations: " + str(self._command_seq.num_iterations) + ")") self.log.info("="*20 + "END OF COMMAND NAMES/DESCRIPTIONS" + "="*20) + def get_log_messages(self): + """Get all log messages as a list. - - + Returns + ------- + list + List of log messages + """ + log_messages = [] + for handler in self.log.handlers: + if isinstance(handler, logging.FileHandler): + handler.flush() # Ensure all messages are written to the log file + with open(handler.baseFilename, 'r') as log_file: + log_messages.extend(log_file.readlines()) + return log_messages + + def clear_log_file(self): + """Clear the log file by truncating its content.""" + if self._log_to_file: + if os.path.exists(self._log_filename): + with open(self._log_filename, 'w') as log_file: + log_file.truncate(0) + self.log.info("Log file cleared.") + else: + self.log.warning("Log file does not exist.") + else: + self.log.warning("Log file is not being used.") diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py index 323359d..10c89a7 100644 --- a/pages/execute-recipe.py +++ b/pages/execute-recipe.py @@ -10,18 +10,38 @@ layout = html.Div( [ html.H1("Execute Recipe"), - dbc.Button("Execute", id="execute-button", n_clicks=0), - dbc.Button("Stop", id="stop-button", n_clicks=0), - dbc.Button("Reset", id="reset-button", n_clicks=0), - html.Div(id="execute-recipe-output"), - dcc.Interval(id="update-interval", interval=1000, n_intervals=0), - # dcc.Textarea( - # id="console-output", readOnly=True, style={"width": "100%", "height": 0} - # ), + dbc.ButtonGroup( + [ + dbc.Button("Execute", id="execute-button", n_clicks=0), + dbc.Button("Stop", id="stop-button", n_clicks=0), + dbc.Button("Reset", id="reset-button", n_clicks=0), + ], + className="mb-2", + ), + html.Div( + id="execute-recipe-output", className="mt-3", style={"display": "none"} + ), + dcc.Interval(id="update-interval", interval=500, n_intervals=0), dcc.Interval(id="interval1", interval=500, n_intervals=0), html.Div(id="hidden-div", style={"display": "none"}), - html.H4(id="div-out", children="Log"), - # html.Iframe(id='console-out',srcDoc='',style={'width': '100%','height':400}), - html.Div(id="console-out2"), - ] + html.H4("Log"), + html.Div( + id="console-out2", + className="log-container", + style={ + "height": "500px", + "overflow-y": "scroll", + "padding": "10px", + "border": "2px solid", + }, + ), + ], + className="container", ) + +@callback( + Output("console-out2", "style"), + [Input("console-out2", "children"), Input("interval1", "n_intervals")], +) +def scroll_to_bottom(children, n): + return {"height": "500px", "overflowY": "scroll", "padding": "10px", "border": "2px solid", "scrollTop": "99999999"} diff --git a/pages/home.py b/pages/home.py index 8e1abec..7e3b257 100644 --- a/pages/home.py +++ b/pages/home.py @@ -4,22 +4,93 @@ dash.register_page(__name__, "/") +# layout = html.Div( +# [ +# html.H1("Home"), +# dcc.Input(id="filename-input", type="text", placeholder="Enter filename name"), +# dbc.Button("Load", id="filename-input-button", n_clicks=0), +# html.Div(id="home-output"), +# dbc.Button("Refresh List", id="home-refresh-list-button", n_clicks=0), +# dash_table.DataTable( +# id="home-recipes-list-table", +# columns=[ +# {"name": "File Name", "id": "file_name"}, +# # {'name':'Posix', 'id':'posix_friendly'} +# ], +# data=[], +# style_table={"width": "300px"}, +# style_cell={"textAlign": "left"}, +# ), +# ] +# ) + layout = html.Div( [ html.H1("Home"), - dcc.Input(id="filename-input", type="text", placeholder="Enter filename name"), - dbc.Button("Load", id="filename-input-button", n_clicks=0), - html.Div(id="home-output"), - dbc.Button("Refresh List", id="home-refresh-list-button", n_clicks=0), - dash_table.DataTable( - id="home-recipes-list-table", - columns=[ - {"name": "File Name", "id": "file_name"}, - # {'name':'YAML', 'id':'yaml_data'} + dbc.Row( + [ + dbc.Col( + dcc.Input( + id="filename-input", + type="text", + placeholder="Enter file name", + className="form-control", + ), + width=6, + ), + dbc.Col( + dbc.Button( + "Load", + id="filename-input-button", + n_clicks=0, + color="primary", + className="btn btn-primary", + ), + width=2, + ), ], - data=[], - style_table={"width": "300px"}, - style_cell={"textAlign": "left"}, + className="mb-3", + ), + dbc.Alert( + id="home-load-file-alert", + color="success", + is_open=False, + # fade=True, + className="mb-3", + ), + dbc.Row( + [ + dbc.Col( + dbc.Button( + "Refresh List", + id="home-refresh-list-button", + n_clicks=0, + color="secondary", + className="btn btn-secondary", + ), + width=2, + ) + ] + ), + dbc.Row( + [ + dbc.Col( + dash_table.DataTable( + id="home-recipes-list-table", + columns=[ + {"name": "File Name", "id": "file_name"}, + ], + data=[], + style_table={"width": "100%"}, + style_cell={"textAlign": "left"}, + style_header={"fontWeight": "bold"}, + page_current=0, + page_size=10, + ), + width=8, + ) + ] ), - ] -) \ No newline at end of file + ], + className="container", +) diff --git a/pages/python-edit-recipe.py b/pages/python-edit-recipe.py index b6d22b2..767a8e1 100644 --- a/pages/python-edit-recipe.py +++ b/pages/python-edit-recipe.py @@ -1,4 +1,4 @@ -from dash import Dash, html, dcc, dash_table +from dash import Dash, html, dcc, dash_table, Input, Output, State, callback import dash_bootstrap_components as dbc import dash import dash_ace @@ -17,7 +17,7 @@ ), dbc.Button("Fill editor", id="refresh-button-ace", n_clicks=0), dbc.Button("Add device", id="add-device-button-ace"), - dbc.Button("Add command", id="add-command-button"), + dbc.Button("Add command", id="add-command-button-ace"), dbc.Button( "Execute and save yaml", id="execute-and-save-button", @@ -43,6 +43,35 @@ keyboard=False, backdrop="static", ), + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Add Command")), + dbc.ModalBody( + [ + dcc.Dropdown( + id="add-command-device-dropdown-ace", + options=[], + value=None, + ), + ] + ), + dbc.ModalBody( + [ + dcc.Dropdown( + id="add-command-command-dropdown-ace", + options=[], + value=None, + ), + ] + ), + dbc.ModalFooter( + dbc.Button("Add", id="add-command-editor-ace") + ), + ], + id="command-add-modal-ace", + keyboard=False, + backdrop="static", + ), dash_ace.DashAceEditor( id="ace-recipe-editor", mode="python", @@ -51,6 +80,7 @@ theme="github", wrapEnabled=True, style={"width": "100%", "height": "550px"}, + cursorStart=333 ), ], className="table-container", @@ -116,3 +146,32 @@ ], className="main-container", ) + +@callback( + Output("command-add-modal-ace", "is_open"), + [ + Input("add-command-editor-ace", "n_clicks"), + Input("add-command-button-ace", "n_clicks"), + ], + State("command-add-modal-ace", "is_open"), +) +def toggle_command_add_modal_ace(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + + +@callback( + Output("device-add-modal-ace", "is_open"), + [ + Input("add-device-button-ace", "n_clicks"), + Input("add-device-editor-ace", "n_clicks"), + ], + [State("device-add-modal-ace", "is_open")], +) +def toggle_device_add_modal_ace(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + + diff --git a/pages/view-recipe.py b/pages/view-recipe.py new file mode 100644 index 0000000..b205e75 --- /dev/null +++ b/pages/view-recipe.py @@ -0,0 +1,205 @@ +from dash import Dash, html, dcc, dash_table, callback, Input, Output, State +import dash_bootstrap_components as dbc +import dash + +dash.register_page(__name__, "/view-recipe") + +layout = html.Div( + [ + html.H1("View Recipe"), + html.Div( + [ + html.Div( + [ + html.H2("Devices"), + dbc.Button("Refresh", id="refresh-button1", n_clicks=0), + dbc.Button("Add device", id="add-device-button"), + dbc.Button("Edit", id="edit-device-button"), + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Editor"), close_button=False + ), + dbc.ModalBody( + [ + dcc.Textarea( + id="device-json-editor", + style={ + "width": "100%", + "height": "200px", + "fontFamily": "monospace", + "backgroundColor": "#f5f5f5", + "border": "1px solid #ccc", + "padding": "10px", + "color": "#333", + }, + ), + html.Div( + id="edit-device-error", + style={"color": "red"}, + ), + html.Div( + id="edit-device-serial-ports-info", + ), + ] + ), + dbc.ModalFooter( + dbc.Button("Save", id="save-device-editor") + ), + ], + id="device-editor-modal", + keyboard=False, + backdrop="static", + ), + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Add Device")), + dbc.ModalBody( + [ + dcc.Dropdown( + id="add-device-dropdown", + options=[], + value=None, + ), + dcc.Textarea( + id="add-device-json-editor", + style={ + "width": "100%", + "height": "200px", + "fontFamily": "monospace", + "backgroundColor": "#f5f5f5", + "border": "1px solid #ccc", + "padding": "10px", + "color": "#333", + }, + ), + html.Div( + id="add-device-error", + style={"color": "red"}, + ), + html.Div( + id="add-device-serial-ports-info", + ), + ] + ), + dbc.ModalFooter( + dbc.Button("Add", id="add-device-editor") + ), + ], + id="device-add-modal", + keyboard=False, + backdrop="static", + ), + html.Div( + children=[dash_table.DataTable(id="devices-table")], + id="devices-table-div", + ), + ], + className="table-container", + ), + html.Div( + [ + html.H2("Commands"), + dbc.Button("Refresh", id="refresh-button2", n_clicks=0), + dbc.Button("Add command", id="add-command-button"), + dbc.Button("Edit", id="edit-command-button"), + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Editor"), close_button=False + ), + dbc.ModalBody( + [ + dcc.Textarea( + id="command-json-editor", + style={ + "width": "100%", + "height": "200px", + "fontFamily": "monospace", + "backgroundColor": "#f5f5f5", + "border": "1px solid #ccc", + "padding": "10px", + "color": "#333", + }, + ), + html.Div( + id="edit-command-error", + style={"color": "red"}, + ), + ] + ), + dbc.ModalFooter( + dbc.Button("Save", id="save-command-editor") + ), + ], + id="command-editor-modal", + keyboard=False, + backdrop="static", + ), + html.Div( + children=[dash_table.DataTable(id="commands-table")], + id="commands-table-div", + ), + # dbc.Accordion( + # [ + # dbc.AccordionItem( + # "item1", title="Item 1", item_id="item1" + # ) + # ], + # id="commands-accordion", + # # start_collapsed=True, + # style={"display": "none"}, + # ), + ], + className="table-container", + ), + html.Div( + [ + html.H2("Command Iterations"), + html.Button("Refresh", id="refresh-button3", n_clicks=0), + html.Div(id="table-container3"), + ], + className="table-container", + ), + ], + className="tables-container", + ), + ], + className="main-container", +) + + +@callback( + Output("device-editor-modal", "is_open"), + [Input("edit-device-button", "n_clicks"), Input("save-device-editor", "n_clicks")], + [State("device-editor-modal", "is_open")], +) +def toggle_device_editor_modal(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + + +@callback( + Output("device-add-modal", "is_open"), + [Input("add-device-button", "n_clicks"), Input("add-device-editor", "n_clicks")], + [State("device-add-modal", "is_open")], +) +def toggle_device_add_modal(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + + +@callback( + Output("command-editor-modal", "is_open"), + [ + Input("edit-command-button", "n_clicks"), + Input("save-command-editor", "n_clicks"), + ], + [State("command-editor-modal", "is_open")], +) +def toggle_command_editor_modal(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open \ No newline at end of file diff --git a/to_load.yaml b/to_load.yaml index 53fc9c5..b99240b 100644 --- a/to_load.yaml +++ b/to_load.yaml @@ -1,12 +1,9 @@ -- - &id001 !!python/object:devices.linear_stage_150.LinearStage150 +- - !!python/object:devices.linear_stage_150.LinearStage150 _name: LinearStage150 _is_initialized: false _port: /dev/cu.URT0 _baudrate: 115200 _timeout: 0.1 - _destination: 0x50 - _source: 0x01 - _channel: 1 ser: !!python/object:serial.serialposix.Serial is_open: false portstr: null @@ -27,22 +24,8 @@ _dtr_state: true _break_state: false _exclusive: null -- - - !!python/object:commands.linear_stage_150_commands.LinearStage150Connect - _receiver: *id001 - _params: - receiver_name: LinearStage150 - delay: 0.0 - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null - _name: LinearStage150Connect receiver_name=LinearStage150 - - - !!python/object:commands.linear_stage_150_commands.LinearStage150EnableMotor - _receiver: *id001 - _params: - receiver_name: LinearStage150 - delay: 0.0 - state: true - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null + _destination: 80 + _source: 1 + _channel: 1 +- [] - ALL diff --git a/to_save.yaml b/to_save.yaml new file mode 100644 index 0000000..d0a701c --- /dev/null +++ b/to_save.yaml @@ -0,0 +1,120 @@ +- - &id001 !!python/object:devices.dummy_heater.DummyHeater + _name: heater1 + _is_initialized: false + _heat_rate: 20.0 + min_heat_rate: 1.0 + max_heat_rate: 50.0 + min_temperature: 25.0 + max_temperature: 100.0 + _temperature: 71.22528451782857 + _hardware_interval: 0.05 + - &id002 !!python/object:devices.dummy_motor.DummyMotor + _name: motor1 + _is_initialized: false + motor: !!python/object:devices.dummy_motor_source.DummyMotorSource + _speed: 20.0 + min_speed: 5.0 + max_speed: 50.0 + min_position: 0.0 + max_position: 100.0 + _position: 57.525955561403954 + _hardware_interval: 0.05 + - &id003 !!python/object:devices.dummy_motor.DummyMotor + _name: motor2 + _is_initialized: false + motor: !!python/object:devices.dummy_motor_source.DummyMotorSource + _speed: 20.0 + min_speed: 5.0 + max_speed: 50.0 + min_position: 0.0 + max_position: 100.0 + _position: 30.78435676094049 + _hardware_interval: 0.05 +- - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize + _receiver: *id003 + _params: + receiver_name: motor2 + delay: 0.0 + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorSetSpeed + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + speed: 10.0 + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + distance: 20.0 + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorSetSpeed + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + speed: 15.0 + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + distance: 30.0 + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterDeinitialize + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + reset_init_flag: true + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + reset_init_flag: true + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize + _receiver: *id003 + _params: + receiver_name: motor2 + delay: 0.0 + reset_init_flag: true + _result: !!python/object:commands.command.CommandResult + _was_successful: null + _message: null +- ALL diff --git a/util.py b/util.py index 485e2fa..3ec9ba5 100644 --- a/util.py +++ b/util.py @@ -78,8 +78,19 @@ def default(self, obj): # "SampleCamera": {"obj": XimeaCamera}, "DummyHeater": {"obj": DummyHeater}, "DummyMotor": {"obj": DummyMotor}, - 'LinearStage150': {'obj': LinearStage150, - 'import': 'from devices.linear_stage_150 import LinearStage150', - 'init': 'LinearStage150(name=\'LinearStage150\', port=\'/dev/cu.URT0\', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)'}, + "LinearStage150": { + "obj": LinearStage150, + "import_device": "from devices.linear_stage_150 import LinearStage150", + "import_commands": "from commands.linear_stage_150_commands import *", + "init": "LinearStage150(name='LinearStage150', port='/dev/cu.URT0', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", + "commands": { + "LinearStage150Connect": "LinearStage150Connect(receiver= '')", + "LinearStage150Initialize": "LinearStage150Initialize(receiver= '')", + "LinearStage150Deinitialize": "LinearStage150Deinitialize(receiver= '')", + "LinearStage150EnableMotor": "LinearStage150EnableMotor(receiver= '')", + "LinearStage150DisableMotor": "LinearStage150DisableMotor(receiver= '')", + "LinearStage150MoveAbsolute": "LinearStage150MoveAbsolute(receiver= '', position= 0)", + "LinearStage150MoveRelative": "LinearStage150MoveRelative(receiver= '', distance= 0)", + }, + }, } -devices_ref_keys = list(devices_ref.keys()) \ No newline at end of file From 40754cb7b9d365e150f38dbfa45d8373201a5bc6 Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 30 Jun 2023 13:45:13 -0500 Subject: [PATCH 014/125] june 30 demo --- app.py | 26 +++++++++------- pages/execute-recipe.py | 16 ++++++---- pages/home.py | 2 +- pages/python-edit-recipe.py | 62 +++---------------------------------- pages/view-recipe.py | 28 +++++++++-------- 5 files changed, 44 insertions(+), 90 deletions(-) diff --git a/app.py b/app.py index a9d4621..9d54043 100644 --- a/app.py +++ b/app.py @@ -105,13 +105,13 @@ def fill_filename_input(active_cell, data): ) def load_data(n): print('load_data') - val = "" - docs = mongo.db["recipes"].find() - for doc in docs: - val += str(doc) - val += "

" + # val = "" + # docs = mongo.db["recipes"].find() + # for doc in docs: + # val += str(doc) + # val += "

" # nval = val. - return DangerouslySetInnerHTML(val) + return (json.dumps(str(com.document))) def kill_execution(): @@ -171,17 +171,17 @@ def emit(self, record): @app.callback(Output("console-out2", "children"), Input("interval1", "n_intervals"), - State('url', 'pathname')) -def update_output(n, url): + [State('url', 'pathname'), State('show-log-switch', 'value')], prevent_initial_call=True) +def update_output(n, url, switch_val): # print(invoker.invoking) - if url == '/execute-recipe': + if url == '/execute-recipe' and switch_val: log_string = '' log_list = invoker.get_log_messages() for msg in log_list: log_string += msg # log_string += '
' return html.Pre(log_string) - return [] + return "" @app.callback( @@ -225,6 +225,7 @@ def get_document_from_db(n_clicks, filename): if filename is not None and filename != "": # Extract the YAML content from the document document = mongo.find_documents("recipes", {"file_name": filename})[0] + invoker.clear_log_file() if os.name == 'posix': if 'posix_friendly' in document and not document['posix_friendly']: return [True, "This recipe is not compatible with your system (POSIX compatability error)", "danger", 8000] @@ -260,7 +261,8 @@ def get_document_from_db(n_clicks, filename): Output("ace-editor-alert", "duration"), ], [Input("refresh-button-ace", "n_clicks")], - State("url", "pathname") + State("url", "pathname"), + # prevent_initial_call=True, ) def fill_ace_editor(n, url): if url == "/python-edit-recipe": @@ -836,11 +838,11 @@ def add_commands_to_recipe_ace(n_clicks, value, command, device_type): Input("commands-table", "active_cell"), ) def edit_command_button(table_div_children): - print('edit_command_button') active_cell = table_div_children # print(active_cell) # if active_cell is not None and active_cell["column_id"] == "params": if active_cell is not None: + print('edit_command_button') return False else: return True diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py index 10c89a7..91b4f07 100644 --- a/pages/execute-recipe.py +++ b/pages/execute-recipe.py @@ -18,6 +18,16 @@ ], className="mb-2", ), + html.Div( + [ + dbc.Switch( + id='show-log-switch', + label='Show log', + value = False, + ) + ], + className="d-flex align-items-center mt-3", + ), html.Div( id="execute-recipe-output", className="mt-3", style={"display": "none"} ), @@ -39,9 +49,3 @@ className="container", ) -@callback( - Output("console-out2", "style"), - [Input("console-out2", "children"), Input("interval1", "n_intervals")], -) -def scroll_to_bottom(children, n): - return {"height": "500px", "overflowY": "scroll", "padding": "10px", "border": "2px solid", "scrollTop": "99999999"} diff --git a/pages/home.py b/pages/home.py index 7e3b257..034855b 100644 --- a/pages/home.py +++ b/pages/home.py @@ -66,7 +66,7 @@ id="home-refresh-list-button", n_clicks=0, color="secondary", - className="btn btn-secondary", + className="btn btn-secondary mb-3", ), width=2, ) diff --git a/pages/python-edit-recipe.py b/pages/python-edit-recipe.py index 767a8e1..f865f4b 100644 --- a/pages/python-edit-recipe.py +++ b/pages/python-edit-recipe.py @@ -15,14 +15,15 @@ dbc.Alert( "Alert", id="ace-editor-alert", is_open=False, duration=500 ), - dbc.Button("Fill editor", id="refresh-button-ace", n_clicks=0), + dbc.ButtonGroup([dbc.Button("Fill editor", id="refresh-button-ace", n_clicks=0), dbc.Button("Add device", id="add-device-button-ace"), dbc.Button("Add command", id="add-command-button-ace"), dbc.Button( "Execute and save yaml", id="execute-and-save-button", n_clicks=0, - ), + ),], className='mb-3'), + dbc.Modal( [ dbc.ModalHeader(dbc.ModalTitle("Add Device")), @@ -85,66 +86,11 @@ ], className="table-container", ), - # html.Div( - # [ - # html.H2("Commands"), - # dbc.Button("Refresh", id="refresh-button2", n_clicks=0), - # dbc.Button("Edit", id="edit-command-button"), - # dbc.Modal( - # [ - # dbc.ModalHeader( - # dbc.ModalTitle("Editor"), close_button=False - # ), - # dbc.ModalBody( - # [ - # dcc.Textarea( - # id="command-json-editor", - # style={ - # "width": "100%", - # "height": "200px", - # "fontFamily": "monospace", - # "backgroundColor": "#f5f5f5", - # "border": "1px solid #ccc", - # "padding": "10px", - # "color": "#333", - # }, - # ), - # html.Div( - # id="edit-command-error", - # style={"color": "red"}, - # ), - # ] - # ), - # dbc.ModalFooter( - # dbc.Button("Save", id="save-command-editor") - # ), - # ], - # id="command-editor-modal", - # keyboard=False, - # backdrop="static", - # ), - # html.Div( - # children=[dash_table.DataTable(id="commands-table")], - # id="commands-table-div", - # ), - # dbc.Accordion( - # [ - # dbc.AccordionItem( - # "item1", title="Item 1", item_id="item1" - # ) - # ], - # id="commands-accordion", - # # start_collapsed=True, - # style={"display": "none"}, - # ), - # ], - # className="table-container", - # ), ], className="tables-container", ), ], - className="main-container", + className="container", ) @callback( diff --git a/pages/view-recipe.py b/pages/view-recipe.py index b205e75..6378d21 100644 --- a/pages/view-recipe.py +++ b/pages/view-recipe.py @@ -12,9 +12,10 @@ html.Div( [ html.H2("Devices"), - dbc.Button("Refresh", id="refresh-button1", n_clicks=0), + dbc.ButtonGroup([dbc.Button("Refresh", id="refresh-button1", n_clicks=0), dbc.Button("Add device", id="add-device-button"), - dbc.Button("Edit", id="edit-device-button"), + dbc.Button("Edit", id="edit-device-button"),], className="mb-3"), + dbc.Modal( [ dbc.ModalHeader( @@ -100,9 +101,10 @@ html.Div( [ html.H2("Commands"), - dbc.Button("Refresh", id="refresh-button2", n_clicks=0), + dbc.ButtonGroup([dbc.Button("Refresh", id="refresh-button2", n_clicks=0), dbc.Button("Add command", id="add-command-button"), - dbc.Button("Edit", id="edit-command-button"), + dbc.Button("Edit", id="edit-command-button"),], className="mb-3"), + dbc.Modal( [ dbc.ModalHeader( @@ -153,19 +155,19 @@ ], className="table-container", ), - html.Div( - [ - html.H2("Command Iterations"), - html.Button("Refresh", id="refresh-button3", n_clicks=0), - html.Div(id="table-container3"), - ], - className="table-container", - ), + # html.Div( + # [ + # html.H2("Command Iterations"), + # html.Button("Refresh", id="refresh-button3", n_clicks=0), + # html.Div(id="table-container3"), + # ], + # className="table-container", + # ), ], className="tables-container", ), ], - className="main-container", + className="container", ) From d0719defbcbda6ab51d0520465cea2225052587d Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 30 Jun 2023 13:46:56 -0500 Subject: [PATCH 015/125] gitignore update --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9b75fe4..64b2cbc 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,6 @@ dmypy.json .pytype/ # Cython debug symbols -cython_debug/ \ No newline at end of file +cython_debug/ + +.vscode/ From ac82f5376db4cf47d6676fea18d7477fb8456482 Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 30 Jun 2023 19:55:51 -0500 Subject: [PATCH 016/125] updated depreciated app.run() --- app.py | 2 +- dashapp-tut.py | 38 ++++++++++ data.csv | 2 + e1.json | 0 e1.yaml | 94 ++++++++++++++++++++++++ e1mongo.yaml | 94 ++++++++++++++++++++++++ e2.yaml | 30 ++++++++ e3.yaml | 31 ++++++++ e4.yaml | 110 ++++++++++++++++++++++++++++ e4mongo.yaml | 110 ++++++++++++++++++++++++++++ out.json | 1 + pages/edit-recipe.py | 169 ------------------------------------------- project_const.py | 28 +++++++ temp.py | 0 temp2.py | 17 +++++ to_save.yaml | 6 +- 16 files changed, 559 insertions(+), 173 deletions(-) create mode 100644 dashapp-tut.py create mode 100644 data.csv create mode 100644 e1.json create mode 100644 e1.yaml create mode 100644 e1mongo.yaml create mode 100644 e2.yaml create mode 100644 e3.yaml create mode 100644 e4.yaml create mode 100644 e4mongo.yaml create mode 100644 out.json delete mode 100644 pages/edit-recipe.py create mode 100644 project_const.py create mode 100644 temp.py create mode 100644 temp2.py diff --git a/app.py b/app.py index 9d54043..4b6df3b 100644 --- a/app.py +++ b/app.py @@ -954,4 +954,4 @@ def handle_exception(e): return "An error occurred. Please try again later." if __name__ == "__main__": - app.run_server(debug=True) + app.run(debug=True) diff --git a/dashapp-tut.py b/dashapp-tut.py new file mode 100644 index 0000000..262e48d --- /dev/null +++ b/dashapp-tut.py @@ -0,0 +1,38 @@ +import dash +from dash import html, Input, Output, State, dcc, dash_table +import pandas as pd +import plotly.express as px +from pymongo import MongoClient +from bson.objectid import ObjectId +from mongodb_helper import MongoDBHelper + + + +# client = MongoClient('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority') +# db = client["diaogroup"] +# collection = db["recipes2"] + +mongo = MongoDBHelper('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority', 'diaogroup') + + +# Define Layout of App +external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] +app = dash.Dash(__name__) + +app.layout = html.Div([ + html.H1('Editor', style={'textAlign': 'center'}), + # interval activated every second or when page refreshed + dcc.Interval(id='interval_db', interval=1000, n_intervals=0), + html.Div(id='mongo-datatable', children=[]), + + html.Div([ + html.Div(id='pie-graph', className='five columns'), + html.Div(id='hist-graph', className='six columns'), + ], className='row'), + dcc.Store(id='changed-cell') +]) + + + +if __name__ == '__main__': + app.run_server(debug=True) \ No newline at end of file diff --git a/data.csv b/data.csv new file mode 100644 index 0000000..781f996 --- /dev/null +++ b/data.csv @@ -0,0 +1,2 @@ +1,2,3,5,5,24 +4,4,5,3,2,ee \ No newline at end of file diff --git a/e1.json b/e1.json new file mode 100644 index 0000000..e69de29 diff --git a/e1.yaml b/e1.yaml new file mode 100644 index 0000000..b9eec5b --- /dev/null +++ b/e1.yaml @@ -0,0 +1,94 @@ +- - &id001 !!python/object:devices.dummy_heater.DummyHeater + _name: heater1 + _is_initialized: false + _heat_rate: 20.0 + min_heat_rate: 1.0 + max_heat_rate: 50.0 + min_temperature: 25.0 + max_temperature: 100.0 + _temperature: 66.70223716282354 + _hardware_interval: 0.05 + - &id002 !!python/object:devices.dummy_motor.DummyMotor + _name: motor1 + _is_initialized: false + motor: !!python/object:devices.dummy_motor_source.DummyMotorSource + _speed: 20.0 + min_speed: 1.0 + max_speed: 50.0 + min_position: 0.0 + max_position: 100.0 + _position: 71.29314197816339 + _hardware_interval: 0.05 +- - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.utility_commands.LoopStartCommand + _params: + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + temperature: 60.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + distance: 20.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + temperature: 25.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + distance: -20.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.utility_commands.LoopEndCommand + _params: + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterDeinitialize + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + reset_init_flag: true + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + reset_init_flag: true + _was_successful: null + _result_message: null +- 3 diff --git a/e1mongo.yaml b/e1mongo.yaml new file mode 100644 index 0000000..b9eec5b --- /dev/null +++ b/e1mongo.yaml @@ -0,0 +1,94 @@ +- - &id001 !!python/object:devices.dummy_heater.DummyHeater + _name: heater1 + _is_initialized: false + _heat_rate: 20.0 + min_heat_rate: 1.0 + max_heat_rate: 50.0 + min_temperature: 25.0 + max_temperature: 100.0 + _temperature: 66.70223716282354 + _hardware_interval: 0.05 + - &id002 !!python/object:devices.dummy_motor.DummyMotor + _name: motor1 + _is_initialized: false + motor: !!python/object:devices.dummy_motor_source.DummyMotorSource + _speed: 20.0 + min_speed: 1.0 + max_speed: 50.0 + min_position: 0.0 + max_position: 100.0 + _position: 71.29314197816339 + _hardware_interval: 0.05 +- - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.utility_commands.LoopStartCommand + _params: + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + temperature: 60.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + distance: 20.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + temperature: 25.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + distance: -20.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.utility_commands.LoopEndCommand + _params: + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterDeinitialize + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + reset_init_flag: true + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + reset_init_flag: true + _was_successful: null + _result_message: null +- 3 diff --git a/e2.yaml b/e2.yaml new file mode 100644 index 0000000..0a8eb9d --- /dev/null +++ b/e2.yaml @@ -0,0 +1,30 @@ + _name: LinearStage150 + _is_initialized: false + _port: /dev/cu.URT0 + _baudrate: 115200 + _timeout: 0.1 + ser: + is_open: false + portstr: null + name: null + _port: null + _baudrate: 9600 + _bytesize: 8 + _parity: N + _stopbits: 1 + _timeout: null + _write_timeout: null + _xonxoff: false + _rtscts: false + _dsrdtr: false + _inter_byte_timeout: null + _rs485_mode: null + _rts_state: true + _dtr_state: true + _break_state: false + _exclusive: null + _destination: 80 + _source: 1 + _channel: 1 +- [] +- ALL diff --git a/e3.yaml b/e3.yaml new file mode 100644 index 0000000..b99240b --- /dev/null +++ b/e3.yaml @@ -0,0 +1,31 @@ +- - !!python/object:devices.linear_stage_150.LinearStage150 + _name: LinearStage150 + _is_initialized: false + _port: /dev/cu.URT0 + _baudrate: 115200 + _timeout: 0.1 + ser: !!python/object:serial.serialposix.Serial + is_open: false + portstr: null + name: null + _port: null + _baudrate: 9600 + _bytesize: 8 + _parity: N + _stopbits: 1 + _timeout: null + _write_timeout: null + _xonxoff: false + _rtscts: false + _dsrdtr: false + _inter_byte_timeout: null + _rs485_mode: null + _rts_state: true + _dtr_state: true + _break_state: false + _exclusive: null + _destination: 80 + _source: 1 + _channel: 1 +- [] +- ALL diff --git a/e4.yaml b/e4.yaml new file mode 100644 index 0000000..83fa98f --- /dev/null +++ b/e4.yaml @@ -0,0 +1,110 @@ +- - &id001 !!python/object:devices.dummy_heater.DummyHeater + _name: heater1 + _is_initialized: false + _heat_rate: 20.0 + min_heat_rate: 1.0 + max_heat_rate: 50.0 + min_temperature: 25.0 + max_temperature: 100.0 + _temperature: 95.71927572168929 + _hardware_interval: 0.05 + - &id002 !!python/object:devices.dummy_motor.DummyMotor + _name: motor1 + _is_initialized: false + motor: !!python/object:devices.dummy_motor_source.DummyMotorSource + _speed: 20.0 + min_speed: 1.0 + max_speed: 50.0 + min_position: 0.0 + max_position: 100.0 + _position: 37.44120879993549 + _hardware_interval: 0.05 + - &id003 !!python/object:devices.dummy_motor.DummyMotor + _name: motor2 + _is_initialized: false + motor: !!python/object:devices.dummy_motor_source.DummyMotorSource + _speed: 20.0 + min_speed: 1.0 + max_speed: 50.0 + min_position: 0.0 + max_position: 100.0 + _position: 82.48283286198111 + _hardware_interval: 0.05 +- - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize + _receiver: *id003 + _params: + receiver_name: motor2 + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + temperature: 60.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorSetSpeed + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 3.0 + speed: 10.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveAbsolute + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + position: 30.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + distance: -20.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterDeinitialize + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + reset_init_flag: true + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + reset_init_flag: true + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize + _receiver: *id003 + _params: + receiver_name: motor2 + delay: 0.0 + reset_init_flag: true + _was_successful: null + _result_message: null +- ALL diff --git a/e4mongo.yaml b/e4mongo.yaml new file mode 100644 index 0000000..83fa98f --- /dev/null +++ b/e4mongo.yaml @@ -0,0 +1,110 @@ +- - &id001 !!python/object:devices.dummy_heater.DummyHeater + _name: heater1 + _is_initialized: false + _heat_rate: 20.0 + min_heat_rate: 1.0 + max_heat_rate: 50.0 + min_temperature: 25.0 + max_temperature: 100.0 + _temperature: 95.71927572168929 + _hardware_interval: 0.05 + - &id002 !!python/object:devices.dummy_motor.DummyMotor + _name: motor1 + _is_initialized: false + motor: !!python/object:devices.dummy_motor_source.DummyMotorSource + _speed: 20.0 + min_speed: 1.0 + max_speed: 50.0 + min_position: 0.0 + max_position: 100.0 + _position: 37.44120879993549 + _hardware_interval: 0.05 + - &id003 !!python/object:devices.dummy_motor.DummyMotor + _name: motor2 + _is_initialized: false + motor: !!python/object:devices.dummy_motor_source.DummyMotorSource + _speed: 20.0 + min_speed: 1.0 + max_speed: 50.0 + min_position: 0.0 + max_position: 100.0 + _position: 82.48283286198111 + _hardware_interval: 0.05 +- - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize + _receiver: *id003 + _params: + receiver_name: motor2 + delay: 0.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + temperature: 60.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorSetSpeed + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 3.0 + speed: 10.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveAbsolute + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + position: 30.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + distance: -20.0 + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_heater_commands.DummyHeaterDeinitialize + _receiver: *id001 + _params: + receiver_name: heater1 + delay: 0.0 + reset_init_flag: true + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize + _receiver: *id002 + _params: + receiver_name: motor1 + delay: 0.0 + reset_init_flag: true + _was_successful: null + _result_message: null + - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize + _receiver: *id003 + _params: + receiver_name: motor2 + delay: 0.0 + reset_init_flag: true + _was_successful: null + _result_message: null +- ALL diff --git a/out.json b/out.json new file mode 100644 index 0000000..51d218b --- /dev/null +++ b/out.json @@ -0,0 +1 @@ + {'index': 0, 'code': 121, 'errmsg': 'Document failed validation', 'errInfo': {'failingDocumentId': ObjectId('6499b75147bea3ddc43d3a44'), 'details': {'operatorName': '$jsonSchema', 'title': 'Solution Object Validation', 'schemaRulesNotSatisfied': [{'operatorName': 'properties', 'propertiesNotSatisfied': [{'propertyName': 'metadata', 'title': 'Metadata Object Validation', 'details': [{'operatorName': 'properties', 'propertiesNotSatisfied': [{'propertyName': 'concentration', 'description': "'concentration' must be a double and is required", 'details': [{'operatorName': 'bsonType', 'specifiedAs': {'bsonType': 'double'}, 'reason': 'type did not match', 'consideredValue': 'float', 'consideredType': 'string'}]}]}]}, {'propertyName': 'result', 'title': 'Result Object Validation', 'details': [{'operatorName': 'properties', 'propertiesNotSatisfied': [{'propertyName': 'uv_vis', 'description': "'uv_vis' must be an objectId and is required", 'details': [{'operatorName': 'bsonType', 'specifiedAs': {'bsonType': 'objectId'}, 'reason': 'type did not match', 'consideredValue': 'object_id', 'consideredType': 'string'}]}, {'propertyName': 't80', 'description': "'t80' must be a double and is required", 'details': [{'operatorName': 'bsonType', 'specifiedAs': {'bsonType': 'double'}, 'reason': 'type did not match', 'consideredValue': 'float', 'consideredType': 'string'}]}]}]}]}]}}} \ No newline at end of file diff --git a/pages/edit-recipe.py b/pages/edit-recipe.py deleted file mode 100644 index fc4bf6e..0000000 --- a/pages/edit-recipe.py +++ /dev/null @@ -1,169 +0,0 @@ -from dash import Dash, html, dcc, dash_table -import dash_bootstrap_components as dbc -import dash - -dash.register_page(__name__, "/edit-recipe") - -layout = html.Div( - [ - html.H1("Edit Recipe"), - html.Div( - [ - html.Div( - [ - html.H2("Devices"), - dbc.Button("Refresh", id="refresh-button1", n_clicks=0), - dbc.Button("Add device", id="add-device-button"), - dbc.Button("Edit", id="edit-device-button"), - dbc.Modal( - [ - dbc.ModalHeader( - dbc.ModalTitle("Editor"), close_button=False - ), - dbc.ModalBody( - [ - dcc.Textarea( - id="device-json-editor", - style={ - "width": "100%", - "height": "200px", - "fontFamily": "monospace", - "backgroundColor": "#f5f5f5", - "border": "1px solid #ccc", - "padding": "10px", - "color": "#333", - }, - ), - html.Div( - id="edit-device-error", - style={"color": "red"}, - ), - html.Div( - id="edit-device-serial-ports-info", - ), - ] - ), - dbc.ModalFooter( - dbc.Button("Save", id="save-device-editor") - ), - ], - id="device-editor-modal", - keyboard=False, - backdrop="static", - ), - dbc.Modal( - [ - dbc.ModalHeader(dbc.ModalTitle("Add Device")), - dbc.ModalBody( - [ - dcc.Dropdown( - id="add-device-dropdown", - options=[], - value=None, - ), - dcc.Textarea( - id="add-device-json-editor", - style={ - "width": "100%", - "height": "200px", - "fontFamily": "monospace", - "backgroundColor": "#f5f5f5", - "border": "1px solid #ccc", - "padding": "10px", - "color": "#333", - }, - ), - html.Div( - id="add-device-error", - style={"color": "red"}, - ), - html.Div( - id="add-device-serial-ports-info", - ), - ] - ), - dbc.ModalFooter( - dbc.Button("Add", id="add-device-editor") - ), - ], - id="device-add-modal", - keyboard=False, - backdrop="static", - ), - html.Div( - children=[dash_table.DataTable(id="devices-table")], - id="devices-table-div", - ), - ], - className="table-container", - ), - html.Div( - [ - html.H2("Commands"), - dbc.Button("Refresh", id="refresh-button2", n_clicks=0), - dbc.Button("Add command", id="add-command-button"), - dbc.Button("Edit", id="edit-command-button"), - dbc.Modal( - [ - dbc.ModalHeader( - dbc.ModalTitle("Editor"), close_button=False - ), - dbc.ModalBody( - [ - dcc.Textarea( - id="command-json-editor", - style={ - "width": "100%", - "height": "200px", - "fontFamily": "monospace", - "backgroundColor": "#f5f5f5", - "border": "1px solid #ccc", - "padding": "10px", - "color": "#333", - }, - ), - html.Div( - id="edit-command-error", - style={"color": "red"}, - ), - ] - ), - dbc.ModalFooter( - dbc.Button("Save", id="save-command-editor") - ), - ], - id="command-editor-modal", - keyboard=False, - backdrop="static", - ), - html.Div( - children=[dash_table.DataTable(id="commands-table")], - id="commands-table-div", - ), - # dbc.Accordion( - # [ - # dbc.AccordionItem( - # "item1", title="Item 1", item_id="item1" - # ) - # ], - # id="commands-accordion", - # # start_collapsed=True, - # style={"display": "none"}, - # ), - ], - className="table-container", - ), - html.Div( - [ - html.H2("Command Iterations"), - html.Button("Refresh", id="refresh-button3", n_clicks=0), - html.Div(id="table-container3"), - ], - className="table-container", - ), - ], - className="tables-container", - ), - ], - className="main-container", -) diff --git a/project_const.py b/project_const.py new file mode 100644 index 0000000..7034283 --- /dev/null +++ b/project_const.py @@ -0,0 +1,28 @@ +from command_sequence import CommandSequence +from command_invoker import CommandInvoker +from commands.command import Command +from commands.utility_commands import LoopStartCommand, LoopEndCommand +from devices.heating_stage import HeatingStage +from devices.multi_stepper import MultiStepper +from devices.newport_esp301 import NewportESP301 +# from devices.stellarnet_spectrometer import StellarNetSpectrometer +# from devices.ximea_camera import XimeaCamera +from devices.dummy_heater import DummyHeater +from devices.dummy_motor import DummyMotor + + +named_devices = { + "PrintingStage": HeatingStage, + "AnnealingStage": HeatingStage, + "MultiStepper1": MultiStepper, + "PrinterMotorX": NewportESP301, + # "Spectrometer": StellarNetSpectrometer, + # "SampleCamera": XimeaCamera, + "DummyHeater": DummyHeater, + "DummyHeater1": DummyHeater, + "DummyHeater2": DummyHeater, + "DummyMotor1": DummyMotor, + "DummyMotor2": DummyMotor, + } +command_directory = "commands/" +approved_devices = list(named_devices.keys()) \ No newline at end of file diff --git a/temp.py b/temp.py new file mode 100644 index 0000000..e69de29 diff --git a/temp2.py b/temp2.py new file mode 100644 index 0000000..b09bd71 --- /dev/null +++ b/temp2.py @@ -0,0 +1,17 @@ +from devices.dummy_heater import DummyHeater +from mongodb_helper import MongoDBHelper + +mongo = MongoDBHelper('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority', 'diaogroup') + +# print(mongo.insert_yaml_file('recipes',"e1.yaml")) +print(mongo.find_documents('recipes',{'file_name':'e1.yaml'})[0]) + +print('done') + + + +mongo.close_connection() + +# client = MongoClient('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority') +# db = client['diaogroup'] +# collection = db['recipes'] \ No newline at end of file diff --git a/to_save.yaml b/to_save.yaml index d0a701c..2480e5a 100644 --- a/to_save.yaml +++ b/to_save.yaml @@ -6,7 +6,7 @@ max_heat_rate: 50.0 min_temperature: 25.0 max_temperature: 100.0 - _temperature: 71.22528451782857 + _temperature: 80.9565265284408 _hardware_interval: 0.05 - &id002 !!python/object:devices.dummy_motor.DummyMotor _name: motor1 @@ -17,7 +17,7 @@ max_speed: 50.0 min_position: 0.0 max_position: 100.0 - _position: 57.525955561403954 + _position: 88.11966994238041 _hardware_interval: 0.05 - &id003 !!python/object:devices.dummy_motor.DummyMotor _name: motor2 @@ -28,7 +28,7 @@ max_speed: 50.0 min_position: 0.0 max_position: 100.0 - _position: 30.78435676094049 + _position: 1.6064267655005238 _hardware_interval: 0.05 - - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize _receiver: *id001 From b4fa47d05c7eefe97728b62ee6ee5aae732c36ff Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 10 Jul 2023 12:53:44 -0500 Subject: [PATCH 017/125] updated ximea to work with pip library --- devices/ximea_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devices/ximea_camera.py b/devices/ximea_camera.py index e53e39d..9d3ed2d 100644 --- a/devices/ximea_camera.py +++ b/devices/ximea_camera.py @@ -2,7 +2,7 @@ from typing import Optional, Tuple, List from PIL import Image -from ximea import xiapi +from ximea import * from .device import Device, check_initialized From 4b16836c7e2735ff5a91444ed3e5e352f16e7940 Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 10 Jul 2023 12:58:48 -0500 Subject: [PATCH 018/125] updated devices to include abstract methods --- devices/device.py | 4 +- devices/heating_stage.py | 17 +++++ devices/kinova_arm.py | 19 +++++ devices/linear_stage_150.py | 133 +++++++++++++++++++++++------------ devices/newport_esp301.py | 21 ++++++ devices/psd6_syringe_pump.py | 25 +++++++ devices/ximea_camera.py | 9 +++ 7 files changed, 180 insertions(+), 48 deletions(-) diff --git a/devices/device.py b/devices/device.py index 83cec59..af6dc94 100644 --- a/devices/device.py +++ b/devices/device.py @@ -95,7 +95,7 @@ def deinitialize(self) -> Tuple[bool, str]: """ pass - # @abstractmethod TODO uncomment and implement method in all devices + @abstractmethod # uncomment and implement method in all devices def get_init_args(self) -> dict: """The get_args abstract method that all devices should implement. Method should return a dict with only the arguments needed to initialize the device. @@ -106,7 +106,7 @@ def get_init_args(self) -> dict: """ pass - # @abstractmethod # TODO uncomment and implement method in all devices + @abstractmethod # uncomment and implement method in all devices def update_init_args(self, args_dict: dict): """The update_init_args abstract method that all devices should implement. Method should update the arguments needed to initialize the device. diff --git a/devices/heating_stage.py b/devices/heating_stage.py index 4b51b10..fc59523 100644 --- a/devices/heating_stage.py +++ b/devices/heating_stage.py @@ -16,6 +16,23 @@ def __init__( super().__init__(name, port, baudrate, timeout) self._heating_timeout = heating_timeout + def get_init_args(self) -> dict: + args_dict = { + "name": self.name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "heating_timeout": self._heating_timeout, + } + return args_dict + + def update_init_args(self, args_dict: dict): + self.name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._heating_timeout = args_dict["heating_timeout"] + # no need to check serial as set_settemp and pid_on has these checks already def initialize(self) -> Tuple[bool, str]: self._is_initialized = True diff --git a/devices/kinova_arm.py b/devices/kinova_arm.py index 96608a7..d2304cc 100644 --- a/devices/kinova_arm.py +++ b/devices/kinova_arm.py @@ -53,6 +53,25 @@ def __init__( self._transport = TCPTransport() self._router = RouterClient(self._transport, RouterClient.basicErrorCallback) + def get_init_args(self) -> dict: + args_dict = { + "name": self.name, + "ip": self._ip, + "username": self._username, + "password": self._password, + "action_timeout": self._action_timeout, + "proportional_gain": self._proportional_gain + } + return args_dict + + def update_init_args(self, args_dict: dict): + self.name = args_dict["name"] + self._ip = args_dict["ip"] + self._username = args_dict["username"] + self._password = args_dict["password"] + self._action_timeout = args_dict["action_timeout"] + self._proportional_gain = args_dict["proportional_gain"] + def connect(self) -> Tuple[bool, str]: self._transport.connect(self._ip, self._port) diff --git a/devices/linear_stage_150.py b/devices/linear_stage_150.py index ce76d59..92652fe 100644 --- a/devices/linear_stage_150.py +++ b/devices/linear_stage_150.py @@ -1,16 +1,26 @@ from typing import Optional, Tuple -from struct import pack,unpack +from struct import pack, unpack import time from .device import SerialDevice, check_serial, check_initialized + class LinearStage150(SerialDevice): - def __init__(self, name: str, port: str = '\'/dev/cu.URT0\'', baudrate: int = 115200, timeout: float | None = 0.1, destination: int = 0x50, source: int = 0x01, channel: int = 1): + def __init__( + self, + name: str, + port: str = "'COM6'", + baudrate: int = 115200, + timeout: float | None = 0.1, + destination: int = 0x50, + source: int = 0x01, + channel: int = 1, + ): super().__init__(name, port, baudrate, timeout) self._destination = destination self._source = source self._channel = channel # print("dest: "+str(self._destination)) - + def get_init_args(self) -> dict: args_dict = { "name": self._name, @@ -36,51 +46,51 @@ def update_init_args(self, args_dict: dict): def initialize(self) -> Tuple[bool, str]: self._is_initialized = False - #lts150: initialize, home it (if needed) + # lts150: initialize, home it (if needed) - #Home Stage; MGMSG_MOT_MOVE_HOME - self.ser.write(pack(' Tuple[bool, str]: - - #i dont think this is needed: lts150: deinitialize + # i dont think this is needed: lts150: deinitialize # if reset_init_flag: //used in other devices self._is_initialized = False return (True, "Successfully deinitialized LTS150.") # return super().deinitialize() - + @check_serial # @check_initialized def get_enabled_state(self) -> bool: self._is_enabled = False - #TODO: lts150 get enabled state, MGMSG_MOD_GET_CHANENABLESTATE + # TODO: lts150 get enabled state, MGMSG_MOD_GET_CHANENABLESTATE # self.ser.write(pack(' Tuple[bool, str]: + + def set_enabled_state(self, state: bool) -> Tuple[bool, str]: if state: - self.ser.write(pack(' float: self._position = 0.0 Device_Unit_SF = 409600 # MGMSG_MOT_GET_POSCOUNTER - self.ser.write(pack(' float: def move_absolute(self, position: float) -> Tuple[bool, str]: if position > 150: return (False, "Position " + str(position) + " is out of range.") - - Device_Unit_SF = 409600 - dUnitpos = int(Device_Unit_SF*position) - self.ser.write(pack(' Tuple[bool, str]: if distance + self.get_position() > 150: - return (False, "Position " + str(distance + self.get_position()) + " is out of range.") + return ( + False, + "Position " + str(distance + self.get_position()) + " is out of range.", + ) Device_Unit_SF = 409600 - dUnitpos = int(Device_Unit_SF*distance) - self.ser.write(pack(' dict: + args_dict = { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "axis_list": self._axis_list, + "default_speed": self._default_speed, + "poll_interval": self._poll_interval, + } + return args_dict + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._axis_list = args_dict["axis_list"] + self._default_speed = args_dict["default_speed"] + self._poll_interval = args_dict["poll_interval"] + @property def default_speed(self) -> float: return self._default_speed diff --git a/devices/psd6_syringe_pump.py b/devices/psd6_syringe_pump.py index 5302f44..8729bc7 100644 --- a/devices/psd6_syringe_pump.py +++ b/devices/psd6_syringe_pump.py @@ -63,6 +63,31 @@ def __init__( # self._min_position = 0 # self._max_position = 6000 + def get_init_args(self) -> dict: + args_dict = { + 'name': self.name, + 'port': self.port, + 'baudrate': self.baudrate, + 'timeout': self.timeout, + 'stroke_volume': self._stroke_volume, + 'stroke_steps': self._stroke_steps, + 'default_flowrate': self._default_flowrate, + 'port_dead_volumes': self._port_dead_volumes, + 'poll_interval': self._poll_interval + } + return args_dict + + def update_init_args(self, args_dict: dict): + self.name = args_dict['name'] + self.port = args_dict['port'] + self.baudrate = args_dict['baudrate'] + self.timeout = args_dict['timeout'] + self._stroke_volume = args_dict['stroke_volume'] + self._stroke_steps = args_dict['stroke_steps'] + self._default_flowrate = args_dict['default_flowrate'] + self._port_dead_volumes = args_dict['port_dead_volumes'] + self._poll_interval = args_dict['poll_interval'] + @property def default_flowrate(self) -> float: return self._default_flowrate diff --git a/devices/ximea_camera.py b/devices/ximea_camera.py index 9d3ed2d..9cc543a 100644 --- a/devices/ximea_camera.py +++ b/devices/ximea_camera.py @@ -27,6 +27,15 @@ def __init__(self, name: str): self._default_wb_kb = 1.305 # self.set_default_params() # done in initalize because cam is not yet open here + def get_init_args(self) -> dict: + args_dict = { + "name": self.name, + } + return args_dict + + def update_init_args(self, args_dict: dict): + self.name = args_dict["name"] + # no setter for imgdataformat at the moment @property def default_imgdataformat(self) -> str: From aa1407ed1ae312eb95be57129555484366cad3e3 Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 10 Jul 2023 12:59:36 -0500 Subject: [PATCH 019/125] updated homepage with more columns --- pages/home.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/pages/home.py b/pages/home.py index 034855b..c5e1dd1 100644 --- a/pages/home.py +++ b/pages/home.py @@ -1,4 +1,4 @@ -from dash import Dash, html, dcc, dash_table +from dash import Dash, html, dcc, dash_table, Input, Output, callback import dash_bootstrap_components as dbc import dash @@ -79,6 +79,9 @@ id="home-recipes-list-table", columns=[ {"name": "File Name", "id": "file_name"}, + {"name": "Posix Compatible", "id": "posix_friendly"}, + {"name": "Viewer Compatible", "id": "dash_friendly"}, + {"name": "Python Code", "id": "python_code"}, ], data=[], style_table={"width": "100%"}, @@ -86,8 +89,58 @@ style_header={"fontWeight": "bold"}, page_current=0, page_size=10, + style_data_conditional=[ + { + "if": { + "column_id": "posix_friendly", + "filter_query": "{posix_friendly} contains true", + }, + "backgroundColor": "#b7e8c4", + "color": "black", + }, + { + "if": { + "column_id": "posix_friendly", + "filter_query": "{posix_friendly} contains false", + }, + "backgroundColor": "#e8b7b7", + "color": "black", + }, + { + "if": { + "column_id": "dash_friendly", + "filter_query": "{dash_friendly} contains true", + }, + "backgroundColor": "#b7e8c4", + "color": "black", + }, + { + "if": { + "column_id": "dash_friendly", + "filter_query": "{dash_friendly} contains false", + }, + "backgroundColor": "#e8b7b7", + "color": "black", + }, + { + "if": { + "column_id": "python_code", + "filter_query": "{python_code} contains true", + }, + "backgroundColor": "#b7e8c4", + "color": "black", + }, + { + "if": { + "column_id": "python_code", + "filter_query": "{python_code} contains false", + }, + "backgroundColor": "#e8b7b7", + "color": "black", + }, + ], ), - width=8, + width=10, ) ] ), From 8f64e9af9f7c27543b09c83f673e3e8eb77f6089 Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 10 Jul 2023 18:54:08 -0500 Subject: [PATCH 020/125] refactoring app.py --- app.py | 978 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 520 insertions(+), 458 deletions(-) diff --git a/app.py b/app.py index 4b6df3b..b05fe24 100644 --- a/app.py +++ b/app.py @@ -1,23 +1,16 @@ from command_sequence import CommandSequence from command_invoker import CommandInvoker import json -from devices.device import Device -from commands.command import Command, CompositeCommand -from devices.device import Device, SerialDevice -import threading +from devices.device import SerialDevice import util from mongodb_helper import MongoDBHelper -import pandas as pd import dash from dash import dcc from dash import html from dash import dash_table from dash.dependencies import Input, Output, State -import random import dash_bootstrap_components as dbc -from dash_dangerously_set_inner_html import DangerouslySetInnerHTML -import os, signal, sys -import logging +import os, signal import inspect try: @@ -31,21 +24,17 @@ print("\nreset complete") com = CommandSequence() -invoker = CommandInvoker(com, log_to_file=True, log_filename='mylog.log') +invoker = CommandInvoker(com, log_to_file=True, log_filename="mylog.log") invoker.clear_log_file() invoker.invoking = False com.load_from_yaml("blank.yaml") mongo = MongoDBHelper( - "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "mongodb+srv://ppahuja2:vDkNu2sKR1eDsOmY@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", "diaogroup", ) - -# out = util.device_to_dict(com.device_list[0]) - - app = dash.Dash( __name__, external_stylesheets=[dbc.themes.BOOTSTRAP], @@ -61,25 +50,67 @@ dbc.NavItem(dbc.NavLink("Edit Recipe (PY)", href="/python-edit-recipe")), dbc.NavItem(dbc.NavLink("Execute Recipe", href="/execute-recipe")), dbc.NavItem(dbc.NavLink("Data", href="/data")), + dbc.NavItem(dbc.NavLink("Manual Control", href="/manual-control")), ], brand="AAMP", brand_href="/", color="primary", dark=True, + id="navbar", + className="mb-3", ) +app.layout = html.Div([dcc.Location(id="url"), navbar, dash.page_container]) + + +@app.callback(Output("navbar", "brand"), Input("url", "pathname")) +def print_pagename(url): # all pages + print("loading: " + url + "\n") + return "AAMP" + + +# --------------------------------------------------- +# Home Page +# --------------------------------------------------- + + @app.callback( Output("home-recipes-list-table", "data"), [Input("home-refresh-list-button", "n_clicks")], # prevent_initial_call=True, ) -def fetch_recipe_list(n_clicks): +def fetch_recipe_list(n_clicks): # homepage docs = mongo.find_documents("recipes", {}) - docs = mongo.db["recipes"].find({}, {"_id": 0, "file_name": 1}) - # print(pd.DataFrame(docs).to_dict('records')) + docs = mongo.db["recipes"].find( + {}, + { + "_id": 0, + "file_name": 1, + "posix_friendly": 1, + "dash_friendly": 1, + "python_code": 1, + }, + ) print("fetch_recipe_list") - return pd.DataFrame(docs).to_dict("records") + data = [] + for doc in docs: + file_name = doc.get("file_name", "") + posix_friendly = doc.get("posix_friendly", True) + dash_friendly = doc.get("dash_friendly", False) + if "python_code" in doc: + python_code = True + else: + python_code = False + data.append( + { + "file_name": file_name, + "posix_friendly": posix_friendly, + "dash_friendly": dash_friendly, + "python_code": python_code, + } + ) + return data @app.callback( @@ -88,147 +119,41 @@ def fetch_recipe_list(n_clicks): State("home-recipes-list-table", "data"), # prevent_initial_call=True, ) -def fill_filename_input(active_cell, data): +def fill_filename_input(active_cell, data): # homepage if active_cell is not None: - print('fill_filename_input') + print("fill_filename_input") return data[active_cell["row"]]["file_name"] if hasattr(com, "document"): - print('fill_filename_input') + print("fill_filename_input") return com.document["file_name"] return "" -@app.callback( - Output("data-output-div", "children"), - Input("load-data-button", "n_clicks"), - prevent_initial_call=True, -) -def load_data(n): - print('load_data') - # val = "" - # docs = mongo.db["recipes"].find() - # for doc in docs: - # val += str(doc) - # val += "

" - # nval = val. - return (json.dumps(str(com.document))) - - -def kill_execution(): - os.kill(os.getpid(), signal.SIGINT) - - -@app.callback( - Output("hidden-div", "children"), - Input("stop-button", "n_clicks"), - prevent_initial_call=True, -) -def stop_execution(n): - print("stop_execution") - # kill_execution() - # print('stop done') - return [] - - - -@app.callback( - Output("execute-recipe-output", "children"), - [Input("execute-button", "n_clicks")], - prevent_initial_call=True, -) -def execute_recipe(n_clicks): - print('execute_recipe') - invoker.invoking = True - invoker.invoke_commands() - invoker.invoking = False - return ["done"] - - -class DashLoggerHandler(logging.StreamHandler): - def __init__(self): - logging.StreamHandler.__init__(self) - self.queue = [] - - def emit(self, record): - msg = self.format(record) - self.queue.append(msg) - - -# logger = logging.getLogger() -# logger.setLevel(logging.DEBUG) -# dashLoggerHandler = DashLoggerHandler() -# logger.addHandler(dashLoggerHandler) - -# logger = logging.getLogger(invoker.log.name) -# logger.setLevel(logging.DEBUG) -# from io import StringIO -# log_capture = StringIO() - -# # Create a stream handler and set its stream to the log_capture object -# stream_handler = logging.StreamHandler(log_capture) -# logger.addHandler(stream_handler) -# log_messages = [] - -@app.callback(Output("console-out2", "children"), - Input("interval1", "n_intervals"), - [State('url', 'pathname'), State('show-log-switch', 'value')], prevent_initial_call=True) -def update_output(n, url, switch_val): - # print(invoker.invoking) - if url == '/execute-recipe' and switch_val: - log_string = '' - log_list = invoker.get_log_messages() - for msg in log_list: - log_string += msg - # log_string += '
' - return html.Pre(log_string) - return "" - - -@app.callback( - Output("console-out2", "children", allow_duplicate=True), - Input("reset-button", "n_clicks"), - prevent_initial_call=True, -) -def reset_console(n): - print('reset_console') - invoker.clear_log_file() - # dashLoggerHandler.queue = [] - return [] - - - - -# import sys -# from io import StringIO -# stringio = StringIO() -# sys.stdout = stringio -# @app.callback(Output('console-output', 'value'), [Input('update-interval', 'n_intervals')]) -# def show_console_output(n): -# # Retrieve console output from StringIO object -# stringio.seek(0) -# console_output = stringio.read() -# return console_output - - @app.callback( [ - Output('home-load-file-alert', 'is_open'), - Output('home-load-file-alert', 'children'), - Output('home-load-file-alert', 'color'), - Output('home-load-file-alert', 'duration'),], + Output("home-load-file-alert", "is_open"), + Output("home-load-file-alert", "children"), + Output("home-load-file-alert", "color"), + Output("home-load-file-alert", "duration"), + ], [Input("filename-input-button", "n_clicks")], [State("filename-input", "value")], prevent_initial_call=True, ) -def get_document_from_db(n_clicks, filename): - print('get_document_from_db') +def get_document_from_db(n_clicks, filename): # homepage + print("get_document_from_db") if filename is not None and filename != "": # Extract the YAML content from the document document = mongo.find_documents("recipes", {"file_name": filename})[0] invoker.clear_log_file() - if os.name == 'posix': - if 'posix_friendly' in document and not document['posix_friendly']: - return [True, "This recipe is not compatible with your system (POSIX compatability error)", "danger", 8000] + if os.name == "posix": + if "posix_friendly" in document and not document["posix_friendly"]: + return [ + True, + "This recipe is not compatible with your system (POSIX compatability error)", + "danger", + 8000, + ] if ( document.get("dash_friendly", "") == False or document.get("python_code", "") == "" @@ -252,93 +177,9 @@ def get_document_from_db(n_clicks, filename): return [True, "No recipe selected", "warning", 3000] -@app.callback( - [ - Output("ace-recipe-editor", "value", allow_duplicate=True), - Output("ace-editor-alert", "is_open"), - Output("ace-editor-alert", "children"), - Output("ace-editor-alert", "color"), - Output("ace-editor-alert", "duration"), - ], - [Input("refresh-button-ace", "n_clicks")], - State("url", "pathname"), - # prevent_initial_call=True, -) -def fill_ace_editor(n, url): - if url == "/python-edit-recipe": - if hasattr(com, "document"): - python_code = com.document.get("python_code", "") - if python_code is not None and python_code != "": - print('fill_ace_editor') - return [str(python_code), True, "Loaded!", "success", 1500] - else: - print('fill_ace_editor') - return ["", True, "No code available", "warning", 1000] - else: - return ["", True, "No code available", "warning", 1000] - else: - return ["", False, "na", "success", 0] - - -@app.callback( - [ - Output("ace-recipe-editor", "value", allow_duplicate=True), - Output("ace-editor-alert", "is_open", allow_duplicate=True), - Output("ace-editor-alert", "children", allow_duplicate=True), - Output("ace-editor-alert", "color", allow_duplicate=True), - Output("ace-editor-alert", "duration", allow_duplicate=True), - ], - Input("execute-and-save-button", "n_clicks"), - [State("ace-recipe-editor", "value")], - prevent_initial_call=True, -) -def execute_and_save(n, value): - print('execute_and_save') - if value is not None and value != "": - try: - exec(value) - doc_id = com.document.get("_id", "") - (mongo.update_yaml_file("recipes", doc_id, {"python_code": value})) - com.load_from_yaml("to_save.yaml") - com.document = mongo.find_documents("recipes", {"_id": doc_id})[0] - return [str(value), True, "Saved!", "success", 1500] - except Exception as e: - return [str(value), True, str(e), "danger", 5000] - else: - return ["", True, "No code to execute", "warning", 1000] - - # return ["", False, "", "success", 1500] - - -# app.layout = html.Div( -# [, , html.Div(id="page-content")] -# ) - -app.layout = html.Div([dcc.Location(id="url"), navbar, dash.page_container]) - - -# @app.callback(Output("page-content", "children"), [Input("url", "pathname")]) -# def display_page(pathname): -# print("\n\nrefreshing to " + pathname) -# if pathname == "/": -# return home_layout -# elif pathname == "/edit-recipe": -# return edit_recipe_layout -# elif pathname == "/execute-recipe": -# return execute_recipe_layout -# elif pathname == "/data": -# return data_layout -# else: -# return html.Div("404") - - -# data_list4 = com.device_list - -# dl5 = [] -# for list in data_list4: -# dl5.append(list.__dict__) - -# print(dl5[2]['motor'].__dict__) +# --------------------------------------------------- +# View Recipe Page +# --------------------------------------------------- @app.callback( @@ -346,8 +187,8 @@ def execute_and_save(n, value): [Input("refresh-button1", "n_clicks"), Input("devices-table", "data")], [State("devices-table-div", "children")], ) -def update_device_table(n_clicks, data, table): - print('update_device_table') +def update_device_table(n_clicks, data, table): # view-recipe page + print("update_device_table") # table_data1 = dl5 table_data1 = com.get_clean_device_list().copy() # print(com.device_list[1].get_init_args()) @@ -371,7 +212,6 @@ def update_device_table(n_clicks, data, table): columns=[ {"name": "Index", "id": "index"}, {"name": "Type", "id": "device_type"}, - # {"name": "Initialized", "id": "_is_initialized"}, {"name": "Parameters", "id": "params"}, ], style_cell={ @@ -396,11 +236,9 @@ def update_device_table(n_clicks, data, table): # tooltip_duration=None, # editable = True, ) - # print("devices table refresh done") return table - @app.callback( Output("commands-table", "data"), Input("save-command-editor", "n_clicks"), @@ -411,19 +249,12 @@ def update_device_table(n_clicks, data, table): ], prevent_initial_call=True, ) -def save_command(n_clicks, active_cell, data, value): - print('save_command') +def save_command(n_clicks, active_cell, data, value): # view-recipe page + print("save_command") if active_cell is not None and data[active_cell["row"]]["params"] != str( json.loads(value) ): - # data[active_cell['row']]['params'] = str(json.loads(value)) - # com.command_list[data[active_cell['row']]['index']] - # print(data[active_cell['row']]['params']) - # print((eval(value))) - com.command_list[data[active_cell["row"]]["index"]][0]._params = eval(value) - # print((com.command_list[data[active_cell['row']]['index']][0]._params)) - # print(com.get_unlooped_command_list()[active_cell['row']]) return None return data @@ -438,8 +269,8 @@ def save_command(n_clicks, active_cell, data, value): ], prevent_initial_call=True, ) -def save_device(n_clicks, active_cell, data, value): - print('save_device') +def save_device(n_clicks, active_cell, data, value): # view-recipe page + print("save_device") if active_cell is not None and data[active_cell["row"]]["params"] != str( json.loads(value) ): @@ -456,72 +287,23 @@ def save_device(n_clicks, active_cell, data, value): [State("commands-table", "active_cell"), State("commands-table", "data")], prevent_initial_call=True, ) -def fill_command_json_editor(is_open, active_cell, data): +def fill_command_json_editor(is_open, active_cell, data): # view-recipe page if active_cell is not None and is_open: - # print(active_cell) - # print(eval(data[active_cell['row']]['params'])) - print('fill_command_json_editor') + print("fill_command_json_editor") return json.dumps(eval(data[active_cell["row"]]["params"]), indent=4) return "" @app.callback( - [ - Output("add-device-dropdown", "options"), - ], + Output("add-device-dropdown", "options"), [Input("device-add-modal", "is_open")], [State("devices-table", "active_cell"), State("devices-table", "data")], prevent_initial_call=True, ) -def fill_device_add_modal(is_open, active_cell, data): - print('fill_device_add_modal') - return [util.approved_devices] - - -@app.callback( - [ - Output("add-device-dropdown-ace", "options"), - Output("add-device-dropdown-ace", "value"), - ], - [Input("device-add-modal-ace", "is_open")], - prevent_initial_call=True, -) -def fill_device_add_modal_ace(is_open): - if is_open: - print('fill_device_add_modal_ace') - return list(util.devices_ref.keys()), "" - return [], "" - - -@app.callback( - [ - Output("add-command-device-dropdown-ace", "options"), - Output("add-command-device-dropdown-ace", "value"), - ], - [Input("command-add-modal-ace", "is_open")], - prevent_initial_call=True, -) -def fill_command_device_add_modal_ace(is_open): - if is_open: - print('fill_command_device_add_modal_ace') - return list(util.devices_ref.keys()), "" - return [], "" - - -@app.callback( - [ - Output("add-command-command-dropdown-ace", "options"), - Output("add-command-command-dropdown-ace", "value"), - ], - [Input("add-command-device-dropdown-ace", "value")], - prevent_initial_call=True, -) -def fill_command_add_modal_ace(device): - if device is not None and device != "": - print('fill_command_add_modal_ace') - return list(util.devices_ref[device]["commands"].keys()), "" - return [], "" +def fill_device_add_modal(is_open, active_cell, data): # view-recipe page + print("fill_device_add_modal") + return util.approved_devices @app.callback( @@ -529,8 +311,8 @@ def fill_command_add_modal_ace(device): [Input("add-device-dropdown", "value"), Input("device-add-modal", "is_open")], prevent_initial_call=True, ) -def fill_device_add_json_editor(value, is_open): - print('fill_device_add_json_editor') +def fill_device_add_json_editor(value, is_open): # view-recipe page + print("fill_device_add_json_editor") if not is_open or value is None: return [""] args_list = inspect.getfullargspec(util.named_devices[value].__init__).args @@ -552,8 +334,8 @@ def fill_device_add_json_editor(value, is_open): [State("devices-table", "active_cell"), State("devices-table", "data")], prevent_initial_call=True, ) -def fill_device_json_editor(is_open, active_cell, data): - print('fill_device_json_editor') +def fill_device_json_editor(is_open, active_cell, data): # view-recipe page + print("fill_device_json_editor") if active_cell is not None and is_open: if _has_serial and isinstance( com.device_by_name[eval(data[active_cell["row"]]["params"])["name"]], @@ -586,16 +368,14 @@ def fill_device_json_editor(is_open, active_cell, data): State("command-editor-modal", "is_open"), prevent_initial_call=True, ) -def enable_save_command_button(value, is_open): +def enable_save_command_button(value, is_open): # view-recipe page if not is_open: return False, "" try: parsed_json = json.loads(value) - # print(type(parsed_json)) - # print(parsed_json) if parsed_json["delay"] < 0: return True, "Delay must be greater than or equal to 0" - print('enable_save_command_button') + print("enable_save_command_button") return False, "" except Exception as e: if type(e) == json.decoder.JSONDecodeError: @@ -609,12 +389,12 @@ def enable_save_command_button(value, is_open): State("device-editor-modal", "is_open"), prevent_initial_call=True, ) -def enable_save_device_button(value, is_open): +def enable_save_device_button(value, is_open): # view-recipe page if not is_open: return False, "" try: parsed_json = json.loads(value) - print('enable_save_device_button') + print("enable_save_device_button") return False, "" except Exception as e: if type(e) == json.decoder.JSONDecodeError: @@ -628,7 +408,7 @@ def enable_save_device_button(value, is_open): State("device-add-modal", "is_open"), prevent_initial_call=True, ) -def enable_add_device_button(value, device_type, is_open): +def enable_add_device_button(value, device_type, is_open): # view-recipe page if value == "": return True, "No device selected" if not is_open: @@ -656,7 +436,7 @@ def enable_add_device_button(value, device_type, is_open): return True, f"Invalid type for {key}. Expected {str(args[key])}" # if not isinstance(parsed_json[key], args[key]): # return True, f"Invalid type for {key}. Expected {str(args[key])}" - print('enable_add_device_button') + print("enable_add_device_button") return False, "" except Exception as e: if type(e) == json.decoder.JSONDecodeError: @@ -665,42 +445,232 @@ def enable_add_device_button(value, device_type, is_open): @app.callback( - Output("add-device-editor-ace", "disabled"), - [ - Input("add-device-dropdown-ace", "value"), - Input("device-add-modal-ace", "is_open"), - ], - State("device-add-modal-ace", "is_open"), - prevent_initial_call=True, + Output("edit-command-button", "disabled"), + Input("commands-table", "active_cell"), ) -def enable_add_device_button_ace(value, is_openInp, is_open): - if value == "" or value is None: +def edit_command_button(table_div_children): # view-recipe page + active_cell = table_div_children + if active_cell is not None: + print("edit_command_button") + return False + else: return True - print('enable_add_device_button_ace') - return False @app.callback( - Output("add-command-editor-ace", "disabled"), - [ - Input("add-command-command-dropdown-ace", "value"), - Input("command-add-modal-ace", "is_open"), - ], - State("command-add-modal-ace", "is_open"), - prevent_initial_call=True, + Output("edit-device-button", "disabled"), + Input("devices-table", "active_cell"), ) -def enable_add_command_button_ace(value, is_openInp, is_open): - if value == "" or value is None: +def edit_device_button(table_div_children): # view-recipe page + print("edit_device_button") + active_cell = table_div_children + if active_cell is not None: + return False + else: return True - print('enable_add_command_button_ace') - return False @app.callback( - [ - Output("ace-recipe-editor", "value"), - Output("ace-editor-alert", "is_open", allow_duplicate=True), - Output("ace-editor-alert", "children", allow_duplicate=True), + Output("commands-table-div", "children"), + [Input("refresh-button2", "n_clicks"), Input("commands-table", "data")], + [State("commands-table-div", "children")], +) +def update_commands_table(n_clicks, data, table): # view-recipe page + print("update_commands_table") + command_list = com.command_list.copy() + command_params = [] + for index, command in enumerate(command_list): + temp_dict_command_params = {"command": type(command[0]).__name__} + temp_dict_command_params.update( + {"params": str(command[0].get_init_args()), "index": index} + ) + command_params.append((temp_dict_command_params)) + # else: + # command_params.append(command._params) + + table_data2 = command_params + # add_command_accordian(0, to_add=[dbc.AccordionItem("new new", title="new new", item_id="new new")]) + + table = dash_table.DataTable( + id="commands-table", + data=table_data2, + columns=[ + {"name": "Index", "id": "index"}, + {"name": "Command", "id": "command"}, + {"name": "Parameters", "id": "params"}, + ], + style_cell={ + "overflow": "hidden", + "textOverflow": "ellipsis", + "maxWidth": 0, + "textAlign": "left", + "padding": "5px", + }, + style_cell_conditional=[ + {"if": {"column_id": "index"}, "width": "5%"}, + {"if": {"column_id": "command"}, "width": "20%"}, + {"if": {"column_id": "params"}, "width": "70%"}, + ], + # tooltip_data=[ + # { + # column: {"value": str(value), "type": "markdown"} + # for column, value in row.items() + # } + # for row in table_data2 + # ], + # tooltip_duration=None, + # editable = True, + ) + return table + + +# --------------------------------------------------- +# Python Edit Recipe Page +# --------------------------------------------------- + + +@app.callback( + [ + Output("ace-recipe-editor", "value", allow_duplicate=True), + Output("ace-editor-alert", "is_open"), + Output("ace-editor-alert", "children"), + Output("ace-editor-alert", "color"), + Output("ace-editor-alert", "duration"), + ], + [Input("refresh-button-ace", "n_clicks")], + State("url", "pathname"), + # prevent_initial_call=True, +) +def fill_ace_editor(n, url): # python-edit-recipe page + if url == "/python-edit-recipe": + if hasattr(com, "document"): + python_code = com.document.get("python_code", "") + if python_code is not None and python_code != "": + print("fill_ace_editor") + return [str(python_code), True, "Loaded!", "success", 1500] + else: + print("fill_ace_editor") + return ["", True, "No code available", "warning", 1000] + else: + return ["", True, "No code available", "warning", 1000] + else: + return ["", False, "na", "success", 0] + + +@app.callback( + [ + Output("ace-recipe-editor", "value", allow_duplicate=True), + Output("ace-editor-alert", "is_open", allow_duplicate=True), + Output("ace-editor-alert", "children", allow_duplicate=True), + Output("ace-editor-alert", "color", allow_duplicate=True), + Output("ace-editor-alert", "duration", allow_duplicate=True), + ], + Input("execute-and-save-button", "n_clicks"), + [State("ace-recipe-editor", "value")], + prevent_initial_call=True, +) +def execute_and_save(n, value): # python-edit-recipe page + print("execute_and_save") + if value is not None and value != "": + try: + exec(value) + doc_id = com.document.get("_id", "") + (mongo.update_yaml_file("recipes", doc_id, {"python_code": value})) + com.load_from_yaml("to_save.yaml") + com.document = mongo.find_documents("recipes", {"_id": doc_id})[0] + return [str(value), True, "Saved!", "success", 1500] + except Exception as e: + return [str(value), True, str(e), "danger", 5000] + else: + return ["", True, "No code to execute", "warning", 1000] + + # return ["", False, "", "success", 1500] + + +@app.callback( + [ + Output("add-device-dropdown-ace", "options"), + Output("add-device-dropdown-ace", "value"), + ], + [Input("device-add-modal-ace", "is_open")], + prevent_initial_call=True, +) +def fill_device_add_modal_ace(is_open): # python-edit-recipe page + if is_open: + print("fill_device_add_modal_ace") + return list(util.devices_ref.keys()), "" + return [], "" + + +@app.callback( + [ + Output("add-command-device-dropdown-ace", "options"), + Output("add-command-device-dropdown-ace", "value"), + ], + [Input("command-add-modal-ace", "is_open")], + prevent_initial_call=True, +) +def fill_command_device_add_modal_ace(is_open): # python-edit-recipe page + if is_open: + print("fill_command_device_add_modal_ace") + return list(util.devices_ref.keys()), "" + return [], "" + + +@app.callback( + [ + Output("add-command-command-dropdown-ace", "options"), + Output("add-command-command-dropdown-ace", "value"), + ], + [Input("add-command-device-dropdown-ace", "value")], + prevent_initial_call=True, +) +def fill_command_add_modal_ace(device): # python-edit-recipe page + if device is not None and device != "": + print("fill_command_add_modal_ace") + return list(util.devices_ref[device]["commands"].keys()), "" + return [], "" + + +@app.callback( + Output("add-device-editor-ace", "disabled"), + [ + Input("add-device-dropdown-ace", "value"), + Input("device-add-modal-ace", "is_open"), + ], + State("device-add-modal-ace", "is_open"), + prevent_initial_call=True, +) +def enable_add_device_button_ace(value, is_openInp, is_open): # python-edit-recipe page + if value == "" or value is None: + return True + print("enable_add_device_button_ace") + return False + + +@app.callback( + Output("add-command-editor-ace", "disabled"), + [ + Input("add-command-command-dropdown-ace", "value"), + Input("command-add-modal-ace", "is_open"), + ], + State("command-add-modal-ace", "is_open"), + prevent_initial_call=True, +) +def enable_add_command_button_ace( + value, is_openInp, is_open +): # python-edit-recipe page + if value == "" or value is None: + return True + print("enable_add_command_button_ace") + return False + + +@app.callback( + [ + Output("ace-recipe-editor", "value"), + Output("ace-editor-alert", "is_open", allow_duplicate=True), + Output("ace-editor-alert", "children", allow_duplicate=True), Output("ace-editor-alert", "color", allow_duplicate=True), Output("ace-editor-alert", "duration", allow_duplicate=True), ], @@ -708,8 +678,8 @@ def enable_add_command_button_ace(value, is_openInp, is_open): [State("ace-recipe-editor", "value"), State("add-device-dropdown-ace", "value")], prevent_initial_call=True, ) -def add_device_to_recipe_ace(n_clicks, value, device_type): - print('add_device_to_recipe_ace') +def add_device_to_recipe_ace(n_clicks, value, device_type): # python-edit-recipe page + print("add_device_to_recipe_ace") if value == "" or value is None: return ["", True, "No code in editor", "warning", 3000] try: @@ -739,32 +709,234 @@ def add_device_to_recipe_ace(n_clicks, value, device_type): Output("ace-editor-alert", "duration", allow_duplicate=True), ], Input("add-command-editor-ace", "n_clicks"), - [State("ace-recipe-editor", "value"), State("add-command-command-dropdown-ace", "value"), State("add-command-device-dropdown-ace", "value")], + [ + State("ace-recipe-editor", "value"), + State("add-command-command-dropdown-ace", "value"), + State("add-command-device-dropdown-ace", "value"), + ], prevent_initial_call=True, ) -def add_commands_to_recipe_ace(n_clicks, value, command, device_type): - print('add_commands_to_recipe_ace') +def add_commands_to_recipe_ace( + n_clicks, value, command, device_type +): # python-edit-recipe page + print("add_commands_to_recipe_ace") og_value = str(value) if value == "" or value is None: return ["", True, "No code in editor", "warning", 3000] try: value = str(value) - command_line = util.devices_ref[device_type]['commands'][command] + command_line = util.devices_ref[device_type]["commands"][command] import_line = util.devices_ref[device_type]["import_commands"] import_device_line = util.devices_ref[device_type]["import_device"] if import_device_line not in value: - raise Exception('Device (or its import \''+import_device_line+'\') not found in recipe') + raise Exception( + "Device (or its import '" + + import_device_line + + "') not found in recipe" + ) if import_line not in value: value = import_line + "\n" + value if "\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)" not in value: - raise Exception('Code is not in valid format') - value = value.replace("\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)", "\nseq.add_command("+command_line+")\n\n\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)") + raise Exception("Code is not in valid format") + value = value.replace( + "\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)", + "\nseq.add_command(" + + command_line + + ")\n\n\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)", + ) return [str(value), True, "Command added successfully", "success", 3000] except Exception as e: print(e) return [og_value, True, str("Error adding command: " + str(e)), "danger", 6000] +# --------------------------------------------------- +# Execute Recipe Page +# --------------------------------------------------- + + +def kill_execution(): # execute-recipe page + os.kill(os.getpid(), signal.SIGINT) + + +@app.callback( + Output("hidden-div", "children"), + Input("stop-button", "n_clicks"), + prevent_initial_call=True, +) +def stop_execution(n): # execute-recipe page + print("stop_execution") + # kill_execution() + return [] + + +@app.callback( + Output("execute-recipe-output", "children"), + [Input("execute-button", "n_clicks")], + prevent_initial_call=True, +) +def execute_recipe(n_clicks): # execute-recipe page + print("execute_recipe") + invoker.invoking = True + invoker.invoke_commands() + invoker.invoking = False + return ["done"] + + +@app.callback( + Output("console-out2", "children"), + Input("interval1", "n_intervals"), + [State("url", "pathname"), State("show-log-switch", "value")], + prevent_initial_call=True, +) +def update_output(n, url, switch_val): # execute-recipe page + if url == "/execute-recipe" and switch_val: + log_string = "" + log_list = invoker.get_log_messages() + for msg in log_list: + log_string += msg + # log_string += '
' + return html.Pre(log_string) + return "" + + +@app.callback( + Output("console-out2", "children", allow_duplicate=True), + Input("reset-button", "n_clicks"), + prevent_initial_call=True, +) +def reset_console(n): # execute-recipe page + print("reset_console") + invoker.clear_log_file() + # dashLoggerHandler.queue = [] + return [] + + +# --------------------------------------------------- +# Data Page +# --------------------------------------------------- + + +def render_dict(data): # data page + if isinstance(data, dict): + return [ + dbc.Accordion( + [ + dbc.AccordionItem( + [ + html.Div( + render_dict(value), + style={"margin-left": "15px"}, + ) + ], + title=key, + ) + for key, value in data.items() + ] + ) + ] + else: + return html.P(str(data)) + + +@app.callback( + Output("data-output-div2", "children"), + Input("load-data-button", "n_clicks"), + prevent_initial_call=True, +) +def load_data_accordion(n): # data page + if not hasattr(com, "document"): + print("load_data_accordion - no document") + return [] + print("load_data_accordion") + return render_dict(com.document) + + +# --------------------------------------------------- +# Manual Control Recipe Page +# --------------------------------------------------- + + +if __name__ == "__main__": + app.run(debug=True) + + +# @app.callback( +# Output("data-output-div", "children"), +# Input("load-data-button", "n_clicks"), +# prevent_initial_call=True, +# ) +# def load_data(n): +# print('load_data') +# # val = "" +# # docs = mongo.db["recipes"].find() +# # for doc in docs: +# # val += str(doc) +# # val += "

" +# # nval = val. +# return (json.dumps(str(com.document))) + + +# class DashLoggerHandler(logging.StreamHandler): +# def __init__(self): +# logging.StreamHandler.__init__(self) +# self.queue = [] + +# def emit(self, record): +# msg = self.format(record) +# self.queue.append(msg) + + +# logger = logging.getLogger() +# logger.setLevel(logging.DEBUG) +# dashLoggerHandler = DashLoggerHandler() +# logger.addHandler(dashLoggerHandler) + +# logger = logging.getLogger(invoker.log.name) +# logger.setLevel(logging.DEBUG) +# from io import StringIO +# log_capture = StringIO() + +# # Create a stream handler and set its stream to the log_capture object +# stream_handler = logging.StreamHandler(log_capture) +# logger.addHandler(stream_handler) +# log_messages = [] + +# import sys +# from io import StringIO +# stringio = StringIO() +# sys.stdout = stringio +# @app.callback(Output('console-output', 'value'), [Input('update-interval', 'n_intervals')]) +# def show_console_output(n): +# # Retrieve console output from StringIO object +# stringio.seek(0) +# console_output = stringio.read() +# return console_output + +# @app.callback(Output("page-content", "children"), [Input("url", "pathname")]) +# def display_page(pathname): +# print("\n\nrefreshing to " + pathname) +# if pathname == "/": +# return home_layout +# elif pathname == "/edit-recipe": +# return edit_recipe_layout +# elif pathname == "/execute-recipe": +# return execute_recipe_layout +# elif pathname == "/data": +# return data_layout +# else: +# return html.Div("404") + + +# data_list4 = com.device_list + +# dl5 = [] +# for list in data_list4: +# dl5.append(list.__dict__) + +# print(dl5[2]['motor'].__dict__) + + # @app.callback( # Output("commands-accordion", "children"), # Input("add-command-button", "n_clicks"), @@ -833,104 +1005,6 @@ def add_commands_to_recipe_ace(n_clicks, value, command, device_type): # return children -@app.callback( - Output("edit-command-button", "disabled"), - Input("commands-table", "active_cell"), -) -def edit_command_button(table_div_children): - active_cell = table_div_children - # print(active_cell) - # if active_cell is not None and active_cell["column_id"] == "params": - if active_cell is not None: - print('edit_command_button') - return False - else: - return True - - -@app.callback( - Output("edit-device-button", "disabled"), - Input("devices-table", "active_cell"), -) -def edit_device_button(table_div_children): - print('edit_device_button') - active_cell = table_div_children - if active_cell is not None: - return False - else: - return True - - -@app.callback( - Output("commands-table-div", "children"), - [Input("refresh-button2", "n_clicks"), Input("commands-table", "data")], - [State("commands-table-div", "children")], -) -def update_commands_table(n_clicks, data, table): - print('update_commands_table') - command_list = com.command_list.copy() - # print(command_list) - # for command in command_list: - # if isinstance(command, CompositeCommand): - # for sub_command in command._command_list: - # sub_command._receiver = sub_command._receiver._name - # sub_command = sub_command.__dict__ - # else: - # # print(command.__dict__) - # command._receiver = command._receiver._name - command_params = [] - for index, command in enumerate(command_list): - # if isinstance(command, CompositeCommand): - # print(type(command).__name__) - temp_dict_command_params = {"command": type(command[0]).__name__} - temp_dict_command_params.update( - {"params": str(command[0].get_init_args()), "index": index} - ) - command_params.append((temp_dict_command_params)) - # print(command._params) - # else: - # command_params.append(command._params) - # print(command_params) - - # print(command_params) - table_data2 = command_params - # print(com.get_unlooped_command_list().copy()[3].__dict__) - # add_command_accordian(0, to_add=[dbc.AccordionItem("new new", title="new new", item_id="new new")]) - - table = dash_table.DataTable( - id="commands-table", - data=table_data2, - columns=[ - {"name": "Index", "id": "index"}, - {"name": "Command", "id": "command"}, - {"name": "Parameters", "id": "params"}, - ], - style_cell={ - "overflow": "hidden", - "textOverflow": "ellipsis", - "maxWidth": 0, - "textAlign": "left", - "padding": "5px", - }, - style_cell_conditional=[ - {"if": {"column_id": "index"}, "width": "5%"}, - {"if": {"column_id": "command"}, "width": "20%"}, - {"if": {"column_id": "params"}, "width": "70%"}, - ], - # tooltip_data=[ - # { - # column: {"value": str(value), "type": "markdown"} - # for column, value in row.items() - # } - # for row in table_data2 - # ], - # tooltip_duration=None, - # editable = True, - ) - # print("commands table refresh done") - return table - - # @app.callback( # Output("table-container3", "children"), # [Input("refresh-button3", "n_clicks")], @@ -943,15 +1017,3 @@ def update_commands_table(n_clicks, data, table): # columns=[{"name": "Name", "id": "Name"}, {"name": "Value", "id": "Value"}], # ) # return table - -@app.server.errorhandler(Exception) -def handle_exception(e): - # Print the error to the console - print("Callback Error:", str(e)) - # Optionally, you can log the error to a file or perform other error handling actions - - # Return a custom error message to display in the app - return "An error occurred. Please try again later." - -if __name__ == "__main__": - app.run(debug=True) From a7e891600856f96c13154d841bb3631df574c01c Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 10 Jul 2023 18:54:26 -0500 Subject: [PATCH 021/125] added manual-control.py --- pages/manual-control.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 pages/manual-control.py diff --git a/pages/manual-control.py b/pages/manual-control.py new file mode 100644 index 0000000..86bd2e6 --- /dev/null +++ b/pages/manual-control.py @@ -0,0 +1,35 @@ +from dash import Dash, html, dcc, dash_table, Input, Output, callback +import dash_bootstrap_components as dbc +import dash + + +dash.register_page(__name__, "/manual-control") + +layout = html.Div( + [ + html.H1("Manual Control", className="mb-3"), + dbc.Row( + [ + dbc.Col( + [ + dcc.Dropdown( + id="manual-control-device-dropdown", + options=[], + value=None, + ) + ] + ), + dbc.Col( + [ + dcc.Dropdown( + id="manual-control-command-dropdown", + options=[], + value=None, + ) + ] + ), + ], + ), + ], + className="container", +) From bd1bb269dcace8f01da949376d9f05b1b80787c0 Mon Sep 17 00:00:00 2001 From: Piyush Date: Wed, 12 Jul 2023 13:24:15 -0500 Subject: [PATCH 022/125] jul 12 prog (manual control) --- app.py | 290 +++++++++++++++++++++++++++++++++++++++- pages/data.py | 8 +- pages/manual-control.py | 20 ++- util.py | 191 +++++++++++++++++++++++++- 4 files changed, 494 insertions(+), 15 deletions(-) diff --git a/app.py b/app.py index b05fe24..fa4bd0b 100644 --- a/app.py +++ b/app.py @@ -31,7 +31,7 @@ mongo = MongoDBHelper( - "mongodb+srv://ppahuja2:vDkNu2sKR1eDsOmY@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "mongodb+srv://ppahuja2:977d12GoQFtlCSOS@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", "diaogroup", ) @@ -47,7 +47,7 @@ children=[ dbc.NavItem(dbc.NavLink("Home", href="/")), dbc.NavItem(dbc.NavLink("View Recipe", href="/view-recipe")), - dbc.NavItem(dbc.NavLink("Edit Recipe (PY)", href="/python-edit-recipe")), + dbc.NavItem(dbc.NavLink("Edit Recipe", href="/python-edit-recipe")), dbc.NavItem(dbc.NavLink("Execute Recipe", href="/execute-recipe")), dbc.NavItem(dbc.NavLink("Data", href="/data")), dbc.NavItem(dbc.NavLink("Manual Control", href="/manual-control")), @@ -857,6 +857,292 @@ def load_data_accordion(n): # data page # --------------------------------------------------- +@app.callback( + Output("manual-control-device-dropdown", "options"), + Input("interval_5s", "n_intervals"), + State("url", "pathname"), +) +def fill_manual_control_device_dropdown(n, url): # manual-control page + if str(url) == "/manual-control": + print("fill_manual_control_device_dropdown") + return list(util.devices_ref.keys()) + + +@app.callback( + [ + Output("manual-control-command-dropdown", "disabled"), + Output("manual-control-command-dropdown", "options"), + ], + Input("manual-control-device-dropdown", "value"), + State("url", "pathname"), + prevent_initial_call=True, +) +def fill_manual_control_command_dropdown(val, url): # manual-control page + if str(url) == "/manual-control": + print("fill_manual_control_command_dropdown") + if val is None or val == "": + return [True, []] + else: + if util.devices_ref_redundancy[val]["serial"] == False: + return [False, list(util.devices_ref[val]["commands"].keys())] + else: + toRet = list(util.devices_ref[val]["commands"].keys()).copy() + for command in util.devices_ref_redundancy[val]["serial_sequence"]: + if command in toRet: + toRet.remove(command) + return [False, toRet] + + +@app.callback( + [Output("manual-control-device-form", "children")], + Input("manual-control-device-dropdown", "value"), + State("url", "pathname"), +) +def create_manual_control_device_form(value, url): + if str(url) == "/manual-control": + print("create_manual_control_device_form") + if value is None or value == "": + return [[]] + else: + args = util.devices_ref_redundancy[value]["init"]["args"] + toRet = [] + for arg in args: + toRet.append( + dbc.Row( + [ + dbc.Label([arg], html_for=str(value + "+" + arg), width=2), + dbc.Col( + [ + dbc.Input( + id=str(value + "+" + arg), + value=args[arg]["default"], + placeholder=args[arg]["notes"], + ), + ], + width=10, + ), + ], + className="mb-2", + ) + ) + + return [toRet] + + +@app.callback( + [Output("manual-control-command-form", "children")], + [ + Input("manual-control-command-dropdown", "value"), + Input("manual-control-device-dropdown", "value"), + ], + [ + State("url", "pathname"), + State("manual-control-device-form", "children"), + ], +) +def create_manual_control_command_form(command, device, url, device_form): + if str(url) == "/manual-control": + print("create_manual_control_command_form") + if command is None or command == "" or device is None or device == "": + return [[]] + else: + toRet = [] + if util.devices_ref_redundancy[device]["serial"] == True: + seq_toRet = [] + for seq_command in util.devices_ref_redundancy[device][ + "serial_sequence" + ]: + seq_toRet.append(dbc.Row([dbc.Label([seq_command])])) + args = util.devices_ref_redundancy[device]["commands"][seq_command][ + "args" + ] + for arg in args: + seq_toRet.append( + dbc.Row( + [ + dbc.Label( + [arg], + html_for=str(device+"+" + seq_command + "+" + arg), + width=2, + ), + dbc.Col( + [ + dbc.Input( + id=str(device +"+"+ seq_command + "+" + arg), + value=args[arg]["default"], + placeholder=args[arg]["notes"], + ), + ], + width=10, + ), + ], + className="mb-2", + ) + ) + toRet.append(dbc.Row(seq_toRet, className="mb-3")) + toRet.append(dbc.Row([dbc.Label([command])])) + args = util.devices_ref_redundancy[device]["commands"][command]["args"] + com_toRet = [] + for arg in args: + com_toRet.append( + dbc.Row( + [ + dbc.Label( + [arg], + html_for=str(device + command + "+" + arg), + width=2, + ), + dbc.Col( + [ + dbc.Input( + id=str(device + command + "+" + arg), + value=args[arg]["default"], + placeholder=args[arg]["notes"], + ), + ], + width=10, + + ), + ], + className="mb-2", + ) + ) + toRet.append(dbc.Row(com_toRet, className="mb-3")) + return [toRet] + + +@app.callback( + Output("manual-control-device-dropdown", "value"), + Input("manual-control-clear-button", "n_clicks"), + State("url", "pathname"), + prevent_initial_call=True, +) +def manual_control_clear_form(n, url): + if str(url) == "/manual-control": + print("manual_control_clear_form") + return None + + +@app.callback( + Output("manual-control-clear-button", "disabled"), + Input("manual-control-device-dropdown", "value"), + State("url", "pathname"), +) +def manual_control_clear_button(value, url): + if str(url) == "/manual-control": + print("manual_control_clear_button") + if value is None or value == "": + return True + else: + return False + + +@app.callback( + Output("manual-control-execute-button", "disabled"), + Input("manual-control-command-dropdown", "value"), + State("url", "pathname"), +) +def manual_control_execute_button(value, url): + if str(url) == "/manual-control": + print("manual_control_execute_button") + if value is None or value == "": + return True + else: + return False + + +@app.callback( + Output("manual-control-command-dropdown", "className"), + Input("manual-control-execute-button", "n_clicks"), + [ + State('url', 'pathname'), + State("manual-control-command-dropdown", "className"), + State("manual-control-device-dropdown", "value"), + State("manual-control-command-dropdown", "value"), + State("manual-control-device-form", "children"), + State("manual-control-command-form", "children"), + ], + prevent_initial_call=True, +) +def manual_control_execute(n, url, opt, device, command, device_form, command_form): + if str(url) == "/manual-control": + print("manual_control_execute") + code = "" + code += util.devices_ref_redundancy[device]["import_device"] + code += "\n" + code += util.devices_ref_redundancy[device]["import_commands"] + code += "\n" + code += "from command_sequence import CommandSequence\nfrom command_invoker import CommandInvoker\n" + + instantiate_code = "" + instantiate_code += util.devices_ref_redundancy[device]["init"]["obj_name"] + instantiate_code += "(" + for i, arg in enumerate(util.devices_ref_redundancy[device]["init"]["args"]): + if i != 0: + instantiate_code += ", " + instantiate_code += arg + "=" + if util.devices_ref_redundancy[device]["init"]["args"][arg]["type"] == str: + instantiate_code += "'" + instantiate_code += str( + device_form[i]["props"]["children"][1]["props"]["children"][0][ + "props" + ]["value"] + ) + instantiate_code += "'" + else: + instantiate_code += str( + device_form[i]["props"]["children"][1]["props"]["children"][0][ + "props" + ]["value"] + ) + instantiate_code += ")" + code_seq = str(device) + "_seq" + code += code_seq + " = CommandSequence()" + code += "\n" + code += code_seq + ".add_device(" + instantiate_code + ")" + code += "\n" + + + if util.devices_ref_redundancy[device]["serial"] == True: + for i, serial_seq_command in enumerate(util.devices_ref_redundancy[device]['serial_sequence']): + code += code_seq + ".add_command(" + str(serial_seq_command)+"(" + for ii, serial_seq_command_arg in enumerate(util.devices_ref_redundancy[device]['commands'][serial_seq_command]['args']): + if ii != 0: + code += ", " + if serial_seq_command_arg == "receiver": + code += serial_seq_command_arg + "="+str(device)+"_seq.device_by_name['"+str(command_form[0]['props']['children'][(2*ii)+1]['props']['children'][1]['props']['children'][0]['props']['value'])+"']" + elif util.devices_ref_redundancy[device]['commands'][serial_seq_command]['args'][serial_seq_command_arg]['type'] == str: + code += serial_seq_command_arg + "="+"'"+str(command_form[0]['props']['children'][(2*ii)+1]['props']['children'][1]['props']['children'][0]['props']['value'])+"'" + else: + code += serial_seq_command_arg + "="+str(command_form[0]['props']['children'][(2*ii)+1]['props']['children'][1]['props']['children'][0]['props']['value']) + + code += "))\n" + + code += code_seq + ".add_command(" + str(command)+"(" + for ii, seq_command_arg in enumerate(util.devices_ref_redundancy[device]['commands'][command]['args']): + if ii != 0: + code += ", " + if seq_command_arg == "receiver": + code += seq_command_arg + "="+str(device)+"_seq.device_by_name['"+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"']" + elif util.devices_ref_redundancy[device]['commands'][command]['args'][seq_command_arg]['type'] == str: + code += seq_command_arg + "="+"'"+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"'" + else: + code += seq_command_arg + "="+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value']) + + code += "))\n" + + code += str(device)+"_seq_invoker = CommandInvoker("+str(device)+"_seq, False, False, False)\n" + code += str(device)+"_seq_invoker.invoke_commands()\n" + + print("\n"+code + "\n") + try: + exec(code) + except Exception as e: + print(e) + + return opt + + if __name__ == "__main__": app.run(debug=True) diff --git a/pages/data.py b/pages/data.py index 39a94e1..a955cc2 100644 --- a/pages/data.py +++ b/pages/data.py @@ -8,10 +8,12 @@ layout = html.Div( [ html.H1("Data"), - dbc.Button("Load data", id="load-data-button", n_clicks=0), + dbc.Button("Load data", id="load-data-button", n_clicks=0, className="mb-3"), # dcc.Textarea( # id="data-output", readOnly=True, style={"width": "100%", "height": 0} # ), - html.Div(id="data-output-div"), - ] + # html.Div(id="data-output-div"), + html.Div(id="data-output-div2"), + ], + className="container", ) diff --git a/pages/manual-control.py b/pages/manual-control.py index 86bd2e6..a07c526 100644 --- a/pages/manual-control.py +++ b/pages/manual-control.py @@ -8,6 +8,15 @@ layout = html.Div( [ html.H1("Manual Control", className="mb-3"), + dcc.Interval(id="interval_5s", interval=500000, n_intervals=0), + dbc.ButtonGroup( + [ + dbc.Button("Execute", id="manual-control-execute-button", n_clicks=0, disabled= True), + dbc.Button("Clear", id="manual-control-clear-button", n_clicks=0, disabled= True), + ], + className="mb-3", + ), + dbc.Row( [ dbc.Col( @@ -16,7 +25,9 @@ id="manual-control-device-dropdown", options=[], value=None, - ) + className = "mb-3", + ), + dbc.Col([], id="manual-control-device-form"), ] ), dbc.Col( @@ -25,10 +36,13 @@ id="manual-control-command-dropdown", options=[], value=None, - ) + disabled=True, + className = "mb-3", + ), + dbc.Col([], id="manual-control-command-form"), ] ), - ], + ], ), ], className="container", diff --git a/util.py b/util.py index 3ec9ba5..ad82a30 100644 --- a/util.py +++ b/util.py @@ -5,7 +5,7 @@ from devices.newport_esp301 import NewportESP301 # from devices.stellarnet_spectrometer import StellarNetSpectrometer -# from devices.ximea_camera import XimeaCamera +from devices.ximea_camera import XimeaCamera from devices.dummy_heater import DummyHeater from devices.dummy_motor import DummyMotor from devices.linear_stage_150 import LinearStage150 @@ -20,7 +20,7 @@ "MultiStepper1": MultiStepper, "PrinterMotorX": NewportESP301, # "Spectrometer": StellarNetSpectrometer, - # "SampleCamera": XimeaCamera, + "SampleCamera": XimeaCamera, "DummyHeater1": DummyHeater, "DummyHeater2": DummyHeater, "DummyMotor": DummyMotor, @@ -69,20 +69,47 @@ def default(self, obj): return super().default(obj) +heating_stage_ref = { + "obj": HeatingStage, + "import_device": "from devices.heating_stage import HeatingStage", + "import_commands": "from commands.heating_stage_commands import *", + "init": "HeatingStage(name='PrintingStage', port='', baudrate=115200, timeout=0.1, heating_timeout=600.0)", + "commands": { + "HeatingStageConnect": "HeatingStageConnect(receiver= '')", + "HeatingStageInitialize": "HeatingStageInitialize(receiver= '')", + "HeatingStageDeinitialize": "HeatingStageDeinitialize(receiver= '')", + "HeatingStageSetTemperature": "HeatingStageSetTemperature(receiver= '', temperature= 0.0)", + "HeatingStageSetSetPoint": "HeatingStageSetSetPoint(receiver= '', temperature= 0.0)", + }, +} + + devices_ref = { - "PrintingStage": {"obj": HeatingStage}, - "AnnealingStage": {"obj": HeatingStage}, - "MultiStepper": {"obj": MultiStepper}, + "PrintingStage": heating_stage_ref, + "AnnealingStage": heating_stage_ref, + "MultiStepper": { + "obj": MultiStepper, + "import_device": "from devices.multi_stepper import MultiStepper", + "import_commands": "from commands.multi_stepper_commands import *", + "init": "MultiStepper(name='MultiStepper', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", + "commands": { + "MultiStepperConnect": "MultiStepperConnect(receiver= '')", + "MultiStepperInitialize": "MultiStepperInitialize(receiver= '')", + "MultiStepperDeinitialize": "MultiStepperDeinitialize(receiver= '')", + "MultiStepperMoveAbsolute": "MultiStepperMoveAbsolute(receiver= '', stepper_number= 0, position= 0)", + "MultiStepperMoveRelative": "MultiStepperMoveRelative(receiver= '', stepper_number= 0, distance= 0)", + }, + }, "PrinterMotorX": {"obj": NewportESP301}, # "Spectrometer": {"obj": StellarNetSpectrometer}, - # "SampleCamera": {"obj": XimeaCamera}, + "XimeaCamera": {"obj": XimeaCamera}, "DummyHeater": {"obj": DummyHeater}, "DummyMotor": {"obj": DummyMotor}, "LinearStage150": { "obj": LinearStage150, "import_device": "from devices.linear_stage_150 import LinearStage150", "import_commands": "from commands.linear_stage_150_commands import *", - "init": "LinearStage150(name='LinearStage150', port='/dev/cu.URT0', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", + "init": "LinearStage150(name='LinearStage150', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", "commands": { "LinearStage150Connect": "LinearStage150Connect(receiver= '')", "LinearStage150Initialize": "LinearStage150Initialize(receiver= '')", @@ -94,3 +121,153 @@ def default(self, obj): }, }, } + + +devices_ref_redundancy = { + "PrintingStage": heating_stage_ref, + "AnnealingStage": heating_stage_ref, + "MultiStepper": { + "obj": MultiStepper, + "import_device": "from devices.multi_stepper import MultiStepper", + "import_commands": "from commands.multi_stepper_commands import *", + "init": "MultiStepper(name='MultiStepper', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", + "commands": { + "MultiStepperConnect": "MultiStepperConnect(receiver= '')", + "MultiStepperInitialize": "MultiStepperInitialize(receiver= '')", + "MultiStepperDeinitialize": "MultiStepperDeinitialize(receiver= '')", + "MultiStepperMoveAbsolute": "MultiStepperMoveAbsolute(receiver= '', stepper_number= 0, position= 0)", + "MultiStepperMoveRelative": "MultiStepperMoveRelative(receiver= '', stepper_number= 0, distance= 0)", + }, + }, + "PrinterMotorX": {"obj": NewportESP301}, + # "Spectrometer": {"obj": StellarNetSpectrometer}, + "XimeaCamera": {"obj": XimeaCamera}, + "DummyHeater": {"obj": DummyHeater}, + "DummyMotor": {"obj": DummyMotor}, + "LinearStage150": { + "obj": LinearStage150, + "serial": True, + "serial_sequence": ["LinearStage150Connect"], + "import_device": "from devices.linear_stage_150 import LinearStage150", + "import_commands": "from commands.linear_stage_150_commands import *", + "init": { + "default_code": "LinearStage150(name='LinearStage150', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", + "obj_name": "LinearStage150", + "args": { + "name": { + "default": "LinearStage150", + "type": str, + "notes": "Name of the device.", + }, + "port": {"default": "COM", "type": str, "notes": "Port"}, + "baudrate": { + "default": 115200, + "type": int, + "notes": "Baudrate", + }, + "timeout": { + "default": 0.1, + "type": float, + "notes": "Timeout", + }, + "destination": { + "default": 0x50, + "type": int, + "notes": "", + }, + "source": { + "default": 0x01, + "type": int, + "notes": "", + }, + "channel": { + "default": 1, + "type": int, + "notes": "", + }, + }, + }, + "commands": { + "LinearStage150Connect": { + "default_code": "LinearStage150Connect(receiver= '')", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + } + }, + }, + "LinearStage150Initialize": { + "default_code": "LinearStage150Initialize(receiver= '')", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + } + }, + }, + "LinearStage150Deinitialize": { + "default_code": "LinearStage150Deinitialize(receiver= '')", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + } + }, + }, + "LinearStage150EnableMotor": { + "default_code": "LinearStage150EnableMotor(receiver= '')", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + } + }, + }, + "LinearStage150DisableMotor": { + "default_code": "LinearStage150DisableMotor(receiver= '')", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + } + }, + }, + "LinearStage150MoveAbsolute": { + "default_code": "LinearStage150MoveAbsolute(receiver= '', position= 0)", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + }, + "position": { + "default": 0, + "type": int, + "notes": "", + }, + }, + }, + "LinearStage150MoveRelative": { + "default_code": "LinearStage150MoveRelative(receiver= '', distance= 0)", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + }, + "distance": { + "default": 0, + "type": int, + "notes": "", + }, + }, + }, + }, + }, +} From 26967f95e3766432d8588f74a1b9e114adee7a35 Mon Sep 17 00:00:00 2001 From: Piyush Pahuja Date: Wed, 12 Jul 2023 20:24:07 -0500 Subject: [PATCH 023/125] merge prep --- .gitignore | 17 ++++ app.py | 64 +++++++----- dashapp-tut.py | 38 -------- dashapp.py | 249 ----------------------------------------------- data.csv | 2 - e1.json | 0 e1.yaml | 94 ------------------ e1mongo.yaml | 94 ------------------ e2.yaml | 30 ------ e3.yaml | 31 ------ e4.yaml | 110 --------------------- e4mongo.yaml | 110 --------------------- out.json | 1 - project_const.py | 28 ------ requirements.txt | 3 + util.py | 111 ++++++++++++++++++++- 16 files changed, 167 insertions(+), 815 deletions(-) delete mode 100644 dashapp-tut.py delete mode 100644 dashapp.py delete mode 100644 data.csv delete mode 100644 e1.json delete mode 100644 e1.yaml delete mode 100644 e1mongo.yaml delete mode 100644 e2.yaml delete mode 100644 e3.yaml delete mode 100644 e4.yaml delete mode 100644 e4mongo.yaml delete mode 100644 out.json delete mode 100644 project_const.py diff --git a/.gitignore b/.gitignore index 64b2cbc..d0bf092 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,20 @@ dmypy.json cython_debug/ .vscode/ + +temp.py +temp2.py +to_save.yaml +to_load.yaml +out.json +dashapp.py +dashapp-tut.py +data.csv +e1.json +e1.yaml +e1mongo.yaml +e2.yaml +e3.yaml +e4.yaml +e4mongo.yaml +project_const.pybuilder diff --git a/app.py b/app.py index fa4bd0b..db869b7 100644 --- a/app.py +++ b/app.py @@ -598,7 +598,7 @@ def execute_and_save(n, value): # python-edit-recipe page def fill_device_add_modal_ace(is_open): # python-edit-recipe page if is_open: print("fill_device_add_modal_ace") - return list(util.devices_ref.keys()), "" + return list(util.devices_ref_redundancy.keys()), "" return [], "" @@ -613,7 +613,7 @@ def fill_device_add_modal_ace(is_open): # python-edit-recipe page def fill_command_device_add_modal_ace(is_open): # python-edit-recipe page if is_open: print("fill_command_device_add_modal_ace") - return list(util.devices_ref.keys()), "" + return list(util.devices_ref_redundancy.keys()), "" return [], "" @@ -628,7 +628,7 @@ def fill_command_device_add_modal_ace(is_open): # python-edit-recipe page def fill_command_add_modal_ace(device): # python-edit-recipe page if device is not None and device != "": print("fill_command_add_modal_ace") - return list(util.devices_ref[device]["commands"].keys()), "" + return list(util.devices_ref_redundancy[device]["commands"].keys()), "" return [], "" @@ -684,8 +684,8 @@ def add_device_to_recipe_ace(n_clicks, value, device_type): # python-edit-recip return ["", True, "No code in editor", "warning", 3000] try: value = str(value) - import_line = util.devices_ref[device_type]["import_device"] - init_line = util.devices_ref[device_type]["init"] + import_line = util.devices_ref_redundancy[device_type]["import_device"] + init_line = util.devices_ref_redundancy[device_type]["init"]['default_code'] if import_line not in value: value = import_line + "\n" + value value = value.replace( @@ -725,9 +725,9 @@ def add_commands_to_recipe_ace( return ["", True, "No code in editor", "warning", 3000] try: value = str(value) - command_line = util.devices_ref[device_type]["commands"][command] - import_line = util.devices_ref[device_type]["import_commands"] - import_device_line = util.devices_ref[device_type]["import_device"] + command_line = util.devices_ref_redundancy[device_type]["commands"][command]['default_code'] + import_line = util.devices_ref_redundancy[device_type]["import_commands"] + import_device_line = util.devices_ref_redundancy[device_type]["import_device"] if import_device_line not in value: raise Exception( "Device (or its import '" @@ -865,7 +865,7 @@ def load_data_accordion(n): # data page def fill_manual_control_device_dropdown(n, url): # manual-control page if str(url) == "/manual-control": print("fill_manual_control_device_dropdown") - return list(util.devices_ref.keys()) + return list(util.devices_ref_redundancy.keys()) @app.callback( @@ -884,9 +884,9 @@ def fill_manual_control_command_dropdown(val, url): # manual-control page return [True, []] else: if util.devices_ref_redundancy[val]["serial"] == False: - return [False, list(util.devices_ref[val]["commands"].keys())] + return [False, list(util.devices_ref_redundancy[val]["commands"].keys())] else: - toRet = list(util.devices_ref[val]["commands"].keys()).copy() + toRet = list(util.devices_ref_redundancy[val]["commands"].keys()).copy() for command in util.devices_ref_redundancy[val]["serial_sequence"]: if command in toRet: toRet.remove(command) @@ -1102,7 +1102,7 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo code += code_seq + ".add_device(" + instantiate_code + ")" code += "\n" - + if util.devices_ref_redundancy[device]["serial"] == True: for i, serial_seq_command in enumerate(util.devices_ref_redundancy[device]['serial_sequence']): code += code_seq + ".add_command(" + str(serial_seq_command)+"(" @@ -1117,23 +1117,33 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo code += serial_seq_command_arg + "="+str(command_form[0]['props']['children'][(2*ii)+1]['props']['children'][1]['props']['children'][0]['props']['value']) code += "))\n" - - code += code_seq + ".add_command(" + str(command)+"(" - for ii, seq_command_arg in enumerate(util.devices_ref_redundancy[device]['commands'][command]['args']): - if ii != 0: - code += ", " - if seq_command_arg == "receiver": - code += seq_command_arg + "="+str(device)+"_seq.device_by_name['"+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"']" - elif util.devices_ref_redundancy[device]['commands'][command]['args'][seq_command_arg]['type'] == str: - code += seq_command_arg + "="+"'"+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"'" - else: - code += seq_command_arg + "="+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value']) - - code += "))\n" - + code += code_seq + ".add_command(" + str(command)+"(" + for ii, seq_command_arg in enumerate(util.devices_ref_redundancy[device]['commands'][command]['args']): + if ii != 0: + code += ", " + if seq_command_arg == "receiver": + code += seq_command_arg + "="+str(device)+"_seq.device_by_name['"+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"']" + elif util.devices_ref_redundancy[device]['commands'][command]['args'][seq_command_arg]['type'] == str: + code += seq_command_arg + "="+"'"+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"'" + else: + code += seq_command_arg + "="+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value']) + + code += "))\n" + else: + code += code_seq + ".add_command(" + str(command)+"(" + for ii, seq_command_arg in enumerate(util.devices_ref_redundancy[device]['commands'][command]['args']): + if ii != 0: + code += ", " + if seq_command_arg == "receiver": + code += seq_command_arg + "="+str(device)+"_seq.device_by_name['"+str(command_form[1]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"']" + elif util.devices_ref_redundancy[device]['commands'][command]['args'][seq_command_arg]['type'] == str: + code += seq_command_arg + "="+"'"+str(command_form[1]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"'" + else: + code += seq_command_arg + "="+str(command_form[1]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value']) + + code += "))\n" code += str(device)+"_seq_invoker = CommandInvoker("+str(device)+"_seq, False, False, False)\n" code += str(device)+"_seq_invoker.invoke_commands()\n" - print("\n"+code + "\n") try: exec(code) diff --git a/dashapp-tut.py b/dashapp-tut.py deleted file mode 100644 index 262e48d..0000000 --- a/dashapp-tut.py +++ /dev/null @@ -1,38 +0,0 @@ -import dash -from dash import html, Input, Output, State, dcc, dash_table -import pandas as pd -import plotly.express as px -from pymongo import MongoClient -from bson.objectid import ObjectId -from mongodb_helper import MongoDBHelper - - - -# client = MongoClient('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority') -# db = client["diaogroup"] -# collection = db["recipes2"] - -mongo = MongoDBHelper('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority', 'diaogroup') - - -# Define Layout of App -external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] -app = dash.Dash(__name__) - -app.layout = html.Div([ - html.H1('Editor', style={'textAlign': 'center'}), - # interval activated every second or when page refreshed - dcc.Interval(id='interval_db', interval=1000, n_intervals=0), - html.Div(id='mongo-datatable', children=[]), - - html.Div([ - html.Div(id='pie-graph', className='five columns'), - html.Div(id='hist-graph', className='six columns'), - ], className='row'), - dcc.Store(id='changed-cell') -]) - - - -if __name__ == '__main__': - app.run_server(debug=True) \ No newline at end of file diff --git a/dashapp.py b/dashapp.py deleted file mode 100644 index 79bd847..0000000 --- a/dashapp.py +++ /dev/null @@ -1,249 +0,0 @@ -# import dash -# import dash_html_components as html -# import dash_core_components as dcc -# from dash.dependencies import Input, Output, State -# from pymongo import MongoClient - -# # MongoDB connection setup -# client = MongoClient('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority') -# db = client['diaogroup'] -# collection = db['recipes'] - -# app = dash.Dash(__name__) - -# app.layout = html.Div([ -# dcc.Input(id='document-name', type='text', placeholder='Enter document name'), -# dcc.Textarea(id='yaml-editor', style={'width': '100%', 'height': '400px'}), -# html.Button('Save Document', id='save-button', n_clicks=0), -# html.Div(id='save-message'), -# html.Hr(), -# dcc.Dropdown( -# id='document-dropdown', -# options=[], -# value='', -# placeholder='Select a document' -# ), -# html.Div(id='yaml-output') -# ]) - - -# @app.callback(Output('document-dropdown', 'options'), Output('document-dropdown', 'value'), -# [Input('document-dropdown', 'value'), Input('save-button', 'n_clicks')], -# [State('document-name', 'value'), State('yaml-editor', 'value')]) -# def update_document_dropdown(selected_document, save_clicks, document_name, yaml_content): -# # Fetch the list of documents from the MongoDB collection -# documents = collection.find({}, {"_id": 0, "name": 1}) -# options = [{'label': doc['name'], 'value': doc['name']} for doc in documents] - -# if selected_document not in [doc['value'] for doc in options]: -# selected_document = options[0]['value'] - -# if save_clicks > 0: -# # Insert the new document into the collection -# new_document = {'name': document_name, 'content': yaml_content} -# collection.insert_one(new_document) - -# return options, selected_document - - -# @app.callback(Output('yaml-editor', 'value'), Output('yaml-output', 'children'), -# [Input('document-dropdown', 'value')]) -# def update_yaml_editor(selected_document): -# # Fetch the selected document from the MongoDB collection -# document = collection.find_one({'name': selected_document}) - -# if document: -# # Extract the YAML content from the document -# yaml_content = document.get('content', '') - -# # Update the YAML output -# yaml_output = html.Pre(yaml_content) - -# return yaml_content, yaml_output - -# return '', '' - - -# @app.callback(Output('save-message', 'children'), -# [Input('save-button', 'n_clicks')], -# [State('document-name', 'value'), State('yaml-editor', 'value')]) -# def save_document(n_clicks, document_name, yaml_content): -# if n_clicks > 0: -# # Insert the new document into the collection -# new_document = {'name': document_name, 'content': yaml_content} -# collection.insert_one(new_document) -# return html.Div('Document saved successfully.') - -# return '' - - -# if __name__ == '__main__': -# app.run_server(debug=True) - - - - - -import dash -import dash_core_components as dcc -import dash_html_components as html -from dash import dash_table -from dash.dependencies import Input, Output, State -from pymongo import MongoClient -import json -from bson import ObjectId - -# MongoDB connection -client = MongoClient('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority') -db = client["diaogroup"] -collection = db["recipes2"] -# Initialize the Dash app -app = dash.Dash(__name__) - -# Define the layout -app.layout = html.Div( - children=[ - html.H1("CRUD App with MongoDB"), - html.Div( - children=[ - html.Div( - children=[ - html.H3("Create Document"), - dcc.Input(id="create-id-input", type="text", placeholder="Enter document ID"), - dcc.Input(id="create-input", type="text", placeholder="Enter document data"), - html.Button("Create", id="create-button", n_clicks=0), - ], - className="crud-item", - ), - html.Div( - children=[ - html.H3("Existing Documents"), - html.Button("Refresh", id="refresh-button", n_clicks=0), - dash_table.DataTable( - id="documents-table", - columns=[ - {"name": "ID", "id": "identifier"}, - {"name": "Data", "id": "data"}, - ], - data=[], - editable=False, - row_selectable="single", - style_cell={"textAlign": "left"}, - style_data={"whiteSpace": "normal", "height": "auto"}, - ), - ], - className="crud-item", - ), - html.Div( - children=[ - html.H3("Selected Document"), - html.Div(id="selected-document-output"), - html.Button("Read", id="read-button", n_clicks=0), - html.Button("Update", id="update-button", n_clicks=0), - html.Button("Delete", id="delete-button", n_clicks=0), - ], - className="crud-item", - ), - ], - className="crud-container", - ), - ] -) -# Update the documents table with existing documents -@app.callback( - Output("documents-table", "data"), - Output("selected-document-output", "children"), - Input("create-button", "n_clicks"), - Input("read-button", "n_clicks"), - Input("update-button", "n_clicks"), - Input("delete-button", "n_clicks"), - Input("refresh-button", "n_clicks"), - State("create-id-input", "value"), - State("create-input", "value"), - State("documents-table", "selected_rows"), - State("documents-table", "data"), -) -def update_documents_table( - create_n_clicks, - read_n_clicks, - update_n_clicks, - delete_n_clicks, - refresh_n_clicks, - id_input_value, - input_value, - selected_rows, - documents, -): - ctx = dash.callback_context - triggered_button_id = ctx.triggered[0]["prop_id"].split(".")[0] - - if triggered_button_id == "create-button" and create_n_clicks > 0 and id_input_value and input_value: - document = {"identifier": id_input_value, "data": input_value} - collection.insert_one(document) - - if triggered_button_id == "refresh-button" and refresh_n_clicks > 0: - documents = list(collection.find()) - - # Convert ObjectId values to strings - for document in documents: - document["_id"] = str(document["_id"]) - - data = [] - if documents: - data = json.loads(json.dumps(documents)) - - selected_document_output = "" - if selected_rows: - selected_document = documents[selected_rows[0]] - selected_document_output = html.Div( - [ - html.H4("Selected Document"), - html.P(f"ID: {selected_document['identifier']}"), - html.P(f"Data: {selected_document['data']}"), - ] - ) - - if triggered_button_id == "read-button" and read_n_clicks > 0 and selected_rows: - selected_document = documents[selected_rows[0]] - selected_document_output = html.Div( - [ - html.H4("Selected Document"), - html.P(f"ID: {selected_document['identifier']}"), - html.P(f"Data: {selected_document['data']}"), - html.P(f"Data3: {selected_document.get('data3', '')}"), - ] - ) - - if triggered_button_id == "update-button" and update_n_clicks > 0 and selected_rows: - selected_document = documents[selected_rows[0]] - # Implement your update logic here - # For example, update the 'data' field with the new value - # new_data_value = "New Value" - # collection.update_one( - # {"identifier": selected_document["identifier"]}, - # {"$set": {"data": new_data_value}}, - # ) - selected_document_output = html.Div( - [ - html.H4("Selected Document"), - html.P(f"ID: {selected_document['identifier']}"), - html.P(f"Data: {selected_document['data']}"), - ] - ) - - if triggered_button_id == "delete-button" and delete_n_clicks > 0 and selected_rows: - selected_document = documents[selected_rows[0]] - collection.delete_one({"identifier": selected_document["identifier"]}) - documents = list(collection.find()) - # Convert ObjectId values to strings - for document in documents: - document["_id"] = str(document["_id"]) - data = json.loads(json.dumps(documents)) - selected_document_output = "" - - return data, selected_document_output - - - -if __name__ == "__main__": - app.run_server(debug=True) diff --git a/data.csv b/data.csv deleted file mode 100644 index 781f996..0000000 --- a/data.csv +++ /dev/null @@ -1,2 +0,0 @@ -1,2,3,5,5,24 -4,4,5,3,2,ee \ No newline at end of file diff --git a/e1.json b/e1.json deleted file mode 100644 index e69de29..0000000 diff --git a/e1.yaml b/e1.yaml deleted file mode 100644 index b9eec5b..0000000 --- a/e1.yaml +++ /dev/null @@ -1,94 +0,0 @@ -- - &id001 !!python/object:devices.dummy_heater.DummyHeater - _name: heater1 - _is_initialized: false - _heat_rate: 20.0 - min_heat_rate: 1.0 - max_heat_rate: 50.0 - min_temperature: 25.0 - max_temperature: 100.0 - _temperature: 66.70223716282354 - _hardware_interval: 0.05 - - &id002 !!python/object:devices.dummy_motor.DummyMotor - _name: motor1 - _is_initialized: false - motor: !!python/object:devices.dummy_motor_source.DummyMotorSource - _speed: 20.0 - min_speed: 1.0 - max_speed: 50.0 - min_position: 0.0 - max_position: 100.0 - _position: 71.29314197816339 - _hardware_interval: 0.05 -- - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.utility_commands.LoopStartCommand - _params: - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - temperature: 60.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - distance: 20.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - temperature: 25.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - distance: -20.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.utility_commands.LoopEndCommand - _params: - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterDeinitialize - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - reset_init_flag: true - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - reset_init_flag: true - _was_successful: null - _result_message: null -- 3 diff --git a/e1mongo.yaml b/e1mongo.yaml deleted file mode 100644 index b9eec5b..0000000 --- a/e1mongo.yaml +++ /dev/null @@ -1,94 +0,0 @@ -- - &id001 !!python/object:devices.dummy_heater.DummyHeater - _name: heater1 - _is_initialized: false - _heat_rate: 20.0 - min_heat_rate: 1.0 - max_heat_rate: 50.0 - min_temperature: 25.0 - max_temperature: 100.0 - _temperature: 66.70223716282354 - _hardware_interval: 0.05 - - &id002 !!python/object:devices.dummy_motor.DummyMotor - _name: motor1 - _is_initialized: false - motor: !!python/object:devices.dummy_motor_source.DummyMotorSource - _speed: 20.0 - min_speed: 1.0 - max_speed: 50.0 - min_position: 0.0 - max_position: 100.0 - _position: 71.29314197816339 - _hardware_interval: 0.05 -- - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.utility_commands.LoopStartCommand - _params: - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - temperature: 60.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - distance: 20.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - temperature: 25.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - distance: -20.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.utility_commands.LoopEndCommand - _params: - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterDeinitialize - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - reset_init_flag: true - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - reset_init_flag: true - _was_successful: null - _result_message: null -- 3 diff --git a/e2.yaml b/e2.yaml deleted file mode 100644 index 0a8eb9d..0000000 --- a/e2.yaml +++ /dev/null @@ -1,30 +0,0 @@ - _name: LinearStage150 - _is_initialized: false - _port: /dev/cu.URT0 - _baudrate: 115200 - _timeout: 0.1 - ser: - is_open: false - portstr: null - name: null - _port: null - _baudrate: 9600 - _bytesize: 8 - _parity: N - _stopbits: 1 - _timeout: null - _write_timeout: null - _xonxoff: false - _rtscts: false - _dsrdtr: false - _inter_byte_timeout: null - _rs485_mode: null - _rts_state: true - _dtr_state: true - _break_state: false - _exclusive: null - _destination: 80 - _source: 1 - _channel: 1 -- [] -- ALL diff --git a/e3.yaml b/e3.yaml deleted file mode 100644 index b99240b..0000000 --- a/e3.yaml +++ /dev/null @@ -1,31 +0,0 @@ -- - !!python/object:devices.linear_stage_150.LinearStage150 - _name: LinearStage150 - _is_initialized: false - _port: /dev/cu.URT0 - _baudrate: 115200 - _timeout: 0.1 - ser: !!python/object:serial.serialposix.Serial - is_open: false - portstr: null - name: null - _port: null - _baudrate: 9600 - _bytesize: 8 - _parity: N - _stopbits: 1 - _timeout: null - _write_timeout: null - _xonxoff: false - _rtscts: false - _dsrdtr: false - _inter_byte_timeout: null - _rs485_mode: null - _rts_state: true - _dtr_state: true - _break_state: false - _exclusive: null - _destination: 80 - _source: 1 - _channel: 1 -- [] -- ALL diff --git a/e4.yaml b/e4.yaml deleted file mode 100644 index 83fa98f..0000000 --- a/e4.yaml +++ /dev/null @@ -1,110 +0,0 @@ -- - &id001 !!python/object:devices.dummy_heater.DummyHeater - _name: heater1 - _is_initialized: false - _heat_rate: 20.0 - min_heat_rate: 1.0 - max_heat_rate: 50.0 - min_temperature: 25.0 - max_temperature: 100.0 - _temperature: 95.71927572168929 - _hardware_interval: 0.05 - - &id002 !!python/object:devices.dummy_motor.DummyMotor - _name: motor1 - _is_initialized: false - motor: !!python/object:devices.dummy_motor_source.DummyMotorSource - _speed: 20.0 - min_speed: 1.0 - max_speed: 50.0 - min_position: 0.0 - max_position: 100.0 - _position: 37.44120879993549 - _hardware_interval: 0.05 - - &id003 !!python/object:devices.dummy_motor.DummyMotor - _name: motor2 - _is_initialized: false - motor: !!python/object:devices.dummy_motor_source.DummyMotorSource - _speed: 20.0 - min_speed: 1.0 - max_speed: 50.0 - min_position: 0.0 - max_position: 100.0 - _position: 82.48283286198111 - _hardware_interval: 0.05 -- - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize - _receiver: *id003 - _params: - receiver_name: motor2 - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - temperature: 60.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorSetSpeed - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 3.0 - speed: 10.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveAbsolute - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - position: 30.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - distance: -20.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterDeinitialize - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - reset_init_flag: true - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - reset_init_flag: true - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize - _receiver: *id003 - _params: - receiver_name: motor2 - delay: 0.0 - reset_init_flag: true - _was_successful: null - _result_message: null -- ALL diff --git a/e4mongo.yaml b/e4mongo.yaml deleted file mode 100644 index 83fa98f..0000000 --- a/e4mongo.yaml +++ /dev/null @@ -1,110 +0,0 @@ -- - &id001 !!python/object:devices.dummy_heater.DummyHeater - _name: heater1 - _is_initialized: false - _heat_rate: 20.0 - min_heat_rate: 1.0 - max_heat_rate: 50.0 - min_temperature: 25.0 - max_temperature: 100.0 - _temperature: 95.71927572168929 - _hardware_interval: 0.05 - - &id002 !!python/object:devices.dummy_motor.DummyMotor - _name: motor1 - _is_initialized: false - motor: !!python/object:devices.dummy_motor_source.DummyMotorSource - _speed: 20.0 - min_speed: 1.0 - max_speed: 50.0 - min_position: 0.0 - max_position: 100.0 - _position: 37.44120879993549 - _hardware_interval: 0.05 - - &id003 !!python/object:devices.dummy_motor.DummyMotor - _name: motor2 - _is_initialized: false - motor: !!python/object:devices.dummy_motor_source.DummyMotorSource - _speed: 20.0 - min_speed: 1.0 - max_speed: 50.0 - min_position: 0.0 - max_position: 100.0 - _position: 82.48283286198111 - _hardware_interval: 0.05 -- - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize - _receiver: *id003 - _params: - receiver_name: motor2 - delay: 0.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterSetTemp - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - temperature: 60.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorSetSpeed - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 3.0 - speed: 10.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveAbsolute - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - position: 30.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - distance: -20.0 - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterDeinitialize - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - reset_init_flag: true - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - reset_init_flag: true - _was_successful: null - _result_message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize - _receiver: *id003 - _params: - receiver_name: motor2 - delay: 0.0 - reset_init_flag: true - _was_successful: null - _result_message: null -- ALL diff --git a/out.json b/out.json deleted file mode 100644 index 51d218b..0000000 --- a/out.json +++ /dev/null @@ -1 +0,0 @@ - {'index': 0, 'code': 121, 'errmsg': 'Document failed validation', 'errInfo': {'failingDocumentId': ObjectId('6499b75147bea3ddc43d3a44'), 'details': {'operatorName': '$jsonSchema', 'title': 'Solution Object Validation', 'schemaRulesNotSatisfied': [{'operatorName': 'properties', 'propertiesNotSatisfied': [{'propertyName': 'metadata', 'title': 'Metadata Object Validation', 'details': [{'operatorName': 'properties', 'propertiesNotSatisfied': [{'propertyName': 'concentration', 'description': "'concentration' must be a double and is required", 'details': [{'operatorName': 'bsonType', 'specifiedAs': {'bsonType': 'double'}, 'reason': 'type did not match', 'consideredValue': 'float', 'consideredType': 'string'}]}]}]}, {'propertyName': 'result', 'title': 'Result Object Validation', 'details': [{'operatorName': 'properties', 'propertiesNotSatisfied': [{'propertyName': 'uv_vis', 'description': "'uv_vis' must be an objectId and is required", 'details': [{'operatorName': 'bsonType', 'specifiedAs': {'bsonType': 'objectId'}, 'reason': 'type did not match', 'consideredValue': 'object_id', 'consideredType': 'string'}]}, {'propertyName': 't80', 'description': "'t80' must be a double and is required", 'details': [{'operatorName': 'bsonType', 'specifiedAs': {'bsonType': 'double'}, 'reason': 'type did not match', 'consideredValue': 'float', 'consideredType': 'string'}]}]}]}]}]}}} \ No newline at end of file diff --git a/project_const.py b/project_const.py deleted file mode 100644 index 7034283..0000000 --- a/project_const.py +++ /dev/null @@ -1,28 +0,0 @@ -from command_sequence import CommandSequence -from command_invoker import CommandInvoker -from commands.command import Command -from commands.utility_commands import LoopStartCommand, LoopEndCommand -from devices.heating_stage import HeatingStage -from devices.multi_stepper import MultiStepper -from devices.newport_esp301 import NewportESP301 -# from devices.stellarnet_spectrometer import StellarNetSpectrometer -# from devices.ximea_camera import XimeaCamera -from devices.dummy_heater import DummyHeater -from devices.dummy_motor import DummyMotor - - -named_devices = { - "PrintingStage": HeatingStage, - "AnnealingStage": HeatingStage, - "MultiStepper1": MultiStepper, - "PrinterMotorX": NewportESP301, - # "Spectrometer": StellarNetSpectrometer, - # "SampleCamera": XimeaCamera, - "DummyHeater": DummyHeater, - "DummyHeater1": DummyHeater, - "DummyHeater2": DummyHeater, - "DummyMotor1": DummyMotor, - "DummyMotor2": DummyMotor, - } -command_directory = "commands/" -approved_devices = list(named_devices.keys()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 346f333..5709580 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,3 +62,6 @@ urllib3==2.0.3 wcwidth==0.2.6 Werkzeug==2.2.3 yarl==1.9.2 +dash-ace +ximea-py +certifi \ No newline at end of file diff --git a/util.py b/util.py index ad82a30..852a9da 100644 --- a/util.py +++ b/util.py @@ -143,7 +143,116 @@ def default(self, obj): # "Spectrometer": {"obj": StellarNetSpectrometer}, "XimeaCamera": {"obj": XimeaCamera}, "DummyHeater": {"obj": DummyHeater}, - "DummyMotor": {"obj": DummyMotor}, + "DummyMotor": { + "obj": DummyMotor, + "serial": True, + "serial_sequence":["DummyMotorInitialize"], + "import_device": "from devices.dummy_motor import DummyMotor", + "import_commands": "from commands.dummy_motor_commands import *", + "init": { + "default_code": "DummyMotor(name='DummyMotor', speed=20.0)", + "obj_name": "DummyMotor", + "args": { + "name": { + "default": "DummyMotor", + "type": str, + "notes": "Name of the device.", + }, + "speed": { + "default": 20.0, + "type": float, + "notes": "Speed of the motor.", + }, + }, + }, + "commands": { + "DummyMotorInitialize": { + "default_code": "DummyMotorInitialize(receiver= '')", + "args": { + "receiver": { + "default": "DummyMotor", + "type": str, + "notes": "Name of the device.", + } + }, + }, + "DummyMotorDeinitialize": { + "default_code": "DummyMotorDeinitialize(receiver= '')", + "args": { + "receiver": { + "default": "DummyMotor", + "type": str, + "notes": "Name of the device.", + } + }, + }, + "DummyMotorSetSpeed": { + "default_code": "DummyMotorSetSpeed(receiver= '', speed= 0.0)", + "args": { + "receiver": { + "default": "DummyMotor", + "type": str, + "notes": "Name of the device.", + }, + "speed": { + "default": 0.0, + "type": float, + "notes": "Speed of the motor.", + }, + }, + }, + "DummyMotorMoveAbsolute": { + "default_code": "DummyMotorMoveAbsolute(receiver= '', position= 0)", + "args": { + "receiver": { + "default": "DummyMotor", + "type": str, + "notes": "Name of the device.", + }, + "position": { + "default": 0, + "type": float, + "notes": "Position to move to.", + }, + }, + }, + "DummyMotorMoveRelative": { + "default_code": "DummyMotorMoveRelative(receiver= '', distance= 0)", + "args": { + "receiver": { + "default": "DummyMotor", + "type": str, + "notes": "Name of the device.", + }, + "distance": { + "default": 0, + "type": float, + "notes": "Distance to move.", + }, + }, + }, + "DummyMotorMoveSpeedAbsolute": { + "default_code": "DummyMotorMoveSpeedAbsolute(receiver= '', position= 0.0, speed= 0.0)", + "args": { + "receiver": { + "default": "DummyMotor", + "type": str, + "notes": "Name of the device.", + }, + "position": { + "default": 0.0, + "type": float, + "notes": "Position to move to.", + }, + "speed": { + "default": 0.0, + "type": float, + "notes": "Speed of the motor.", + }, + }, + } + }, + }, "LinearStage150": { "obj": LinearStage150, "serial": True, From a999ffd573870469d80c6a2e5e8cbcd92557a4c2 Mon Sep 17 00:00:00 2001 From: Piyush Pahuja Date: Wed, 12 Jul 2023 20:47:23 -0500 Subject: [PATCH 024/125] heating stage updated for devices_ref_redundancy --- util.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 91 insertions(+), 8 deletions(-) diff --git a/util.py b/util.py index 852a9da..ba7ed7f 100644 --- a/util.py +++ b/util.py @@ -71,15 +71,98 @@ def default(self, obj): heating_stage_ref = { "obj": HeatingStage, + "serial": True, + "serial_sequence": ["HeatingStageConnect", "HeatingStageInitialize"], "import_device": "from devices.heating_stage import HeatingStage", "import_commands": "from commands.heating_stage_commands import *", - "init": "HeatingStage(name='PrintingStage', port='', baudrate=115200, timeout=0.1, heating_timeout=600.0)", + "init": { + "default_code": "HeatingStage(name='Stage', port='', baudrate=115200, timeout=0.1, heating_timeout=600.0)", + "obj_name": "HeatingStage", + "args": { + "name": { + "default": "Stage", + "type": "str", + "notes": "Name of the device", + }, + "port": {"default": "COM", "type": str, "notes": "Port"}, + "baudrate": { + "default": 115200, + "type": int, + "notes": "Baudrate", + }, + "timeout": { + "default": 0.1, + "type": float, + "notes": "Timeout", + }, + "heating_timeout": { + "default": 600.0, + "type": float, + "notes": "Heating timeout", + }, + }, + }, "commands": { - "HeatingStageConnect": "HeatingStageConnect(receiver= '')", - "HeatingStageInitialize": "HeatingStageInitialize(receiver= '')", - "HeatingStageDeinitialize": "HeatingStageDeinitialize(receiver= '')", - "HeatingStageSetTemperature": "HeatingStageSetTemperature(receiver= '', temperature= 0.0)", - "HeatingStageSetSetPoint": "HeatingStageSetSetPoint(receiver= '', temperature= 0.0)", + "HeatingStageConnect": { + "default_code": "HeatingStageConnect(receiver= '')", + "args": { + "receiver": { + "default": "Stage", + "type": str, + "notes": "Name of the device", + } + }, + }, + "HeatingStageInitialize": { + "default_code": "HeatingStageInitialize(receiver= '')", + "args": { + "receiver": { + "default": "Stage", + "type": str, + "notes": "Name of the device", + } + }, + }, + "HeatingStageDeinitialize": { + "default_code": "HeatingStageDeinitialize(receiver= '')", + "args": { + "receiver": { + "default": "Stage", + "type": str, + "notes": "Name of the device", + } + }, + }, + "HeatingStageSetTemperature": { + "default_code": "HeatingStageSetTemperature(receiver= '', temperature= 0.0)", + "args": { + "receiver": { + "default": "Stage", + "type": str, + "notes": "Name of the device", + }, + "temperature": { + "default": 0.0, + "type": float, + "notes": "Temperature", + }, + }, + }, + "HeatingStageSetSetPoint": { + "default_code": "HeatingStageSetSetPoint(receiver= '', temperature= 0.0)", + "args": { + "receiver": { + "default": "Stage", + "type": str, + "notes": "Name of the device", + }, + "temperature": { + "default": 0.0, + "type": float, + "notes": "Temperature", + }, + }, + }, }, } @@ -146,7 +229,7 @@ def default(self, obj): "DummyMotor": { "obj": DummyMotor, "serial": True, - "serial_sequence":["DummyMotorInitialize"], + "serial_sequence": ["DummyMotorInitialize"], "import_device": "from devices.dummy_motor import DummyMotor", "import_commands": "from commands.dummy_motor_commands import *", "init": { @@ -250,7 +333,7 @@ def default(self, obj): "notes": "Speed of the motor.", }, }, - } + }, }, }, "LinearStage150": { From 75afc0a114f036e094b8c633e2465e03beab8208 Mon Sep 17 00:00:00 2001 From: Piyush Pahuja Date: Wed, 12 Jul 2023 20:55:57 -0500 Subject: [PATCH 025/125] partially updated multi stepper util definition --- util.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/util.py b/util.py index ba7ed7f..b169596 100644 --- a/util.py +++ b/util.py @@ -12,6 +12,7 @@ from devices.device import Device, MiscDeviceClass import json import numpy as np +from typing import Tuple, Union named_devices = { @@ -211,10 +212,47 @@ def default(self, obj): "AnnealingStage": heating_stage_ref, "MultiStepper": { "obj": MultiStepper, + "serial": True, + "serial_sequence": ["MultiStepperConnect", "MultiStepperInitialize"], "import_device": "from devices.multi_stepper import MultiStepper", "import_commands": "from commands.multi_stepper_commands import *", - "init": "MultiStepper(name='MultiStepper', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", - "commands": { + "init": { + "default_code": "MultiStepper(name='MultiStepper', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", + "obj_name": "MultiStepper", + "args": { + "name": { + "default": "MultiStepper", + "type": str, + "notes": "Name of the device", + }, + "port": { + "default": "COM", + "type": str, + "notes": "Port of the device", + }, + "baudrate": { + "default": 115200, + "type": int, + "notes": "Baudrate of the device", + }, + "timeout": { + "default": 1.0, + "type": float, + "notes": "Timeout of the device", + }, + "stepper_list":{ + "default": (1,), + "type": Tuple[int, ...], + "notes": "List of stepper numbers", + }, + "move_timeout": { + "default": 30.0, + "type": float, + "notes": "Timeout for move commands", + }, + } + }, + "commands": { #TODO update these to match redundancy changes "MultiStepperConnect": "MultiStepperConnect(receiver= '')", "MultiStepperInitialize": "MultiStepperInitialize(receiver= '')", "MultiStepperDeinitialize": "MultiStepperDeinitialize(receiver= '')", From 2535e35016837eff0da6589da092c703c382c116 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 13 Jul 2023 10:55:55 -0500 Subject: [PATCH 026/125] added database browser --- .gitignore | 3 ++- app.py | 42 ++++++++++++++++++++++++++++++++++--- pages/data.py | 2 +- pages/database.py | 38 +++++++++++++++++++++++++++++++++ pages/home.py | 2 +- pages/manual-control.py | 4 ++-- pages/python-edit-recipe.py | 2 +- 7 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 pages/database.py diff --git a/.gitignore b/.gitignore index d0bf092..9a9afc3 100644 --- a/.gitignore +++ b/.gitignore @@ -161,4 +161,5 @@ e2.yaml e3.yaml e4.yaml e4mongo.yaml -project_const.pybuilder +project_const.py +pw.py diff --git a/app.py b/app.py index db869b7..0f240e4 100644 --- a/app.py +++ b/app.py @@ -12,6 +12,7 @@ import dash_bootstrap_components as dbc import os, signal import inspect +from pw import mongo_username, mongo_password try: import serial.tools.list_ports @@ -31,7 +32,7 @@ mongo = MongoDBHelper( - "mongodb+srv://ppahuja2:977d12GoQFtlCSOS@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "mongodb+srv://"+mongo_username+":"+mongo_password+"@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", "diaogroup", ) @@ -49,8 +50,9 @@ dbc.NavItem(dbc.NavLink("View Recipe", href="/view-recipe")), dbc.NavItem(dbc.NavLink("Edit Recipe", href="/python-edit-recipe")), dbc.NavItem(dbc.NavLink("Execute Recipe", href="/execute-recipe")), - dbc.NavItem(dbc.NavLink("Data", href="/data")), dbc.NavItem(dbc.NavLink("Manual Control", href="/manual-control")), + dbc.NavItem(dbc.NavLink("Document", href="/data")), + dbc.NavItem(dbc.NavLink("Database", href="/database")), ], brand="AAMP", brand_href="/", @@ -859,7 +861,7 @@ def load_data_accordion(n): # data page @app.callback( Output("manual-control-device-dropdown", "options"), - Input("interval_5s", "n_intervals"), + Input("interval-manual-control", "n_intervals"), State("url", "pathname"), ) def fill_manual_control_device_dropdown(n, url): # manual-control page @@ -1153,6 +1155,40 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo return opt +#--------------------------------------------------------------- +# Database page +#--------------------------------------------------------------- + +@app.callback( + Output("database-collection-dropdown", "options"), + Input("interval-database", "n_intervals"), + State("url", "pathname"), +) +def fill_database_collection_dropdown(n, url): # database page + if str(url) == "/database": + print("fill_database_collection_dropdown") + return mongo.db.list_collection_names() + + +@app.callback( + [Output('database-document-dropdown', 'disabled'),Output('database-document-dropdown', 'options')], + Input('database-collection-dropdown', 'value'), + State("url", "pathname"), + prevent_initial_call=True, +) +def fill_database_document_dropdown(collection, url): + if str(url) == "/database": + if collection is not None and collection != "": + print("fill_database_document_dropdown") + docs = list(mongo.db[collection].find({})) + toRet = [] + for doc in docs: + toRet.append(str(doc['_id'])) + return False, toRet + else: + return True, [] + + if __name__ == "__main__": app.run(debug=True) diff --git a/pages/data.py b/pages/data.py index a955cc2..c85660f 100644 --- a/pages/data.py +++ b/pages/data.py @@ -3,7 +3,7 @@ import dash -dash.register_page(__name__, "/data") +dash.register_page(__name__, path="/data", title='Recipe Document', name='Recipe Document') layout = html.Div( [ diff --git a/pages/database.py b/pages/database.py new file mode 100644 index 0000000..623168e --- /dev/null +++ b/pages/database.py @@ -0,0 +1,38 @@ +from dash import Dash, html, dcc, dash_table, callback, Input, Output, State +import dash_bootstrap_components as dbc +import dash + +dash.register_page(__name__, path="/database", title="Database Browser", name="Database Browser") + +layout = html.Div( + [ + html.H1("Database Browser", className="mb-3"), + dcc.Interval(id="interval-database", interval=500000, n_intervals=0), + dbc.Row( + [ + dbc.Col( + [ + dcc.Dropdown( + id="database-collection-dropdown", + options=[], + value=None, + className="mb-3", + ), + ] + ), + dbc.Col( + [ + dcc.Dropdown( + id="database-document-dropdown", + options=[], + value=None, + className="mb-3", + disabled=True, + ) + ] + ) + ] + ), + ], + className="container", +) diff --git a/pages/home.py b/pages/home.py index c5e1dd1..e9b6b2a 100644 --- a/pages/home.py +++ b/pages/home.py @@ -81,7 +81,7 @@ {"name": "File Name", "id": "file_name"}, {"name": "Posix Compatible", "id": "posix_friendly"}, {"name": "Viewer Compatible", "id": "dash_friendly"}, - {"name": "Python Code", "id": "python_code"}, + {"name": "Python Code Available", "id": "python_code"}, ], data=[], style_table={"width": "100%"}, diff --git a/pages/manual-control.py b/pages/manual-control.py index a07c526..dc3d3b2 100644 --- a/pages/manual-control.py +++ b/pages/manual-control.py @@ -3,12 +3,12 @@ import dash -dash.register_page(__name__, "/manual-control") +dash.register_page(__name__, path="/manual-control", title="Manual Control", name="Manual Control") layout = html.Div( [ html.H1("Manual Control", className="mb-3"), - dcc.Interval(id="interval_5s", interval=500000, n_intervals=0), + dcc.Interval(id="interval-manual-control", interval=500000, n_intervals=0), dbc.ButtonGroup( [ dbc.Button("Execute", id="manual-control-execute-button", n_clicks=0, disabled= True), diff --git a/pages/python-edit-recipe.py b/pages/python-edit-recipe.py index f865f4b..8c5adea 100644 --- a/pages/python-edit-recipe.py +++ b/pages/python-edit-recipe.py @@ -3,7 +3,7 @@ import dash import dash_ace -dash.register_page(__name__, "/python-edit-recipe") +dash.register_page(__name__, path="/python-edit-recipe", title="Edit Recipe", name="Edit Recipe") layout = html.Div( [ From 75e09434caa6196721f3de179ff40320a5990f5e Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 13 Jul 2023 12:39:13 -0500 Subject: [PATCH 027/125] updated database --- app.py | 302 ++++++++++++++++++++++++++++++++++++++-------- pages/database.py | 25 +++- 2 files changed, 275 insertions(+), 52 deletions(-) diff --git a/app.py b/app.py index 0f240e4..e8f15c0 100644 --- a/app.py +++ b/app.py @@ -13,6 +13,7 @@ import os, signal import inspect from pw import mongo_username, mongo_password +from bson.objectid import ObjectId try: import serial.tools.list_ports @@ -32,7 +33,11 @@ mongo = MongoDBHelper( - "mongodb+srv://"+mongo_username+":"+mongo_password+"@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "mongodb+srv://" + + mongo_username + + ":" + + mongo_password + + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", "diaogroup", ) @@ -687,7 +692,7 @@ def add_device_to_recipe_ace(n_clicks, value, device_type): # python-edit-recip try: value = str(value) import_line = util.devices_ref_redundancy[device_type]["import_device"] - init_line = util.devices_ref_redundancy[device_type]["init"]['default_code'] + init_line = util.devices_ref_redundancy[device_type]["init"]["default_code"] if import_line not in value: value = import_line + "\n" + value value = value.replace( @@ -727,7 +732,9 @@ def add_commands_to_recipe_ace( return ["", True, "No code in editor", "warning", 3000] try: value = str(value) - command_line = util.devices_ref_redundancy[device_type]["commands"][command]['default_code'] + command_line = util.devices_ref_redundancy[device_type]["commands"][command][ + "default_code" + ] import_line = util.devices_ref_redundancy[device_type]["import_commands"] import_device_line = util.devices_ref_redundancy[device_type]["import_device"] if import_device_line not in value: @@ -819,7 +826,7 @@ def reset_console(n): # execute-recipe page # --------------------------------------------------- -def render_dict(data): # data page +def render_dict(data): # data (and maybe database) page if isinstance(data, dict): return [ dbc.Accordion( @@ -886,7 +893,10 @@ def fill_manual_control_command_dropdown(val, url): # manual-control page return [True, []] else: if util.devices_ref_redundancy[val]["serial"] == False: - return [False, list(util.devices_ref_redundancy[val]["commands"].keys())] + return [ + False, + list(util.devices_ref_redundancy[val]["commands"].keys()), + ] else: toRet = list(util.devices_ref_redundancy[val]["commands"].keys()).copy() for command in util.devices_ref_redundancy[val]["serial_sequence"]: @@ -964,13 +974,21 @@ def create_manual_control_command_form(command, device, url, device_form): [ dbc.Label( [arg], - html_for=str(device+"+" + seq_command + "+" + arg), + html_for=str( + device + "+" + seq_command + "+" + arg + ), width=2, ), dbc.Col( [ dbc.Input( - id=str(device +"+"+ seq_command + "+" + arg), + id=str( + device + + "+" + + seq_command + + "+" + + arg + ), value=args[arg]["default"], placeholder=args[arg]["notes"], ), @@ -1003,7 +1021,6 @@ def create_manual_control_command_form(command, device, url, device_form): ), ], width=10, - ), ], className="mb-2", @@ -1057,7 +1074,7 @@ def manual_control_execute_button(value, url): Output("manual-control-command-dropdown", "className"), Input("manual-control-execute-button", "n_clicks"), [ - State('url', 'pathname'), + State("url", "pathname"), State("manual-control-command-dropdown", "className"), State("manual-control-device-dropdown", "value"), State("manual-control-command-dropdown", "value"), @@ -1098,97 +1115,284 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo ]["value"] ) instantiate_code += ")" - code_seq = str(device) + "_seq" + code_seq = str(device) + "_seq" code += code_seq + " = CommandSequence()" code += "\n" code += code_seq + ".add_device(" + instantiate_code + ")" code += "\n" - if util.devices_ref_redundancy[device]["serial"] == True: - for i, serial_seq_command in enumerate(util.devices_ref_redundancy[device]['serial_sequence']): - code += code_seq + ".add_command(" + str(serial_seq_command)+"(" - for ii, serial_seq_command_arg in enumerate(util.devices_ref_redundancy[device]['commands'][serial_seq_command]['args']): + for i, serial_seq_command in enumerate( + util.devices_ref_redundancy[device]["serial_sequence"] + ): + code += code_seq + ".add_command(" + str(serial_seq_command) + "(" + for ii, serial_seq_command_arg in enumerate( + util.devices_ref_redundancy[device]["commands"][serial_seq_command][ + "args" + ] + ): if ii != 0: code += ", " if serial_seq_command_arg == "receiver": - code += serial_seq_command_arg + "="+str(device)+"_seq.device_by_name['"+str(command_form[0]['props']['children'][(2*ii)+1]['props']['children'][1]['props']['children'][0]['props']['value'])+"']" - elif util.devices_ref_redundancy[device]['commands'][serial_seq_command]['args'][serial_seq_command_arg]['type'] == str: - code += serial_seq_command_arg + "="+"'"+str(command_form[0]['props']['children'][(2*ii)+1]['props']['children'][1]['props']['children'][0]['props']['value'])+"'" + code += ( + serial_seq_command_arg + + "=" + + str(device) + + "_seq.device_by_name['" + + str( + command_form[0]["props"]["children"][(2 * ii) + 1][ + "props" + ]["children"][1]["props"]["children"][0]["props"][ + "value" + ] + ) + + "']" + ) + elif ( + util.devices_ref_redundancy[device]["commands"][ + serial_seq_command + ]["args"][serial_seq_command_arg]["type"] + == str + ): + code += ( + serial_seq_command_arg + + "=" + + "'" + + str( + command_form[0]["props"]["children"][(2 * ii) + 1][ + "props" + ]["children"][1]["props"]["children"][0]["props"][ + "value" + ] + ) + + "'" + ) else: - code += serial_seq_command_arg + "="+str(command_form[0]['props']['children'][(2*ii)+1]['props']['children'][1]['props']['children'][0]['props']['value']) - + code += ( + serial_seq_command_arg + + "=" + + str( + command_form[0]["props"]["children"][(2 * ii) + 1][ + "props" + ]["children"][1]["props"]["children"][0]["props"][ + "value" + ] + ) + ) + code += "))\n" - code += code_seq + ".add_command(" + str(command)+"(" - for ii, seq_command_arg in enumerate(util.devices_ref_redundancy[device]['commands'][command]['args']): + code += code_seq + ".add_command(" + str(command) + "(" + for ii, seq_command_arg in enumerate( + util.devices_ref_redundancy[device]["commands"][command]["args"] + ): if ii != 0: code += ", " if seq_command_arg == "receiver": - code += seq_command_arg + "="+str(device)+"_seq.device_by_name['"+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"']" - elif util.devices_ref_redundancy[device]['commands'][command]['args'][seq_command_arg]['type'] == str: - code += seq_command_arg + "="+"'"+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"'" + code += ( + seq_command_arg + + "=" + + str(device) + + "_seq.device_by_name['" + + str( + command_form[2]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] + ) + + "']" + ) + elif ( + util.devices_ref_redundancy[device]["commands"][command]["args"][ + seq_command_arg + ]["type"] + == str + ): + code += ( + seq_command_arg + + "=" + + "'" + + str( + command_form[2]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] + ) + + "'" + ) else: - code += seq_command_arg + "="+str(command_form[2]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value']) - + code += ( + seq_command_arg + + "=" + + str( + command_form[2]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] + ) + ) + code += "))\n" else: - code += code_seq + ".add_command(" + str(command)+"(" - for ii, seq_command_arg in enumerate(util.devices_ref_redundancy[device]['commands'][command]['args']): + code += code_seq + ".add_command(" + str(command) + "(" + for ii, seq_command_arg in enumerate( + util.devices_ref_redundancy[device]["commands"][command]["args"] + ): if ii != 0: code += ", " if seq_command_arg == "receiver": - code += seq_command_arg + "="+str(device)+"_seq.device_by_name['"+str(command_form[1]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"']" - elif util.devices_ref_redundancy[device]['commands'][command]['args'][seq_command_arg]['type'] == str: - code += seq_command_arg + "="+"'"+str(command_form[1]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value'])+"'" + code += ( + seq_command_arg + + "=" + + str(device) + + "_seq.device_by_name['" + + str( + command_form[1]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] + ) + + "']" + ) + elif ( + util.devices_ref_redundancy[device]["commands"][command]["args"][ + seq_command_arg + ]["type"] + == str + ): + code += ( + seq_command_arg + + "=" + + "'" + + str( + command_form[1]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] + ) + + "'" + ) else: - code += seq_command_arg + "="+str(command_form[1]['props']['children'][ii]['props']['children'][1]['props']['children'][0]['props']['value']) - + code += ( + seq_command_arg + + "=" + + str( + command_form[1]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] + ) + ) + code += "))\n" - code += str(device)+"_seq_invoker = CommandInvoker("+str(device)+"_seq, False, False, False)\n" - code += str(device)+"_seq_invoker.invoke_commands()\n" - print("\n"+code + "\n") + code += ( + str(device) + + "_seq_invoker = CommandInvoker(" + + str(device) + + "_seq, False, False, False)\n" + ) + code += str(device) + "_seq_invoker.invoke_commands()\n" + print("\n" + code + "\n") try: exec(code) except Exception as e: - print(e) - + print(e) + return opt -#--------------------------------------------------------------- +# --------------------------------------------------------------- # Database page -#--------------------------------------------------------------- +# --------------------------------------------------------------- + @app.callback( - Output("database-collection-dropdown", "options"), + Output("database-db-dropdown", "options"), Input("interval-database", "n_intervals"), State("url", "pathname"), ) -def fill_database_collection_dropdown(n, url): # database page +def fill_database_db_dropdown(n, url): if str(url) == "/database": - print("fill_database_collection_dropdown") - return mongo.db.list_collection_names() + print("fill_database_db_dropdown") + return list(mongo.client.list_database_names()) @app.callback( - [Output('database-document-dropdown', 'disabled'),Output('database-document-dropdown', 'options')], - Input('database-collection-dropdown', 'value'), + [ + Output("database-collection-dropdown", "disabled"), + Output("database-collection-dropdown", "options"), + ], + [Input("database-db-dropdown", "value")], State("url", "pathname"), prevent_initial_call=True, ) -def fill_database_document_dropdown(collection, url): +def fill_database_collection_dropdown(db, url): # database page + if str(url) == "/database": + print("fill_database_collection_dropdown") + if db is not None and db != "": + return False, list(mongo.client[db].list_collection_names()) + return True, [] + + +@app.callback( + [ + Output("database-document-dropdown", "disabled"), + Output("database-document-dropdown", "options"), + ], + Input("database-collection-dropdown", "value"), + [State("url", "pathname"), State("database-db-dropdown", "value")], + prevent_initial_call=True, +) +def fill_database_document_dropdown(collection, url, db): if str(url) == "/database": if collection is not None and collection != "": print("fill_database_document_dropdown") - docs = list(mongo.db[collection].find({})) + docs = list(mongo.client[db][collection].find({})) toRet = [] for doc in docs: - toRet.append(str(doc['_id'])) + toRet.append(str(doc["_id"])) return False, toRet else: return True, [] +@app.callback( + Output("database-collection-schema", "children"), + [ + Input("database-collection-dropdown", "value"), + Input("database-db-dropdown", "value"), + ], + [State("url", "pathname")], + prevent_initial_call=True, +) +def fill_database_collection_schema(collection, db, url): + if str(url) == "/database": + print("fill_database_collection_schema") + if collection is not None and collection != "" and db is not None and db != "": + try: + schema = ( + mongo.client[db].get_collection(collection).options()["validator"] + ) + return render_dict(schema) + except Exception as e: + return ["Validation rules missing or something went wrong."] + return [] + + +@app.callback( + Output("database-document-viewer", "children"), + [ + Input("database-document-dropdown", "value"), + Input("database-collection-dropdown", "value"), + ], + [State("url", "pathname"), State("database-db-dropdown", "value")], + prevent_initial_call=True, +) +def fill_database_document_viewer(document, collection, url, db): + if str(url) == "/database": + print("fill_database_document_viewer") + if document is not None and document != "" and db is not None and db != "": + # try: + doc = mongo.client[db][collection].find({"_id": ObjectId(document)})[0] + return render_dict(doc) + # except Exception as e: + # return ["Document missing or something went wrong"] + return [] + + if __name__ == "__main__": app.run(debug=True) diff --git a/pages/database.py b/pages/database.py index 623168e..c3cbfed 100644 --- a/pages/database.py +++ b/pages/database.py @@ -2,7 +2,9 @@ import dash_bootstrap_components as dbc import dash -dash.register_page(__name__, path="/database", title="Database Browser", name="Database Browser") +dash.register_page( + __name__, path="/database", title="Database Browser", name="Database Browser" +) layout = html.Div( [ @@ -10,6 +12,18 @@ dcc.Interval(id="interval-database", interval=500000, n_intervals=0), dbc.Row( [ + dbc.Col( + [ + dcc.Dropdown( + id="database-db-dropdown", + options=[], + value=None, + className="mb-3", + placeholder="Select a database", + ), + dbc.Col([], id="database-db-data"), + ] + ), dbc.Col( [ dcc.Dropdown( @@ -17,7 +31,10 @@ options=[], value=None, className="mb-3", + disabled=True, + placeholder="Select a collection", ), + dbc.Col([], id="database-collection-schema"), ] ), dbc.Col( @@ -28,9 +45,11 @@ value=None, className="mb-3", disabled=True, - ) + placeholder="Select a document", + ), + dbc.Col([], id="database-document-viewer"), ] - ) + ), ] ), ], From 6a448b5fb8dffa4d621cefb672092940186eeaf9 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 13 Jul 2023 18:29:07 -0500 Subject: [PATCH 028/125] added devices to util and simplified schema viewer --- app.py | 24 +++ util.py | 470 +++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 356 insertions(+), 138 deletions(-) diff --git a/app.py b/app.py index e8f15c0..ad33f80 100644 --- a/app.py +++ b/app.py @@ -1349,6 +1349,29 @@ def fill_database_document_dropdown(collection, url, db): return True, [] +def process_schema(schema): + if isinstance(schema, dict): + if "bsonType" not in list(schema.keys()): + for key in list(schema.keys()): + schema[key] = process_schema(schema[key]) + elif "bsonType" in list(schema.keys()): + schema['Properties'] = {"Data Type": schema['bsonType']} + del schema['bsonType'] + if "description" in list(schema.keys()): + schema['Properties'].update({"Description": schema['description']}) + del schema['description'] + if "required" in list(schema.keys()): + # schema['Properties'].update({"Required": schema['required']}) + del schema['required'] + if "properties" in list(schema.keys()): + schema["Variables"] = process_schema(schema['properties']) + del schema['properties'] + if "title" in list(schema.keys()): + del schema['title'] + + return schema + + @app.callback( Output("database-collection-schema", "children"), [ @@ -1366,6 +1389,7 @@ def fill_database_collection_schema(collection, db, url): schema = ( mongo.client[db].get_collection(collection).options()["validator"] ) + schema = process_schema(schema) return render_dict(schema) except Exception as e: return ["Validation rules missing or something went wrong."] diff --git a/util.py b/util.py index b169596..647ea90 100644 --- a/util.py +++ b/util.py @@ -208,6 +208,132 @@ def default(self, obj): devices_ref_redundancy = { + "LinearStage150": { + "obj": LinearStage150, + "serial": True, + "serial_sequence": ["LinearStage150Connect"], + "import_device": "from devices.linear_stage_150 import LinearStage150", + "import_commands": "from commands.linear_stage_150_commands import *", + "init": { + "default_code": "LinearStage150(name='LinearStage150', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", + "obj_name": "LinearStage150", + "args": { + "name": { + "default": "LinearStage150", + "type": str, + "notes": "Name of the device.", + }, + "port": {"default": "COM", "type": str, "notes": "Port"}, + "baudrate": { + "default": 115200, + "type": int, + "notes": "Baudrate", + }, + "timeout": { + "default": 0.1, + "type": float, + "notes": "Timeout", + }, + "destination": { + "default": 0x50, + "type": int, + "notes": "", + }, + "source": { + "default": 0x01, + "type": int, + "notes": "", + }, + "channel": { + "default": 1, + "type": int, + "notes": "", + }, + }, + }, + "commands": { + "LinearStage150Connect": { + "default_code": "LinearStage150Connect(receiver= '')", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + } + }, + }, + "LinearStage150Initialize": { + "default_code": "LinearStage150Initialize(receiver= '')", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + } + }, + }, + "LinearStage150Deinitialize": { + "default_code": "LinearStage150Deinitialize(receiver= '')", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + } + }, + }, + "LinearStage150EnableMotor": { + "default_code": "LinearStage150EnableMotor(receiver= '')", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + } + }, + }, + "LinearStage150DisableMotor": { + "default_code": "LinearStage150DisableMotor(receiver= '')", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + } + }, + }, + "LinearStage150MoveAbsolute": { + "default_code": "LinearStage150MoveAbsolute(receiver= '', position= 0)", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + }, + "position": { + "default": 0, + "type": int, + "notes": "", + }, + }, + }, + "LinearStage150MoveRelative": { + "default_code": "LinearStage150MoveRelative(receiver= '', distance= 0)", + "args": { + "receiver": { + "default": "LinearStage150", + "type": str, + "notes": "", + }, + "distance": { + "default": 0, + "type": int, + "notes": "", + }, + }, + }, + }, + }, "PrintingStage": heating_stage_ref, "AnnealingStage": heating_stage_ref, "MultiStepper": { @@ -240,7 +366,7 @@ def default(self, obj): "type": float, "notes": "Timeout of the device", }, - "stepper_list":{ + "stepper_list": { "default": (1,), "type": Tuple[int, ...], "notes": "List of stepper numbers", @@ -250,20 +376,211 @@ def default(self, obj): "type": float, "notes": "Timeout for move commands", }, - } + }, }, - "commands": { #TODO update these to match redundancy changes - "MultiStepperConnect": "MultiStepperConnect(receiver= '')", - "MultiStepperInitialize": "MultiStepperInitialize(receiver= '')", - "MultiStepperDeinitialize": "MultiStepperDeinitialize(receiver= '')", - "MultiStepperMoveAbsolute": "MultiStepperMoveAbsolute(receiver= '', stepper_number= 0, position= 0)", - "MultiStepperMoveRelative": "MultiStepperMoveRelative(receiver= '', stepper_number= 0, distance= 0)", + "commands": { + "MultiStepperConnect": { + "default_code": "MultiStepperConnect(receiver= '')", + "args": { + "receiver": { + "default": "MultiStepper", + "type": str, + "notes": "Name of the device", + } + }, + }, + "MultiStepperInitialize": { + "default_code": "MultiStepperInitialize(receiver= '')", + "args": { + "receiver": { + "default": "MultiStepper", + "type": str, + "notes": "Name of the device", + } + }, + }, + "MultiStepperDeinitialize": { + "default_code": "MultiStepperDeinitialize(receiver= '')", + "args": { + "receiver": { + "default": "MultiStepper", + "type": str, + "notes": "Name of the device", + } + }, + }, + "MultiStepperMoveAbsolute": { + "default_code": "MultiStepperMoveAbsolute(receiver= '', stepper_number= 0, position= 0)", + "args": { + "receiver": { + "default": "MultiStepper", + "type": str, + "notes": "Name of the device", + }, + "stepper_number": { + "default": 0, + "type": int, + "notes": "Number of the stepper", + }, + "position": { + "default": 0.0, + "type": float, + "notes": "Position to move to", + }, + }, + }, + "MultiStepperMoveRelative": { + "default_code": "MultiStepperMoveRelative(receiver= '', stepper_number= 0, distance= 0)", + "args": { + "receiver": { + "default": "MultiStepper", + "type": str, + "notes": "Name of the device", + }, + "stepper_number": { + "default": 0, + "type": int, + "notes": "Number of the stepper", + }, + "distance": { + "default": 0.0, + "type": float, + "notes": "Distance to move", + }, + }, + }, + }, + }, + "NewportESP301": { + "obj": NewportESP301, + "serial": True, + "serial_sequence": ["NewportESP301Connect", "NewportESP301Initialize"], + "import_device": "from devices.newport_esp301 import NewportESP301", + "import_commands": "from commands.newport_esp301_commands import *", + "init": { + "default_code": "NewportESP301(name='NewportESP301', port='', baudrate=921600, timeout=1.0, axis_list = (1,), default_speed=20.0, poll_interval=0.1)", + "obj_name": "NewportESP301", + "args": { + "name": { + "default": "NewportESP301", + "type": str, + "notes": "Name of the device", + }, + "port": { + "default": "COM", + "type": str, + "notes": "Port of the device", + }, + "baudrate": { + "default": 921600, + "type": int, + "notes": "Baudrate of the device", + }, + "timeout": { + "default": 1.0, + "type": float, + "notes": "Timeout of the device", + }, + "axis_list": { + "default": (1,), + "type": Tuple[int, ...], + "notes": "List of axis numbers", + }, + "default_speed": { + "default": 20.0, + "type": float, + "notes": "Default speed of the device", + }, + "poll_interval": { + "default": 0.1, + "type": float, + "notes": "Poll interval of the device", + }, + }, + }, + "commands": { + "NewportESP301Connect": { + "default_code": "NewportESP301Connect(receiver= '')", + "args": { + "receiver": { + "default": "NewportESP301", + "type": str, + "notes": "Name of the device", + } + }, + }, + "NewportESP301Initialize": { + "default_code": "NewportESP301Initialize(receiver= '')", + "args": { + "receiver": { + "default": "NewportESP301", + "type": str, + "notes": "Name of the device", + } + }, + }, + "NewportESP301Deinitialize": { + "default_code": "NewportESP301Deinitialize(receiver= '')", + "args": { + "receiver": { + "default": "NewportESP301", + "type": str, + "notes": "Name of the device", + } + }, + }, + "NewportESP301MoveSpeedAbsolute": { + "default_code": "NewportESP301MoveSpeedAbsolute(receiver= '', axis= 1, position= 0, speed= 20.0)", + "args": { + "receiver": { + "default": "NewportESP301", + "type": str, + "notes": "Name of the device", + }, + "axis": { + "default": 1, + "type": int, + "notes": "Axis number", + }, + "position": { + "default": 0.0, + "type": float, + "notes": "Position to move to", + }, + "speed": { + "default": 20.0, + "type": float, + "notes": "Speed to move at", + }, + }, + }, + "NewportESP301MoveSpeedRelative": { + "default_code": "NewportESP301MoveSpeedRelative(receiver= '', axis= 1, distance= 0, speed= 20.0)", + "args": { + "receiver": { + "default": "NewportESP301", + "type": str, + "notes": "Name of the device", + }, + "axis": { + "default": 1, + "type": int, + "notes": "Axis number", + }, + "distance": { + "default": 0.0, + "type": float, + "notes": "Distance to move", + }, + "speed": { + "default": 20.0, + "type": float, + "notes": "Speed to move at", + }, + }, + }, }, }, - "PrinterMotorX": {"obj": NewportESP301}, - # "Spectrometer": {"obj": StellarNetSpectrometer}, - "XimeaCamera": {"obj": XimeaCamera}, - "DummyHeater": {"obj": DummyHeater}, "DummyMotor": { "obj": DummyMotor, "serial": True, @@ -374,130 +691,7 @@ def default(self, obj): }, }, }, - "LinearStage150": { - "obj": LinearStage150, - "serial": True, - "serial_sequence": ["LinearStage150Connect"], - "import_device": "from devices.linear_stage_150 import LinearStage150", - "import_commands": "from commands.linear_stage_150_commands import *", - "init": { - "default_code": "LinearStage150(name='LinearStage150', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", - "obj_name": "LinearStage150", - "args": { - "name": { - "default": "LinearStage150", - "type": str, - "notes": "Name of the device.", - }, - "port": {"default": "COM", "type": str, "notes": "Port"}, - "baudrate": { - "default": 115200, - "type": int, - "notes": "Baudrate", - }, - "timeout": { - "default": 0.1, - "type": float, - "notes": "Timeout", - }, - "destination": { - "default": 0x50, - "type": int, - "notes": "", - }, - "source": { - "default": 0x01, - "type": int, - "notes": "", - }, - "channel": { - "default": 1, - "type": int, - "notes": "", - }, - }, - }, - "commands": { - "LinearStage150Connect": { - "default_code": "LinearStage150Connect(receiver= '')", - "args": { - "receiver": { - "default": "LinearStage150", - "type": str, - "notes": "", - } - }, - }, - "LinearStage150Initialize": { - "default_code": "LinearStage150Initialize(receiver= '')", - "args": { - "receiver": { - "default": "LinearStage150", - "type": str, - "notes": "", - } - }, - }, - "LinearStage150Deinitialize": { - "default_code": "LinearStage150Deinitialize(receiver= '')", - "args": { - "receiver": { - "default": "LinearStage150", - "type": str, - "notes": "", - } - }, - }, - "LinearStage150EnableMotor": { - "default_code": "LinearStage150EnableMotor(receiver= '')", - "args": { - "receiver": { - "default": "LinearStage150", - "type": str, - "notes": "", - } - }, - }, - "LinearStage150DisableMotor": { - "default_code": "LinearStage150DisableMotor(receiver= '')", - "args": { - "receiver": { - "default": "LinearStage150", - "type": str, - "notes": "", - } - }, - }, - "LinearStage150MoveAbsolute": { - "default_code": "LinearStage150MoveAbsolute(receiver= '', position= 0)", - "args": { - "receiver": { - "default": "LinearStage150", - "type": str, - "notes": "", - }, - "position": { - "default": 0, - "type": int, - "notes": "", - }, - }, - }, - "LinearStage150MoveRelative": { - "default_code": "LinearStage150MoveRelative(receiver= '', distance= 0)", - "args": { - "receiver": { - "default": "LinearStage150", - "type": str, - "notes": "", - }, - "distance": { - "default": 0, - "type": int, - "notes": "", - }, - }, - }, - }, - }, + # "Spectrometer": {"obj": StellarNetSpectrometer}, + # "XimeaCamera": {"obj": XimeaCamera}, + # "DummyHeater": {"obj": DummyHeater},› } From d1d556de5c334a21fa059a4cc75c78c6503e26c2 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 13 Jul 2023 18:31:57 -0500 Subject: [PATCH 029/125] Delete temp.py --- temp.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 temp.py diff --git a/temp.py b/temp.py deleted file mode 100644 index e69de29..0000000 From 21fccec5ccf5d9290ba9779ed73c83f5d68ad7f8 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 13 Jul 2023 18:32:14 -0500 Subject: [PATCH 030/125] Delete temp2.py --- temp2.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 temp2.py diff --git a/temp2.py b/temp2.py deleted file mode 100644 index b09bd71..0000000 --- a/temp2.py +++ /dev/null @@ -1,17 +0,0 @@ -from devices.dummy_heater import DummyHeater -from mongodb_helper import MongoDBHelper - -mongo = MongoDBHelper('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority', 'diaogroup') - -# print(mongo.insert_yaml_file('recipes',"e1.yaml")) -print(mongo.find_documents('recipes',{'file_name':'e1.yaml'})[0]) - -print('done') - - - -mongo.close_connection() - -# client = MongoClient('mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority') -# db = client['diaogroup'] -# collection = db['recipes'] \ No newline at end of file From 5971dbda555fbedbc13ac3fd3ea1781a70765c81 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 13 Jul 2023 18:53:44 -0500 Subject: [PATCH 031/125] separated load recipe and added manual ctrl alert --- app.py | 48 +++++--- pages/data.py | 2 +- pages/home.py | 234 ++++++++++++++++++------------------ pages/load-recipe.py | 131 ++++++++++++++++++++ pages/manual-control.py | 23 ++-- pages/python-edit-recipe.py | 4 +- 6 files changed, 300 insertions(+), 142 deletions(-) create mode 100644 pages/load-recipe.py diff --git a/app.py b/app.py index ad33f80..ca22732 100644 --- a/app.py +++ b/app.py @@ -52,12 +52,20 @@ navbar = dbc.NavbarSimple( children=[ dbc.NavItem(dbc.NavLink("Home", href="/")), - dbc.NavItem(dbc.NavLink("View Recipe", href="/view-recipe")), - dbc.NavItem(dbc.NavLink("Edit Recipe", href="/python-edit-recipe")), - dbc.NavItem(dbc.NavLink("Execute Recipe", href="/execute-recipe")), + dbc.DropdownMenu( + children=[ + dbc.DropdownMenuItem("Load Recipe", href="/load-recipe"), + dbc.DropdownMenuItem("View Recipe", href="/view-recipe"), + dbc.DropdownMenuItem("Edit Recipe", href="/edit-recipe"), + dbc.DropdownMenuItem("Execute Recipe", href="/execute-recipe"), + dbc.DropdownMenuItem("Document", href="/data"), + ], + nav=True, + in_navbar=True, + label="Recipe", + ), dbc.NavItem(dbc.NavLink("Manual Control", href="/manual-control")), - dbc.NavItem(dbc.NavLink("Document", href="/data")), - dbc.NavItem(dbc.NavLink("Database", href="/database")), + dbc.NavItem(dbc.NavLink("Database Browser", href="/database")), ], brand="AAMP", brand_href="/", @@ -549,7 +557,7 @@ def update_commands_table(n_clicks, data, table): # view-recipe page # prevent_initial_call=True, ) def fill_ace_editor(n, url): # python-edit-recipe page - if url == "/python-edit-recipe": + if url == "/edit-recipe": if hasattr(com, "document"): python_code = com.document.get("python_code", "") if python_code is not None and python_code != "": @@ -1071,7 +1079,13 @@ def manual_control_execute_button(value, url): @app.callback( - Output("manual-control-command-dropdown", "className"), + [ + Output("manual-control-command-dropdown", "className"), + Output("manual-control-alert", "is_open", allow_duplicate=True), + Output("manual-control-alert", "children", allow_duplicate=True), + Output("manual-control-alert", "color", allow_duplicate=True), + Output("manual-control-alert", "duration", allow_duplicate=True), + ], Input("manual-control-execute-button", "n_clicks"), [ State("url", "pathname"), @@ -1086,6 +1100,8 @@ def manual_control_execute_button(value, url): def manual_control_execute(n, url, opt, device, command, device_form, command_form): if str(url) == "/manual-control": print("manual_control_execute") + if device is None or device == "" or command is None or command == "": + return opt, True, "Something went wrong. Check the fields below.", "danger", 3000 code = "" code += util.devices_ref_redundancy[device]["import_device"] code += "\n" @@ -1288,8 +1304,10 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo print("\n" + code + "\n") try: exec(code) + return opt, True, "Execution complete.", "success", 2000 except Exception as e: print(e) + return opt, True, "Something went wrong. Check console.", "danger", 3000 return opt @@ -1355,19 +1373,19 @@ def process_schema(schema): for key in list(schema.keys()): schema[key] = process_schema(schema[key]) elif "bsonType" in list(schema.keys()): - schema['Properties'] = {"Data Type": schema['bsonType']} - del schema['bsonType'] + schema["Properties"] = {"Data Type": schema["bsonType"]} + del schema["bsonType"] if "description" in list(schema.keys()): - schema['Properties'].update({"Description": schema['description']}) - del schema['description'] + schema["Properties"].update({"Description": schema["description"]}) + del schema["description"] if "required" in list(schema.keys()): # schema['Properties'].update({"Required": schema['required']}) - del schema['required'] + del schema["required"] if "properties" in list(schema.keys()): - schema["Variables"] = process_schema(schema['properties']) - del schema['properties'] + schema["Variables"] = process_schema(schema["properties"]) + del schema["properties"] if "title" in list(schema.keys()): - del schema['title'] + del schema["title"] return schema diff --git a/pages/data.py b/pages/data.py index c85660f..6ab2b8f 100644 --- a/pages/data.py +++ b/pages/data.py @@ -7,7 +7,7 @@ layout = html.Div( [ - html.H1("Data"), + html.H1("View Recipe Document"), dbc.Button("Load data", id="load-data-button", n_clicks=0, className="mb-3"), # dcc.Textarea( # id="data-output", readOnly=True, style={"width": "100%", "height": 0} diff --git a/pages/home.py b/pages/home.py index e9b6b2a..93ed475 100644 --- a/pages/home.py +++ b/pages/home.py @@ -27,123 +27,123 @@ layout = html.Div( [ html.H1("Home"), - dbc.Row( - [ - dbc.Col( - dcc.Input( - id="filename-input", - type="text", - placeholder="Enter file name", - className="form-control", - ), - width=6, - ), - dbc.Col( - dbc.Button( - "Load", - id="filename-input-button", - n_clicks=0, - color="primary", - className="btn btn-primary", - ), - width=2, - ), - ], - className="mb-3", - ), - dbc.Alert( - id="home-load-file-alert", - color="success", - is_open=False, - # fade=True, - className="mb-3", - ), - dbc.Row( - [ - dbc.Col( - dbc.Button( - "Refresh List", - id="home-refresh-list-button", - n_clicks=0, - color="secondary", - className="btn btn-secondary mb-3", - ), - width=2, - ) - ] - ), - dbc.Row( - [ - dbc.Col( - dash_table.DataTable( - id="home-recipes-list-table", - columns=[ - {"name": "File Name", "id": "file_name"}, - {"name": "Posix Compatible", "id": "posix_friendly"}, - {"name": "Viewer Compatible", "id": "dash_friendly"}, - {"name": "Python Code Available", "id": "python_code"}, - ], - data=[], - style_table={"width": "100%"}, - style_cell={"textAlign": "left"}, - style_header={"fontWeight": "bold"}, - page_current=0, - page_size=10, - style_data_conditional=[ - { - "if": { - "column_id": "posix_friendly", - "filter_query": "{posix_friendly} contains true", - }, - "backgroundColor": "#b7e8c4", - "color": "black", - }, - { - "if": { - "column_id": "posix_friendly", - "filter_query": "{posix_friendly} contains false", - }, - "backgroundColor": "#e8b7b7", - "color": "black", - }, - { - "if": { - "column_id": "dash_friendly", - "filter_query": "{dash_friendly} contains true", - }, - "backgroundColor": "#b7e8c4", - "color": "black", - }, - { - "if": { - "column_id": "dash_friendly", - "filter_query": "{dash_friendly} contains false", - }, - "backgroundColor": "#e8b7b7", - "color": "black", - }, - { - "if": { - "column_id": "python_code", - "filter_query": "{python_code} contains true", - }, - "backgroundColor": "#b7e8c4", - "color": "black", - }, - { - "if": { - "column_id": "python_code", - "filter_query": "{python_code} contains false", - }, - "backgroundColor": "#e8b7b7", - "color": "black", - }, - ], - ), - width=10, - ) - ] - ), + # dbc.Row( + # [ + # dbc.Col( + # dcc.Input( + # id="filename-input", + # type="text", + # placeholder="Enter file name", + # className="form-control", + # ), + # width=6, + # ), + # dbc.Col( + # dbc.Button( + # "Load", + # id="filename-input-button", + # n_clicks=0, + # color="primary", + # className="btn btn-primary", + # ), + # width=2, + # ), + # ], + # className="mb-3", + # ), + # dbc.Alert( + # id="home-load-file-alert", + # color="success", + # is_open=False, + # # fade=True, + # className="mb-3", + # ), + # dbc.Row( + # [ + # dbc.Col( + # dbc.Button( + # "Refresh List", + # id="home-refresh-list-button", + # n_clicks=0, + # color="secondary", + # className="btn btn-secondary mb-3", + # ), + # width=2, + # ) + # ] + # ), + # dbc.Row( + # [ + # dbc.Col( + # dash_table.DataTable( + # id="home-recipes-list-table", + # columns=[ + # {"name": "File Name", "id": "file_name"}, + # {"name": "Posix Compatible", "id": "posix_friendly"}, + # {"name": "Viewer Compatible", "id": "dash_friendly"}, + # {"name": "Python Code Available", "id": "python_code"}, + # ], + # data=[], + # style_table={"width": "100%"}, + # style_cell={"textAlign": "left"}, + # style_header={"fontWeight": "bold"}, + # page_current=0, + # page_size=10, + # style_data_conditional=[ + # { + # "if": { + # "column_id": "posix_friendly", + # "filter_query": "{posix_friendly} contains true", + # }, + # "backgroundColor": "#b7e8c4", + # "color": "black", + # }, + # { + # "if": { + # "column_id": "posix_friendly", + # "filter_query": "{posix_friendly} contains false", + # }, + # "backgroundColor": "#e8b7b7", + # "color": "black", + # }, + # { + # "if": { + # "column_id": "dash_friendly", + # "filter_query": "{dash_friendly} contains true", + # }, + # "backgroundColor": "#b7e8c4", + # "color": "black", + # }, + # { + # "if": { + # "column_id": "dash_friendly", + # "filter_query": "{dash_friendly} contains false", + # }, + # "backgroundColor": "#e8b7b7", + # "color": "black", + # }, + # { + # "if": { + # "column_id": "python_code", + # "filter_query": "{python_code} contains true", + # }, + # "backgroundColor": "#b7e8c4", + # "color": "black", + # }, + # { + # "if": { + # "column_id": "python_code", + # "filter_query": "{python_code} contains false", + # }, + # "backgroundColor": "#e8b7b7", + # "color": "black", + # }, + # ], + # ), + # width=10, + # ) + # ] + # ), ], className="container", ) diff --git a/pages/load-recipe.py b/pages/load-recipe.py new file mode 100644 index 0000000..94ebb47 --- /dev/null +++ b/pages/load-recipe.py @@ -0,0 +1,131 @@ +from dash import Dash, html, dcc, dash_table, Input, Output, callback +import dash_bootstrap_components as dbc +import dash + +dash.register_page(__name__, path="/load-recipe", title="Load Recipe", name="Load Recipe") + + + +layout = html.Div( + [ + html.H1("Load Recipe"), + dbc.Row( + [ + dbc.Col( + dcc.Input( + id="filename-input", + type="text", + placeholder="Enter file name", + className="form-control", + ), + width=6, + ), + dbc.Col( + dbc.Button( + "Load", + id="filename-input-button", + n_clicks=0, + color="primary", + className="btn btn-primary", + ), + width=2, + ), + ], + className="mb-3", + ), + dbc.Alert( + id="home-load-file-alert", + color="success", + is_open=False, + # fade=True, + className="mb-3", + ), + dbc.Row( + [ + dbc.Col( + dbc.Button( + "Refresh List", + id="home-refresh-list-button", + n_clicks=0, + color="secondary", + className="btn btn-secondary mb-3", + ), + width=2, + ) + ] + ), + dbc.Row( + [ + dbc.Col( + dash_table.DataTable( + id="home-recipes-list-table", + columns=[ + {"name": "File Name", "id": "file_name"}, + {"name": "Posix Compatible", "id": "posix_friendly"}, + {"name": "Viewer Compatible", "id": "dash_friendly"}, + {"name": "Python Code Available", "id": "python_code"}, + ], + data=[], + style_table={"width": "100%"}, + style_cell={"textAlign": "left"}, + style_header={"fontWeight": "bold"}, + page_current=0, + page_size=10, + style_data_conditional=[ + { + "if": { + "column_id": "posix_friendly", + "filter_query": "{posix_friendly} contains true", + }, + "backgroundColor": "#b7e8c4", + "color": "black", + }, + { + "if": { + "column_id": "posix_friendly", + "filter_query": "{posix_friendly} contains false", + }, + "backgroundColor": "#e8b7b7", + "color": "black", + }, + { + "if": { + "column_id": "dash_friendly", + "filter_query": "{dash_friendly} contains true", + }, + "backgroundColor": "#b7e8c4", + "color": "black", + }, + { + "if": { + "column_id": "dash_friendly", + "filter_query": "{dash_friendly} contains false", + }, + "backgroundColor": "#e8b7b7", + "color": "black", + }, + { + "if": { + "column_id": "python_code", + "filter_query": "{python_code} contains true", + }, + "backgroundColor": "#b7e8c4", + "color": "black", + }, + { + "if": { + "column_id": "python_code", + "filter_query": "{python_code} contains false", + }, + "backgroundColor": "#e8b7b7", + "color": "black", + }, + ], + ), + width=10, + ) + ] + ), + ], + className="container", +) diff --git a/pages/manual-control.py b/pages/manual-control.py index dc3d3b2..b7c6afe 100644 --- a/pages/manual-control.py +++ b/pages/manual-control.py @@ -3,20 +3,29 @@ import dash -dash.register_page(__name__, path="/manual-control", title="Manual Control", name="Manual Control") +dash.register_page( + __name__, path="/manual-control", title="Manual Control", name="Manual Control" +) layout = html.Div( [ html.H1("Manual Control", className="mb-3"), dcc.Interval(id="interval-manual-control", interval=500000, n_intervals=0), - dbc.ButtonGroup( + dbc.ButtonGroup( [ - dbc.Button("Execute", id="manual-control-execute-button", n_clicks=0, disabled= True), - dbc.Button("Clear", id="manual-control-clear-button", n_clicks=0, disabled= True), + dbc.Button( + "Execute", + id="manual-control-execute-button", + n_clicks=0, + disabled=True, + ), + dbc.Button( + "Clear", id="manual-control-clear-button", n_clicks=0, disabled=True + ), ], className="mb-3", ), - + dbc.Alert("Alert", id="manual-control-alert", is_open=False, duration=500), dbc.Row( [ dbc.Col( @@ -25,7 +34,7 @@ id="manual-control-device-dropdown", options=[], value=None, - className = "mb-3", + className="mb-3", ), dbc.Col([], id="manual-control-device-form"), ] @@ -37,7 +46,7 @@ options=[], value=None, disabled=True, - className = "mb-3", + className="mb-3", ), dbc.Col([], id="manual-control-command-form"), ] diff --git a/pages/python-edit-recipe.py b/pages/python-edit-recipe.py index 8c5adea..cd8228c 100644 --- a/pages/python-edit-recipe.py +++ b/pages/python-edit-recipe.py @@ -3,11 +3,11 @@ import dash import dash_ace -dash.register_page(__name__, path="/python-edit-recipe", title="Edit Recipe", name="Edit Recipe") +dash.register_page(__name__, path="/edit-recipe", title="Edit Recipe", name="Edit Recipe") layout = html.Div( [ - html.H1("Edit Recipe in Python"), + html.H1("Edit Recipe Code"), html.Div( [ html.Div( From e9b6c42aec0977ef150bfcafb91f1d564faa2105 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 13 Jul 2023 20:25:57 -0500 Subject: [PATCH 032/125] added logging for manual control --- app.py | 99 ++++++++++++++++++++++++++++++++--------- console_interceptor.py | 59 ++++++++++++++++++++++++ pages/manual-control.py | 48 +++++++++++++++++++- util.py | 2 +- 4 files changed, 186 insertions(+), 22 deletions(-) create mode 100644 console_interceptor.py diff --git a/app.py b/app.py index ca22732..ddb0276 100644 --- a/app.py +++ b/app.py @@ -14,6 +14,7 @@ import inspect from pw import mongo_username, mongo_password from bson.objectid import ObjectId +from console_interceptor import ConsoleInterceptor try: import serial.tools.list_ports @@ -927,24 +928,44 @@ def create_manual_control_device_form(value, url): args = util.devices_ref_redundancy[value]["init"]["args"] toRet = [] for arg in args: - toRet.append( - dbc.Row( - [ - dbc.Label([arg], html_for=str(value + "+" + arg), width=2), - dbc.Col( - [ - dbc.Input( - id=str(value + "+" + arg), - value=args[arg]["default"], - placeholder=args[arg]["notes"], - ), - ], - width=10, - ), - ], - className="mb-2", + if arg == 'port': + toRet.append( + dbc.Row( + [ + dbc.Label(dbc.NavLink(arg, n_clicks=0, id='manual-control-port-field', style={'cursor': 'pointer', 'color': 'blue', 'textDecoration': 'underline'}), html_for=str(value + "+" + arg), width=2), + dbc.Col( + [ + dbc.Input( + id=str(value + "+" + arg), + value=args[arg]["default"], + placeholder=args[arg]["notes"], + ), + ], + width=10, + ), + ], + className="mb-2", + ) + ) + else: + toRet.append( + dbc.Row( + [ + dbc.Label([arg], html_for=str(value + "+" + arg), width=2), + dbc.Col( + [ + dbc.Input( + id=str(value + "+" + arg), + value=args[arg]["default"], + placeholder=args[arg]["notes"], + ), + ], + width=10, + ), + ], + className="mb-2", + ) ) - ) return [toRet] @@ -1085,6 +1106,9 @@ def manual_control_execute_button(value, url): Output("manual-control-alert", "children", allow_duplicate=True), Output("manual-control-alert", "color", allow_duplicate=True), Output("manual-control-alert", "duration", allow_duplicate=True), + Output('manual-control-execute-modal', 'is_open'), + Output('manual-control-execute-modal-body', 'children'), + Output('manual-control-execute-modal-body-code', 'children') ], Input("manual-control-execute-button", "n_clicks"), [ @@ -1301,16 +1325,51 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo + "_seq, False, False, False)\n" ) code += str(device) + "_seq_invoker.invoke_commands()\n" - print("\n" + code + "\n") + interceptor = ConsoleInterceptor() + print('\n') + interceptor.start_interception() + print(code) + interceptor.stop_interception() + code_output = interceptor.get_intercepted_messages() + code_log_string = "" + for msg in code_output: + code_log_string += msg + interceptor = ConsoleInterceptor() + interceptor.start_interception() try: exec(code) - return opt, True, "Execution complete.", "success", 2000 + interceptor.stop_interception() + messages = interceptor.get_intercepted_messages() + log_string = "" + for msg in messages: + log_string += msg + return opt, True, "Execution complete.", "success", 0, True, html.Pre(log_string), html.Pre(code_log_string) except Exception as e: print(e) - return opt, True, "Something went wrong. Check console.", "danger", 3000 + interceptor.stop_interception() + messages = interceptor.get_intercepted_messages() + log_string = "" + for msg in messages: + log_string += msg + return opt, True, "Something went wrong. Check logs.", "danger", 0, True, html.Pre(log_string), html.Pre(code_log_string) return opt +@app.callback( + [Output('manual-control-port-modal', 'is_open'), Output('manual-control-serial-ports-info', 'children')], + Input('manual-control-port-field', 'n_clicks'), + prevent_initial_call=True +) +def open_fill_manual_control_serial(n): + if _has_serial and n != 0: + ports = serial.tools.list_ports.comports() + str_ports = "" + for port, desc, hwid in sorted(ports): + str_ports += f"{port}: {desc} [{hwid}]\n" + lines = str_ports.splitlines() + return True, [html.Div([html.Div(line) for line in lines])] + return False, [] + # --------------------------------------------------------------- # Database page diff --git a/console_interceptor.py b/console_interceptor.py new file mode 100644 index 0000000..358a910 --- /dev/null +++ b/console_interceptor.py @@ -0,0 +1,59 @@ +import sys +import logging + +class ConsoleInterceptor: + def __init__(self): + self.stdout = sys.stdout + self.stderr = sys.stderr + self.intercepted_messages = [] + + def start_interception(self): + sys.stdout = self.InterceptedStream(self.stdout, self._handle_message) + sys.stderr = self.InterceptedStream(self.stderr, self._handle_message) + self._configure_logger() + + def stop_interception(self): + sys.stdout = self.stdout + sys.stderr = self.stderr + + def get_intercepted_messages(self): + return self.intercepted_messages + + def _handle_message(self, message): + # Store the intercepted message for later use + self.intercepted_messages.append(message) + + def _configure_logger(self): + logger = logging.getLogger() + logger.addHandler(self._get_logging_handler()) + logger.addFilter(self._get_logging_filter()) + + def _get_logging_handler(self): + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + return handler + + def _get_logging_filter(self): + return self.InterceptedMessageFilter(self.intercepted_messages) + + class InterceptedStream: + def __init__(self, stream, handler): + self.stream = stream + self.handler = handler + + def write(self, message): + self.handler(message) + self.stream.write(message) + + def flush(self): + self.stream.flush() + + class InterceptedMessageFilter(logging.Filter): + def __init__(self, intercepted_messages): + super().__init__() + self.intercepted_messages = intercepted_messages + + def filter(self, record): + message = record.getMessage() + return message not in self.intercepted_messages + diff --git a/pages/manual-control.py b/pages/manual-control.py index b7c6afe..0432647 100644 --- a/pages/manual-control.py +++ b/pages/manual-control.py @@ -25,7 +25,53 @@ ], className="mb-3", ), - dbc.Alert("Alert", id="manual-control-alert", is_open=False, duration=500), + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Execute")), + dbc.ModalBody( + [ + dbc.Alert( + "Alert", + id="manual-control-alert", + is_open=False, + duration=500, + ), + html.Div( + id="manual-control-execute-modal-body-code", + className="log-container", + style={ + "height": "160px", + "overflow-y": "scroll", + "padding": "10px", + "border": "2px solid", + "margin-bottom": "10px" + }, + ), + html.Div( + id="manual-control-execute-modal-body", + className="log-container", + style={ + "height": "300px", + "overflow-y": "scroll", + "padding": "10px", + "border": "2px solid", + }, + ), + ] + ), + ], + id="manual-control-execute-modal", + size="xl", + ), + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("COM Port Information")), + dbc.ModalBody([ + html.Div(id='manual-control-serial-ports-info') + ]) + ], + id='manual-control-port-modal' + ), dbc.Row( [ dbc.Col( diff --git a/util.py b/util.py index 647ea90..6819704 100644 --- a/util.py +++ b/util.py @@ -82,7 +82,7 @@ def default(self, obj): "args": { "name": { "default": "Stage", - "type": "str", + "type": str, "notes": "Name of the device", }, "port": {"default": "COM", "type": str, "notes": "Port"}, From 8d5d02bd436484a76c8bc7023cbc8f42385ba4c4 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 13 Jul 2023 20:28:55 -0500 Subject: [PATCH 033/125] fixed heating stage command name --- util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util.py b/util.py index 6819704..64e8ea6 100644 --- a/util.py +++ b/util.py @@ -134,8 +134,8 @@ def default(self, obj): } }, }, - "HeatingStageSetTemperature": { - "default_code": "HeatingStageSetTemperature(receiver= '', temperature= 0.0)", + "HeatingStageSetTemp": { + "default_code": "HeatingStageSetTemp(receiver= '', temperature= 0.0)", "args": { "receiver": { "default": "Stage", From ed26b801a3bd16006b6def5d7906743ccaa51619 Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 14 Jul 2023 13:45:56 -0500 Subject: [PATCH 034/125] added current keithley to branch and util --- app.py | 2 +- commands/keithley_2450_commands.py | 123 ++++++++++++++++++ devices/keithley_2450.py | 176 ++++++++++++++++++++++++++ util.py | 196 +++++++++++++++++++++++++++++ 4 files changed, 496 insertions(+), 1 deletion(-) create mode 100644 commands/keithley_2450_commands.py create mode 100644 devices/keithley_2450.py diff --git a/app.py b/app.py index ddb0276..8960e37 100644 --- a/app.py +++ b/app.py @@ -1358,7 +1358,7 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo @app.callback( [Output('manual-control-port-modal', 'is_open'), Output('manual-control-serial-ports-info', 'children')], Input('manual-control-port-field', 'n_clicks'), - prevent_initial_call=True + prevent_initial_call=True, suppress_callback_exceptions=True ) def open_fill_manual_control_serial(n): if _has_serial and n != 0: diff --git a/commands/keithley_2450_commands.py b/commands/keithley_2450_commands.py new file mode 100644 index 0000000..8db552e --- /dev/null +++ b/commands/keithley_2450_commands.py @@ -0,0 +1,123 @@ +from typing import List + +from devices.device import Device +from .command import Command, CommandResult, CompositeCommand +from devices.keithley_2450 import Keithley2450 + +class KeithleyParentCommand(Command): + """Parent class for all Keithley2450 commands.""" + receiver_cls = Keithley2450 + + def __init__(self, receiver: Keithley2450, **kwargs): + super().__init__(receiver, **kwargs) + +class Keithley2450Initialize(KeithleyParentCommand): + """Initialize the SMU by resetting it""" + + def __init__(self, receiver: Keithley2450, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + +class Keithley2450Deinitialize(KeithleyParentCommand): + """Deinitialize the SMU""" + + #TODO: create command to deinitialize keithley2450 if necessary + + def __init__(self, reciever: Keithley2450, reset_init_flag: bool = True, **kwargs): + super().__init__(reciever, **kwargs) + self._params['reset_init_flag'] = reset_init_flag + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize(self._params['reset_init_flag'])) + + +class KeithleyWait(KeithleyParentCommand): + """Wait for all pending operations to finish""" + + def __init__(self, receiver: Keithley2450, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.wait()) + +class KeithleyWriteCommand(KeithleyParentCommand): + """Write arbitrary SCPI ASCII command""" + + def __init__(self, receiver: Keithley2450, command: str, **kwargs): + super().__init__(receiver, **kwargs) + self._params['command'] = command + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.write_command(self._params['command'])) + +class KeithleySetTerminal(KeithleyParentCommand): + """Set the terminal position of the SMU""" + + def __init__(self, receiver: Keithley2450, position: str = 'front', **kwargs): + super().__init__(receiver, **kwargs) + self._params['position'] = position + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.terminal_pos(self._params['position'])) + +class KeithleyErrorCheck(KeithleyParentCommand): + """Check for errors that occur during measurement""" + + def __init__(self, receiver: Keithley2450, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.error_check()) + +class KeithleyClearBuffer(KeithleyParentCommand): + """Clear the data storage buffer within the Keithley2450""" + def __init__(self, receiver: Keithley2450, buffer: str = "defbuffer1", **kwargs): + super().__init__(receiver, **kwargs) + self._params['buffer'] = buffer + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.clear_buffer(self._params['buffer'])) + +class KeithleyIVCharacteristic(KeithleyParentCommand): + """I-V linear sweep sourcing voltage and measuring current""" + + def __init__(self, receiver: Keithley2450, ilimit: float, vmin: float, vmax: float, delay: float, steps: int = 60, **kwargs): + super().__init__(receiver, **kwargs) + self._params['ilimit'] = ilimit + self._params['vmin'] = vmin + self._params['vmax'] = vmax + self._params['delay'] = delay + self._params['steps'] = steps + + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.IV_characteristic(self._params['ilimit'], self._params['vmin'], + self._params['vmax'], self._params['steps'], self._params['delay'])) + + +class KeithleyFourPoint(KeithleyParentCommand): + """Four collinear point sheet resistance measurement""" + + def __init__(self, receiver: Keithley2450, test_curr: float, vlimit: float, curr_reversal: bool = False, **kwargs): + super().__init__(receiver, **kwargs) + self._params['test_curr'] = test_curr + self._params['vlimit'] = vlimit + self._params['curr_reversal'] = curr_reversal + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.four_point(self._params['test_curr'], self._params['vlimit'], self._params['curr_reversal'])) + +class KeithleyGetData(KeithleyParentCommand): + """Retrieve data""" + + def __init__(self, receiver: Keithley2450, filename: str = None, four_point: bool = False, **kwargs): + super().__init__(receiver, **kwargs) + self._params['filename'] = filename + self._params['four_point'] = four_point + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_data(self._params['filename'], self._params['four_point'])) + + diff --git a/devices/keithley_2450.py b/devices/keithley_2450.py new file mode 100644 index 0000000..1639d3e --- /dev/null +++ b/devices/keithley_2450.py @@ -0,0 +1,176 @@ +"""Requires download of NI-VISA with equivalent bitness""" +import pyvisa +from time import sleep +from datetime import datetime +import pandas as pd +from typing import Optional, Tuple +from .device import Device, check_initialized + + + +class Keithley2450(Device): + save_directory = 'data/keithley_2450/' + + def __init__(self, name: str, ID: str, query_delay: int): + super().__init__(name) + self.ID = ID + self.query_delay = query_delay + self.keithley = pyvisa.ResourceManager().open_resource(self.ID) + print(self.keithley.query('*IDN?')) + sleep(self.query_delay) + + @property #TODO: Check if necessary + def terminal_pos(self) -> str: + return self.position + + def initialize(self) -> Tuple[bool, str]: + self.keithley.write('*RST') + self._is_initialized = True + return (True, "Initialized Keithley2450 by performing device reset") + + def deinitialize(self) -> Tuple[bool, str]: + #TODO: Keithley deinitialization steps + self.keithley.close() + self._is_initialized = False + return(True, "Deinitialized Keithley2450") + + @check_initialized + def wait(self): + self.keithley.query('*OPC?') + if self.keithley.query('*ESR?') != 1: + self.keithley.write('*CLS') + print("Wait Completed, Standard Event Status Register cleared") + + #TODO: Test wait function below + """ + def wait(self): + self.keithley.write('*OPC?') + while True: + try: + self.keithley.read() + break + except pyvisa.errors.VisaIOError: + continue + """ + + @check_initialized + def write_command(self, command: str): + self.keithley.write(command) + + @check_initialized + def terminal_pos(self, position: str = 'front'): + """Set terminal positions to front or rear""" + if position == "rear": + self.keithley.write('ROUT:TERM REAR') + self.wait() + elif position == "front": + self.keithley.write('ROUT:TERM FRONT') + self.wait() + else: + raise Exception(f"Expected 'rear' or 'front', found {position}") + @check_initialized + def error_check(self): + """Check for errors raised by Keithley2450""" + self.keithley.write('SYSTem:ERRor:COUNt?') + num_errors = int(self.keithley.read()) + if num_errors != 0: + errors = [] + for i in range(num_errors): + self.keithley.write('SYSTem:ERRor:NEXT?') + errors.append(self.device.read()) + errors = ''.join(errors) + raise Warning(f"An error has occurred:\n {errors}") + + @check_initialized + def clear_buffer(self, buffer: str = 'defbuffer1') -> Tuple[bool, str]: + """Clear the data storage buffer within the Keithley2450""" + self.keithley.write(f':TRACe:CLEar "{buffer}"') + return (True, f'Cleared buffer {buffer}') + + + @check_initialized + def IV_characteristic(self, ilimit: float, vmin: float, vmax: float, delay: float, steps: int = 60): + """Sourcing voltage and measuring current with linear sweep""" + if ilimit <= 1e-9 and ilimit >= 1.05: + raise Exception(f"Expected source current limit between 1nA and 1.05A") + + if vmin < -210 or vmin > 210: + raise Exception(f"Voltage minimum out of range -210V to 210V") + + if vmax < -210 or vmin > 210: + raise Exception(f"Voltage maximum out of range -210V to 210V") + if vmax < vmin: + raise Exception(f"Voltage minimum is greater than voltage maximum") + + if delay < 50e-6: + raise Exception(f"Delay value too small, must be greater than 50 µs") + + self.keithley.write('*RST') + self.keithley.write('SENS:CURR:RANG:AUTO ON') + self.keithley.write('SYST:RSEN ON') + self.keithley.write(f"SOURce:VOLT:ILIMit {ilimit}") + self.keithley.write(f"SOURce:SWE:VOLT:LIN {vmin}, {vmax}, {steps}, {delay}") + self.keithley.write('INIT') + self.keithley.write('*WAI') + + @check_initialized + def four_point(self, test_curr: float, vlimit: float, curr_reversal: bool = False) -> float: + """Measure resistance through four-point collinear probe method""" + + self.keithley.write('*RST') + self.keithley.write('SENS:VOLT:RANG:AUTO ON') + self.keithley.write('SYST:RSEN ON') + self.keithley.write(f"SOURce:CURR:VLIMit {vlimit}") + self.keithley.write(f"SOURce:CURR {test_curr}") + self.keithley.write(':OUTP ON') + self.keithley.write('*WAI') + sleep(2) + res1 = self.keithley.query(':READ?') + self.keithley.write(':OUTP OFF') + four_point_data = res1 + if curr_reversal: + self.keithley.write(f"SOURce:CURR:VLIMit {vlimit}") + self.keithley.write(f"SOURce:CURR {-1*test_curr}") + self.keithley.write(':OUTP ON') + self.keithley.write('*WAI') + sleep(2) + res2 = self.keithley.query('READ?') + self.keithley.write(':OUTP OFF') + four_point_data = (res1 + res2) / 2 + return four_point_data + + + @check_initialized + def get_data(self, filename: str = None, four_point: bool = False): + """Retrieve data from buffer and save locally""" + + if not four_point: + self.keithley.write(':TRACe:ACTual:END?') + end_index = int(self.keithley.read()) + self.keithley.write(f':TRACe:DATA? 1, {end_index}, "defbuffer1", RELative, SOURce, READing') + results = self.keithley.read() + results = results.split(',') + results = list(map(float, results)) + data = { + 'time' : results[::3], + 'source' : results[1::3], + 'reading' : results[2::3] + } + else: + self.keithley.write(':TRACe:ACTual:END?') + end_index = int(self.keithley.read()) + self.keithley.write(f':TRACe:DATA? 1, {end_index}, "defbuffer1"') + data = self.keithley.read() + + if filename is None: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = timestamp + + dataframe = pd.DataFrame(data) + + fullfilename = self.save_directory + filename + '.csv' + dataframe.to_csv(fullfilename, mode = 'w', index = False) + + return (True, "Saved data to at " + fullfilename) + + diff --git a/util.py b/util.py index 64e8ea6..5ed9152 100644 --- a/util.py +++ b/util.py @@ -9,6 +9,7 @@ from devices.dummy_heater import DummyHeater from devices.dummy_motor import DummyMotor from devices.linear_stage_150 import LinearStage150 +from devices.keithley_2450 import Keithley2450 from devices.device import Device, MiscDeviceClass import json import numpy as np @@ -334,6 +335,201 @@ def default(self, obj): }, }, }, + "Keithley2450": { # TODO finish this + "obj": Keithley2450, + "serial": True, + "serial_sequence": ["Keithley2450Initialize"], + "import_device": "from devices.keithley_2450 import Keithley2450", + "import_commands": "from commands.keithley_2450_commands import *", + "init": { + "default_code": "Keithley2450(name='Keithley2450', ID='', query_delay=0)", + "obj_name": "Keithley2450", + "args": { + "name": { + "default": "Keithley2450", + "type": str, + "notes": "Name of the device", + }, + "ID": { + "default": "", + "type": str, + "notes": "ID of the device", + }, + "query_delay": { + "default": 0, + "type": int, + "notes": "Delay between queries", + }, + }, + }, + "commands": { + "Keithley2450Initialize": { + "default_code": "Keithley2450Initialize(receiver= '')", + "args": { + "receiver": { + "default": "Keithley2450", + "type": str, + "notes": "", + } + }, + }, + "Keithley2450Deinitialize": { + "default_code": "Keithley2450Deinitialize(receiver= '')", + "args": { + "receiver": { + "default": "Keithley2450", + "type": str, + "notes": "", + } + }, + }, + "KeithleyWait": { + "default_code": "KeithleyWait(receiver= '')", + "args": { + "receiver": { + "default": "Keithley2450", + "type": str, + "notes": "", + } + }, + }, + "KeithleyWriteCommand": { + "default_code": "KeithleyWriteCommand(receiver= '', command= '')", + "args": { + "receiver": { + "default": "Keithley2450", + "type": str, + "notes": "", + }, + "command": { + "default": "", + "type": str, + "notes": "", + }, + }, + }, + "KeithleySetTerminal": { + "default_code": "KeithleySetTerminal(receiver= '', position= 'front')", + "args": { + "receiver": { + "default": "Keithley2450", + "type": str, + "notes": "", + }, + "position": { + "default": "front", + "type": str, + "notes": "", + }, + }, + }, + "KeithleyErrorCheck": { + "default_code": "KeithleyErrorCheck(receiver= '')", + "args": { + "receiver": { + "default": "Keithley2450", + "type": str, + "notes": "", + } + }, + }, + "KeithleyClearBuffer": { + "default_code": "KeithleyClearBuffer(receiver= '', buffer='defbuffer1')", + "args": { + "receiver": { + "default": "Keithley2450", + "type": str, + "notes": "", + }, + "buffer": { + "default": "defbuffer1", + "type": str, + "notes": "", + }, + }, + }, + "KeithleyIVCharacteristic": { + "default_code": "KeithleyIVCharacteristic(receiver= '', ilimit=0.0, vmin=0.0, vmax=0.0, delay=0.0, steps=60)", + "args": { + "receiver": { + "default": "Keithley2450", + "type": str, + "notes": "", + }, + "ilimit": { + "default": 0.0, + "type": float, + "notes": "", + }, + "vmin": { + "default": 0.0, + "type": float, + "notes": "", + }, + "vmax": { + "default": 0.0, + "type": float, + "notes": "", + }, + "delay": { + "default": 0.0, + "type": float, + "notes": "", + }, + "steps": { + "default": 60, + "type": int, + "notes": "", + }, + }, + }, + "KeithleyFourPoint": { + "default_code": "KeithleyFourPoint(receiver= '', test_curr=0.0, vlimit=0.0, curr_reversal = False)", + "args": { + "receiver": { + "default": "Keithley2450", + "type": str, + "notes": "", + }, + "test_curr": { + "default": 0.0, + "type": float, + "notes": "", + }, + "vlimit": { + "default": 0.0, + "type": float, + "notes": "", + }, + "curr_reversal": { + "default": False, + "type": bool, + "notes": "", + }, + }, + }, + "KeithleyGetData": { + "default_code": "KeithleyGetData(receiver= '', filename=None, four_point= False)", + "args": { + "receiver": { + "default": "Keithley2450", + "type": str, + "notes": "", + }, + "filename": { + "default": None, + "type": str, + "notes": "", + }, + "four_point": { + "default": False, + "type": bool, + "notes": "", + }, + }, + }, + }, + }, "PrintingStage": heating_stage_ref, "AnnealingStage": heating_stage_ref, "MultiStepper": { From 7526ae8f1e811906fa80e7543c0966666a577caf Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 14 Jul 2023 13:49:46 -0500 Subject: [PATCH 035/125] added abstract methods to current keithley --- devices/keithley_2450.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/devices/keithley_2450.py b/devices/keithley_2450.py index 1639d3e..38a42ba 100644 --- a/devices/keithley_2450.py +++ b/devices/keithley_2450.py @@ -19,6 +19,19 @@ def __init__(self, name: str, ID: str, query_delay: int): print(self.keithley.query('*IDN?')) sleep(self.query_delay) + def get_init_args(self) -> dict: + args_dict = { + "name": self.name, + "ID": self.ID, + "query_delay": self.query_delay, + } + return args_dict + + def update_init_args(self, args_dict: dict): + self.name = args_dict["name"] + self.ID = args_dict["ID"] + self.query_delay = args_dict["query_delay"] + @property #TODO: Check if necessary def terminal_pos(self) -> str: return self.position From bb6cb2c2e5817ca0b714c9122c843c8e3766c7a3 Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 14 Jul 2023 14:36:36 -0500 Subject: [PATCH 036/125] upgraded manual execute modal --- app.py | 159 ++++++++++++++++++++++++++++++++++------ pages/manual-control.py | 43 +++++++---- 2 files changed, 165 insertions(+), 37 deletions(-) diff --git a/app.py b/app.py index 8960e37..977fe0c 100644 --- a/app.py +++ b/app.py @@ -928,11 +928,24 @@ def create_manual_control_device_form(value, url): args = util.devices_ref_redundancy[value]["init"]["args"] toRet = [] for arg in args: - if arg == 'port': + if arg == "port": toRet.append( dbc.Row( [ - dbc.Label(dbc.NavLink(arg, n_clicks=0, id='manual-control-port-field', style={'cursor': 'pointer', 'color': 'blue', 'textDecoration': 'underline'}), html_for=str(value + "+" + arg), width=2), + dbc.Label( + dbc.NavLink( + arg, + n_clicks=0, + id="manual-control-port-field", + style={ + "cursor": "pointer", + "color": "blue", + "textDecoration": "underline", + }, + ), + html_for=str(value + "+" + arg), + width=2, + ), dbc.Col( [ dbc.Input( @@ -951,7 +964,9 @@ def create_manual_control_device_form(value, url): toRet.append( dbc.Row( [ - dbc.Label([arg], html_for=str(value + "+" + arg), width=2), + dbc.Label( + [arg], html_for=str(value + "+" + arg), width=2 + ), dbc.Col( [ dbc.Input( @@ -1086,7 +1101,7 @@ def manual_control_clear_button(value, url): @app.callback( - Output("manual-control-execute-button", "disabled"), + Output("manual-control-open-execute-modal-button", "disabled"), Input("manual-control-command-dropdown", "value"), State("url", "pathname"), ) @@ -1099,6 +1114,21 @@ def manual_control_execute_button(value, url): return False +@app.callback( + [ + Output("manual-control-execute-modal", "is_open", allow_duplicate=True), + Output("manual-control-execute-modal-body", "children", allow_duplicate=True), + ], + Input("manual-control-open-execute-modal-button", "n_clicks"), + State("url", "pathname"), + prevent_initial_call=True, +) +def open_manual_control_execute_modal(n, url): + if str(url) == "/manual-control": + print("open_manual_control_execute_modal") + return True, [] + + @app.callback( [ Output("manual-control-command-dropdown", "className"), @@ -1106,11 +1136,11 @@ def manual_control_execute_button(value, url): Output("manual-control-alert", "children", allow_duplicate=True), Output("manual-control-alert", "color", allow_duplicate=True), Output("manual-control-alert", "duration", allow_duplicate=True), - Output('manual-control-execute-modal', 'is_open'), - Output('manual-control-execute-modal-body', 'children'), - Output('manual-control-execute-modal-body-code', 'children') + Output( + "manual-control-execute-modal-body-code", "children", allow_duplicate=True + ), ], - Input("manual-control-execute-button", "n_clicks"), + Input("manual-control-open-execute-modal-button", "n_clicks"), [ State("url", "pathname"), State("manual-control-command-dropdown", "className"), @@ -1121,11 +1151,19 @@ def manual_control_execute_button(value, url): ], prevent_initial_call=True, ) -def manual_control_execute(n, url, opt, device, command, device_form, command_form): +def manual_control_execute_fill_code( + n, url, opt, device, command, device_form, command_form +): if str(url) == "/manual-control": - print("manual_control_execute") + print("manual_control_execute_fill_code") if device is None or device == "" or command is None or command == "": - return opt, True, "Something went wrong. Check the fields below.", "danger", 3000 + return ( + opt, + True, + "Something went wrong. Check the fields below.", + "danger", + 3000, + ) code = "" code += util.devices_ref_redundancy[device]["import_device"] code += "\n" @@ -1157,7 +1195,7 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo instantiate_code += ")" code_seq = str(device) + "_seq" code += code_seq + " = CommandSequence()" - code += "\n" + code += "\n\n" code += code_seq + ".add_device(" + instantiate_code + ")" code += "\n" @@ -1268,7 +1306,7 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo ) ) - code += "))\n" + code += "))\n\n" else: code += code_seq + ".add_command(" + str(command) + "(" for ii, seq_command_arg in enumerate( @@ -1317,16 +1355,16 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo ) ) - code += "))\n" + code += "))\n\n" code += ( str(device) + "_seq_invoker = CommandInvoker(" + str(device) + "_seq, False, False, False)\n" ) - code += str(device) + "_seq_invoker.invoke_commands()\n" + code += str(device) + "_seq_invoker.invoke_commands()" interceptor = ConsoleInterceptor() - print('\n') + print("\n") interceptor.start_interception() print(code) interceptor.stop_interception() @@ -1334,16 +1372,79 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo code_log_string = "" for msg in code_output: code_log_string += msg + # interceptor = ConsoleInterceptor() + # interceptor.start_interception() + # try: + # exec(code) + # interceptor.stop_interception() + # messages = interceptor.get_intercepted_messages() + # log_string = "" + # for msg in messages: + # if msg == '\r': + # log_string += '\n' + # else: + # log_string += msg + # print(messages) + return ( + opt, + True, + "Command(s) ready to execute.", + "warning", + 0, + html.Pre(code_log_string), + ) + # except Exception as e: + # print(e) + # # interceptor.stop_interception() + # # messages = interceptor.get_intercepted_messages() + # # log_string = "" + # # for msg in messages: + # # log_string += msg + # return opt, True, "Something went wrong. Check logs.", "Warning", 0, True, html.Pre(code_log_string) + + return opt + + +@app.callback( + [ + Output("manual-control-alert", "is_open"), + Output("manual-control-alert", "children"), + Output("manual-control-alert", "color"), + Output("manual-control-alert", "duration"), + Output("manual-control-execute-modal-body", "children"), + ], + [Input("manual-control-execute-button", "n_clicks")], + [ + State("url", "pathname"), + State("manual-control-execute-modal-body-code", "children"), + ], + prevent_initial_call=True, +) +def manual_control_execute_code(n, url, code): + if str(url) == "/manual-control": + print("manual_control_execute_code") interceptor = ConsoleInterceptor() + # print('\n') + # interceptor.start_interception() + # print(code) + # interceptor.stop_interception() + # code_output = interceptor.get_intercepted_messages() + # code_log_string = "" + # for msg in code_output: + # code_log_string += msg + # interceptor = ConsoleInterceptor() interceptor.start_interception() try: - exec(code) + exec(code["props"]["children"]) interceptor.stop_interception() messages = interceptor.get_intercepted_messages() log_string = "" for msg in messages: - log_string += msg - return opt, True, "Execution complete.", "success", 0, True, html.Pre(log_string), html.Pre(code_log_string) + if msg == "\r": + log_string += "\n" + else: + log_string += msg + return True, "Execution complete.", "success", 0, html.Pre(log_string) except Exception as e: print(e) interceptor.stop_interception() @@ -1351,14 +1452,24 @@ def manual_control_execute(n, url, opt, device, command, device_form, command_fo log_string = "" for msg in messages: log_string += msg - return opt, True, "Something went wrong. Check logs.", "danger", 0, True, html.Pre(log_string), html.Pre(code_log_string) + return ( + True, + "Something went wrong. Check logs.", + "danger", + 0, + html.Pre(log_string), + ) + return False, "", "success", 0, html.Pre("") - return opt @app.callback( - [Output('manual-control-port-modal', 'is_open'), Output('manual-control-serial-ports-info', 'children')], - Input('manual-control-port-field', 'n_clicks'), - prevent_initial_call=True, suppress_callback_exceptions=True + [ + Output("manual-control-port-modal", "is_open"), + Output("manual-control-serial-ports-info", "children"), + ], + Input("manual-control-port-field", "n_clicks"), + prevent_initial_call=True, + suppress_callback_exceptions=True, ) def open_fill_manual_control_serial(n): if _has_serial and n != 0: diff --git a/pages/manual-control.py b/pages/manual-control.py index 0432647..bdc8e76 100644 --- a/pages/manual-control.py +++ b/pages/manual-control.py @@ -14,8 +14,8 @@ dbc.ButtonGroup( [ dbc.Button( - "Execute", - id="manual-control-execute-button", + "Open Execute Window", + id="manual-control-open-execute-modal-button", n_clicks=0, disabled=True, ), @@ -30,11 +30,30 @@ dbc.ModalHeader(dbc.ModalTitle("Execute")), dbc.ModalBody( [ - dbc.Alert( - "Alert", - id="manual-control-alert", - is_open=False, - duration=500, + dbc.Row( + [ + dbc.Col( + [ + dbc.Button( + "Execute", + id="manual-control-execute-button", + n_clicks=0, + style={"width": "100%"}, + ) + ], + width=2, + ), + dbc.Col( + [ + dbc.Alert( + "Alert", + id="manual-control-alert", + is_open=False, + duration=500, + ) + ] + ), + ] ), html.Div( id="manual-control-execute-modal-body-code", @@ -44,7 +63,7 @@ "overflow-y": "scroll", "padding": "10px", "border": "2px solid", - "margin-bottom": "10px" + "margin-bottom": "10px", }, ), html.Div( @@ -65,12 +84,10 @@ ), dbc.Modal( [ - dbc.ModalHeader(dbc.ModalTitle("COM Port Information")), - dbc.ModalBody([ - html.Div(id='manual-control-serial-ports-info') - ]) + dbc.ModalHeader(dbc.ModalTitle("COM Port Information")), + dbc.ModalBody([html.Div(id="manual-control-serial-ports-info")]), ], - id='manual-control-port-modal' + id="manual-control-port-modal", ), dbc.Row( [ From fc62e22b7cf913d7f5ed9ce360af61ef61677007 Mon Sep 17 00:00:00 2001 From: Piyush Date: Wed, 19 Jul 2023 01:31:18 -0500 Subject: [PATCH 037/125] many many changes --- app.py | 362 ++++++++++++++++++++++++++++++++++++---- command_sequence.py | 194 +++++++++++++++++---- pages/execute-recipe.py | 63 +++++-- pages/load-recipe.py | 14 +- pages/manual-control.py | 10 +- pages/view-recipe.py | 86 ++++++++-- util.py | 205 ++++++++++++++++++----- 7 files changed, 797 insertions(+), 137 deletions(-) diff --git a/app.py b/app.py index 977fe0c..c06b597 100644 --- a/app.py +++ b/app.py @@ -52,21 +52,33 @@ navbar = dbc.NavbarSimple( children=[ - dbc.NavItem(dbc.NavLink("Home", href="/")), + dbc.NavItem(dbc.NavLink("Home", href="/", external_link=True)), dbc.DropdownMenu( children=[ - dbc.DropdownMenuItem("Load Recipe", href="/load-recipe"), - dbc.DropdownMenuItem("View Recipe", href="/view-recipe"), - dbc.DropdownMenuItem("Edit Recipe", href="/edit-recipe"), - dbc.DropdownMenuItem("Execute Recipe", href="/execute-recipe"), - dbc.DropdownMenuItem("Document", href="/data"), + dbc.DropdownMenuItem( + "Load Recipe", href="/load-recipe", external_link=True + ), + dbc.DropdownMenuItem( + "View Recipe", href="/view-recipe", external_link=True + ), + dbc.DropdownMenuItem( + "Edit Code", href="/edit-recipe", external_link=True + ), + dbc.DropdownMenuItem( + "Execute Recipe", href="/execute-recipe", external_link=True + ), + dbc.DropdownMenuItem("Document", href="/data", external_link=True), ], nav=True, in_navbar=True, label="Recipe", ), - dbc.NavItem(dbc.NavLink("Manual Control", href="/manual-control")), - dbc.NavItem(dbc.NavLink("Database Browser", href="/database")), + dbc.NavItem( + dbc.NavLink("Manual Control", href="/manual-control", external_link=True) + ), + dbc.NavItem( + dbc.NavLink("Database Browser", href="/database", external_link=True) + ), ], brand="AAMP", brand_href="/", @@ -86,6 +98,24 @@ def print_pagename(url): # all pages return "AAMP" +def update_upstream_recipe_dict(): + print("update_upstream_recipe_dict") + if "document" in list(com.__dict__.keys()): + recipe_dict = com.get_recipe() + com.document["recipe_dict"] = { + "devices": recipe_dict[0], + "commands": recipe_dict[1], + } + mongo.db["recipes"].update_one( + {"_id": com.document["_id"]}, {"$set": com.document} + ) + print("successfully updated recipe_dict upstream") + return True + else: + print("com.document not found") + return False + + # --------------------------------------------------- # Home Page # --------------------------------------------------- @@ -132,17 +162,18 @@ def fetch_recipe_list(n_clicks): # homepage @app.callback( Output("filename-input", "value"), Input("home-recipes-list-table", "active_cell"), - State("home-recipes-list-table", "data"), + [State("url", "pathname"), State("home-recipes-list-table", "data")], # prevent_initial_call=True, ) -def fill_filename_input(active_cell, data): # homepage - if active_cell is not None: - print("fill_filename_input") - return data[active_cell["row"]]["file_name"] - if hasattr(com, "document"): - print("fill_filename_input") - return com.document["file_name"] - return "" +def fill_filename_input(active_cell, url, data): # homepage + if str(url) == "/load-recipe": + if active_cell is not None: + print("fill_filename_input") + return data[active_cell["row"]]["file_name"] + if "document" in list(com.__dict__.keys()): + print("fill_filename_input") + return com.document["file_name"] + return "" @app.callback( @@ -162,6 +193,15 @@ def get_document_from_db(n_clicks, filename): # homepage # Extract the YAML content from the document document = mongo.find_documents("recipes", {"file_name": filename})[0] invoker.clear_log_file() + try: + com.clear_recipe() + com.load_from_dict(document["recipe_dict"]) + com.document = document + print("loaded from dict") + return [True, "Recipe loaded", "success", 10000] + + except Exception as e: + print("Failed load from dict: " + str(e)) if os.name == "posix": if "posix_friendly" in document and not document["posix_friendly"]: return [ @@ -193,6 +233,31 @@ def get_document_from_db(n_clicks, filename): # homepage return [True, "No recipe selected", "warning", 3000] +@app.callback( + [ + Output("home-load-file-alert", "is_open", allow_duplicate=True), + Output("home-load-file-alert", "children", allow_duplicate=True), + Output("home-load-file-alert", "color", allow_duplicate=True), + Output("home-load-file-alert", "duration", allow_duplicate=True), + ], + Input("home-create-new-recipe-button", "n_clicks"), + [State("url", "pathname"), State("filename-input", "value")], + prevent_initial_call=True, +) +def create_new_recipe_doc(n, url, name): + if str(url) == "/load-recipe": + print("create_new_recipe_doc") + mongo.db["recipes"].insert_one( + { + "file_name": name, + "recipe_dict": {"devices": [], "commands": []}, + "dash_friendly": True, + } + ) + + return [True, "Recipe created", "success", 10000] + + # --------------------------------------------------- # View Recipe Page # --------------------------------------------------- @@ -271,12 +336,19 @@ def save_command(n_clicks, active_cell, data, value): # view-recipe page json.loads(value) ): com.command_list[data[active_cell["row"]]["index"]][0]._params = eval(value) + update_upstream_recipe_dict() return None return data @app.callback( - Output("devices-table", "data"), + [ + Output("devices-table", "data"), + Output("view-recipe-alert", "is_open", allow_duplicate=True), + Output("view-recipe-alert", "children", allow_duplicate=True), + Output("view-recipe-alert", "color", allow_duplicate=True), + Output("view-recipe-alert", "duration", allow_duplicate=True), + ], Input("save-device-editor", "n_clicks"), [ State("devices-table", "active_cell"), @@ -293,8 +365,9 @@ def save_device(n_clicks, active_cell, data, value): # view-recipe page # data_row = data[active_cell["row"]] params = eval(value) com.device_by_name[params["name"]].update_init_args(params) - return None - return data + update_success = update_upstream_recipe_dict() + return None, True, "Device updated.", "success", 3000 + return data, False, "Something went wrong. Device not updated.", "danger", 3000 @app.callback( @@ -319,7 +392,7 @@ def fill_command_json_editor(is_open, active_cell, data): # view-recipe page ) def fill_device_add_modal(is_open, active_cell, data): # view-recipe page print("fill_device_add_modal") - return util.approved_devices + return list(util.devices_ref_redundancy.keys()) @app.callback( @@ -331,13 +404,18 @@ def fill_device_add_json_editor(value, is_open): # view-recipe page print("fill_device_add_json_editor") if not is_open or value is None: return [""] - args_list = inspect.getfullargspec(util.named_devices[value].__init__).args + # args_list = inspect.getfullargspec(util.devices_ref_redundancy[value]['obj'].__init__).args args_dict = {} + # for arg in args_list: + # if arg != "self" and arg != "name": + # args_dict[arg] = None + # if arg == "name": + # args_dict[arg] = value + args_list = list(util.devices_ref_redundancy[value]["init"]["args"].keys()) for arg in args_list: - if arg != "self" and arg != "name": - args_dict[arg] = None - if arg == "name": - args_dict[arg] = value + args_dict[arg] = util.devices_ref_redundancy[value]["init"]["args"][arg][ + "default" + ] return [(json.dumps(args_dict, indent=4))] @@ -363,7 +441,7 @@ def fill_device_json_editor(is_open, active_cell, data): # view-recipe page str_ports += f"{port}: {desc} [{hwid}]\n" lines = str_ports.splitlines() device_port_html = [ - html.Div(["COM Port Info:"], style={"font-weight": "bold"}) + html.Div(["COM Port Info:"], style={"fontWeight": "bold"}) ] device_port_html.append(html.Div([html.Div(line) for line in lines])) else: @@ -449,7 +527,7 @@ def enable_add_device_button(value, device_type, is_open): # view-recipe page + str(args[key]) ) if type((parsed_json[key])) != args[key]: - return True, f"Invalid type for {key}. Expected {str(args[key])}" + return False, f"Invalid type for {key}. Expected {str(args[key])}" # if not isinstance(parsed_json[key], args[key]): # return True, f"Invalid type for {key}. Expected {str(args[key])}" print("enable_add_device_button") @@ -457,7 +535,34 @@ def enable_add_device_button(value, device_type, is_open): # view-recipe page except Exception as e: if type(e) == json.decoder.JSONDecodeError: return True, "Invalid JSON" - return True, str(type(e)) + return False, str(type(e)) + + +@app.callback( + [ + Output("view-recipe-alert", "is_open"), + Output("view-recipe-alert", "children"), + Output("view-recipe-alert", "color"), + Output("view-recipe-alert", "duration"), + ], + [Input("add-device-editor", "n_clicks")], + [ + State("add-device-dropdown", "value"), + State("url", "pathname"), + State("add-device-json-editor", "value"), + ], + prevent_initial_call=True, +) +def view_recipe_add_device(n, device_type, url, device_dict): + if str(url) == "/view-recipe": + com.add_device_from_dict(device_type, json.loads(device_dict)) + update_success = update_upstream_recipe_dict() + return ( + True, + f"Added {device_type}. Database updated: {update_success}", + "success", + 3000, + ) @app.callback( @@ -478,7 +583,6 @@ def edit_command_button(table_div_children): # view-recipe page Input("devices-table", "active_cell"), ) def edit_device_button(table_div_children): # view-recipe page - print("edit_device_button") active_cell = table_div_children if active_cell is not None: return False @@ -486,6 +590,61 @@ def edit_device_button(table_div_children): # view-recipe page return True +@app.callback( + Output("delete-device-button", "disabled"), + Input("devices-table", "active_cell"), +) +def view_recipe_enable_delete_device_button(table_div_children): + active_cell = table_div_children + if active_cell is not None: + return False + else: + return True + + +@app.callback( + Output("delete-command-button", "disabled"), Input("commands-table", "active_cell") +) +def view_recipe_enable_delete_command_button(table_div_children): + active_cell = table_div_children + if active_cell is not None: + return False + else: + return True + + +@app.callback( + Output("devices-table", "data", allow_duplicate=True), + Input("delete-device-button", "n_clicks"), + [ + State("url", "pathname"), + State("devices-table", "active_cell"), + State("devices-table", "data"), + ], + prevent_initial_call=True, +) +def view_recipe_delete_device(n, url, active_cell, data): + if str(url) == "/view-recipe": + com.remove_device_by_index(data[active_cell["row"]]["index"]) + update_success = update_upstream_recipe_dict() + return None + +@app.callback( + Output("commands-table", "data", allow_duplicate=True), + Input("delete-command-button", "n_clicks"), + [ + State("url", "pathname"), + State("commands-table", "active_cell"), + State("commands-table", "data"), + ], + prevent_initial_call=True, +) +def view_recipe_delete_command(n, url, active_cell, data): + if str(url) == "/view-recipe": + com.remove_command(data[active_cell["row"]]["index"]) + update_success = update_upstream_recipe_dict() + return None + @app.callback( Output("commands-table-div", "children"), [Input("refresh-button2", "n_clicks"), Input("commands-table", "data")], @@ -540,6 +699,122 @@ def update_commands_table(n_clicks, data, table): # view-recipe page return table +@app.callback( + Output("view-recipe-command-add-modal", "is_open"), + Input("add-command-open-modal-button", "n_clicks"), + [State("url", "pathname")], + prevent_initial_call=True, +) +def view_recipe_open_add_command_modal(n, url): + if url == "/view-recipe": + return True + + +@app.callback( + Output("view-recipe-add-command-device-dropdown", "options"), + Input("view-recipe-command-add-modal", "is_open"), + State("url", "pathname"), + prevent_initial_call=True, +) +def view_recipe_fill_add_command_device_dropdown(is_open, url): + if url == "/view-recipe": + print("view_recipe_fill_add_command_device_dropdown") + return list(util.devices_ref_redundancy.keys()) + + +@app.callback( + Output("view-recipe-add-command-command-dropdown", "options"), + Input("view-recipe-add-command-device-dropdown", "value"), + State("url", "pathname"), + prevent_initial_call=True, +) +def view_recipe_fill_add_command_command_dropdown(device_type, url): + if url == "/view-recipe": + if device_type is None or device_type == "": + return [] + return list(util.devices_ref_redundancy[device_type]["commands"].keys()) + + +@app.callback( + Output("view-recipe-add-command-json-editor", "value"), + Input("view-recipe-add-command-command-dropdown", "value"), + [ + State("url", "pathname"), + State("view-recipe-add-command-device-dropdown", "value"), + ], + prevent_initial_call=True, +) +def view_recipe_fill_add_command_json_editor(command_type, url, device_type): + if url == "/view-recipe": + if command_type is None or command_type == "": + return "" + args_dict = {} + args_list = util.devices_ref_redundancy[device_type]["commands"][command_type][ + "args" + ] + for arg in args_list: + args_dict[arg] = args_list[arg]["default"] + args_dict["delay"] = 0.0 + return json.dumps(args_dict, indent=4) + + +@app.callback( + [ + Output("view-recipe-add-command-editor", "disabled"), + Output("add-command-error", "children"), + ], + Input("view-recipe-add-command-json-editor", "value"), + State("url", "pathname"), +) +def view_recipe_check_add_command_json(value, url): + if str(url) == "/view-recipe": + if value == "" or value is None: + return True, [] + try: + json.loads(value) + return False, [] + except Exception as e: + return True, ["Invalid JSON: " + str(e)] + + +@app.callback( + [ + Output("view-recipe-command-add-modal", "is_open", allow_duplicate=True), + Output("view-recipe-alert", "is_open", allow_duplicate=True), + Output("view-recipe-alert", "children", allow_duplicate=True), + Output("view-recipe-alert", "color", allow_duplicate=True), + Output("view-recipe-alert", "duration", allow_duplicate=True), + Output("commands-table", "data", allow_duplicate=True), + ], + Input("view-recipe-add-command-editor", "n_clicks"), + [ + State("url", "pathname"), + State("view-recipe-add-command-device-dropdown", "value"), + State("view-recipe-add-command-command-dropdown", "value"), + State("view-recipe-add-command-json-editor", "value"), + ], + prevent_initial_call=True, +) +def view_recipe_add_command(n, url, device_type, command_type, json_value): + if str(url) == "/view-recipe": + if json_value == "" or json_value is None: + return [False, True, ["Something went wrong"], "danger", 3000] + try: + com.add_command_from_dict(device_type, command_type, json.loads(json_value)) + update_upstream_recipe_dict() + return [False, True, ["Command added"], "success", 3000, None] + except Exception as e: + print(str(e)) + return [ + False, + True, + ["Something went wrong: " + str(e)], + "danger", + 3000, + None, + ] + + # --------------------------------------------------- # Python Edit Recipe Page # --------------------------------------------------- @@ -784,7 +1059,7 @@ def kill_execution(): # execute-recipe page ) def stop_execution(n): # execute-recipe page print("stop_execution") - # kill_execution() + kill_execution() return [] @@ -804,11 +1079,11 @@ def execute_recipe(n_clicks): # execute-recipe page @app.callback( Output("console-out2", "children"), Input("interval1", "n_intervals"), - [State("url", "pathname"), State("show-log-switch", "value")], + [State("url", "pathname")], prevent_initial_call=True, ) -def update_output(n, url, switch_val): # execute-recipe page - if url == "/execute-recipe" and switch_val: +def update_output(n, url): # execute-recipe page + if url == "/execute-recipe": log_string = "" log_list = invoker.get_log_messages() for msg in log_list: @@ -830,6 +1105,24 @@ def reset_console(n): # execute-recipe page return [] +@app.callback( + Output("execute-recipe-upload-document", "value"), + Input("reset-button", "n_clicks"), + State("url", "pathname"), +) +def execute_recipe_load_document_viewer(n, url): + if str(url) == "/execute-recipe": + if com.device_list != []: + toRet = "" + recipe_ec = com.get_recipe() + for device in recipe_ec[0]: + toRet += str(device) + "\n" + toRet += "\n" + for command in recipe_ec[1]: + toRet += str(command) + "\n" + return [toRet] + + # --------------------------------------------------- # Data Page # --------------------------------------------------- @@ -1469,7 +1762,6 @@ def manual_control_execute_code(n, url, code): ], Input("manual-control-port-field", "n_clicks"), prevent_initial_call=True, - suppress_callback_exceptions=True, ) def open_fill_manual_control_serial(n): if _has_serial and n != 0: diff --git a/command_sequence.py b/command_sequence.py index cc03138..d0e0b86 100644 --- a/command_sequence.py +++ b/command_sequence.py @@ -8,6 +8,7 @@ from commands.command import Command from devices.device import Device from commands.utility_commands import LoopStartCommand, LoopEndCommand +import inspect import util @@ -17,14 +18,18 @@ # Make functional with yaml safe_load # reconsider add_commands functionality for list of commands + class CommandSequence: """Maintains a command list with optional iterations and looping. Supports saving/loading.""" - recipe_directory = '' + + recipe_directory = "" def __init__(self): self.device_list = [] + self.db_device_list = [] self.command_list = [] - self.num_iterations = 'ALL' + self.db_command_list = [] + self.num_iterations = "ALL" # self.processed_devices = [] # self.processed_commands = [] # self.processed_delays = [] @@ -64,7 +69,7 @@ def remove_device(self, receiver_name: str): if device.name == receiver_name: del self.device_list[ndx] self.update_device_by_name() - break # Each device should have a unique name, always + break # Each device should have a unique name, always def remove_device_by_index(self, index: Optional[int] = None): """Remove a device from the device list by index then update the device dict. @@ -79,7 +84,9 @@ def remove_device_by_index(self, index: Optional[int] = None): del self.device_list[index] self.update_device_by_name() - def add_command(self, command: Union[Command, List[Command]], index: Optional[int] = None): + def add_command( + self, command: Union[Command, List[Command]], index: Optional[int] = None + ): """Add a command to the command list. Parameters @@ -95,7 +102,7 @@ def add_command(self, command: Union[Command, List[Command]], index: Optional[in self.command_list.append(command) else: self.command_list.insert(index, command) - + def remove_command(self, index: Optional[int] = None): """Remove a command from the command list. @@ -118,7 +125,12 @@ def move_command_by_index(self, old_index: int, new_index: int): new_index : int The index to move the command(s) to """ - if old_index >= 0 and old_index <= len(self.command_list) - 1 and new_index >= 0 and new_index <= len(self.command_list) - 1: + if ( + old_index >= 0 + and old_index <= len(self.command_list) - 1 + and new_index >= 0 + and new_index <= len(self.command_list) - 1 + ): if old_index != new_index: self.command_list.insert(new_index, self.command_list.pop(old_index)) else: @@ -144,7 +156,12 @@ def add_loop_end(self, index: Optional[int] = None): """ self.add_command(LoopEndCommand(), index) - def add_command_iteration(self, command: Union[Command, List[Command]], index: Optional[int] = None, iteration: Optional[int] = None): + def add_command_iteration( + self, + command: Union[Command, List[Command]], + index: Optional[int] = None, + iteration: Optional[int] = None, + ): """Add command(s) as iterations to a pre-existing command in the command list. Parameters @@ -168,7 +185,9 @@ def add_command_iteration(self, command: Union[Command, List[Command]], index: O for ndx in range(len(command)): self.command_list[index].insert(iteration, command.pop(-1)) - def remove_command_iteration(self, index: Optional[int] = None, iteration: Optional[int] = None): + def remove_command_iteration( + self, index: Optional[int] = None, iteration: Optional[int] = None + ): """Remove a command iteration from a command's iteration list. Parameters @@ -196,9 +215,16 @@ def move_command_iteration_by_index(self, index: int, old_iter: int, new_iter: i new_iter : int The new iteration index to move the iteration to """ - if old_iter >= 0 and old_iter <= len(self.command_list[index]) - 1 and new_iter >= 0 and new_iter <= len(self.command_list[index]) - 1: + if ( + old_iter >= 0 + and old_iter <= len(self.command_list[index]) - 1 + and new_iter >= 0 + and new_iter <= len(self.command_list[index]) - 1 + ): if old_iter != new_iter: - self.command_list[index].insert(new_iter, self.command_list[index].pop(old_iter)) + self.command_list[index].insert( + new_iter, self.command_list[index].pop(old_iter) + ) else: print("Invalid indices") @@ -235,7 +261,7 @@ def verify_command_list(self) -> Tuple[bool, str]: # Important Note!: So far, this doesn't consider composite commands since they can hide away other commands which may break the entire recipe # One way is to unwrap the composite command, but keep in mind since composite commands behave like commands, this means a composite command # can have composite commands. Therefore, composite commands can be arbitrarily deep and can potentially be recursive, causing an infinite loop - + # iteration lists must have commands of same class? Not necessarily. Currently not enforced loop_start_location = [] loop_end_location = [] @@ -250,10 +276,13 @@ def verify_command_list(self) -> Tuple[bool, str]: return False, "There can only be one loop start" if len(loop_end_location) > 1: return False, "There can only be one loop end" - + if len(loop_start_location) != len(loop_end_location): - return False, "There must either be no loop start/end or exactly one of each" - + return ( + False, + "There must either be no loop start/end or exactly one of each", + ) + if len(loop_start_location) > 0: if len(self.command_list[loop_start_location[0][0]]) > 1: return False, "Loop start command can not have additional iterations" @@ -299,7 +328,7 @@ def verify(self) -> Tuple[bool, str]: is_valid, message = self.verify_num_iterations() if not is_valid: return (is_valid, message) - return True, "All checks passed" + return True, "All checks passed" def update_device_by_name(self): """Update the device dict that stores each device/receiver by it's name.""" @@ -316,15 +345,17 @@ def get_unlooped_command_list(self) -> List[Command]: List[Command] The unlooped command list """ - unlooped_list= [] + unlooped_list = [] command_generator = self.yield_next_command() for command in command_generator: if not isinstance(command, Command): break unlooped_list.append(command) - return unlooped_list + return unlooped_list - def yield_next_command(self) -> Generator[Union[Command, Tuple[bool, str]], None, None]: + def yield_next_command( + self, + ) -> Generator[Union[Command, Tuple[bool, str]], None, None]: """A generator that yields each command sequentially, accounting for loops. Yields @@ -348,7 +379,7 @@ def yield_next_command(self) -> Generator[Union[Command, Tuple[bool, str]], None index = 0 iteration = 0 loop_start_index = None - if self.num_iterations == 'ALL': + if self.num_iterations == "ALL": max_iterations = self.get_max_iteration() else: max_iterations = self.num_iterations @@ -360,7 +391,7 @@ def yield_next_command(self) -> Generator[Union[Command, Tuple[bool, str]], None command = self.command_list[index][-1] else: command = self.command_list[index][iteration] - + if isinstance(command, LoopStartCommand): loop_start_index = index index += 1 @@ -402,7 +433,7 @@ def save_to_yaml(self, filename: str): """ filename = self.recipe_directory + filename data_list = [self.device_list, self.command_list, self.num_iterations] - with open(filename, 'w') as file: + with open(filename, "w") as file: yaml.dump(data_list, file, default_flow_style=False, sort_keys=False) def load_from_yaml(self, filename: str): @@ -434,7 +465,7 @@ def get_command_names(self, unloop: bool = False) -> List[str]: Returns ------- List[str] - The list of command names + The list of command names """ name_list = [] if unloop: @@ -446,7 +477,9 @@ def get_command_names(self, unloop: bool = False) -> List[str]: if iter_ndx == 0: name_list.append(iter_command.name) else: - name_list.append(" IterIndex" + str(iter_ndx) + ": " + iter_command.name) + name_list.append( + " IterIndex" + str(iter_ndx) + ": " + iter_command.name + ) return name_list def get_command_names_descriptions(self, unloop: bool = False) -> List[List[str]]: @@ -460,27 +493,46 @@ def get_command_names_descriptions(self, unloop: bool = False) -> List[List[str] Returns ------- List[List[str]] - The list of [command names, command descriptions] + The list of [command names, command descriptions] """ name_desc_list = [] if unloop: for command in self.yield_next_command(): - name_desc_list.append(["Name: " + command.name, "Description: " + command.description]) + name_desc_list.append( + ["Name: " + command.name, "Description: " + command.description] + ) else: for command in self.command_list: for iter_ndx, iter_command in enumerate(command): if iter_ndx == 0: - name_desc_list.append(["Name: " + iter_command.name, "Description: " + iter_command.description]) + name_desc_list.append( + [ + "Name: " + iter_command.name, + "Description: " + iter_command.description, + ] + ) else: - name_desc_list.append([" IterIndex" + str(iter_ndx) + ": " + "Name: " + iter_command.name, - " IterIndex" + str(iter_ndx) + ": " + "Description: " + iter_command.description]) + name_desc_list.append( + [ + " IterIndex" + + str(iter_ndx) + + ": " + + "Name: " + + iter_command.name, + " IterIndex" + + str(iter_ndx) + + ": " + + "Description: " + + iter_command.description, + ] + ) return name_desc_list def print_command_names(self): """Print the command names with any loop.""" for name in self.get_command_names(unloop=False): print(name) - + def print_unlooped_command_names(self): """Print the command names unlooped.""" for name in self.get_command_names(unloop=True): @@ -497,7 +549,7 @@ def print_unlooped_command_names_descriptions(self): for name_desc in self.get_command_names_descriptions(unloop=True): print(name_desc[0]) print(name_desc[1]) - + def get_device_names_classes(self) -> List[List[str]]: name_cls_list = [] for device in self.device_list: @@ -508,7 +560,9 @@ def count_loop_commands(self) -> int: loop_marker_count = 0 for index, command_iters in enumerate(self.command_list): for iter_index, command in enumerate(command_iters): - if isinstance(command, LoopStartCommand) or isinstance(command, LoopEndCommand): + if isinstance(command, LoopStartCommand) or isinstance( + command, LoopEndCommand + ): loop_marker_count += 1 return loop_marker_count @@ -519,7 +573,9 @@ def remove_all_loop_commands(self): while self.count_loop_commands() != 0: for index, command_iters in enumerate(self.command_list): for iter_index, command in enumerate(command_iters): - if isinstance(command, LoopStartCommand) or isinstance(command, LoopEndCommand): + if isinstance(command, LoopStartCommand) or isinstance( + command, LoopEndCommand + ): # delete the command del self.command_list[index][iter_index] if len(self.command_list[index]) == 0: @@ -544,4 +600,76 @@ def get_clean_device_list(self): # device_list[index].append(util.device_to_dict(self.device_list[index])) device_list_ret.append(device_list_temp) return device_list_ret - \ No newline at end of file + + def get_recipe(self): + """Returns a list for use with the dashboard.""" + devices = [] + commands = [] + for i, device in enumerate(self.device_list): + devices.append( + {str(device.__class__.__name__): {"args": device.get_init_args()}} + ) + for i, command in enumerate(self.command_list): + arg_params = {} + util_arg_params = util.devices_ref_redundancy[ + str(command[0]._receiver.__class__.__name__) + ]["commands"][str(command[0].__class__.__name__)]["args"] + for arg in util_arg_params: + if arg == "receiver": + arg_params["receiver_name"] = command[0]._params["receiver_name"] + else: + arg_params[arg] = command[0]._params[arg] + commands.append( + { + str(command[0].__class__.__name__): { + "args": arg_params, + "delay": command[0]._params["delay"], + "device": str(command[0]._receiver.__class__.__name__), + } + } + ) + return [devices, commands] + + def load_from_dict(self, recipe_dict): + """Loads a recipe from a dictionary.""" + self.device_list = [] + self.command_list = [] + for device in recipe_dict["devices"]: + for device_type, device_params in device.items(): + self.add_device( + util.devices_ref_redundancy[device_type]["obj"]( + **device_params["args"] + ) + ) + self.update_device_by_name() + for command in recipe_dict["commands"]: + for command_type, command_params in command.items(): + rec_name = command_params["args"]["receiver_name"] + del command_params["args"]["receiver_name"] + self.add_command( + util.devices_ref_redundancy[command_params["device"]]["commands"][ + command_type + ]["obj"]( + receiver=self.device_by_name[rec_name], **command_params["args"] + ) + ) + + def add_device_from_dict(self, device_type, device_dict): + self.add_device(util.devices_ref_redundancy[device_type]["obj"](**device_dict)) + self.update_device_by_name() + + def add_command_from_dict(self, device_type, command_type, command_dict): + command_dict_receiver = command_dict["receiver"] + command_dict_delay = command_dict['delay'] + del command_dict["receiver"] + del command_dict["delay"] + self.add_command(util.devices_ref_redundancy[device_type]['commands'][command_type]['obj'](receiver=self.device_by_name[command_dict_receiver], delay = command_dict_delay, **command_dict)) + + def clear_recipe(self): + """Clears the recipe.""" + self.device_list = [] + self.db_device_list = [] + self.command_list = [] + self.db_command_list = [] + self.num_iterations = "ALL" + self.device_by_name = {} diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py index 91b4f07..13ece93 100644 --- a/pages/execute-recipe.py +++ b/pages/execute-recipe.py @@ -13,31 +13,49 @@ dbc.ButtonGroup( [ dbc.Button("Execute", id="execute-button", n_clicks=0), - dbc.Button("Stop", id="stop-button", n_clicks=0), - dbc.Button("Reset", id="reset-button", n_clicks=0), + dbc.Button("Clear Log", id="reset-button", n_clicks=0), + dbc.Button( + "Emergency Stop", id="stop-button", n_clicks=0, color="danger" + ), ], - className="mb-2", - ), - html.Div( - [ - dbc.Switch( - id='show-log-switch', - label='Show log', - value = False, - ) - ], - className="d-flex align-items-center mt-3", + className="mb-3", ), + # html.Div( + # [ + # dbc.Switch( + # id='show-log-switch', + # label='Show log', + # value = False, + # style={'display': 'none'} + # ) + # ], + # className="d-flex align-items-center mt-3", + # ), html.Div( id="execute-recipe-output", className="mt-3", style={"display": "none"} ), dcc.Interval(id="update-interval", interval=500, n_intervals=0), - dcc.Interval(id="interval1", interval=500, n_intervals=0), + dcc.Interval(id="interval1", interval=50, n_intervals=0), html.Div(id="hidden-div", style={"display": "none"}), + dbc.Row( + [ + dbc.Col( + [ + html.H4("Recipe Data"), + dbc.Textarea( + id="execute-recipe-upload-document", + className="log-container mb-3", + readOnly=True, + style={"height": "200px"}, + ), + ] + ), + ] + ), html.H4("Log"), html.Div( id="console-out2", - className="log-container", + className="log-container mb-3", style={ "height": "500px", "overflow-y": "scroll", @@ -45,7 +63,20 @@ "border": "2px solid", }, ), + dbc.Row( + [ + dbc.Col( + [ + html.H4("Description"), + dbc.Textarea( + id="execute-recipe-upload-description", + className="log-container mb-3", + style={"height": "200px"}, + ), + ] + ), + ] + ), ], className="container", ) - diff --git a/pages/load-recipe.py b/pages/load-recipe.py index 94ebb47..8d2709b 100644 --- a/pages/load-recipe.py +++ b/pages/load-recipe.py @@ -43,15 +43,25 @@ dbc.Row( [ dbc.Col( - dbc.Button( + [dbc.Button( "Refresh List", id="home-refresh-list-button", n_clicks=0, color="secondary", className="btn btn-secondary mb-3", ), + ], width=2, - ) + ), + dbc.Col([ + dbc.Button( + "Create New Recipe", + id="home-create-new-recipe-button", + n_clicks=0, + + ) + ], + ) ] ), dbc.Row( diff --git a/pages/manual-control.py b/pages/manual-control.py index bdc8e76..05d77af 100644 --- a/pages/manual-control.py +++ b/pages/manual-control.py @@ -99,7 +99,15 @@ value=None, className="mb-3", ), - dbc.Col([], id="manual-control-device-form"), + dbc.Col( + [ + dbc.NavLink( + id="manual-control-port-field", + style={"display": "none"}, + ), + ], + id="manual-control-device-form", + ), ] ), dbc.Col( diff --git a/pages/view-recipe.py b/pages/view-recipe.py index 6378d21..bcb107c 100644 --- a/pages/view-recipe.py +++ b/pages/view-recipe.py @@ -9,13 +9,25 @@ html.H1("View Recipe"), html.Div( [ + dbc.Alert( + id="view-recipe-alert", + color="success", + is_open=False, + fade=True, + className="mb-3", + ), html.Div( [ html.H2("Devices"), - dbc.ButtonGroup([dbc.Button("Refresh", id="refresh-button1", n_clicks=0), - dbc.Button("Add device", id="add-device-button"), - dbc.Button("Edit", id="edit-device-button"),], className="mb-3"), - + dbc.ButtonGroup( + [ + dbc.Button("Refresh", id="refresh-button1", n_clicks=0), + dbc.Button("Add device", id="add-device-button"), + dbc.Button("Edit", id="edit-device-button"), + dbc.Button('Delete', id='delete-device-button'), + ], + className="mb-3", + ), dbc.Modal( [ dbc.ModalHeader( @@ -61,6 +73,7 @@ id="add-device-dropdown", options=[], value=None, + className="mb-2", ), dcc.Textarea( id="add-device-json-editor", @@ -76,7 +89,7 @@ ), html.Div( id="add-device-error", - style={"color": "red"}, + style={"color": "red", "display": "none"}, ), html.Div( id="add-device-serial-ports-info", @@ -96,15 +109,22 @@ id="devices-table-div", ), ], - className="table-container", + className="table-container mb-3", ), html.Div( [ html.H2("Commands"), - dbc.ButtonGroup([dbc.Button("Refresh", id="refresh-button2", n_clicks=0), - dbc.Button("Add command", id="add-command-button"), - dbc.Button("Edit", id="edit-command-button"),], className="mb-3"), - + dbc.ButtonGroup( + [ + dbc.Button("Refresh", id="refresh-button2", n_clicks=0), + dbc.Button( + "Add command", id="add-command-open-modal-button" + ), + dbc.Button("Edit", id="edit-command-button"), + dbc.Button('Delete', id='delete-command-button'), + ], + className="mb-3", + ), dbc.Modal( [ dbc.ModalHeader( @@ -138,6 +158,50 @@ keyboard=False, backdrop="static", ), + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Add Command")), + dbc.ModalBody( + [ + dcc.Dropdown( + id="view-recipe-add-command-device-dropdown", + options=[], + value=None, + ), + dcc.Dropdown( + id="view-recipe-add-command-command-dropdown", + options=[], + value=None, + className="mb-2", + ), + dcc.Textarea( + id="view-recipe-add-command-json-editor", + style={ + "width": "100%", + "height": "200px", + "fontFamily": "monospace", + "backgroundColor": "#f5f5f5", + "border": "1px solid #ccc", + "padding": "10px", + "color": "#333", + }, + ), + html.Div( + id="add-command-error", + style={"color": "red"}, + ), + ] + ), + dbc.ModalFooter( + dbc.Button( + "Add", id="view-recipe-add-command-editor" + ) + ), + ], + id="view-recipe-command-add-modal", + keyboard=False, + backdrop="static", + ), html.Div( children=[dash_table.DataTable(id="commands-table")], id="commands-table-div", @@ -204,4 +268,4 @@ def toggle_device_add_modal(n1, n2, is_open): def toggle_command_editor_modal(n1, n2, is_open): if n1 or n2: return not is_open - return is_open \ No newline at end of file + return is_open diff --git a/util.py b/util.py index 5ed9152..2b6fbdf 100644 --- a/util.py +++ b/util.py @@ -11,6 +11,18 @@ from devices.linear_stage_150 import LinearStage150 from devices.keithley_2450 import Keithley2450 from devices.device import Device, MiscDeviceClass + +from commands.linear_stage_150_commands import * +from commands.dummy_heater_commands import * +from commands.dummy_motor_commands import * +from commands.dummy_meter_commands import * +from commands.keithley_2450_commands import * +from commands.ximea_camera_commands import * +from commands.utility_commands import * +from commands.heating_stage_commands import * +from commands.multi_stepper_commands import * +from commands.newport_esp301_commands import * + import json import numpy as np from typing import Tuple, Union @@ -58,6 +70,10 @@ def device_to_dict(device: Device): return device.get_init_args() +def evaluate(eval_str): + return eval(eval_str) + + class Encoder(json.JSONEncoder): def default(self, obj): if ( @@ -114,6 +130,7 @@ def default(self, obj): "notes": "Name of the device", } }, + "obj": HeatingStageConnect, }, "HeatingStageInitialize": { "default_code": "HeatingStageInitialize(receiver= '')", @@ -124,6 +141,7 @@ def default(self, obj): "notes": "Name of the device", } }, + "obj": HeatingStageInitialize, }, "HeatingStageDeinitialize": { "default_code": "HeatingStageDeinitialize(receiver= '')", @@ -134,6 +152,7 @@ def default(self, obj): "notes": "Name of the device", } }, + "obj": HeatingStageDeinitialize, }, "HeatingStageSetTemp": { "default_code": "HeatingStageSetTemp(receiver= '', temperature= 0.0)", @@ -149,6 +168,7 @@ def default(self, obj): "notes": "Temperature", }, }, + "obj": HeatingStageSetTemp, }, "HeatingStageSetSetPoint": { "default_code": "HeatingStageSetSetPoint(receiver= '', temperature= 0.0)", @@ -164,48 +184,12 @@ def default(self, obj): "notes": "Temperature", }, }, + "obj": HeatingStageSetSetPoint, }, }, } -devices_ref = { - "PrintingStage": heating_stage_ref, - "AnnealingStage": heating_stage_ref, - "MultiStepper": { - "obj": MultiStepper, - "import_device": "from devices.multi_stepper import MultiStepper", - "import_commands": "from commands.multi_stepper_commands import *", - "init": "MultiStepper(name='MultiStepper', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", - "commands": { - "MultiStepperConnect": "MultiStepperConnect(receiver= '')", - "MultiStepperInitialize": "MultiStepperInitialize(receiver= '')", - "MultiStepperDeinitialize": "MultiStepperDeinitialize(receiver= '')", - "MultiStepperMoveAbsolute": "MultiStepperMoveAbsolute(receiver= '', stepper_number= 0, position= 0)", - "MultiStepperMoveRelative": "MultiStepperMoveRelative(receiver= '', stepper_number= 0, distance= 0)", - }, - }, - "PrinterMotorX": {"obj": NewportESP301}, - # "Spectrometer": {"obj": StellarNetSpectrometer}, - "XimeaCamera": {"obj": XimeaCamera}, - "DummyHeater": {"obj": DummyHeater}, - "DummyMotor": {"obj": DummyMotor}, - "LinearStage150": { - "obj": LinearStage150, - "import_device": "from devices.linear_stage_150 import LinearStage150", - "import_commands": "from commands.linear_stage_150_commands import *", - "init": "LinearStage150(name='LinearStage150', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", - "commands": { - "LinearStage150Connect": "LinearStage150Connect(receiver= '')", - "LinearStage150Initialize": "LinearStage150Initialize(receiver= '')", - "LinearStage150Deinitialize": "LinearStage150Deinitialize(receiver= '')", - "LinearStage150EnableMotor": "LinearStage150EnableMotor(receiver= '')", - "LinearStage150DisableMotor": "LinearStage150DisableMotor(receiver= '')", - "LinearStage150MoveAbsolute": "LinearStage150MoveAbsolute(receiver= '', position= 0)", - "LinearStage150MoveRelative": "LinearStage150MoveRelative(receiver= '', distance= 0)", - }, - }, -} devices_ref_redundancy = { @@ -262,6 +246,7 @@ def default(self, obj): "notes": "", } }, + "obj": LinearStage150Connect, }, "LinearStage150Initialize": { "default_code": "LinearStage150Initialize(receiver= '')", @@ -272,6 +257,7 @@ def default(self, obj): "notes": "", } }, + "obj": LinearStage150Initialize, }, "LinearStage150Deinitialize": { "default_code": "LinearStage150Deinitialize(receiver= '')", @@ -282,6 +268,7 @@ def default(self, obj): "notes": "", } }, + "obj": LinearStage150Deinitialize, }, "LinearStage150EnableMotor": { "default_code": "LinearStage150EnableMotor(receiver= '')", @@ -292,6 +279,7 @@ def default(self, obj): "notes": "", } }, + "obj": LinearStage150EnableMotor, }, "LinearStage150DisableMotor": { "default_code": "LinearStage150DisableMotor(receiver= '')", @@ -302,6 +290,7 @@ def default(self, obj): "notes": "", } }, + "obj": LinearStage150DisableMotor, }, "LinearStage150MoveAbsolute": { "default_code": "LinearStage150MoveAbsolute(receiver= '', position= 0)", @@ -317,6 +306,7 @@ def default(self, obj): "notes": "", }, }, + "obj": LinearStage150MoveAbsolute, }, "LinearStage150MoveRelative": { "default_code": "LinearStage150MoveRelative(receiver= '', distance= 0)", @@ -332,10 +322,11 @@ def default(self, obj): "notes": "", }, }, + "obj": LinearStage150MoveRelative, }, }, }, - "Keithley2450": { # TODO finish this + "Keithley2450": { "obj": Keithley2450, "serial": True, "serial_sequence": ["Keithley2450Initialize"], @@ -372,6 +363,7 @@ def default(self, obj): "notes": "", } }, + "obj": Keithley2450Initialize, }, "Keithley2450Deinitialize": { "default_code": "Keithley2450Deinitialize(receiver= '')", @@ -382,6 +374,7 @@ def default(self, obj): "notes": "", } }, + "obj": Keithley2450Deinitialize, }, "KeithleyWait": { "default_code": "KeithleyWait(receiver= '')", @@ -392,6 +385,7 @@ def default(self, obj): "notes": "", } }, + "obj": KeithleyWait, }, "KeithleyWriteCommand": { "default_code": "KeithleyWriteCommand(receiver= '', command= '')", @@ -407,6 +401,7 @@ def default(self, obj): "notes": "", }, }, + "obj": KeithleyWriteCommand, }, "KeithleySetTerminal": { "default_code": "KeithleySetTerminal(receiver= '', position= 'front')", @@ -422,6 +417,7 @@ def default(self, obj): "notes": "", }, }, + "obj": KeithleySetTerminal, }, "KeithleyErrorCheck": { "default_code": "KeithleyErrorCheck(receiver= '')", @@ -432,6 +428,7 @@ def default(self, obj): "notes": "", } }, + "obj": KeithleyErrorCheck, }, "KeithleyClearBuffer": { "default_code": "KeithleyClearBuffer(receiver= '', buffer='defbuffer1')", @@ -447,6 +444,7 @@ def default(self, obj): "notes": "", }, }, + "obj": KeithleyClearBuffer, }, "KeithleyIVCharacteristic": { "default_code": "KeithleyIVCharacteristic(receiver= '', ilimit=0.0, vmin=0.0, vmax=0.0, delay=0.0, steps=60)", @@ -482,6 +480,7 @@ def default(self, obj): "notes": "", }, }, + "obj": KeithleyIVCharacteristic, }, "KeithleyFourPoint": { "default_code": "KeithleyFourPoint(receiver= '', test_curr=0.0, vlimit=0.0, curr_reversal = False)", @@ -507,6 +506,7 @@ def default(self, obj): "notes": "", }, }, + "obj": KeithleyFourPoint, }, "KeithleyGetData": { "default_code": "KeithleyGetData(receiver= '', filename=None, four_point= False)", @@ -527,6 +527,7 @@ def default(self, obj): "notes": "", }, }, + "obj": KeithleyGetData, }, }, }, @@ -809,6 +810,7 @@ def default(self, obj): "notes": "Name of the device.", } }, + "obj": DummyMotorInitialize, }, "DummyMotorDeinitialize": { "default_code": "DummyMotorDeinitialize(receiver= '')", @@ -819,6 +821,7 @@ def default(self, obj): "notes": "Name of the device.", } }, + "obj": DummyMotorDeinitialize, }, "DummyMotorSetSpeed": { "default_code": "DummyMotorSetSpeed(receiver= '', speed= 0.0)", @@ -834,6 +837,7 @@ def default(self, obj): "notes": "Speed of the motor.", }, }, + "obj": DummyMotorSetSpeed, }, "DummyMotorMoveAbsolute": { "default_code": "DummyMotorMoveAbsolute(receiver= '', position= 0)", @@ -849,6 +853,7 @@ def default(self, obj): "notes": "Position to move to.", }, }, + "obj": DummyMotorMoveAbsolute, }, "DummyMotorMoveRelative": { "default_code": "DummyMotorMoveRelative(receiver= '', distance= 0)", @@ -864,6 +869,7 @@ def default(self, obj): "notes": "Distance to move.", }, }, + "obj": DummyMotorMoveRelative, }, "DummyMotorMoveSpeedAbsolute": { "default_code": "DummyMotorMoveSpeedAbsolute(receiver= '', position= 0.0, speed= 0.0)", @@ -884,10 +890,131 @@ def default(self, obj): "notes": "Speed of the motor.", }, }, + "obj": DummyMotorMoveSpeedAbsolute, }, }, }, # "Spectrometer": {"obj": StellarNetSpectrometer}, # "XimeaCamera": {"obj": XimeaCamera}, - # "DummyHeater": {"obj": DummyHeater},› + "DummyHeater": { + "obj": DummyHeater, + "serial": True, + "serial_sequence": ["DummyHeaterInitialize"], + "import_device": "from devices.dummy_heater import DummyHeater", + "import_commands": "from commands.dummy_heater_commands import *", + "init": { + "default_code": "DummyHeater(name='DummyHeater', heat_rate=20.0)", + "obj_name": "DummyHeater", + "args": { + "name": { + "default": "DummyHeater", + "type": str, + "notes": "Name of the device.", + }, + "heat_rate": { + "default": 20.0, + "type": float, + "notes": "Heat rate of the heater.", + }, + }, + }, + "commands": { + "DummyHeaterInitialize": { + "default_code": "DummyHeaterInitialize(receiver= '')", + "args": { + "receiver": { + "default": "DummyHeater", + "type": str, + "notes": "Name of the device.", + } + }, + "obj": DummyHeaterInitialize, + }, + "DummyHeaterDeinitialize": { + "default_code": "DummyHeaterDeinitialize(receiver= '')", + "args": { + "receiver": { + "default": "DummyHeater", + "type": str, + "notes": "Name of the device.", + } + }, + "obj": DummyHeaterDeinitialize, + }, + "DummyHeaterSetHeatRate": { + "default_code": "DummyHeaterSetHeatRate(receiver= '', heat_rate= 0.0)", + "args": { + "receiver": { + "default": "DummyHeater", + "type": str, + "notes": "Name of the device.", + }, + "heat_rate": { + "default": 0.0, + "type": float, + "notes": "Heat rate of the heater.", + }, + }, + "obj": DummyHeaterSetHeatRate, + }, + "DummyHeaterSetTemp": { + "default_code": "DummyHeaterSetTemp(receiver= '', temperature= 0.0)", + "args": { + "receiver": { + "default": "DummyHeater", + "type": str, + "notes": "Name of the device.", + }, + "temperature": { + "default": 0.0, + "type": float, + "notes": "Temperature to set the heater to.", + }, + }, + "obj": DummyHeaterSetTemp, + }, + }, + }, } + + + + + +# devices_ref = { +# "PrintingStage": heating_stage_ref, +# "AnnealingStage": heating_stage_ref, +# "MultiStepper": { +# "obj": MultiStepper, +# "import_device": "from devices.multi_stepper import MultiStepper", +# "import_commands": "from commands.multi_stepper_commands import *", +# "init": "MultiStepper(name='MultiStepper', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", +# "commands": { +# "MultiStepperConnect": "MultiStepperConnect(receiver= '')", +# "MultiStepperInitialize": "MultiStepperInitialize(receiver= '')", +# "MultiStepperDeinitialize": "MultiStepperDeinitialize(receiver= '')", +# "MultiStepperMoveAbsolute": "MultiStepperMoveAbsolute(receiver= '', stepper_number= 0, position= 0)", +# "MultiStepperMoveRelative": "MultiStepperMoveRelative(receiver= '', stepper_number= 0, distance= 0)", +# }, +# }, +# "PrinterMotorX": {"obj": NewportESP301}, +# # "Spectrometer": {"obj": StellarNetSpectrometer}, +# "XimeaCamera": {"obj": XimeaCamera}, +# "DummyHeater": {"obj": DummyHeater}, +# "DummyMotor": {"obj": DummyMotor}, +# "LinearStage150": { +# "obj": LinearStage150, +# "import_device": "from devices.linear_stage_150 import LinearStage150", +# "import_commands": "from commands.linear_stage_150_commands import *", +# "init": "LinearStage150(name='LinearStage150', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", +# "commands": { +# "LinearStage150Connect": "LinearStage150Connect(receiver= '')", +# "LinearStage150Initialize": "LinearStage150Initialize(receiver= '')", +# "LinearStage150Deinitialize": "LinearStage150Deinitialize(receiver= '')", +# "LinearStage150EnableMotor": "LinearStage150EnableMotor(receiver= '')", +# "LinearStage150DisableMotor": "LinearStage150DisableMotor(receiver= '')", +# "LinearStage150MoveAbsolute": "LinearStage150MoveAbsolute(receiver= '', position= 0)", +# "LinearStage150MoveRelative": "LinearStage150MoveRelative(receiver= '', distance= 0)", +# }, +# }, +# } From c984e2998569f2b6e4f13a7f0f41e027101ea179 Mon Sep 17 00:00:00 2001 From: Piyush Date: Wed, 19 Jul 2023 12:40:52 -0500 Subject: [PATCH 038/125] added execution saving --- app.py | 49 ++++++++++++++++++++++ pages/execute-recipe.py | 93 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/app.py b/app.py index c06b597..f936e81 100644 --- a/app.py +++ b/app.py @@ -116,6 +116,19 @@ def update_upstream_recipe_dict(): return False +def update_execution_upstream(execution): + if "document" in list(com.__dict__.keys()): + com.document["executions"].append(execution) + mongo.db["recipes"].update_one( + {"_id": com.document["_id"]}, {"$set": com.document} + ) + print("successfully updated execution upstream") + return True + else: + print("com.document not found") + return False + + # --------------------------------------------------- # Home Page # --------------------------------------------------- @@ -252,6 +265,7 @@ def create_new_recipe_doc(n, url, name): "file_name": name, "recipe_dict": {"devices": [], "commands": []}, "dash_friendly": True, + "executions": [], } ) @@ -629,6 +643,7 @@ def view_recipe_delete_device(n, url, active_cell, data): update_success = update_upstream_recipe_dict() return None + @app.callback( Output("commands-table", "data", allow_duplicate=True), Input("delete-command-button", "n_clicks"), @@ -645,6 +660,7 @@ def view_recipe_delete_command(n, url, active_cell, data): update_success = update_upstream_recipe_dict() return None + @app.callback( Output("commands-table-div", "children"), [Input("refresh-button2", "n_clicks"), Input("commands-table", "data")], @@ -1123,6 +1139,39 @@ def execute_recipe_load_document_viewer(n, url): return [toRet] +@app.callback( + [Output("execute-recipe-upload-data-output", "children")], + Input("execute-recipe-upload-data-button", "n_clicks"), + [ + State("url", "pathname"), + State("execute-recipe-upload-name", "value"), + State("execute-recipe-upload-document", "value"), + State("console-out2", "children"), + State("execute-recipe-upload-description", "value"), + State("execute-recipe-upload-files", "contents"), + ], + prevent_initial_call=True, +) +def execute_recipe_upload_data( + n_clicks, url, name, recipe_data, console_log, description, files +): + if str(url) == "/execute-recipe": + print("execute_recipe_upload_data") + execution = {} + execution["name"] = name + if isinstance(recipe_data, list): + execution["recipe"] = recipe_data[0].split("\n") + else: + execution["recipe"] = recipe_data.split("\n") + execution["description"] = description + execution["log"] = str(console_log["props"]["children"]).split("\n") + exec_success = update_execution_upstream(execution) + if exec_success: + return [html.P("Data uploaded successfully")] + else: + return [html.P("Error uploading data")] + + # --------------------------------------------------- # Data Page # --------------------------------------------------- diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py index 13ece93..8e4dd64 100644 --- a/pages/execute-recipe.py +++ b/pages/execute-recipe.py @@ -1,4 +1,4 @@ -from dash import Dash, html, dcc, dash_table, Input, Output, callback +from dash import Dash, html, dcc, dash_table, Input, Output, callback, State import dash_bootstrap_components as dbc import dash import logging @@ -20,6 +20,20 @@ ], className="mb-3", ), + dbc.Row( + [ + dbc.Col( + [ + dbc.Input( + id="execute-recipe-upload-name", + placeholder="Name this execution", + type="text", + ), + ] + ) + ], + className="mb-3", + ), # html.Div( # [ # dbc.Switch( @@ -75,8 +89,85 @@ ), ] ), + dbc.Col( + [ + html.H4("Upload Files"), + dcc.Upload( + id="execute-recipe-upload-files", + children=html.Div( + [ + "Drag and Drop or ", + html.A( + "Select Files", + style={ + "color": "blue", + "textDecoration": "underline", + }, + ), + " or ", + html.A( + "Replace Selected", + id="execute-recipe-clear-files", + style={ + "color": "blue", + "textDecoration": "underline", + }, + ), + html.Div(id="execute-recipe-upload-files-names"), + ] + ), + style={ + "width": "100%", + "height": "200px", + "lineHeight": "60px", + "borderWidth": "1px", + "borderStyle": "dashed", + "borderRadius": "5px", + "textAlign": "center", + }, + multiple=True, + ), + ] + ), ] ), + dbc.Button( + "Save and Upload Data", + id="execute-recipe-upload-data-button", + className="mb-3", + color="primary", + ), + html.Div(id="execute-recipe-upload-data-output"), ], className="container", ) + + +@callback( + Output("execute-recipe-upload-files-names", "children"), + Input("execute-recipe-upload-files", "filename"), + State("url", "pathname"), + prevent_initial_call=True, +) +def add_filenames_to_upload_box(filenames, url): + if str(url) == "/execute-recipe": + if filenames is None or filenames == []: + return "" + toRet = "" + for i, filename in enumerate(filenames): + if i < len(filenames) - 1: + toRet += filename + ", " + else: + toRet += filename + return toRet + + +@callback( + Output("execute-recipe-upload-files", "filename"), + Input("execute-recipe-clear-files", "n_clicks"), + State("url", "pathname"), + prevent_initial_call=True, +) +def clear_selected_files(n, url): + if str(url) == "/execute-recipe": + return None From 19d2931ed5d106e9b16d30ec31665a187e20b567 Mon Sep 17 00:00:00 2001 From: Piyush Date: Wed, 19 Jul 2023 15:44:26 -0500 Subject: [PATCH 039/125] changed desc to notes and del interceptor --- app.py | 9 ++++++--- pages/execute-recipe.py | 4 ++-- pages/load-recipe.py | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index f936e81..6659233 100644 --- a/app.py +++ b/app.py @@ -1147,13 +1147,13 @@ def execute_recipe_load_document_viewer(n, url): State("execute-recipe-upload-name", "value"), State("execute-recipe-upload-document", "value"), State("console-out2", "children"), - State("execute-recipe-upload-description", "value"), + State("execute-recipe-upload-notes", "value"), State("execute-recipe-upload-files", "contents"), ], prevent_initial_call=True, ) def execute_recipe_upload_data( - n_clicks, url, name, recipe_data, console_log, description, files + n_clicks, url, name, recipe_data, console_log, notes, files ): if str(url) == "/execute-recipe": print("execute_recipe_upload_data") @@ -1163,7 +1163,7 @@ def execute_recipe_upload_data( execution["recipe"] = recipe_data[0].split("\n") else: execution["recipe"] = recipe_data.split("\n") - execution["description"] = description + execution["notes"] = notes execution["log"] = str(console_log["props"]["children"]).split("\n") exec_success = update_execution_upstream(execution) if exec_success: @@ -1711,6 +1711,7 @@ def manual_control_execute_fill_code( print(code) interceptor.stop_interception() code_output = interceptor.get_intercepted_messages() + del interceptor code_log_string = "" for msg in code_output: code_log_string += msg @@ -1780,6 +1781,7 @@ def manual_control_execute_code(n, url, code): exec(code["props"]["children"]) interceptor.stop_interception() messages = interceptor.get_intercepted_messages() + del interceptor log_string = "" for msg in messages: if msg == "\r": @@ -1791,6 +1793,7 @@ def manual_control_execute_code(n, url, code): print(e) interceptor.stop_interception() messages = interceptor.get_intercepted_messages() + del interceptor log_string = "" for msg in messages: log_string += msg diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py index 8e4dd64..45ae5ca 100644 --- a/pages/execute-recipe.py +++ b/pages/execute-recipe.py @@ -81,9 +81,9 @@ [ dbc.Col( [ - html.H4("Description"), + html.H4("Notes"), dbc.Textarea( - id="execute-recipe-upload-description", + id="execute-recipe-upload-notes", className="log-container mb-3", style={"height": "200px"}, ), diff --git a/pages/load-recipe.py b/pages/load-recipe.py index 8d2709b..dcd38f3 100644 --- a/pages/load-recipe.py +++ b/pages/load-recipe.py @@ -70,7 +70,7 @@ dash_table.DataTable( id="home-recipes-list-table", columns=[ - {"name": "File Name", "id": "file_name"}, + {"name": "Recipe Name", "id": "file_name"}, {"name": "Posix Compatible", "id": "posix_friendly"}, {"name": "Viewer Compatible", "id": "dash_friendly"}, {"name": "Python Code Available", "id": "python_code"}, From 9ae7ceb27340ce12b81a25c1d28be400af4a1793 Mon Sep 17 00:00:00 2001 From: Piyush Date: Wed, 19 Jul 2023 18:37:10 -0500 Subject: [PATCH 040/125] moved navbar button --- app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index 6659233..9bdcade 100644 --- a/app.py +++ b/app.py @@ -52,12 +52,12 @@ navbar = dbc.NavbarSimple( children=[ - dbc.NavItem(dbc.NavLink("Home", href="/", external_link=True)), + dbc.NavItem(dbc.NavLink("Load", href="/load-recipe", external_link=True)), dbc.DropdownMenu( children=[ - dbc.DropdownMenuItem( - "Load Recipe", href="/load-recipe", external_link=True - ), + # dbc.DropdownMenuItem( + # "Load Recipe", href="/load-recipe", external_link=True + # ), dbc.DropdownMenuItem( "View Recipe", href="/view-recipe", external_link=True ), From 1932de7e5ee027a9ec110364736e67da86870efd Mon Sep 17 00:00:00 2001 From: Piyush Pahuja Date: Wed, 19 Jul 2023 21:24:40 -0500 Subject: [PATCH 041/125] updated lts serial sequence --- util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util.py b/util.py index 2b6fbdf..beea50f 100644 --- a/util.py +++ b/util.py @@ -196,7 +196,7 @@ def default(self, obj): "LinearStage150": { "obj": LinearStage150, "serial": True, - "serial_sequence": ["LinearStage150Connect"], + "serial_sequence": ["LinearStage150Connect", "LinearStage150EnableMotor"], "import_device": "from devices.linear_stage_150 import LinearStage150", "import_commands": "from commands.linear_stage_150_commands import *", "init": { From 858466ff41b4c9150a80e14ba0f286e75d1f8407 Mon Sep 17 00:00:00 2001 From: Piyush Pahuja Date: Mon, 24 Jul 2023 16:20:36 -0500 Subject: [PATCH 042/125] added utility commands and updated page metadata --- app.py | 14 ++++---- command_sequence.py | 3 ++ devices/utility_device.py | 11 ++++++ pages/execute-recipe.py | 2 +- pages/view-recipe.py | 2 +- util.py | 70 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 devices/utility_device.py diff --git a/app.py b/app.py index 9bdcade..8a52a56 100644 --- a/app.py +++ b/app.py @@ -533,13 +533,13 @@ def enable_add_device_button(value, device_type, is_open): # view-recipe page ) parsed_json = json.loads(value) for key in parsed_json: - print("\n" + key) - print( - "input: " - + str(type((parsed_json[key]))) - + ", expected: " - + str(args[key]) - ) + # print("\n" + key) + # print( + # "input: " + # + str(type((parsed_json[key]))) + # + ", expected: " + # + str(args[key]) + # ) if type((parsed_json[key])) != args[key]: return False, f"Invalid type for {key}. Expected {str(args[key])}" # if not isinstance(parsed_json[key], args[key]): diff --git a/command_sequence.py b/command_sequence.py index d0e0b86..2de3f58 100644 --- a/command_sequence.py +++ b/command_sequence.py @@ -659,6 +659,9 @@ def add_device_from_dict(self, device_type, device_dict): self.update_device_by_name() def add_command_from_dict(self, device_type, command_type, command_dict): + if device_type == "UtilityCommands": + self.add_command(util.devices_ref_redundancy[device_type]['commands'][command_type]['obj'](**command_dict)) + return command_dict_receiver = command_dict["receiver"] command_dict_delay = command_dict['delay'] del command_dict["receiver"] diff --git a/devices/utility_device.py b/devices/utility_device.py new file mode 100644 index 0000000..823e2cf --- /dev/null +++ b/devices/utility_device.py @@ -0,0 +1,11 @@ +from .device import Device + +class UtilityCommands(Device): + def __init__(self, name: str): + super().__init__(name) + + def get_init_args(self) -> dict: + return {"name": self._name} + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] \ No newline at end of file diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py index 45ae5ca..f19d98b 100644 --- a/pages/execute-recipe.py +++ b/pages/execute-recipe.py @@ -5,7 +5,7 @@ from dash_dangerously_set_inner_html import DangerouslySetInnerHTML -dash.register_page(__name__, "/execute-recipe") +dash.register_page(__name__, path="/execute-recipe", name="Execute Recipe", title="Execute Recipe") layout = html.Div( [ diff --git a/pages/view-recipe.py b/pages/view-recipe.py index bcb107c..afdf251 100644 --- a/pages/view-recipe.py +++ b/pages/view-recipe.py @@ -2,7 +2,7 @@ import dash_bootstrap_components as dbc import dash -dash.register_page(__name__, "/view-recipe") +dash.register_page(__name__, path="/view-recipe", name="View Recipe", title="View Recipe") layout = html.Div( [ diff --git a/util.py b/util.py index beea50f..20492ce 100644 --- a/util.py +++ b/util.py @@ -11,6 +11,7 @@ from devices.linear_stage_150 import LinearStage150 from devices.keithley_2450 import Keithley2450 from devices.device import Device, MiscDeviceClass +from devices.utility_device import UtilityCommands from commands.linear_stage_150_commands import * from commands.dummy_heater_commands import * @@ -22,6 +23,7 @@ from commands.heating_stage_commands import * from commands.multi_stepper_commands import * from commands.newport_esp301_commands import * +from commands.utility_commands import * import json import numpy as np @@ -193,6 +195,64 @@ def default(self, obj): devices_ref_redundancy = { + "UtilityCommands":{ + "obj": UtilityCommands, + "serial": False, + "import_device": "from devices.utility_commands import UtilityCommands", + "import_commands": "from commands.utility_commands import *", + "init":{ + "default_code": "# Utility Commands used", + "obj_name": "UtilityCommands", + "args": {} + }, + "commands":{ + "LoopStartCommand":{ + "default_code": "LoopStartCommand()", + "args": {}, + "obj": LoopStartCommand, + }, + "LoopEndCommand":{ + "default_code": "LoopEndCommand()", + "args": {}, + "obj": LoopEndCommand, + }, + "DelayPauseCommand":{ + "default_code": "DelayPauseCommand(delay=0.0)",\ + "args": { + "delay": { + "default": 0.0, + "type": float, + "notes": "Delay in seconds", + } + }, + "obj": DelayPauseCommand, + }, + "NotifySlackCommand":{ + "default_code": "NotifySlackCommand(message='Hello World')", + "args": { + "message": { + "default": "Hello World", + "type": str, + "notes": "Message to send to slack", + } + }, + "obj": NotifySlackCommand, + }, + "LogUserMessageCommand":{ + "default_code": "LogUserMessageCommand(message='Hello World')", + "args": { + "message": { + "default": "Hello World", + "type": str, + "notes": "Message to log", + } + }, + "obj": LogUserMessageCommand, + } + + } + } + , "LinearStage150": { "obj": LinearStage150, "serial": True, @@ -585,6 +645,7 @@ def default(self, obj): "notes": "Name of the device", } }, + "obj": MultiStepperConnect, }, "MultiStepperInitialize": { "default_code": "MultiStepperInitialize(receiver= '')", @@ -595,6 +656,7 @@ def default(self, obj): "notes": "Name of the device", } }, + "obj": MultiStepperInitialize, }, "MultiStepperDeinitialize": { "default_code": "MultiStepperDeinitialize(receiver= '')", @@ -605,6 +667,7 @@ def default(self, obj): "notes": "Name of the device", } }, + "obj": MultiStepperDeinitialize, }, "MultiStepperMoveAbsolute": { "default_code": "MultiStepperMoveAbsolute(receiver= '', stepper_number= 0, position= 0)", @@ -625,6 +688,7 @@ def default(self, obj): "notes": "Position to move to", }, }, + "obj": MultiStepperMoveAbsolute, }, "MultiStepperMoveRelative": { "default_code": "MultiStepperMoveRelative(receiver= '', stepper_number= 0, distance= 0)", @@ -645,6 +709,7 @@ def default(self, obj): "notes": "Distance to move", }, }, + "obj": MultiStepperMoveRelative, }, }, }, @@ -705,6 +770,7 @@ def default(self, obj): "notes": "Name of the device", } }, + "obj": NewportESP301Connect, }, "NewportESP301Initialize": { "default_code": "NewportESP301Initialize(receiver= '')", @@ -715,6 +781,7 @@ def default(self, obj): "notes": "Name of the device", } }, + "obj": NewportESP301Initialize, }, "NewportESP301Deinitialize": { "default_code": "NewportESP301Deinitialize(receiver= '')", @@ -725,6 +792,7 @@ def default(self, obj): "notes": "Name of the device", } }, + "obj": NewportESP301Deinitialize, }, "NewportESP301MoveSpeedAbsolute": { "default_code": "NewportESP301MoveSpeedAbsolute(receiver= '', axis= 1, position= 0, speed= 20.0)", @@ -750,6 +818,7 @@ def default(self, obj): "notes": "Speed to move at", }, }, + "obj": NewportESP301MoveSpeedAbsolute, }, "NewportESP301MoveSpeedRelative": { "default_code": "NewportESP301MoveSpeedRelative(receiver= '', axis= 1, distance= 0, speed= 20.0)", @@ -775,6 +844,7 @@ def default(self, obj): "notes": "Speed to move at", }, }, + "obj": NewportESP301MoveSpeedRelative, }, }, }, From bd4e17c55980d66d13c64d00b897fb6a0aa33cc2 Mon Sep 17 00:00:00 2001 From: Piyush Pahuja Date: Mon, 24 Jul 2023 16:30:57 -0500 Subject: [PATCH 043/125] clean but all reqmts --- requirements.txt | Bin 1133 -> 1546 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5709580049e32c827600b176a2f1fb1d2d5c8e84..20b06ac208ba683b2549bd898f4fecc32e3ace2c 100644 GIT binary patch literal 1546 zcma)+-EI>>429->bKvb%P1mX&%X_9W6{b6@Q6CNJ;zVS>qx&~fJ#DxfGinDiJiD~m_tYVF3uZ-5Uukn~P5PN*Bj!N#COwu6JynTZ zXKWyPqUT)4NWbTT9I=M%-)H|1ApO>2fOT0m0YTX$oPTSE(FRS8U)6)iQ-HA}< z4cF5T*awLr((Gw&OUewuEofL_512v;^fKN*6`%cyG5!kcoH>~iobz6 zl^lL$3-aSiSN=Evbh<48IMJsClG354I8yDHDxyQ F{s9|342}Q* literal 1133 zcmZ`(v5wm?4BhoF7GgWOy9^q-bSQd2feuASMo|)7WJ#6i=A5tZqns zA)hYH*_ctk=&d53sy{~x#`JpBy`otCA-K+_;*1$|ztbB^vYhSRL5ORWuoBkt_Xux7`(k2^MYVhHicAZB4ajs ztDg`@PIJkN0Br=cESb}}P`==8D~|wQY!E>F7Q`dfOQS9`9Vwn&NplTLCnNY6UFj}7K{F(M^pJ=DK`=p5V<4q~#+nztq$nIm zJbVsRp}?RAy;CPl%qa!%+v;%-i%uooT@1cLdGTme{urUqh;KetrtmOtr##ocY?}fg z_r=FI8dkLj^_C979|Q;!s;j^Gn!;}pPt@3|S1Q*zf2Y%GKI$jp91^#xQ9nbm>10=b zHfov!`=hs@_JrZKD$dht)&TY@lfBZ}$1}*ta%GC}Lzszd`IrFI%Nq3Rn-3w~jM-y% zHmY@m!B`#fC>Pkqo#=A)=mpjE_@eScVnW-uJtCWB$uJ9~1mbd@R-@Y%17(>~Jf|kC zkXt0ZkrO-t$gbGnaU4C40|XbKdASX2G$Jt#>%eYiFMS_Le8+Fu`Vl>UetP}d#8AIx zSJ|gXrEPC7-WS(in*^imYH>~fhbOYk)q2d>1xE!NB0dctZzqvq#RfJC z{k!D%Fha45WIdxX<^TX+ilbwOewN!f(wpy?1&j3HK9>L*qs#?$)y>}gk#R_{3YYl9 gmFwNj;w0ixEmQ#1gU92>$U60oe+cUG^6_u*2h-zPH2?qr From f1e522659780265dc01990136f38036528d3122e Mon Sep 17 00:00:00 2001 From: Piyush Pahuja Date: Mon, 24 Jul 2023 16:51:05 -0500 Subject: [PATCH 044/125] simplified reqmts --- pages/execute-recipe.py | 1 - requirements.txt | Bin 1546 -> 360 bytes 2 files changed, 1 deletion(-) diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py index f19d98b..f888eef 100644 --- a/pages/execute-recipe.py +++ b/pages/execute-recipe.py @@ -2,7 +2,6 @@ import dash_bootstrap_components as dbc import dash import logging -from dash_dangerously_set_inner_html import DangerouslySetInnerHTML dash.register_page(__name__, path="/execute-recipe", name="Execute Recipe", title="Execute Recipe") diff --git a/requirements.txt b/requirements.txt index 20b06ac208ba683b2549bd898f4fecc32e3ace2c..bedb147adc4c9f99296121324a712ee37aa2ec43 100644 GIT binary patch delta 145 zcmeC;dBLRo|6ejgDnk)N2}34B8W8g`a4{q^>429->bKvb%P1mX&%X_9W6{b6@Q6CNJ;zVS>qx&~fJ#DxfGinDiJiD~m_tYVF3uZ-5Uukn~P5PN*Bj!N#COwu6JynTZ zXKWyPqUT)4NWbTT9I=M%-)H|1ApO>2fOT0m0YTX$oPTSE(FRS8U)6)iQ-HA}< z4cF5T*awLr((Gw&OUewuEofL_512v;^fKN*6`%cyG5!kcoH>~iobz6 zl^lL$3-aSiSN=Evbh<48IMJsClG354I8yDHDxyQ F{s9|342}Q* From 2de0e075c0f66e1d11f2c715a15e30f57763fd9d Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 25 Jul 2023 20:40:28 -0500 Subject: [PATCH 045/125] added execution options on view recipe --- pages/view-recipe.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pages/view-recipe.py b/pages/view-recipe.py index afdf251..f917248 100644 --- a/pages/view-recipe.py +++ b/pages/view-recipe.py @@ -217,8 +217,15 @@ # style={"display": "none"}, # ), ], - className="table-container", + className="table-container mb-3", ), + + html.Div( + [ + html.H2("Execution Options"), + ], + className="table-container mb-3", + ) # html.Div( # [ # html.H2("Command Iterations"), From 3078c31b572d3ec8905d6f36cf2801ea138e0f17 Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 25 Jul 2023 20:41:20 -0500 Subject: [PATCH 046/125] Delete to_load.yaml --- to_load.yaml | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 to_load.yaml diff --git a/to_load.yaml b/to_load.yaml deleted file mode 100644 index b99240b..0000000 --- a/to_load.yaml +++ /dev/null @@ -1,31 +0,0 @@ -- - !!python/object:devices.linear_stage_150.LinearStage150 - _name: LinearStage150 - _is_initialized: false - _port: /dev/cu.URT0 - _baudrate: 115200 - _timeout: 0.1 - ser: !!python/object:serial.serialposix.Serial - is_open: false - portstr: null - name: null - _port: null - _baudrate: 9600 - _bytesize: 8 - _parity: N - _stopbits: 1 - _timeout: null - _write_timeout: null - _xonxoff: false - _rtscts: false - _dsrdtr: false - _inter_byte_timeout: null - _rs485_mode: null - _rts_state: true - _dtr_state: true - _break_state: false - _exclusive: null - _destination: 80 - _source: 1 - _channel: 1 -- [] -- ALL From 4ebfb8bb2cd19d786968738f237817c5d1d4418a Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 25 Jul 2023 20:41:34 -0500 Subject: [PATCH 047/125] Delete to_save.yaml --- to_save.yaml | 120 --------------------------------------------------- 1 file changed, 120 deletions(-) delete mode 100644 to_save.yaml diff --git a/to_save.yaml b/to_save.yaml deleted file mode 100644 index 2480e5a..0000000 --- a/to_save.yaml +++ /dev/null @@ -1,120 +0,0 @@ -- - &id001 !!python/object:devices.dummy_heater.DummyHeater - _name: heater1 - _is_initialized: false - _heat_rate: 20.0 - min_heat_rate: 1.0 - max_heat_rate: 50.0 - min_temperature: 25.0 - max_temperature: 100.0 - _temperature: 80.9565265284408 - _hardware_interval: 0.05 - - &id002 !!python/object:devices.dummy_motor.DummyMotor - _name: motor1 - _is_initialized: false - motor: !!python/object:devices.dummy_motor_source.DummyMotorSource - _speed: 20.0 - min_speed: 5.0 - max_speed: 50.0 - min_position: 0.0 - max_position: 100.0 - _position: 88.11966994238041 - _hardware_interval: 0.05 - - &id003 !!python/object:devices.dummy_motor.DummyMotor - _name: motor2 - _is_initialized: false - motor: !!python/object:devices.dummy_motor_source.DummyMotorSource - _speed: 20.0 - min_speed: 5.0 - max_speed: 50.0 - min_position: 0.0 - max_position: 100.0 - _position: 1.6064267655005238 - _hardware_interval: 0.05 -- - - !!python/object:commands.dummy_heater_commands.DummyHeaterInitialize - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorInitialize - _receiver: *id003 - _params: - receiver_name: motor2 - delay: 0.0 - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorSetSpeed - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - speed: 10.0 - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - distance: 20.0 - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorSetSpeed - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - speed: 15.0 - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorMoveRelative - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - distance: 30.0 - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null - - - !!python/object:commands.dummy_heater_commands.DummyHeaterDeinitialize - _receiver: *id001 - _params: - receiver_name: heater1 - delay: 0.0 - reset_init_flag: true - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize - _receiver: *id002 - _params: - receiver_name: motor1 - delay: 0.0 - reset_init_flag: true - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null - - - !!python/object:commands.dummy_motor_commands.DummyMotorDeinitialize - _receiver: *id003 - _params: - receiver_name: motor2 - delay: 0.0 - reset_init_flag: true - _result: !!python/object:commands.command.CommandResult - _was_successful: null - _message: null -- ALL From 801d329ecdb1f41271cdb61b5c0a56834b29be97 Mon Sep 17 00:00:00 2001 From: Piyush Date: Wed, 26 Jul 2023 21:15:50 -0500 Subject: [PATCH 048/125] execution options loading and saving from db --- app.py | 291 +++++++++++++------------------------------ command_sequence.py | 5 +- pages/view-recipe.py | 69 ++++++---- 3 files changed, 135 insertions(+), 230 deletions(-) diff --git a/app.py b/app.py index 8a52a56..b25b08e 100644 --- a/app.py +++ b/app.py @@ -34,11 +34,7 @@ mongo = MongoDBHelper( - "mongodb+srv://" - + mongo_username - + ":" - + mongo_password - + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "mongodb+srv://" + mongo_username + ":" + mongo_password + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", "diaogroup", ) @@ -53,31 +49,21 @@ navbar = dbc.NavbarSimple( children=[ dbc.NavItem(dbc.NavLink("Load", href="/load-recipe", external_link=True)), + dbc.NavItem(dbc.NavLink("View", href="/view-recipe", external_link=True)), + dbc.NavItem(dbc.NavLink("Execute", href="/execute-recipe", external_link=True)), + dbc.NavItem(dbc.NavLink("Manual Control", href="/manual-control", external_link=True)), + dbc.NavItem(dbc.NavLink("Database Browser", href="/database", external_link=True)), dbc.DropdownMenu( children=[ # dbc.DropdownMenuItem( # "Load Recipe", href="/load-recipe", external_link=True # ), - dbc.DropdownMenuItem( - "View Recipe", href="/view-recipe", external_link=True - ), - dbc.DropdownMenuItem( - "Edit Code", href="/edit-recipe", external_link=True - ), - dbc.DropdownMenuItem( - "Execute Recipe", href="/execute-recipe", external_link=True - ), + dbc.DropdownMenuItem("Edit Code", href="/edit-recipe", external_link=True), dbc.DropdownMenuItem("Document", href="/data", external_link=True), ], nav=True, in_navbar=True, - label="Recipe", - ), - dbc.NavItem( - dbc.NavLink("Manual Control", href="/manual-control", external_link=True) - ), - dbc.NavItem( - dbc.NavLink("Database Browser", href="/database", external_link=True) + label="Tools", ), ], brand="AAMP", @@ -105,10 +91,9 @@ def update_upstream_recipe_dict(): com.document["recipe_dict"] = { "devices": recipe_dict[0], "commands": recipe_dict[1], + "execution_options": recipe_dict[2] } - mongo.db["recipes"].update_one( - {"_id": com.document["_id"]}, {"$set": com.document} - ) + mongo.db["recipes"].update_one({"_id": com.document["_id"]}, {"$set": com.document}) print("successfully updated recipe_dict upstream") return True else: @@ -119,9 +104,7 @@ def update_upstream_recipe_dict(): def update_execution_upstream(execution): if "document" in list(com.__dict__.keys()): com.document["executions"].append(execution) - mongo.db["recipes"].update_one( - {"_id": com.document["_id"]}, {"$set": com.document} - ) + mongo.db["recipes"].update_one({"_id": com.document["_id"]}, {"$set": com.document}) print("successfully updated execution upstream") return True else: @@ -223,10 +206,7 @@ def get_document_from_db(n_clicks, filename): # homepage "danger", 8000, ] - if ( - document.get("dash_friendly", "") == False - or document.get("python_code", "") == "" - ): + if document.get("dash_friendly", "") == False or document.get("python_code", "") == "": yaml_content = document.get("yaml_data", "") # Update the YAML output with open("to_load.yaml", "w") as file: @@ -263,7 +243,11 @@ def create_new_recipe_doc(n, url, name): mongo.db["recipes"].insert_one( { "file_name": name, - "recipe_dict": {"devices": [], "commands": []}, + "recipe_dict": { + "devices": [], + "commands": [], + "execution_options": {"output_files": [], "default_execution_record_name": "Execution - " + str(name)}, + }, "dash_friendly": True, "executions": [], } @@ -346,9 +330,7 @@ def update_device_table(n_clicks, data, table): # view-recipe page ) def save_command(n_clicks, active_cell, data, value): # view-recipe page print("save_command") - if active_cell is not None and data[active_cell["row"]]["params"] != str( - json.loads(value) - ): + if active_cell is not None and data[active_cell["row"]]["params"] != str(json.loads(value)): com.command_list[data[active_cell["row"]]["index"]][0]._params = eval(value) update_upstream_recipe_dict() return None @@ -373,9 +355,7 @@ def save_command(n_clicks, active_cell, data, value): # view-recipe page ) def save_device(n_clicks, active_cell, data, value): # view-recipe page print("save_device") - if active_cell is not None and data[active_cell["row"]]["params"] != str( - json.loads(value) - ): + if active_cell is not None and data[active_cell["row"]]["params"] != str(json.loads(value)): # data_row = data[active_cell["row"]] params = eval(value) com.device_by_name[params["name"]].update_init_args(params) @@ -427,9 +407,7 @@ def fill_device_add_json_editor(value, is_open): # view-recipe page # args_dict[arg] = value args_list = list(util.devices_ref_redundancy[value]["init"]["args"].keys()) for arg in args_list: - args_dict[arg] = util.devices_ref_redundancy[value]["init"]["args"][arg][ - "default" - ] + args_dict[arg] = util.devices_ref_redundancy[value]["init"]["args"][arg]["default"] return [(json.dumps(args_dict, indent=4))] @@ -454,9 +432,7 @@ def fill_device_json_editor(is_open, active_cell, data): # view-recipe page for port, desc, hwid in sorted(ports): str_ports += f"{port}: {desc} [{hwid}]\n" lines = str_ports.splitlines() - device_port_html = [ - html.Div(["COM Port Info:"], style={"fontWeight": "bold"}) - ] + device_port_html = [html.Div(["COM Port Info:"], style={"fontWeight": "bold"})] device_port_html.append(html.Div([html.Div(line) for line in lines])) else: device_port_html = "" @@ -526,11 +502,7 @@ def enable_add_device_button(value, device_type, is_open): # view-recipe page args = {} for param in sig.parameters.values(): arg_type = param.annotation - args[param.name] = ( - typing.get_args(arg_type)[0] - if typing.get_origin(arg_type) is typing.Union - else arg_type - ) + args[param.name] = typing.get_args(arg_type)[0] if typing.get_origin(arg_type) is typing.Union else arg_type parsed_json = json.loads(value) for key in parsed_json: # print("\n" + key) @@ -616,9 +588,7 @@ def view_recipe_enable_delete_device_button(table_div_children): return True -@app.callback( - Output("delete-command-button", "disabled"), Input("commands-table", "active_cell") -) +@app.callback(Output("delete-command-button", "disabled"), Input("commands-table", "active_cell")) def view_recipe_enable_delete_command_button(table_div_children): active_cell = table_div_children if active_cell is not None: @@ -672,9 +642,7 @@ def update_commands_table(n_clicks, data, table): # view-recipe page command_params = [] for index, command in enumerate(command_list): temp_dict_command_params = {"command": type(command[0]).__name__} - temp_dict_command_params.update( - {"params": str(command[0].get_init_args()), "index": index} - ) + temp_dict_command_params.update({"params": str(command[0].get_init_args()), "index": index}) command_params.append((temp_dict_command_params)) # else: # command_params.append(command._params) @@ -765,9 +733,7 @@ def view_recipe_fill_add_command_json_editor(command_type, url, device_type): if command_type is None or command_type == "": return "" args_dict = {} - args_list = util.devices_ref_redundancy[device_type]["commands"][command_type][ - "args" - ] + args_list = util.devices_ref_redundancy[device_type]["commands"][command_type]["args"] for arg in args_list: args_dict[arg] = args_list[arg]["default"] args_dict["delay"] = 0.0 @@ -831,6 +797,37 @@ def view_recipe_add_command(n, url, device_type, command_type, json_value): ] +@app.callback( + [Output("view-recipe-execution-options-output-files", "value"), Output("view-recipe-execution-options-default-execution-record-name", "value")], + Input("url", "pathname"), +) +def view_recipe_fill_execution_options(url): + if str(url) == "/view-recipe": + if "document" in com.__dict__.keys(): + ls = com.document["recipe_dict"]["execution_options"]["output_files"] + filesToRet = "" + for item in ls: + filesToRet += item + "\n" + return [filesToRet, com.document["recipe_dict"]["execution_options"]["default_execution_record_name"]] + return ["", ""] + + +@app.callback( + [Output('view-recipe-execution-options-saved-label', "children"), Output('view-recipe-execution-options-saved-label', 'style')], + Input('view-recipe-execution-options-save-button', "n_clicks"), + [State('view-recipe-execution-options-output-files', "value"), State('view-recipe-execution-options-default-execution-record-name', "value"), State("url", "pathname")], + prevent_initial_call=True, +) +def view_recipe_save_execution_options(n, filenames, default_execution_record_name, url): + if str(url) == '/view-recipe': + com.execution_options["output_files"] = filenames.splitlines() + com.execution_options["default_execution_record_name"] = default_execution_record_name + success = update_upstream_recipe_dict() + if success: + return ["Saved!", {"display": "block"}] + else: + return ['Something went wrong', {"display": "block"}] + return ["", {"display": "none"}] # --------------------------------------------------- # Python Edit Recipe Page # --------------------------------------------------- @@ -964,9 +961,7 @@ def enable_add_device_button_ace(value, is_openInp, is_open): # python-edit-rec State("command-add-modal-ace", "is_open"), prevent_initial_call=True, ) -def enable_add_command_button_ace( - value, is_openInp, is_open -): # python-edit-recipe page +def enable_add_command_button_ace(value, is_openInp, is_open): # python-edit-recipe page if value == "" or value is None: return True print("enable_add_command_button_ace") @@ -997,9 +992,7 @@ def add_device_to_recipe_ace(n_clicks, value, device_type): # python-edit-recip value = import_line + "\n" + value value = value.replace( "##################################################\n##### Add commands to the command sequence", - "seq.add_device(" - + init_line - + ")\n\n##################################################\n##### Add commands to the command sequence", + "seq.add_device(" + init_line + ")\n\n##################################################\n##### Add commands to the command sequence", ) return [str(value), True, "Device added successfully", "success", 3000] except Exception as e: @@ -1023,35 +1016,25 @@ def add_device_to_recipe_ace(n_clicks, value, device_type): # python-edit-recip ], prevent_initial_call=True, ) -def add_commands_to_recipe_ace( - n_clicks, value, command, device_type -): # python-edit-recipe page +def add_commands_to_recipe_ace(n_clicks, value, command, device_type): # python-edit-recipe page print("add_commands_to_recipe_ace") og_value = str(value) if value == "" or value is None: return ["", True, "No code in editor", "warning", 3000] try: value = str(value) - command_line = util.devices_ref_redundancy[device_type]["commands"][command][ - "default_code" - ] + command_line = util.devices_ref_redundancy[device_type]["commands"][command]["default_code"] import_line = util.devices_ref_redundancy[device_type]["import_commands"] import_device_line = util.devices_ref_redundancy[device_type]["import_device"] if import_device_line not in value: - raise Exception( - "Device (or its import '" - + import_device_line - + "') not found in recipe" - ) + raise Exception("Device (or its import '" + import_device_line + "') not found in recipe") if import_line not in value: value = import_line + "\n" + value if "\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)" not in value: raise Exception("Code is not in valid format") value = value.replace( "\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)", - "\nseq.add_command(" - + command_line - + ")\n\n\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)", + "\nseq.add_command(" + command_line + ")\n\n\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)", ) return [str(value), True, "Command added successfully", "success", 3000] except Exception as e: @@ -1152,9 +1135,7 @@ def execute_recipe_load_document_viewer(n, url): ], prevent_initial_call=True, ) -def execute_recipe_upload_data( - n_clicks, url, name, recipe_data, console_log, notes, files -): +def execute_recipe_upload_data(n_clicks, url, name, recipe_data, console_log, notes, files): if str(url) == "/execute-recipe": print("execute_recipe_upload_data") execution = {} @@ -1306,9 +1287,7 @@ def create_manual_control_device_form(value, url): toRet.append( dbc.Row( [ - dbc.Label( - [arg], html_for=str(value + "+" + arg), width=2 - ), + dbc.Label([arg], html_for=str(value + "+" + arg), width=2), dbc.Col( [ dbc.Input( @@ -1347,34 +1326,22 @@ def create_manual_control_command_form(command, device, url, device_form): toRet = [] if util.devices_ref_redundancy[device]["serial"] == True: seq_toRet = [] - for seq_command in util.devices_ref_redundancy[device][ - "serial_sequence" - ]: + for seq_command in util.devices_ref_redundancy[device]["serial_sequence"]: seq_toRet.append(dbc.Row([dbc.Label([seq_command])])) - args = util.devices_ref_redundancy[device]["commands"][seq_command][ - "args" - ] + args = util.devices_ref_redundancy[device]["commands"][seq_command]["args"] for arg in args: seq_toRet.append( dbc.Row( [ dbc.Label( [arg], - html_for=str( - device + "+" + seq_command + "+" + arg - ), + html_for=str(device + "+" + seq_command + "+" + arg), width=2, ), dbc.Col( [ dbc.Input( - id=str( - device - + "+" - + seq_command - + "+" - + arg - ), + id=str(device + "+" + seq_command + "+" + arg), value=args[arg]["default"], placeholder=args[arg]["notes"], ), @@ -1478,9 +1445,7 @@ def open_manual_control_execute_modal(n, url): Output("manual-control-alert", "children", allow_duplicate=True), Output("manual-control-alert", "color", allow_duplicate=True), Output("manual-control-alert", "duration", allow_duplicate=True), - Output( - "manual-control-execute-modal-body-code", "children", allow_duplicate=True - ), + Output("manual-control-execute-modal-body-code", "children", allow_duplicate=True), ], Input("manual-control-open-execute-modal-button", "n_clicks"), [ @@ -1493,9 +1458,7 @@ def open_manual_control_execute_modal(n, url): ], prevent_initial_call=True, ) -def manual_control_execute_fill_code( - n, url, opt, device, command, device_form, command_form -): +def manual_control_execute_fill_code(n, url, opt, device, command, device_form, command_form): if str(url) == "/manual-control": print("manual_control_execute_fill_code") if device is None or device == "" or command is None or command == "": @@ -1522,18 +1485,10 @@ def manual_control_execute_fill_code( instantiate_code += arg + "=" if util.devices_ref_redundancy[device]["init"]["args"][arg]["type"] == str: instantiate_code += "'" - instantiate_code += str( - device_form[i]["props"]["children"][1]["props"]["children"][0][ - "props" - ]["value"] - ) + instantiate_code += str(device_form[i]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) instantiate_code += "'" else: - instantiate_code += str( - device_form[i]["props"]["children"][1]["props"]["children"][0][ - "props" - ]["value"] - ) + instantiate_code += str(device_form[i]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) instantiate_code += ")" code_seq = str(device) + "_seq" code += code_seq + " = CommandSequence()" @@ -1542,15 +1497,9 @@ def manual_control_execute_fill_code( code += "\n" if util.devices_ref_redundancy[device]["serial"] == True: - for i, serial_seq_command in enumerate( - util.devices_ref_redundancy[device]["serial_sequence"] - ): + for i, serial_seq_command in enumerate(util.devices_ref_redundancy[device]["serial_sequence"]): code += code_seq + ".add_command(" + str(serial_seq_command) + "(" - for ii, serial_seq_command_arg in enumerate( - util.devices_ref_redundancy[device]["commands"][serial_seq_command][ - "args" - ] - ): + for ii, serial_seq_command_arg in enumerate(util.devices_ref_redundancy[device]["commands"][serial_seq_command]["args"]): if ii != 0: code += ", " if serial_seq_command_arg == "receiver": @@ -1560,30 +1509,17 @@ def manual_control_execute_fill_code( + str(device) + "_seq.device_by_name['" + str( - command_form[0]["props"]["children"][(2 * ii) + 1][ - "props" - ]["children"][1]["props"]["children"][0]["props"][ - "value" - ] + command_form[0]["props"]["children"][(2 * ii) + 1]["props"]["children"][1]["props"]["children"][0]["props"]["value"] ) + "']" ) - elif ( - util.devices_ref_redundancy[device]["commands"][ - serial_seq_command - ]["args"][serial_seq_command_arg]["type"] - == str - ): + elif util.devices_ref_redundancy[device]["commands"][serial_seq_command]["args"][serial_seq_command_arg]["type"] == str: code += ( serial_seq_command_arg + "=" + "'" + str( - command_form[0]["props"]["children"][(2 * ii) + 1][ - "props" - ]["children"][1]["props"]["children"][0]["props"][ - "value" - ] + command_form[0]["props"]["children"][(2 * ii) + 1]["props"]["children"][1]["props"]["children"][0]["props"]["value"] ) + "'" ) @@ -1592,19 +1528,13 @@ def manual_control_execute_fill_code( serial_seq_command_arg + "=" + str( - command_form[0]["props"]["children"][(2 * ii) + 1][ - "props" - ]["children"][1]["props"]["children"][0]["props"][ - "value" - ] + command_form[0]["props"]["children"][(2 * ii) + 1]["props"]["children"][1]["props"]["children"][0]["props"]["value"] ) ) code += "))\n" code += code_seq + ".add_command(" + str(command) + "(" - for ii, seq_command_arg in enumerate( - util.devices_ref_redundancy[device]["commands"][command]["args"] - ): + for ii, seq_command_arg in enumerate(util.devices_ref_redundancy[device]["commands"][command]["args"]): if ii != 0: code += ", " if seq_command_arg == "receiver": @@ -1613,47 +1543,28 @@ def manual_control_execute_fill_code( + "=" + str(device) + "_seq.device_by_name['" - + str( - command_form[2]["props"]["children"][ii]["props"][ - "children" - ][1]["props"]["children"][0]["props"]["value"] - ) + + str(command_form[2]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + "']" ) - elif ( - util.devices_ref_redundancy[device]["commands"][command]["args"][ - seq_command_arg - ]["type"] - == str - ): + elif util.devices_ref_redundancy[device]["commands"][command]["args"][seq_command_arg]["type"] == str: code += ( seq_command_arg + "=" + "'" - + str( - command_form[2]["props"]["children"][ii]["props"][ - "children" - ][1]["props"]["children"][0]["props"]["value"] - ) + + str(command_form[2]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + "'" ) else: code += ( seq_command_arg + "=" - + str( - command_form[2]["props"]["children"][ii]["props"][ - "children" - ][1]["props"]["children"][0]["props"]["value"] - ) + + str(command_form[2]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) ) code += "))\n\n" else: code += code_seq + ".add_command(" + str(command) + "(" - for ii, seq_command_arg in enumerate( - util.devices_ref_redundancy[device]["commands"][command]["args"] - ): + for ii, seq_command_arg in enumerate(util.devices_ref_redundancy[device]["commands"][command]["args"]): if ii != 0: code += ", " if seq_command_arg == "receiver": @@ -1662,48 +1573,26 @@ def manual_control_execute_fill_code( + "=" + str(device) + "_seq.device_by_name['" - + str( - command_form[1]["props"]["children"][ii]["props"][ - "children" - ][1]["props"]["children"][0]["props"]["value"] - ) + + str(command_form[1]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + "']" ) - elif ( - util.devices_ref_redundancy[device]["commands"][command]["args"][ - seq_command_arg - ]["type"] - == str - ): + elif util.devices_ref_redundancy[device]["commands"][command]["args"][seq_command_arg]["type"] == str: code += ( seq_command_arg + "=" + "'" - + str( - command_form[1]["props"]["children"][ii]["props"][ - "children" - ][1]["props"]["children"][0]["props"]["value"] - ) + + str(command_form[1]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + "'" ) else: code += ( seq_command_arg + "=" - + str( - command_form[1]["props"]["children"][ii]["props"][ - "children" - ][1]["props"]["children"][0]["props"]["value"] - ) + + str(command_form[1]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) ) code += "))\n\n" - code += ( - str(device) - + "_seq_invoker = CommandInvoker(" - + str(device) - + "_seq, False, False, False)\n" - ) + code += str(device) + "_seq_invoker = CommandInvoker(" + str(device) + "_seq, False, False, False)\n" code += str(device) + "_seq_invoker.invoke_commands()" interceptor = ConsoleInterceptor() print("\n") @@ -1918,9 +1807,7 @@ def fill_database_collection_schema(collection, db, url): print("fill_database_collection_schema") if collection is not None and collection != "" and db is not None and db != "": try: - schema = ( - mongo.client[db].get_collection(collection).options()["validator"] - ) + schema = mongo.client[db].get_collection(collection).options()["validator"] schema = process_schema(schema) return render_dict(schema) except Exception as e: diff --git a/command_sequence.py b/command_sequence.py index 2de3f58..9822fad 100644 --- a/command_sequence.py +++ b/command_sequence.py @@ -29,6 +29,7 @@ def __init__(self): self.db_device_list = [] self.command_list = [] self.db_command_list = [] + self.execution_options = {} self.num_iterations = "ALL" # self.processed_devices = [] # self.processed_commands = [] @@ -605,6 +606,7 @@ def get_recipe(self): """Returns a list for use with the dashboard.""" devices = [] commands = [] + execution_options = self.execution_options for i, device in enumerate(self.device_list): devices.append( {str(device.__class__.__name__): {"args": device.get_init_args()}} @@ -628,7 +630,7 @@ def get_recipe(self): } } ) - return [devices, commands] + return [devices, commands, execution_options] def load_from_dict(self, recipe_dict): """Loads a recipe from a dictionary.""" @@ -653,6 +655,7 @@ def load_from_dict(self, recipe_dict): receiver=self.device_by_name[rec_name], **command_params["args"] ) ) + self.execution_options = recipe_dict["execution_options"] def add_device_from_dict(self, device_type, device_dict): self.add_device(util.devices_ref_redundancy[device_type]["obj"](**device_dict)) diff --git a/pages/view-recipe.py b/pages/view-recipe.py index f917248..31102be 100644 --- a/pages/view-recipe.py +++ b/pages/view-recipe.py @@ -24,15 +24,13 @@ dbc.Button("Refresh", id="refresh-button1", n_clicks=0), dbc.Button("Add device", id="add-device-button"), dbc.Button("Edit", id="edit-device-button"), - dbc.Button('Delete', id='delete-device-button'), + dbc.Button("Delete", id="delete-device-button"), ], className="mb-3", ), dbc.Modal( [ - dbc.ModalHeader( - dbc.ModalTitle("Editor"), close_button=False - ), + dbc.ModalHeader(dbc.ModalTitle("Editor"), close_button=False), dbc.ModalBody( [ dcc.Textarea( @@ -56,9 +54,7 @@ ), ] ), - dbc.ModalFooter( - dbc.Button("Save", id="save-device-editor") - ), + dbc.ModalFooter(dbc.Button("Save", id="save-device-editor")), ], id="device-editor-modal", keyboard=False, @@ -96,9 +92,7 @@ ), ] ), - dbc.ModalFooter( - dbc.Button("Add", id="add-device-editor") - ), + dbc.ModalFooter(dbc.Button("Add", id="add-device-editor")), ], id="device-add-modal", keyboard=False, @@ -117,19 +111,15 @@ dbc.ButtonGroup( [ dbc.Button("Refresh", id="refresh-button2", n_clicks=0), - dbc.Button( - "Add command", id="add-command-open-modal-button" - ), + dbc.Button("Add command", id="add-command-open-modal-button"), dbc.Button("Edit", id="edit-command-button"), - dbc.Button('Delete', id='delete-command-button'), + dbc.Button("Delete", id="delete-command-button"), ], className="mb-3", ), dbc.Modal( [ - dbc.ModalHeader( - dbc.ModalTitle("Editor"), close_button=False - ), + dbc.ModalHeader(dbc.ModalTitle("Editor"), close_button=False), dbc.ModalBody( [ dcc.Textarea( @@ -150,9 +140,7 @@ ), ] ), - dbc.ModalFooter( - dbc.Button("Save", id="save-command-editor") - ), + dbc.ModalFooter(dbc.Button("Save", id="save-command-editor")), ], id="command-editor-modal", keyboard=False, @@ -192,11 +180,7 @@ ), ] ), - dbc.ModalFooter( - dbc.Button( - "Add", id="view-recipe-add-command-editor" - ) - ), + dbc.ModalFooter(dbc.Button("Add", id="view-recipe-add-command-editor")), ], id="view-recipe-command-add-modal", keyboard=False, @@ -219,12 +203,43 @@ ], className="table-container mb-3", ), - html.Div( [ html.H2("Execution Options"), + dbc.Row( + [ + dbc.Row( + [ + dbc.Col( + [ + html.H5("Output Files"), + dbc.Textarea( + id="view-recipe-execution-options-output-files", + placeholder="Enter one filename with extension per line", + ), + ] + ), + dbc.Col( + [ + html.H5("Default Execution Record Name"), + dbc.Col( + [ + dbc.Input( + id="view-recipe-execution-options-default-execution-record-name", + ) + ] + ), + ] + ), + ], + className="mb-3", + ), + ] + ), + dbc.Button("Save Options", id="view-recipe-execution-options-save-button", n_clicks=0, className="mb-3"), + dbc.Label("Execution Options Saved", id="view-recipe-execution-options-saved-label", style={"display": "none"}), ], - className="table-container mb-3", + className="table-container mb-5", ) # html.Div( # [ From fd56b35e999e5462c13b84372b6b9d3bd1548841 Mon Sep 17 00:00:00 2001 From: Piyush Date: Wed, 26 Jul 2023 21:28:56 -0500 Subject: [PATCH 049/125] formatting --- app.py | 162 +++++++++++++++++++++++++++++++--------- command_sequence.py | 52 ++++++------- pages/execute-recipe.py | 8 +- pages/load-recipe.py | 34 ++++----- pages/manual-control.py | 8 +- pages/view-recipe.py | 17 ++++- util.py | 39 ++++------ 7 files changed, 200 insertions(+), 120 deletions(-) diff --git a/app.py b/app.py index b25b08e..4ca5eca 100644 --- a/app.py +++ b/app.py @@ -34,7 +34,11 @@ mongo = MongoDBHelper( - "mongodb+srv://" + mongo_username + ":" + mongo_password + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "mongodb+srv://" + + mongo_username + + ":" + + mongo_password + + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", "diaogroup", ) @@ -91,7 +95,7 @@ def update_upstream_recipe_dict(): com.document["recipe_dict"] = { "devices": recipe_dict[0], "commands": recipe_dict[1], - "execution_options": recipe_dict[2] + "execution_options": recipe_dict[2], } mongo.db["recipes"].update_one({"_id": com.document["_id"]}, {"$set": com.document}) print("successfully updated recipe_dict upstream") @@ -246,7 +250,10 @@ def create_new_recipe_doc(n, url, name): "recipe_dict": { "devices": [], "commands": [], - "execution_options": {"output_files": [], "default_execution_record_name": "Execution - " + str(name)}, + "execution_options": { + "output_files": [], + "default_execution_record_name": "Execution - " + str(name), + }, }, "dash_friendly": True, "executions": [], @@ -502,7 +509,11 @@ def enable_add_device_button(value, device_type, is_open): # view-recipe page args = {} for param in sig.parameters.values(): arg_type = param.annotation - args[param.name] = typing.get_args(arg_type)[0] if typing.get_origin(arg_type) is typing.Union else arg_type + args[param.name] = ( + typing.get_args(arg_type)[0] + if typing.get_origin(arg_type) is typing.Union + else arg_type + ) parsed_json = json.loads(value) for key in parsed_json: # print("\n" + key) @@ -798,7 +809,10 @@ def view_recipe_add_command(n, url, device_type, command_type, json_value): @app.callback( - [Output("view-recipe-execution-options-output-files", "value"), Output("view-recipe-execution-options-default-execution-record-name", "value")], + [ + Output("view-recipe-execution-options-output-files", "value"), + Output("view-recipe-execution-options-default-execution-record-name", "value"), + ], Input("url", "pathname"), ) def view_recipe_fill_execution_options(url): @@ -808,26 +822,38 @@ def view_recipe_fill_execution_options(url): filesToRet = "" for item in ls: filesToRet += item + "\n" - return [filesToRet, com.document["recipe_dict"]["execution_options"]["default_execution_record_name"]] + return [ + filesToRet, + com.document["recipe_dict"]["execution_options"]["default_execution_record_name"], + ] return ["", ""] @app.callback( - [Output('view-recipe-execution-options-saved-label', "children"), Output('view-recipe-execution-options-saved-label', 'style')], - Input('view-recipe-execution-options-save-button', "n_clicks"), - [State('view-recipe-execution-options-output-files', "value"), State('view-recipe-execution-options-default-execution-record-name', "value"), State("url", "pathname")], - prevent_initial_call=True, + [ + Output("view-recipe-execution-options-saved-label", "children"), + Output("view-recipe-execution-options-saved-label", "style"), + ], + Input("view-recipe-execution-options-save-button", "n_clicks"), + [ + State("view-recipe-execution-options-output-files", "value"), + State("view-recipe-execution-options-default-execution-record-name", "value"), + State("url", "pathname"), + ], + prevent_initial_call=True, ) def view_recipe_save_execution_options(n, filenames, default_execution_record_name, url): - if str(url) == '/view-recipe': + if str(url) == "/view-recipe": com.execution_options["output_files"] = filenames.splitlines() com.execution_options["default_execution_record_name"] = default_execution_record_name success = update_upstream_recipe_dict() if success: return ["Saved!", {"display": "block"}] else: - return ['Something went wrong', {"display": "block"}] + return ["Something went wrong", {"display": "block"}] return ["", {"display": "none"}] + + # --------------------------------------------------- # Python Edit Recipe Page # --------------------------------------------------- @@ -992,7 +1018,9 @@ def add_device_to_recipe_ace(n_clicks, value, device_type): # python-edit-recip value = import_line + "\n" + value value = value.replace( "##################################################\n##### Add commands to the command sequence", - "seq.add_device(" + init_line + ")\n\n##################################################\n##### Add commands to the command sequence", + "seq.add_device(" + + init_line + + ")\n\n##################################################\n##### Add commands to the command sequence", ) return [str(value), True, "Device added successfully", "success", 3000] except Exception as e: @@ -1027,14 +1055,18 @@ def add_commands_to_recipe_ace(n_clicks, value, command, device_type): # python import_line = util.devices_ref_redundancy[device_type]["import_commands"] import_device_line = util.devices_ref_redundancy[device_type]["import_device"] if import_device_line not in value: - raise Exception("Device (or its import '" + import_device_line + "') not found in recipe") + raise Exception( + "Device (or its import '" + import_device_line + "') not found in recipe" + ) if import_line not in value: value = import_line + "\n" + value if "\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)" not in value: raise Exception("Code is not in valid format") value = value.replace( "\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)", - "\nseq.add_command(" + command_line + ")\n\n\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)", + "\nseq.add_command(" + + command_line + + ")\n\n\nrecipe_file = 'to_save.yaml'\nseq.save_to_yaml(recipe_file)", ) return [str(value), True, "Command added successfully", "success", 3000] except Exception as e: @@ -1485,10 +1517,14 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, instantiate_code += arg + "=" if util.devices_ref_redundancy[device]["init"]["args"][arg]["type"] == str: instantiate_code += "'" - instantiate_code += str(device_form[i]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + instantiate_code += str( + device_form[i]["props"]["children"][1]["props"]["children"][0]["props"]["value"] + ) instantiate_code += "'" else: - instantiate_code += str(device_form[i]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + instantiate_code += str( + device_form[i]["props"]["children"][1]["props"]["children"][0]["props"]["value"] + ) instantiate_code += ")" code_seq = str(device) + "_seq" code += code_seq + " = CommandSequence()" @@ -1497,9 +1533,13 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, code += "\n" if util.devices_ref_redundancy[device]["serial"] == True: - for i, serial_seq_command in enumerate(util.devices_ref_redundancy[device]["serial_sequence"]): + for i, serial_seq_command in enumerate( + util.devices_ref_redundancy[device]["serial_sequence"] + ): code += code_seq + ".add_command(" + str(serial_seq_command) + "(" - for ii, serial_seq_command_arg in enumerate(util.devices_ref_redundancy[device]["commands"][serial_seq_command]["args"]): + for ii, serial_seq_command_arg in enumerate( + util.devices_ref_redundancy[device]["commands"][serial_seq_command]["args"] + ): if ii != 0: code += ", " if serial_seq_command_arg == "receiver": @@ -1509,17 +1549,26 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, + str(device) + "_seq.device_by_name['" + str( - command_form[0]["props"]["children"][(2 * ii) + 1]["props"]["children"][1]["props"]["children"][0]["props"]["value"] + command_form[0]["props"]["children"][(2 * ii) + 1]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] ) + "']" ) - elif util.devices_ref_redundancy[device]["commands"][serial_seq_command]["args"][serial_seq_command_arg]["type"] == str: + elif ( + util.devices_ref_redundancy[device]["commands"][serial_seq_command]["args"][ + serial_seq_command_arg + ]["type"] + == str + ): code += ( serial_seq_command_arg + "=" + "'" + str( - command_form[0]["props"]["children"][(2 * ii) + 1]["props"]["children"][1]["props"]["children"][0]["props"]["value"] + command_form[0]["props"]["children"][(2 * ii) + 1]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] ) + "'" ) @@ -1528,13 +1577,17 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, serial_seq_command_arg + "=" + str( - command_form[0]["props"]["children"][(2 * ii) + 1]["props"]["children"][1]["props"]["children"][0]["props"]["value"] + command_form[0]["props"]["children"][(2 * ii) + 1]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] ) ) code += "))\n" code += code_seq + ".add_command(" + str(command) + "(" - for ii, seq_command_arg in enumerate(util.devices_ref_redundancy[device]["commands"][command]["args"]): + for ii, seq_command_arg in enumerate( + util.devices_ref_redundancy[device]["commands"][command]["args"] + ): if ii != 0: code += ", " if seq_command_arg == "receiver": @@ -1543,28 +1596,47 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, + "=" + str(device) + "_seq.device_by_name['" - + str(command_form[2]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + + str( + command_form[2]["props"]["children"][ii]["props"]["children"][1][ + "props" + ]["children"][0]["props"]["value"] + ) + "']" ) - elif util.devices_ref_redundancy[device]["commands"][command]["args"][seq_command_arg]["type"] == str: + elif ( + util.devices_ref_redundancy[device]["commands"][command]["args"][ + seq_command_arg + ]["type"] + == str + ): code += ( seq_command_arg + "=" + "'" - + str(command_form[2]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + + str( + command_form[2]["props"]["children"][ii]["props"]["children"][1][ + "props" + ]["children"][0]["props"]["value"] + ) + "'" ) else: code += ( seq_command_arg + "=" - + str(command_form[2]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + + str( + command_form[2]["props"]["children"][ii]["props"]["children"][1][ + "props" + ]["children"][0]["props"]["value"] + ) ) code += "))\n\n" else: code += code_seq + ".add_command(" + str(command) + "(" - for ii, seq_command_arg in enumerate(util.devices_ref_redundancy[device]["commands"][command]["args"]): + for ii, seq_command_arg in enumerate( + util.devices_ref_redundancy[device]["commands"][command]["args"] + ): if ii != 0: code += ", " if seq_command_arg == "receiver": @@ -1573,26 +1645,48 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, + "=" + str(device) + "_seq.device_by_name['" - + str(command_form[1]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + + str( + command_form[1]["props"]["children"][ii]["props"]["children"][1][ + "props" + ]["children"][0]["props"]["value"] + ) + "']" ) - elif util.devices_ref_redundancy[device]["commands"][command]["args"][seq_command_arg]["type"] == str: + elif ( + util.devices_ref_redundancy[device]["commands"][command]["args"][ + seq_command_arg + ]["type"] + == str + ): code += ( seq_command_arg + "=" + "'" - + str(command_form[1]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + + str( + command_form[1]["props"]["children"][ii]["props"]["children"][1][ + "props" + ]["children"][0]["props"]["value"] + ) + "'" ) else: code += ( seq_command_arg + "=" - + str(command_form[1]["props"]["children"][ii]["props"]["children"][1]["props"]["children"][0]["props"]["value"]) + + str( + command_form[1]["props"]["children"][ii]["props"]["children"][1][ + "props" + ]["children"][0]["props"]["value"] + ) ) code += "))\n\n" - code += str(device) + "_seq_invoker = CommandInvoker(" + str(device) + "_seq, False, False, False)\n" + code += ( + str(device) + + "_seq_invoker = CommandInvoker(" + + str(device) + + "_seq, False, False, False)\n" + ) code += str(device) + "_seq_invoker.invoke_commands()" interceptor = ConsoleInterceptor() print("\n") diff --git a/command_sequence.py b/command_sequence.py index 9822fad..e8ff61f 100644 --- a/command_sequence.py +++ b/command_sequence.py @@ -85,9 +85,7 @@ def remove_device_by_index(self, index: Optional[int] = None): del self.device_list[index] self.update_device_by_name() - def add_command( - self, command: Union[Command, List[Command]], index: Optional[int] = None - ): + def add_command(self, command: Union[Command, List[Command]], index: Optional[int] = None): """Add a command to the command list. Parameters @@ -223,9 +221,7 @@ def move_command_iteration_by_index(self, index: int, old_iter: int, new_iter: i and new_iter <= len(self.command_list[index]) - 1 ): if old_iter != new_iter: - self.command_list[index].insert( - new_iter, self.command_list[index].pop(old_iter) - ) + self.command_list[index].insert(new_iter, self.command_list[index].pop(old_iter)) else: print("Invalid indices") @@ -478,9 +474,7 @@ def get_command_names(self, unloop: bool = False) -> List[str]: if iter_ndx == 0: name_list.append(iter_command.name) else: - name_list.append( - " IterIndex" + str(iter_ndx) + ": " + iter_command.name - ) + name_list.append(" IterIndex" + str(iter_ndx) + ": " + iter_command.name) return name_list def get_command_names_descriptions(self, unloop: bool = False) -> List[List[str]]: @@ -561,9 +555,7 @@ def count_loop_commands(self) -> int: loop_marker_count = 0 for index, command_iters in enumerate(self.command_list): for iter_index, command in enumerate(command_iters): - if isinstance(command, LoopStartCommand) or isinstance( - command, LoopEndCommand - ): + if isinstance(command, LoopStartCommand) or isinstance(command, LoopEndCommand): loop_marker_count += 1 return loop_marker_count @@ -574,9 +566,7 @@ def remove_all_loop_commands(self): while self.count_loop_commands() != 0: for index, command_iters in enumerate(self.command_list): for iter_index, command in enumerate(command_iters): - if isinstance(command, LoopStartCommand) or isinstance( - command, LoopEndCommand - ): + if isinstance(command, LoopStartCommand) or isinstance(command, LoopEndCommand): # delete the command del self.command_list[index][iter_index] if len(self.command_list[index]) == 0: @@ -608,9 +598,7 @@ def get_recipe(self): commands = [] execution_options = self.execution_options for i, device in enumerate(self.device_list): - devices.append( - {str(device.__class__.__name__): {"args": device.get_init_args()}} - ) + devices.append({str(device.__class__.__name__): {"args": device.get_init_args()}}) for i, command in enumerate(self.command_list): arg_params = {} util_arg_params = util.devices_ref_redundancy[ @@ -639,9 +627,7 @@ def load_from_dict(self, recipe_dict): for device in recipe_dict["devices"]: for device_type, device_params in device.items(): self.add_device( - util.devices_ref_redundancy[device_type]["obj"]( - **device_params["args"] - ) + util.devices_ref_redundancy[device_type]["obj"](**device_params["args"]) ) self.update_device_by_name() for command in recipe_dict["commands"]: @@ -649,11 +635,9 @@ def load_from_dict(self, recipe_dict): rec_name = command_params["args"]["receiver_name"] del command_params["args"]["receiver_name"] self.add_command( - util.devices_ref_redundancy[command_params["device"]]["commands"][ - command_type - ]["obj"]( - receiver=self.device_by_name[rec_name], **command_params["args"] - ) + util.devices_ref_redundancy[command_params["device"]]["commands"][command_type][ + "obj" + ](receiver=self.device_by_name[rec_name], **command_params["args"]) ) self.execution_options = recipe_dict["execution_options"] @@ -663,13 +647,23 @@ def add_device_from_dict(self, device_type, device_dict): def add_command_from_dict(self, device_type, command_type, command_dict): if device_type == "UtilityCommands": - self.add_command(util.devices_ref_redundancy[device_type]['commands'][command_type]['obj'](**command_dict)) + self.add_command( + util.devices_ref_redundancy[device_type]["commands"][command_type]["obj"]( + **command_dict + ) + ) return command_dict_receiver = command_dict["receiver"] - command_dict_delay = command_dict['delay'] + command_dict_delay = command_dict["delay"] del command_dict["receiver"] del command_dict["delay"] - self.add_command(util.devices_ref_redundancy[device_type]['commands'][command_type]['obj'](receiver=self.device_by_name[command_dict_receiver], delay = command_dict_delay, **command_dict)) + self.add_command( + util.devices_ref_redundancy[device_type]["commands"][command_type]["obj"]( + receiver=self.device_by_name[command_dict_receiver], + delay=command_dict_delay, + **command_dict + ) + ) def clear_recipe(self): """Clears the recipe.""" diff --git a/pages/execute-recipe.py b/pages/execute-recipe.py index f888eef..0055896 100644 --- a/pages/execute-recipe.py +++ b/pages/execute-recipe.py @@ -13,9 +13,7 @@ [ dbc.Button("Execute", id="execute-button", n_clicks=0), dbc.Button("Clear Log", id="reset-button", n_clicks=0), - dbc.Button( - "Emergency Stop", id="stop-button", n_clicks=0, color="danger" - ), + dbc.Button("Emergency Stop", id="stop-button", n_clicks=0, color="danger"), ], className="mb-3", ), @@ -44,9 +42,7 @@ # ], # className="d-flex align-items-center mt-3", # ), - html.Div( - id="execute-recipe-output", className="mt-3", style={"display": "none"} - ), + html.Div(id="execute-recipe-output", className="mt-3", style={"display": "none"}), dcc.Interval(id="update-interval", interval=500, n_intervals=0), dcc.Interval(id="interval1", interval=50, n_intervals=0), html.Div(id="hidden-div", style={"display": "none"}), diff --git a/pages/load-recipe.py b/pages/load-recipe.py index dcd38f3..5cac21f 100644 --- a/pages/load-recipe.py +++ b/pages/load-recipe.py @@ -5,7 +5,6 @@ dash.register_page(__name__, path="/load-recipe", title="Load Recipe", name="Load Recipe") - layout = html.Div( [ html.H1("Load Recipe"), @@ -43,25 +42,26 @@ dbc.Row( [ dbc.Col( - [dbc.Button( - "Refresh List", - id="home-refresh-list-button", - n_clicks=0, - color="secondary", - className="btn btn-secondary mb-3", - ), + [ + dbc.Button( + "Refresh List", + id="home-refresh-list-button", + n_clicks=0, + color="secondary", + className="btn btn-secondary mb-3", + ), ], width=2, ), - dbc.Col([ - dbc.Button( - "Create New Recipe", - id="home-create-new-recipe-button", - n_clicks=0, - - ) - ], - ) + dbc.Col( + [ + dbc.Button( + "Create New Recipe", + id="home-create-new-recipe-button", + n_clicks=0, + ) + ], + ), ] ), dbc.Row( diff --git a/pages/manual-control.py b/pages/manual-control.py index 05d77af..153ecb6 100644 --- a/pages/manual-control.py +++ b/pages/manual-control.py @@ -3,9 +3,7 @@ import dash -dash.register_page( - __name__, path="/manual-control", title="Manual Control", name="Manual Control" -) +dash.register_page(__name__, path="/manual-control", title="Manual Control", name="Manual Control") layout = html.Div( [ @@ -19,9 +17,7 @@ n_clicks=0, disabled=True, ), - dbc.Button( - "Clear", id="manual-control-clear-button", n_clicks=0, disabled=True - ), + dbc.Button("Clear", id="manual-control-clear-button", n_clicks=0, disabled=True), ], className="mb-3", ), diff --git a/pages/view-recipe.py b/pages/view-recipe.py index 31102be..60a5cdc 100644 --- a/pages/view-recipe.py +++ b/pages/view-recipe.py @@ -180,7 +180,9 @@ ), ] ), - dbc.ModalFooter(dbc.Button("Add", id="view-recipe-add-command-editor")), + dbc.ModalFooter( + dbc.Button("Add", id="view-recipe-add-command-editor") + ), ], id="view-recipe-command-add-modal", keyboard=False, @@ -236,8 +238,17 @@ ), ] ), - dbc.Button("Save Options", id="view-recipe-execution-options-save-button", n_clicks=0, className="mb-3"), - dbc.Label("Execution Options Saved", id="view-recipe-execution-options-saved-label", style={"display": "none"}), + dbc.Button( + "Save Options", + id="view-recipe-execution-options-save-button", + n_clicks=0, + className="mb-3", + ), + dbc.Label( + "Execution Options Saved", + id="view-recipe-execution-options-saved-label", + style={"display": "none"}, + ), ], className="table-container mb-5", ) diff --git a/util.py b/util.py index 20492ce..6631ee2 100644 --- a/util.py +++ b/util.py @@ -78,11 +78,7 @@ def evaluate(eval_str): class Encoder(json.JSONEncoder): def default(self, obj): - if ( - isinstance(obj, Device) - or isinstance(obj, Command) - or isinstance(obj, MiscDeviceClass) - ): + if isinstance(obj, Device) or isinstance(obj, Command) or isinstance(obj, MiscDeviceClass): return obj.__dict__ elif isinstance(obj, np.ndarray): return obj.tolist() @@ -192,32 +188,30 @@ def default(self, obj): } - - devices_ref_redundancy = { - "UtilityCommands":{ + "UtilityCommands": { "obj": UtilityCommands, "serial": False, "import_device": "from devices.utility_commands import UtilityCommands", "import_commands": "from commands.utility_commands import *", - "init":{ + "init": { "default_code": "# Utility Commands used", "obj_name": "UtilityCommands", - "args": {} + "args": {}, }, - "commands":{ - "LoopStartCommand":{ + "commands": { + "LoopStartCommand": { "default_code": "LoopStartCommand()", "args": {}, "obj": LoopStartCommand, }, - "LoopEndCommand":{ + "LoopEndCommand": { "default_code": "LoopEndCommand()", "args": {}, "obj": LoopEndCommand, }, - "DelayPauseCommand":{ - "default_code": "DelayPauseCommand(delay=0.0)",\ + "DelayPauseCommand": { + "default_code": "DelayPauseCommand(delay=0.0)", "args": { "delay": { "default": 0.0, @@ -227,7 +221,7 @@ def default(self, obj): }, "obj": DelayPauseCommand, }, - "NotifySlackCommand":{ + "NotifySlackCommand": { "default_code": "NotifySlackCommand(message='Hello World')", "args": { "message": { @@ -238,7 +232,7 @@ def default(self, obj): }, "obj": NotifySlackCommand, }, - "LogUserMessageCommand":{ + "LogUserMessageCommand": { "default_code": "LogUserMessageCommand(message='Hello World')", "args": { "message": { @@ -248,11 +242,9 @@ def default(self, obj): } }, "obj": LogUserMessageCommand, - } - - } - } - , + }, + }, + }, "LinearStage150": { "obj": LinearStage150, "serial": True, @@ -1048,9 +1040,6 @@ def default(self, obj): } - - - # devices_ref = { # "PrintingStage": heating_stage_ref, # "AnnealingStage": heating_stage_ref, From 656cc2003bb1f9b13a9aa3b518c0a3f610815624 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 27 Jul 2023 15:36:59 -0500 Subject: [PATCH 050/125] file upload after execution works --- app.py | 47 ++++++++++++++------ db/{gridfs.py => validation/gridfs_local.py} | 8 ++-- 2 files changed, 39 insertions(+), 16 deletions(-) rename db/{gridfs.py => validation/gridfs_local.py} (72%) diff --git a/app.py b/app.py index 4ca5eca..4104476 100644 --- a/app.py +++ b/app.py @@ -15,6 +15,9 @@ from pw import mongo_username, mongo_password from bson.objectid import ObjectId from console_interceptor import ConsoleInterceptor +from gridfs import GridFS +import base64 +import io try: import serial.tools.list_ports @@ -41,6 +44,7 @@ + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", "diaogroup", ) +mongo_gridfs = GridFS(mongo.db, collection="recipes") app = dash.Dash( __name__, @@ -1137,21 +1141,28 @@ def reset_console(n): # execute-recipe page @app.callback( - Output("execute-recipe-upload-document", "value"), + [ + Output("execute-recipe-upload-document", "value"), + Output("execute-recipe-upload-name", "value"), + ], Input("reset-button", "n_clicks"), State("url", "pathname"), ) def execute_recipe_load_document_viewer(n, url): if str(url) == "/execute-recipe": - if com.device_list != []: - toRet = "" - recipe_ec = com.get_recipe() - for device in recipe_ec[0]: - toRet += str(device) + "\n" - toRet += "\n" - for command in recipe_ec[1]: - toRet += str(command) + "\n" - return [toRet] + if "document" in list(com.__dict__.keys()): + if com.device_list != []: + toRet = "" + recipe_ec = com.get_recipe() + for device in recipe_ec[0]: + toRet += str(device) + "\n" + toRet += "\n" + for command in recipe_ec[1]: + toRet += str(command) + "\n" + if "default_execution_record_name" in list(com.execution_options.keys()): + return [toRet, com.execution_options["default_execution_record_name"]] + return [toRet, "Execution"] + return ["", "No Recipe Loaded"] @app.callback( @@ -1164,21 +1175,31 @@ def execute_recipe_load_document_viewer(n, url): State("console-out2", "children"), State("execute-recipe-upload-notes", "value"), State("execute-recipe-upload-files", "contents"), + State("execute-recipe-upload-files", "filename"), ], prevent_initial_call=True, ) -def execute_recipe_upload_data(n_clicks, url, name, recipe_data, console_log, notes, files): +def execute_recipe_upload_data( + n_clicks, url, name, recipe_data, console_log, notes, files, filenames +): if str(url) == "/execute-recipe": print("execute_recipe_upload_data") execution = {} execution["name"] = name - if isinstance(recipe_data, list): + if isinstance(recipe_data, list) and recipe_data is not None and recipe_data != []: execution["recipe"] = recipe_data[0].split("\n") - else: + elif recipe_data is not None and recipe_data != "": execution["recipe"] = recipe_data.split("\n") execution["notes"] = notes execution["log"] = str(console_log["props"]["children"]).split("\n") + execution["files"] = [] + if isinstance(files, list): + for i, file in enumerate(files): + # print(file) + file_bytes = base64.b64decode(file + "==") + execution["files"].append(mongo_gridfs.put(file_bytes, filename=filenames[i])) exec_success = update_execution_upstream(execution) + # exec_success = False if exec_success: return [html.P("Data uploaded successfully")] else: diff --git a/db/gridfs.py b/db/validation/gridfs_local.py similarity index 72% rename from db/gridfs.py rename to db/validation/gridfs_local.py index 19650a5..44d3216 100644 --- a/db/gridfs.py +++ b/db/validation/gridfs_local.py @@ -1,10 +1,11 @@ from mongodb_helper import MongoDBHelper from gridfs import GridFS from bson import ObjectId +from pw import mongo_password, mongo_username, mongo_uri mongo = MongoDBHelper( - "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + mongo_uri, "diaogroup", ) @@ -18,10 +19,11 @@ # create ObjectId(file_id) in document to point to csv # Retrieve the CSV file from GridFS -gridfs_file = fs.find_one({"filename": "data.csv"}) +gridfs_file = fs.find_one({"_id": ObjectId("64c2d215255f257ee66d30e9")}) # Read the CSV data from the file csv_data = gridfs_file.read() # Print the CSV data -print(csv_data.decode()) +print(csv_data) +# print(csv_data.decode()) From ff3d2cf48cff2a233f7e28a85bba98fbe6c02e78 Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 27 Jul 2023 15:37:20 -0500 Subject: [PATCH 051/125] moved file --- db/{validation => }/gridfs_local.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename db/{validation => }/gridfs_local.py (100%) diff --git a/db/validation/gridfs_local.py b/db/gridfs_local.py similarity index 100% rename from db/validation/gridfs_local.py rename to db/gridfs_local.py From 100378df63ce6bc065033fa43ae525b43c81ff5a Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 27 Jul 2023 15:39:35 -0500 Subject: [PATCH 052/125] rearranged execution options --- pages/view-recipe.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pages/view-recipe.py b/pages/view-recipe.py index 60a5cdc..53e7fc0 100644 --- a/pages/view-recipe.py +++ b/pages/view-recipe.py @@ -212,15 +212,6 @@ [ dbc.Row( [ - dbc.Col( - [ - html.H5("Output Files"), - dbc.Textarea( - id="view-recipe-execution-options-output-files", - placeholder="Enter one filename with extension per line", - ), - ] - ), dbc.Col( [ html.H5("Default Execution Record Name"), @@ -233,6 +224,15 @@ ), ] ), + dbc.Col( + [ + html.H5("Output Files"), + dbc.Textarea( + id="view-recipe-execution-options-output-files", + placeholder="Enter one filename with extension per line", + ), + ] + ), ], className="mb-3", ), From 7f57fa95797caabbda54c1084a321c8fc8b8a362 Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 28 Jul 2023 12:04:59 -0500 Subject: [PATCH 053/125] added dstore to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9a9afc3..3dbbb92 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,4 @@ e4.yaml e4mongo.yaml project_const.py pw.py +.DS_Store From 1e5308b86c8456498301d84aa75bc8752ecd9806 Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 28 Jul 2023 12:06:34 -0500 Subject: [PATCH 054/125] added festo modules as of 4123ed0 --- commands/festo_solenoid_valve_commands.py | 66 ++++++++++++++ devices/festo_solenoid_valve.py | 69 ++++++++++++++ util.py | 104 +++++++++++++++++++++- 3 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 commands/festo_solenoid_valve_commands.py create mode 100644 devices/festo_solenoid_valve.py diff --git a/commands/festo_solenoid_valve_commands.py b/commands/festo_solenoid_valve_commands.py new file mode 100644 index 0000000..6b3ed22 --- /dev/null +++ b/commands/festo_solenoid_valve_commands.py @@ -0,0 +1,66 @@ +# modules for device as of commit 4123ed0 + +from typing import List + +from devices.festo_solenoid_valve import FestoSolenoidValve +from .command import Command, CommandResult + + +class FestoParentCommand(Command): + """Parent class for all Festo Solenoid Valve commands.""" + + receiver_cls = FestoSolenoidValve + + def __init__(self, receiver: FestoSolenoidValve, **kwargs): + super().__init__(receiver, **kwargs) + + +class FestoInitialize(FestoParentCommand): + """Initialize the solenoid valve""" + + def __init__(self, receiver: FestoSolenoidValve, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class FestoDeinitialize(FestoParentCommand): + """Deinitialize the solenoid valve""" + + def __init__(self, receiver: FestoSolenoidValve, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize()) + + +class FestoValveOpen(FestoParentCommand): + """Open the solenoid valve and keep open""" + + def __init__(self, receiver: FestoSolenoidValve, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.valve_open()) + + +class FestoValveClosed(FestoParentCommand): + """Close the solenoid valve and keep closed""" + + def __init__(self, receiver: FestoSolenoidValve, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.valve_closed()) + + +class FestoOpenTimed(FestoParentCommand): + """Open the valve for a set time then close""" + + def __init__(self, receiver: FestoSolenoidValve, time: int, **kwargs): + super().__init__(receiver, **kwargs) + self._params["time"] = time + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.open_timed(self._params["time"])) diff --git a/devices/festo_solenoid_valve.py b/devices/festo_solenoid_valve.py new file mode 100644 index 0000000..83029ea --- /dev/null +++ b/devices/festo_solenoid_valve.py @@ -0,0 +1,69 @@ +# modules for device as of commit 4123ed0 + +"""Requires 'StandardFirmata' basic example uploaded on Arduino Uno""" +from typing import Tuple, Optional +import time +import pyfirmata + +from .device import ArduinoSerialDevice, check_initialized, check_serial + + +class FestoSolenoidValve(ArduinoSerialDevice): + def __init__( + self, + name: str, + numchannel: int, + port: str = "COM5", + baudrate: int = 9600, + timeout: float = 0.1, + ): + super().__init__(name, port, baudrate, timeout) + self.board = pyfirmata.Arduino(self.port) + self.numchannel = numchannel + self.pin = self.board.get_pin(f"d:{numchannel}:o") + + def get_init_args(self) -> dict: + args_dict = { + "name": self.name, + "numchannel": self.numchannel, + "port": self.port, + "baudrate": self.baudrate, + "timeout": self.timeout, + } + return args_dict + + def update_init_args(self, args_dict: dict): + self.name = args_dict["name"] + self.numchannel = args_dict["numchannel"] + self.port = args_dict["port"] + self.pin = self.board.get_pin(f"d:{self.port}:o") # TODO: Check if this is necessary + self.baudrate = args_dict["baudrate"] + self.timeout = args_dict["timeout"] + + def initialize(self) -> Tuple[bool, str]: + self._is_initialized = True + # TODO: solenoid valve initialize + return (True, "Solenoid valve initialized") + + def deinitialize(self) -> Tuple[bool, str]: + self._is_initialized = False + self.board.exit() + return (True, "Solenoid valve deinitialized") + + @check_serial + @check_initialized + def valve_open(self) -> Tuple[bool, str]: + self.pin.write(1) + return (True, "Solenoid valve is open") + + @check_serial + def valve_closed(self) -> Tuple[bool, str]: + self.pin.write(0) + return (True, "Solenoid valve is closed") + + @check_serial + def open_timed(self, time: int) -> Tuple[bool, str]: + self.pin.write(1) + self.board.pass_time(time) + self.pin.write(0) + return (True, f"Solenoid valve was opened for {time} seconds") diff --git a/util.py b/util.py index 6631ee2..655a3d3 100644 --- a/util.py +++ b/util.py @@ -3,8 +3,7 @@ from devices.heating_stage import HeatingStage from devices.multi_stepper import MultiStepper from devices.newport_esp301 import NewportESP301 - -# from devices.stellarnet_spectrometer import StellarNetSpectrometer +from devices.festo_solenoid_valve import FestoSolenoidValve from devices.ximea_camera import XimeaCamera from devices.dummy_heater import DummyHeater from devices.dummy_motor import DummyMotor @@ -18,6 +17,7 @@ from commands.dummy_motor_commands import * from commands.dummy_meter_commands import * from commands.keithley_2450_commands import * +from commands.festo_solenoid_valve_commands import * from commands.ximea_camera_commands import * from commands.utility_commands import * from commands.heating_stage_commands import * @@ -245,6 +245,106 @@ def default(self, obj): }, }, }, + "FestoSolenoidValve": { + "obj": FestoSolenoidValve, + "serial": True, + "serial_sequence": ["FestoInitialize"], + "import_device": "from devices.festo_solenoid_valve import FestoSolenoidValve", + "import_commands": "from commands.festo_solenoid_valve_commands import *", + "init": { + "default_code": "FestoSolenoidValve(name='FestoSolenoidValve', numchannel=, port='COM5', baudrate=9600, timeout=0.1)", + "obj_name": "FestoSolenoidValve", + "args": { + "name": { + "default": "FestoSolenoidValve", + "type": str, + "notes": "Name of the device.", + }, + "numchannel": { + "default": 1, + "type": int, + "notes": "", + }, + "port": { + "default": "COM5", + "type": str, + "notes": "Port", + }, + "baudrate": { + "default": 9600, + "type": int, + "notes": "Baudrate", + }, + "timeout": { + "default": 0.1, + "type": float, + "notes": "Timeout", + }, + }, + }, + "commands": { + "FestoInitialize": { + "default_code": "FestoInitialize(receiver= '')", + "args": { + "receiver": { + "default": "FestoSolenoidValve", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": FestoInitialize, + }, + "FestoDeinitialize": { + "default_code": "FestoDeinitialize(receiver= '')", + "args": { + "receiver": { + "default": "FestoSolenoidValve", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": FestoDeinitialize, + }, + "FestoValveOpen": { + "default_code": "FestoValveOpen(receiver= '')", + "args": { + "receiver": { + "default": "FestoSolenoidValve", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": FestoValveOpen, + }, + "FestoValveClosed": { + "default_code": "FestoValveClosed(receiver= '')", + "args": { + "receiver": { + "default": "FestoSolenoidValve", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": FestoValveClosed, + }, + "FestoOpenTimed": { + "default_code": "FestoOpenTimed(receiver= '', time=0)", + "args": { + "receiver": { + "default": "FestoSolenoidValve", + "type": str, + "notes": "Name of the device", + }, + "time": { + "default": 0, + "type": int, + "notes": "Time to keep the valve open", + }, + }, + "obj": FestoOpenTimed, + }, + }, + }, "LinearStage150": { "obj": LinearStage150, "serial": True, From 51a8d88123e0f7e3ee1d7e03347a04e4a5ba8367 Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 31 Jul 2023 12:51:58 -0500 Subject: [PATCH 055/125] added mts50_z8 --- devices/mts50_z8.py | 167 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 devices/mts50_z8.py diff --git a/devices/mts50_z8.py b/devices/mts50_z8.py new file mode 100644 index 0000000..c3ae497 --- /dev/null +++ b/devices/mts50_z8.py @@ -0,0 +1,167 @@ +from typing import Optional, Tuple +from struct import pack, unpack +from .device import SerialDevice, check_serial, check_initialized +import time + + +class MTS50_Z8(SerialDevice): + def __init__( + self, + name: str, + port: str = "'COM6'", + baudrate: int = 115200, + timeout: float | None = 1, + destination: int = 0x50, + source: int = 0x01, + channel: int = 1, + ): + super().__init__(name, port, baudrate, timeout) + self._destination = destination + self._source = source + self._channel = channel + + def get_init_args(self) -> dict: + args_dict = { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "destination": self._destination, + "source": self._source, + "channel": self._channel, + } + return args_dict + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._destination = args_dict["destination"] + self._source = args_dict["source"] + self._channel = args_dict["channel"] + + @check_serial + def initialize(self) -> Tuple[bool, str]: + self._is_initialized = False + + # Home Stage; MGMSG_MOT_MOVE_HOME + self.ser.write(pack(" Tuple[bool, str]: + # i dont think this is needed: deinitialize + # if reset_init_flag: //used in other devices + self._is_initialized = False + return (True, "Successfully deinitialized MTS50_Z8.") + # return super().deinitialize() + + @check_serial + # @check_initialized + def get_enabled_state(self) -> bool: + self._is_enabled = False + + # TODO: mts50-z8 get enabled state, MGMSG_MOD_GET_CHANENABLESTATE + # self.ser.write(pack(' Tuple[bool, str]: + if state: + self.ser.write(pack(" Tuple[bool, str]: + if position > 150: + return (False, "Position " + str(position) + " is out of range.") + + Device_Unit_SF = 409600 + dUnitpos = int(Device_Unit_SF * position) + self.ser.write( + pack( + " Tuple[bool, str]: + if distance + self.get_position() > 150: + return ( + False, + "Position " + str(distance + self.get_position()) + " is out of range.", + ) + + Device_Unit_SF = 409600 + dUnitpos = int(Device_Unit_SF * distance) + self.ser.write( + pack( + " Date: Mon, 31 Jul 2023 12:59:17 -0500 Subject: [PATCH 056/125] added mts50_z8 commands and to util --- commands/mts50_z8_commands.py | 86 ++++++++++++++++++++++ util.py | 135 ++++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 commands/mts50_z8_commands.py diff --git a/commands/mts50_z8_commands.py b/commands/mts50_z8_commands.py new file mode 100644 index 0000000..ce6f74a --- /dev/null +++ b/commands/mts50_z8_commands.py @@ -0,0 +1,86 @@ +from devices.device import Device +from .command import Command, CommandResult +from devices.mts50_z8 import MTS50_Z8 + + +class MTS50_Z8ParentCommand(Command): + """Parent class for all MTS50_Z8 commands.""" + + receiver_cls = MTS50_Z8 + + def __init__(self, receiver: MTS50_Z8, **kwargs): + super().__init__(receiver, **kwargs) + + +class MTS50_Z8Connect(MTS50_Z8ParentCommand): + """Open a serial port for the stage.""" + + def __init__(self, receiver: MTS50_Z8, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.start_serial()) + + +class MTS50_Z8Initialize(MTS50_Z8ParentCommand): + """Initialize the stage by homing it.""" + + def __init__(self, receiver: MTS50_Z8, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class MTS50_Z8Deinitialize(MTS50_Z8ParentCommand): + """Deinitialize the stage.""" + + def __init__(self, receiver: MTS50_Z8, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize()) + + +class MTS50_Z8EnableMotor(MTS50_Z8ParentCommand): + """Enable the stage motor.""" + + def __init__(self, receiver: MTS50_Z8, **kwargs): + super().__init__(receiver, **kwargs) + self._params["state"] = True + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_enabled_state(self._params["state"])) + + +class MTS50_Z8DisableMotor(MTS50_Z8ParentCommand): + """Disable the stage motor.""" + + def __init__(self, receiver: MTS50_Z8, **kwargs): + super().__init__(receiver, **kwargs) + self._params["state"] = False + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_enabled_state(self._params["state"])) + + +class MTS50_Z8MoveAbsolute(MTS50_Z8ParentCommand): + """Move the stage to an absolute position.""" + + def __init__(self, receiver: MTS50_Z8, position: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params["position"] = position + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_absolute(self._params["position"])) + + +class MTS50_Z8MoveRelative(MTS50_Z8ParentCommand): + """Move the stage by a relative distance.""" + + def __init__(self, receiver: MTS50_Z8, distance: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params["distance"] = distance + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_relative(self._params["distance"])) diff --git a/util.py b/util.py index 655a3d3..dfedc13 100644 --- a/util.py +++ b/util.py @@ -8,11 +8,13 @@ from devices.dummy_heater import DummyHeater from devices.dummy_motor import DummyMotor from devices.linear_stage_150 import LinearStage150 +from devices.mts50_z8 import MTS50_Z8 from devices.keithley_2450 import Keithley2450 from devices.device import Device, MiscDeviceClass from devices.utility_device import UtilityCommands from commands.linear_stage_150_commands import * +from commands.mts50_z8_commands import * from commands.dummy_heater_commands import * from commands.dummy_motor_commands import * from commands.dummy_meter_commands import * @@ -478,6 +480,139 @@ def default(self, obj): }, }, }, + "MTS50_Z8": { + "obj": MTS50_Z8, + "serial": True, + "serial_sequence": ["MTS50_Z8Connect", "MTS50_Z8EnableMotor"], + "import_device": "from devices.mts50_z8 import MTS50_Z8", + "import_commands": "from commands.mts50_z8_commands import *", + "init": { + "default_code": "MTS50_Z8(name='MTS50_Z8', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", + "obj_name": "MTS50_Z8", + "args": { + "name": { + "default": "MTS50_Z8", + "type": str, + "notes": "Name of the device.", + }, + "port": {"default": "COM", "type": str, "notes": "Port"}, + "baudrate": { + "default": 115200, + "type": int, + "notes": "Baudrate", + }, + "timeout": { + "default": 0.1, + "type": float, + "notes": "Timeout", + }, + "destination": { + "default": 0x50, + "type": int, + "notes": "", + }, + "source": { + "default": 0x01, + "type": int, + "notes": "", + }, + "channel": { + "default": 1, + "type": int, + "notes": "", + }, + }, + }, + "commands": { + "MTS50_Z8Connect": { + "default_code": "MTS50_Z8Connect(receiver= '')", + "args": { + "receiver": { + "default": "MTS50_Z8", + "type": str, + "notes": "", + } + }, + "obj": MTS50_Z8Connect, + }, + "MTS50_Z8Initialize": { + "default_code": "MTS50_Z8Initialize(receiver= '')", + "args": { + "receiver": { + "default": "MTS50_Z8", + "type": str, + "notes": "", + } + }, + "obj": MTS50_Z8Initialize, + }, + "MTS50_Z8Deinitialize": { + "default_code": "MTS50_Z8Deinitialize(receiver= '')", + "args": { + "receiver": { + "default": "MTS50_Z8", + "type": str, + "notes": "", + } + }, + "obj": MTS50_Z8Deinitialize, + }, + "MTS50_Z8EnableMotor": { + "default_code": "MTS50_Z8EnableMotor(receiver= '')", + "args": { + "receiver": { + "default": "MTS50_Z8", + "type": str, + "notes": "", + } + }, + "obj": MTS50_Z8EnableMotor, + }, + "MTS50_Z8DisableMotor": { + "default_code": "MTS50_Z8DisableMotor(receiver= '')", + "args": { + "receiver": { + "default": "MTS50_Z8", + "type": str, + "notes": "", + } + }, + "obj": MTS50_Z8DisableMotor, + }, + "MTS50_Z8MoveAbsolute": { + "default_code": "MTS50_Z8MoveAbsolute(receiver= '', position= 0)", + "args": { + "receiver": { + "default": "MTS50_Z8", + "type": str, + "notes": "", + }, + "position": { + "default": 0, + "type": int, + "notes": "", + }, + }, + "obj": MTS50_Z8MoveAbsolute, + }, + "MTS50_Z8MoveRelative": { + "default_code": "MTS50_Z8MoveRelative(receiver= '', distance= 0)", + "args": { + "receiver": { + "default": "MTS50_Z8", + "type": str, + "notes": "", + }, + "distance": { + "default": 0, + "type": int, + "notes": "", + }, + }, + "obj": MTS50_Z8MoveRelative, + }, + }, + }, "Keithley2450": { "obj": Keithley2450, "serial": True, From 8ae12274e1ffe3b27ee207382f5789172c18a876 Mon Sep 17 00:00:00 2001 From: Piyush Date: Fri, 4 Aug 2023 12:51:34 -0500 Subject: [PATCH 057/125] working changes --- app.py | 3 +++ devices/mts50_z8.py | 1 + pages/real-time-telemetry.py | 20 ++++++++++++++++++++ util.py | 12 ++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 pages/real-time-telemetry.py diff --git a/app.py b/app.py index 4104476..c728741 100644 --- a/app.py +++ b/app.py @@ -66,6 +66,9 @@ # dbc.DropdownMenuItem( # "Load Recipe", href="/load-recipe", external_link=True # ), + dbc.DropdownMenuItem( + "Real Time Telemetry", href="/real-time-telemetry", external_link=True + ), dbc.DropdownMenuItem("Edit Code", href="/edit-recipe", external_link=True), dbc.DropdownMenuItem("Document", href="/data", external_link=True), ], diff --git a/devices/mts50_z8.py b/devices/mts50_z8.py index c3ae497..f3dc2c8 100644 --- a/devices/mts50_z8.py +++ b/devices/mts50_z8.py @@ -4,6 +4,7 @@ import time +# uses the kdc101 motor controller class MTS50_Z8(SerialDevice): def __init__( self, diff --git a/pages/real-time-telemetry.py b/pages/real-time-telemetry.py new file mode 100644 index 0000000..f3dc5c6 --- /dev/null +++ b/pages/real-time-telemetry.py @@ -0,0 +1,20 @@ +from dash import Dash, html, dcc, Input, Output, callback +import dash_bootstrap_components as dbc +import dash +from util import devices_ref_redundancy + +dash.register_page( + __name__, path="/real-time-telemetry", name="Real Time Telemetry", title="Real Time Telemetry" +) + +layout = html.Div( + [ + html.H1("Real Time Telemetry", className="mb-3"), + dcc.Dropdown( + options=list(devices_ref_redundancy.keys()), id="real-time-telemetry-device-dropdown" + ), + dcc.Interval(id="interval-real-time-telemetry", interval=500, n_intervals=0), + html.Div(id="real-time-telemetry-div"), + ], + className="container", +) diff --git a/util.py b/util.py index dfedc13..a270318 100644 --- a/util.py +++ b/util.py @@ -353,6 +353,18 @@ def default(self, obj): "serial_sequence": ["LinearStage150Connect", "LinearStage150EnableMotor"], "import_device": "from devices.linear_stage_150 import LinearStage150", "import_commands": "from commands.linear_stage_150_commands import *", + "telemetry": { + "parameters": [ + { + "position": { + "function_name": "get_position()", + "data_type": "float", + "units": "mm", + } + } + ], + "options": {"custom_init_args": ["port"]}, + }, "init": { "default_code": "LinearStage150(name='LinearStage150', port='', baudrate=115200, timeout=0.1, destination=0x50, source=0x01, channel=1)", "obj_name": "LinearStage150", From be4e617c2aacd379201dac17dc0597816cf43a7b Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 14 Aug 2023 16:27:24 -0700 Subject: [PATCH 058/125] edits --- app.py | 37 ++- devices/linear_stage_150.py | 12 +- pages/data.py | 2 +- pages/python-edit-recipe.py | 37 +-- recipe_tool.py | 640 +++++++++++++++++++++++++++--------- 5 files changed, 532 insertions(+), 196 deletions(-) diff --git a/app.py b/app.py index c728741..d094b2c 100644 --- a/app.py +++ b/app.py @@ -1550,17 +1550,16 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, device_form[i]["props"]["children"][1]["props"]["children"][0]["props"]["value"] ) instantiate_code += ")" - code_seq = str(device) + "_seq" - code += code_seq + " = CommandSequence()" + code += str(device) + "_seq" + " = CommandSequence()" code += "\n\n" - code += code_seq + ".add_device(" + instantiate_code + ")" + code += str(device) + "_seq" + ".add_device(" + instantiate_code + ")" code += "\n" if util.devices_ref_redundancy[device]["serial"] == True: for i, serial_seq_command in enumerate( util.devices_ref_redundancy[device]["serial_sequence"] ): - code += code_seq + ".add_command(" + str(serial_seq_command) + "(" + code += str(device) + "_seq" + ".add_command(" + str(serial_seq_command) + "(" for ii, serial_seq_command_arg in enumerate( util.devices_ref_redundancy[device]["commands"][serial_seq_command]["args"] ): @@ -1608,7 +1607,7 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, ) code += "))\n" - code += code_seq + ".add_command(" + str(command) + "(" + code += str(device) + "_seq" + ".add_command(" + str(command) + "(" for ii, seq_command_arg in enumerate( util.devices_ref_redundancy[device]["commands"][command]["args"] ): @@ -1657,7 +1656,7 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, code += "))\n\n" else: - code += code_seq + ".add_command(" + str(command) + "(" + code += str(device) + "_seq" + ".add_command(" + str(command) + "(" for ii, seq_command_arg in enumerate( util.devices_ref_redundancy[device]["commands"][command]["args"] ): @@ -1954,6 +1953,32 @@ def fill_database_document_viewer(document, collection, url, db): return [] +# --------------------------------------------------------------- +# Real Time Telemetry page +# --------------------------------------------------------------- + + +@app.callback( + [Output("real-time-telemetry-div", "children")], + [Input("interval-real-time-telemetry", "n_intervals")], + [Input("real-time-telemetry-device-dropdown", "value"), State("url", "pathname")], + prevent_initial_call=True, +) +def fill_real_time_telemetry(n, device, url): + if str(url) == "/real-time-telemetry" and device is not None and device != "": + print("fill_real_time_telemetry") + if "telemetry" in list(util.devices_ref_redundancy[device].keys()): + device_parameter_options = util.devices_ref_redundancy[device]["telemetry"]["options"] + for parameter in util.devices_ref_redundancy[device]["telemetry"]: + print(parameter) + parameter_options = util.devices_ref_redundancy[device]["telemetry"]["parameters"][ + parameter + ] + + return [""] + return [""] + + if __name__ == "__main__": app.run(debug=True) diff --git a/devices/linear_stage_150.py b/devices/linear_stage_150.py index 92652fe..0aebb8a 100644 --- a/devices/linear_stage_150.py +++ b/devices/linear_stage_150.py @@ -49,9 +49,7 @@ def initialize(self) -> Tuple[bool, str]: # lts150: initialize, home it (if needed) # Home Stage; MGMSG_MOT_MOVE_HOME - self.ser.write( - pack(" bool: def set_enabled_state(self, state: bool) -> Tuple[bool, str]: if state: - self.ser.write( - pack(" float: self._position = 0.0 Device_Unit_SF = 409600 # MGMSG_MOT_GET_POSCOUNTER - self.ser.write( - pack(" 0: print(Fore.RED + "Recipe already has loop commands. Remove them and re-add them.") @@ -469,29 +585,40 @@ def add_loop(): print(Fore.RED + "There are currently no commands.") return - loop_indices = select_multiple_commands("Select the FIRST and LAST commands of the loop section:", validator_func=valid_loop_count) + loop_indices = select_multiple_commands( + "Select the FIRST and LAST commands of the loop section:", validator_func=valid_loop_count + ) if len(loop_indices) == 0: print(Fore.RED + "Loop was NOT added to the recipe.") return - + loop_indices.sort() loop_start_index = loop_indices[0] loop_end_index = loop_indices[1] + 1 seq.add_loop_end(loop_end_index) seq.add_loop_start(loop_start_index) print(Fore.GREEN + "Loop has been added to recipe!") - + + def remove_loops(): - response = questionary.confirm("Are you sure you want to delete all loop commands?", default=False).ask() + response = questionary.confirm( + "Are you sure you want to delete all loop commands?", default=False + ).ask() if response: loop_command_count = seq.count_loop_commands() seq.remove_all_loop_commands() - print(Fore.GREEN + "Removed " + str(loop_command_count) + " loop commands. If these loop commands were erroneously part of an iteration then you should fix your recipe.") + print( + Fore.GREEN + + "Removed " + + str(loop_command_count) + + " loop commands. If these loop commands were erroneously part of an iteration then you should fix your recipe." + ) else: print(Fore.RED + "No loop commands were deleted") return + def get_all_command_classes(command_dir: str): # initialize lists module_names = [] @@ -504,7 +631,7 @@ def get_all_command_classes(command_dir: str): for file in listdir(command_dir): if isfile(join(command_dir, file)) and file != "__init__.py" and file.split(".")[1] == "py": # get rid of the file extension and replace / with . - module_name = join(command_dir, file).split(".")[0].replace("/",".") + module_name = join(command_dir, file).split(".")[0].replace("/", ".") module_names.append(module_name) # import each module @@ -513,12 +640,13 @@ def get_all_command_classes(command_dir: str): module = importlib.import_module(module_name) for name, cls in inspect.getmembers(module, inspect.isclass): if issubclass(cls, Command) and cls is not Command and cls not in cls_list: - if not 'ParentCommand' in name: + if not "ParentCommand" in name: cls_list.append(cls) cls_name_list.append(name) cls_dict[name] = cls return cls_dict + def get_all_commands_classes_for_receiver(command_dir: str, receiver_class): cls_dict = get_all_command_classes(command_dir) @@ -528,12 +656,12 @@ def get_all_commands_classes_for_receiver(command_dir: str, receiver_class): valid_dict[name] = cls return valid_dict + ################################################## # Iteration Menu ################################################## def iteration_menu(): while True: - if isinstance(seq.num_iterations, str): # Because I want the quotes to appear if it is a string num_iter_str = "'" + seq.num_iterations + "'" @@ -549,11 +677,14 @@ def iteration_menu(): "Set Number of Loop Iterations (currently=" + num_iter_str + ")": set_num_iterations, "Back to Main Menu": main_menu, } - prompt = questionary.select(command_menu_prompt, choices=list(command_menu_options.keys()), style=custom_style) + prompt = questionary.select( + command_menu_prompt, choices=list(command_menu_options.keys()), style=custom_style + ) response = prompt.ask() response_function = command_menu_options[response] response_function() + def add_command_iteration(): if len(seq.command_list) == 0: print(Fore.RED + "There are currently no commands.") @@ -562,7 +693,7 @@ def add_command_iteration(): command_index = select_command("Select the command to add an iteration for:") if command_index is None: return - + for iteration in seq.command_list[command_index]: if isinstance(iteration, LoopStartCommand) or isinstance(iteration, LoopEndCommand): print(Fore.RED + "Cannot add iteration to a Loop Start/End Command") @@ -572,18 +703,27 @@ def add_command_iteration(): device = first_command_iteration._receiver command_class = first_command_iteration.__class__ - arg_dict = prompt_signature_args(command_class.__init__, ['receiver']) - arg_dict['receiver'] = device - arg_dict['delay'] = prompt_delay() + arg_dict = prompt_signature_args(command_class.__init__, ["receiver"]) + arg_dict["receiver"] = device + arg_dict["delay"] = prompt_delay() - append_insert = questionary.select("Append this command to the iteration list or insert at a specific position?", choices=['Append','Insert'], style=custom_style).ask() + append_insert = questionary.select( + "Append this command to the iteration list or insert at a specific position?", + choices=["Append", "Insert"], + style=custom_style, + ).ask() if append_insert == "Append": seq.add_command_iteration(command_class(**arg_dict), index=command_index) else: - insert_index = select_command_iteration(command_index, "Select the position to insert the new command iteration:") + insert_index = select_command_iteration( + command_index, "Select the position to insert the new command iteration:" + ) if insert_index is None: return - seq.add_command_iteration(command_class(**arg_dict), index=command_index, iteration=insert_index) + seq.add_command_iteration( + command_class(**arg_dict), index=command_index, iteration=insert_index + ) + def remove_command_iterations(): if len(seq.command_list) == 0: @@ -595,21 +735,31 @@ def remove_command_iterations(): return if len(seq.command_list[command_index]) == 1: - print(Fore.RED + "Selected command only has 1 command iteration. To change it, insert a new command or command iteration in its place then delete the old one.") + print( + Fore.RED + + "Selected command only has 1 command iteration. To change it, insert a new command or command iteration in its place then delete the old one." + ) return - del_indices = select_multiple_command_iterations(command_index, "Select the command iterations(s) you would like to remove:") + del_indices = select_multiple_command_iterations( + command_index, "Select the command iterations(s) you would like to remove:" + ) if len(del_indices) == 0: - print(Fore.RED + "No command iterations were deleted (Select command iterations with Space Bar)") + print( + Fore.RED + + "No command iterations were deleted (Select command iterations with Space Bar)" + ) return - + if len(del_indices) == len(seq.command_list[command_index]): print(Fore.RED + "There must be at least one iteration remaining") return - + # Must sort in descending order otherwise removing earlier commands will shift later commands del_indices.sort(reverse=True) - response = questionary.confirm("Are you sure you want to delete the command iterations(s)?", default=False).ask() + response = questionary.confirm( + "Are you sure you want to delete the command iterations(s)?", default=False + ).ask() if response: for del_index in del_indices: seq.remove_command_iteration(command_index, del_index) @@ -618,6 +768,7 @@ def remove_command_iterations(): print(Fore.RED + "No command iterations were deleted") return + def move_command_iteration(): if len(seq.command_list) == 0: print(Fore.RED + "There are currently no commands.") @@ -631,23 +782,36 @@ def move_command_iteration(): print(Fore.RED + "There is currently only one command iteration.") return - old_index = select_command_iteration(command_index, "Select the command iteration you would like to move:") + old_index = select_command_iteration( + command_index, "Select the command iteration you would like to move:" + ) if old_index is None: return - new_index = select_command_iteration(command_index, "Select where to move the command iteration to:") + new_index = select_command_iteration( + command_index, "Select where to move the command iteration to:" + ) if new_index is None: return seq.move_command_iteration_by_index(command_index, old_index, new_index) + def set_num_iterations(): print("") is_valid = False while not is_valid: - print(Fore.WHITE + "The current number of iterations is: " + Fore.YELLOW + str(seq.num_iterations)) + print( + Fore.WHITE + + "The current number of iterations is: " + + Fore.YELLOW + + str(seq.num_iterations) + ) print("") - response = questionary.text("Enter the number of iterations to perform (minimum = 1, automatic = 'ALL'):", default="ALL").ask() - if response == 'ALL': - num_iterations = 'ALL' + response = questionary.text( + "Enter the number of iterations to perform (minimum = 1, automatic = 'ALL'):", + default="ALL", + ).ask() + if response == "ALL": + num_iterations = "ALL" is_valid = True else: try: @@ -655,11 +819,18 @@ def set_num_iterations(): if num_iterations >= 1: is_valid = True else: - print(Fore.RED + "Invalid value for num iterations. It must be an integer >= 1 or the string 'ALL'.") + print( + Fore.RED + + "Invalid value for num iterations. It must be an integer >= 1 or the string 'ALL'." + ) except ValueError: - print(Fore.RED + "Invalid value for num iterations. It must be an integer >= 1 or the string 'ALL'.") + print( + Fore.RED + + "Invalid value for num iterations. It must be an integer >= 1 or the string 'ALL'." + ) seq.num_iterations = num_iterations + ################################################## # Execute Recipe ################################################## @@ -667,34 +838,55 @@ def execute_recipe(): if len(seq.command_list) == 0: print(Fore.RED + "There are currently no commands.") return - - log_options = ["Log with default timestamped filename", "Log with specified filename", "No logging"] - response = questionary.select("Select logging option:", choices=log_options, default=log_options[0], style=custom_style).ask() - + + log_options = [ + "Log with default timestamped filename", + "Log with specified filename", + "No logging", + ] + response = questionary.select( + "Select logging option:", choices=log_options, default=log_options[0], style=custom_style + ).ask() + if response == log_options[0]: log_to_file = True log_filename = None print(Fore.GREEN + "Log filename will be displayed before and after execution!") elif response == log_options[1]: log_to_file = True - log_filename = questionary.path("Enter log file to create:", default=log_directory, validate=lambda file: valid_log_file(file, log_directory), style=custom_style).ask() + log_filename = questionary.path( + "Enter log file to create:", + default=log_directory, + validate=lambda file: valid_log_file(file, log_directory), + style=custom_style, + ).ask() print(Fore.GREEN + "Log messages will be saved to '" + log_filename + "'!") else: log_to_file = False log_filename = None print(Fore.YELLOW + "Messages will not be logged to file!") - alert_slack = questionary.confirm("Do you want to alert Slack if the recipe fails?", default=True).ask() + alert_slack = questionary.confirm( + "Do you want to alert Slack if the recipe fails?", default=True + ).ask() response = questionary.confirm("Are you ready to execute the recipe?").ask() if response: invoker = CommandInvoker(seq, log_to_file, log_filename, alert_slack) invocation_successful = invoker.invoke_commands() if invocation_successful: - print(Fore.CYAN + "Recipe execution complete." + Fore.GREEN + " Execution was successful.") + print( + Fore.CYAN + "Recipe execution complete." + Fore.GREEN + " Execution was successful." + ) else: - print(Fore.CYAN + "Recipe execution complete." + Fore.RED + " Execution encountered errors.") + print( + Fore.CYAN + + "Recipe execution complete." + + Fore.RED + + " Execution encountered errors." + ) print("") + ################################################## # Execute Manual Commands ################################################## @@ -703,7 +895,7 @@ def execute_manual(): if len(seq.device_list) == 0: print(Fore.RED + "There are currently no devices. Add a device to execute commands for it.") return - + # Warning! Manual command execution can alter the state of your devices which may or may not be desired! # For example, it can cause a device to have its is_initialized flag set to True, which can be dumped to a .yaml file # This means loading the "recipe" later will start with an initialized device even if it may not be initialized in reality @@ -712,25 +904,36 @@ def execute_manual(): # For example, you may actually want to manually get the device to a certain state before executing the full recipe # Therefore, manual commands executed here have the option to operate on the same device/receiver objects as the recipe using a shallow copy # So use carefully! Try not to save to .yaml file after executing commands manually, instead reload this program to make changes then save - + # response = questionary.confirm("WARNING! Executing commands manually can alter the state of your devices which may be undesirable. Continue?", default=False, style=questionary.Style([("question", "fg:#ff0000"),])).ask() - print(Fore.RED + "WARNING! Executing commands manually can alter the state of your devices which may be undesirable.") - print(Fore.RED + "*BUG - Performing a deepcopy after previously performing a shallow copy results in an error.") + print( + Fore.RED + + "WARNING! Executing commands manually can alter the state of your devices which may be undesirable." + ) + print( + Fore.RED + + "*BUG - Performing a deepcopy after previously performing a shallow copy results in an error." + ) # response = questionary.confirm("Do you want to preserve the state of the devices after execution? (Recommend No!)", default=False, style=questionary.Style([("question", "fg:#ff0000"),])).ask()) copy_options = [ "No, DO NOT preserve the state of the devices (Recommended, performs a deepcopy and creates new device objects)", - "Yes, preserve the state of the devices (performs a shallow copy and references the original device objects)" + "Yes, preserve the state of the devices (performs a shallow copy and references the original device objects)", ] - prompt = questionary.select("Do you want to preserve the state of the devices after execution?", choices=copy_options, default=copy_options[0], style=custom_style) + prompt = questionary.select( + "Do you want to preserve the state of the devices after execution?", + choices=copy_options, + default=copy_options[0], + style=custom_style, + ) response = prompt.ask() - + if response == copy_options[0]: # Do not preserve the device state seq_manual = copy.deepcopy(seq) else: # Preserve the device state seq_manual = copy.copy(seq) - + seq_manual.command_list = [] seq_manual.num_iterations = 1 @@ -744,7 +947,9 @@ def execute_manual(): device = seq_manual.device_list[device_index] - valid_command_dict = get_all_commands_classes_for_receiver(command_directory, device.__class__) + valid_command_dict = get_all_commands_classes_for_receiver( + command_directory, device.__class__ + ) valid_command_names_desc = [] for name, cls in valid_command_dict.items(): valid_command_names_desc.append("{:30.30s}".format(name) + "- " + cls.__doc__) @@ -752,58 +957,97 @@ def execute_manual(): # Choose command loop while True: - response = questionary.select("Choose the command to add:", choices=valid_command_names_desc, style=custom_style).ask() + response = questionary.select( + "Choose the command to add:", choices=valid_command_names_desc, style=custom_style + ).ask() if response == "Go back": break command_class_name = response.split(" ")[0] command_class = valid_command_dict[command_class_name] - arg_dict = prompt_signature_args(command_class.__init__, ['receiver']) - arg_dict['receiver'] = device - arg_dict['delay'] = prompt_delay() - - response = questionary.confirm("Are you sure you want to execute this command?", default=True, style=questionary.Style([("question", "fg:#ff0000"),])).ask() + arg_dict = prompt_signature_args(command_class.__init__, ["receiver"]) + arg_dict["receiver"] = device + arg_dict["delay"] = prompt_delay() + + response = questionary.confirm( + "Are you sure you want to execute this command?", + default=True, + style=questionary.Style( + [ + ("question", "fg:#ff0000"), + ] + ), + ).ask() if not response: break seq_manual.command_list = [] seq_manual.add_command(command_class(**arg_dict)) - invoker_manual = CommandInvoker(seq_manual, log_to_file=False, log_filename=None, alert_slack=False) + invoker_manual = CommandInvoker( + seq_manual, log_to_file=False, log_filename=None, alert_slack=False + ) invocation_successful = invoker_manual.invoke_commands() if invocation_successful: - print(Fore.CYAN + "Manual command execution complete." + Fore.GREEN + " Execution was successful.") + print( + Fore.CYAN + + "Manual command execution complete." + + Fore.GREEN + + " Execution was successful." + ) else: - print(Fore.CYAN + "Manual command execution complete." + Fore.RED + " Execution encountered errors.") + print( + Fore.CYAN + + "Manual command execution complete." + + Fore.RED + + " Execution encountered errors." + ) print("") - ################################################## # Print Help ################################################## def print_help(): - print('') - print(Fore.GREEN + "="*10 + " How to use this tool? " +"="*140) + print("") + print(Fore.GREEN + "=" * 10 + " How to use this tool? " + "=" * 140) # print(rainbow('#?')*1 + Fore.CYAN + " How to use this tool " + rainbow('#?')*14) - print('') - print(Fore.WHITE + " 1) Add devices that will be needed for the recipe. You will be prompted for parameters (e.g. COM port, timeout, motor numbers, etc.)") - print(" 2) Add commands for any device that has been added. You will be prompted for parameters if any. (e.g. temperature, speed, position, etc.)") - print(" 3) After adding commands, you can designate a subsection of the recipe as a looped section using 'Add Loop'. The looped section will execute a specified number of iterations.") - print(" 4) For each command, you can add additional 'command iterations' which are additional commands that correspond to each loop iteration.") - print(" (If there are more loop iterations (at least 1) than command's 'command iterations', the latest command iteration will be used.)") - print(" 5) You can set the integer number of loop iterations in the command iteration menu. A string value of 'ALL' will automatically determine the largest command iteration length and use that.") + print("") + print( + Fore.WHITE + + " 1) Add devices that will be needed for the recipe. You will be prompted for parameters (e.g. COM port, timeout, motor numbers, etc.)" + ) + print( + " 2) Add commands for any device that has been added. You will be prompted for parameters if any. (e.g. temperature, speed, position, etc.)" + ) + print( + " 3) After adding commands, you can designate a subsection of the recipe as a looped section using 'Add Loop'. The looped section will execute a specified number of iterations." + ) + print( + " 4) For each command, you can add additional 'command iterations' which are additional commands that correspond to each loop iteration." + ) + print( + " (If there are more loop iterations (at least 1) than command's 'command iterations', the latest command iteration will be used.)" + ) + print( + " 5) You can set the integer number of loop iterations in the command iteration menu. A string value of 'ALL' will automatically determine the largest command iteration length and use that." + ) print(" 6) Frequently save you recipe to a .yaml file so it can be loaded later") - print(" 7) When ready to execute the recipe you will be prompted for logging options, slack alerts, and a final confirmation before execution.") - print(" 8) During execution the status will be updated live and logged to the screen and log file. After completion you will be returned to the main menu.") - print('') + print( + " 7) When ready to execute the recipe you will be prompted for logging options, slack alerts, and a final confirmation before execution." + ) + print( + " 8) During execution the status will be updated live and logged to the screen and log file. After completion you will be returned to the main menu." + ) + print("") # print(rainbow('#?')*17) - print(Fore.GREEN + "="*173) + print(Fore.GREEN + "=" * 173) # print(rainbow("#")*10) # print(rainbow("?")*10) # print(rainbow_bg()*10) print("") + ################################################## # Quit ################################################## @@ -811,33 +1055,39 @@ def quit_program(): print(Fore.RED + "User quit program") sys.exit() + ################################################## # Useful Functions ################################################## -def select_device(prompt_message, allow_backout = True): - device_names_classes = seq.get_device_names_classes() # [name, class name] of every device +def select_device(prompt_message, allow_backout=True): + device_names_classes = seq.get_device_names_classes() # [name, class name] of every device display_device_menu_options = [] for name_class in device_names_classes: - display_device_menu_options.append("Name: {:20.20s} Class: {}".format(name_class[0], name_class[1])) + display_device_menu_options.append( + "Name: {:20.20s} Class: {}".format(name_class[0], name_class[1]) + ) if allow_backout: display_device_menu_options.append("Go back") - prompt = questionary.select(prompt_message, choices=display_device_menu_options, style=custom_style) + prompt = questionary.select( + prompt_message, choices=display_device_menu_options, style=custom_style + ) response = prompt.ask() if response == "Go back": response = None else: response = display_device_menu_options.index(response) - return response # index of the device + return response # index of the device + -def select_command(prompt_message, allow_backout = True): +def select_command(prompt_message, allow_backout=True): command_names = seq.get_command_names() # Any command with additional iterations is marked with * for ndx, name in enumerate(command_names): - if 'IterIndex' in name: - command_names[ndx-1] = "*" + command_names[ndx-1] + if "IterIndex" in name: + command_names[ndx - 1] = "*" + command_names[ndx - 1] # Delete the iterations from the list for ndx, name in enumerate(command_names): - while 'IterIndex' in command_names[ndx]: + while "IterIndex" in command_names[ndx]: del command_names[ndx] # Format the string with spaces for ndx, name in enumerate(command_names): @@ -854,17 +1104,18 @@ def select_command(prompt_message, allow_backout = True): response = None else: response = command_names.index(response) - return response # index of the command + return response # index of the command + -def select_multiple_commands(prompt_message, validator_func = None): +def select_multiple_commands(prompt_message, validator_func=None): command_names = seq.get_command_names() # Any command with additional iterations is marked with * for ndx, name in enumerate(command_names): - if 'IterIndex' in name: - command_names[ndx-1] = "*" + command_names[ndx-1] + if "IterIndex" in name: + command_names[ndx - 1] = "*" + command_names[ndx - 1] # Delete the iterations from the list for ndx, name in enumerate(command_names): - while 'IterIndex' in command_names[ndx]: + while "IterIndex" in command_names[ndx]: del command_names[ndx] # Format the string with spaces for ndx, name in enumerate(command_names): @@ -876,25 +1127,31 @@ def select_multiple_commands(prompt_message, validator_func = None): if validator_func is None: prompt = questionary.checkbox(prompt_message, choices=command_names, style=custom_style) else: - prompt = questionary.checkbox(prompt_message, choices=command_names, validate=lambda resp: validator_func(resp), style=custom_style) + prompt = questionary.checkbox( + prompt_message, + choices=command_names, + validate=lambda resp: validator_func(resp), + style=custom_style, + ) response_list = prompt.ask() index_list = [] for response in response_list: index_list.append(command_names.index(response)) return index_list -def select_command_iteration(command_index, prompt_message, allow_backout = True): + +def select_command_iteration(command_index, prompt_message, allow_backout=True): iteration_names = [] for iteration in seq.command_list[command_index]: iteration_names.append(iteration.name) - + for ndx, name in enumerate(iteration_names): name_parts = name.strip().split(" ") iteration_names[ndx] = "{:4s}".format(str(ndx)) + "{:30s}".format(name_parts.pop(0)) if len(name_parts) > 0: for param in name_parts: iteration_names[ndx] += " " + param - + if allow_backout: iteration_names.append("Go back") prompt = questionary.select(prompt_message, choices=iteration_names, style=custom_style) @@ -903,13 +1160,14 @@ def select_command_iteration(command_index, prompt_message, allow_backout = True response = None else: response = iteration_names.index(response) - return response # index of the command iteration + return response # index of the command iteration + def select_multiple_command_iterations(command_index, prompt_message): iteration_names = [] for iteration in seq.command_list[command_index]: iteration_names.append(iteration.name) - + for ndx, name in enumerate(iteration_names): name_parts = name.strip().split(" ") iteration_names[ndx] = "{:4s}".format(str(ndx)) + "{:30s}".format(name_parts.pop(0)) @@ -924,12 +1182,13 @@ def select_multiple_command_iterations(command_index, prompt_message): index_list.append(iteration_names.index(response)) return index_list + def prompt_signature_args(func, ignored_args): # ignored args, list of strings for arg names # returns dictionary of args sig = inspect.signature(func) arg_dict = {} - ignored_args.extend(['self', 'kwargs', 'args']) + ignored_args.extend(["self", "kwargs", "args"]) for param in sig.parameters.values(): if not param.name in ignored_args: @@ -938,8 +1197,21 @@ def prompt_signature_args(func, ignored_args): default = "N/A" else: default = str(param.default) - print(Fore.WHITE + "Parameter: " + Fore.GREEN + param.name + Fore.WHITE + " type: " + Fore.YELLOW + str(param.annotation) + Fore.WHITE + " default: " + Fore.YELLOW + default) - + print( + Fore.WHITE + + "Parameter: " + + Fore.GREEN + + param.name + + Fore.WHITE + + " type: " + + Fore.YELLOW + + str(param.annotation) + + Fore.WHITE + + " default: " + + Fore.YELLOW + + default + ) + if default == "N/A": response = questionary.text("Enter value for the parameter").ask() else: @@ -948,13 +1220,17 @@ def prompt_signature_args(func, ignored_args): arg_dict[param.name] = eval(response) return arg_dict + def prompt_delay(): # print(Fore.WHITE + "Parameter: " + Fore.GREEN + "delay" + Fore.WHITE + " type: " + Fore.YELLOW + "float" + Fore.WHITE + " default: " + Fore.YELLOW + "0.0") is_valid = False while not is_valid: - response = questionary.text("Enter delay (in seconds) before this command executes. (0.0 = no delay, P = Pause & wait for ENTER before execute):", default="0.0").ask() - if response == 'P' or response == 'PAUSE': - delay = 'PAUSE' + response = questionary.text( + "Enter delay (in seconds) before this command executes. (0.0 = no delay, P = Pause & wait for ENTER before execute):", + default="0.0", + ).ask() + if response == "P" or response == "PAUSE": + delay = "PAUSE" is_valid = True else: try: @@ -962,31 +1238,71 @@ def prompt_delay(): if delay >= 0.0: is_valid = True else: - print(Fore.RED + "Invalid value for delay. It must be an number >= 0.0 or the string 'P' or 'PAUSE'.") + print( + Fore.RED + + "Invalid value for delay. It must be an number >= 0.0 or the string 'P' or 'PAUSE'." + ) except ValueError: - print(Fore.RED + "Invalid value for delay. It must be an number >= 0.0 or the string 'P' or 'PAUSE'.") + print( + Fore.RED + + "Invalid value for delay. It must be an number >= 0.0 or the string 'P' or 'PAUSE'." + ) return delay + def print_eval_warning(): print(Fore.RED + "Your response will be evaluated directly with eval()!") - print(Fore.RED + "Examples: string = 'COM9', integer = 5, float = 5.0, list of ints = [1, 2, 3], tuple of ints = (1, 2, 3)") + print( + Fore.RED + + "Examples: string = 'COM9', integer = 5, float = 5.0, list of ints = [1, 2, 3], tuple of ints = (1, 2, 3)" + ) + def print_intro(): - print(Fore.GREEN + Style.BRIGHT + '='*50) - print(" "*15 + "Command Recipe Tool") - print(" "*13 + "8/12/2021 - Justin Kwok") - print('='*50) - print('') + print(Fore.GREEN + Style.BRIGHT + "=" * 50) + print(" " * 15 + "Command Recipe Tool") + print(" " * 13 + "8/12/2021 - Justin Kwok") + print("=" * 50) + print("") + def rainbow(char): - string = Fore.RED + char + Fore.YELLOW + char + Fore.GREEN + char + Fore.CYAN + char + Fore.BLUE + char + Fore.MAGENTA + char + string = ( + Fore.RED + + char + + Fore.YELLOW + + char + + Fore.GREEN + + char + + Fore.CYAN + + char + + Fore.BLUE + + char + + Fore.MAGENTA + + char + ) return string + def rainbow_bg(): char = " " - string = Back.RED + char + Back.YELLOW + char + Back.GREEN + char + Back.CYAN + char + Back.BLUE + char + Back.MAGENTA + char + string = ( + Back.RED + + char + + Back.YELLOW + + char + + Back.GREEN + + char + + Back.CYAN + + char + + Back.BLUE + + char + + Back.MAGENTA + + char + ) return string + ################################################## # Questionary validator functions ################################################## @@ -998,31 +1314,35 @@ def valid_yml(file): else: return True + def valid_loop_count(response_list): if len(response_list) == 0 or len(response_list) == 2: return True else: - return "You must select 2 options only or select none to exit" + return "You must select 2 options only or select none to exit" + def valid_save_file(file, save_directory): if file.lower() == "quit": return True if not save_directory in file: return "You must save in the directory: " + save_directory - if not '.yaml' in file: + if not ".yaml" in file: return "File must have a '.yaml' extension" else: return True + def valid_log_file(file, log_directory): if not log_directory in file: return "You must save log in the directory: " + log_directory - if not '.log' in file: + if not ".log" in file: return "File must have a '.log' extension" if isfile(file): return "You must create a new log file" else: return True + if __name__ == "__main__": - main() \ No newline at end of file + main() From 1536da67288b6fcb06139674aa9bb6501b61dfe0 Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 14 Aug 2023 16:36:52 -0700 Subject: [PATCH 059/125] added new devices and commands modules --- commands/festo_solenoid_valve_commands.py | 38 +++++++--- commands/mfc_commands.py | 92 +++++++++++++++++++++++ commands/oxygen_sensor_commands.py | 54 +++++++++++++ commands/sht85_sensor_commands.py | 64 ++++++++++++++++ devices/festo_solenoid_valve.py | 73 ++++++++---------- devices/mfc.py | 51 +++++++++++++ devices/oxygen_sensor.py | 33 ++++++++ devices/sht85_sensor.py | 45 +++++++++++ util.py | 48 +++++++++--- 9 files changed, 433 insertions(+), 65 deletions(-) create mode 100644 commands/mfc_commands.py create mode 100644 commands/oxygen_sensor_commands.py create mode 100644 commands/sht85_sensor_commands.py create mode 100644 devices/mfc.py create mode 100644 devices/oxygen_sensor.py create mode 100644 devices/sht85_sensor.py diff --git a/commands/festo_solenoid_valve_commands.py b/commands/festo_solenoid_valve_commands.py index 6b3ed22..bae8bcc 100644 --- a/commands/festo_solenoid_valve_commands.py +++ b/commands/festo_solenoid_valve_commands.py @@ -1,7 +1,4 @@ -# modules for device as of commit 4123ed0 - from typing import List - from devices.festo_solenoid_valve import FestoSolenoidValve from .command import Command, CommandResult @@ -15,6 +12,18 @@ def __init__(self, receiver: FestoSolenoidValve, **kwargs): super().__init__(receiver, **kwargs) +class FestoConnect(Command): + """Open the serial port to the festo valve""" + + receiver_cls = FestoSolenoidValve + + def __init__(self, receiver: FestoSolenoidValve, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.start_serial()) + + class FestoInitialize(FestoParentCommand): """Initialize the solenoid valve""" @@ -38,29 +47,34 @@ def execute(self) -> None: class FestoValveOpen(FestoParentCommand): """Open the solenoid valve and keep open""" - def __init__(self, receiver: FestoSolenoidValve, **kwargs): + def __init__(self, receiver: FestoSolenoidValve, valve_num: int, **kwargs): super().__init__(receiver, **kwargs) + self._params["valve_num"] = valve_num def execute(self) -> None: - self._result = CommandResult(*self._receiver.valve_open()) + self._result = CommandResult( + *self._receiver.valve_open(self._params["valve_num"]) + ) class FestoValveClosed(FestoParentCommand): """Close the solenoid valve and keep closed""" - def __init__(self, receiver: FestoSolenoidValve, **kwargs): + def __init__(self, receiver: FestoSolenoidValve, valve_num: int, **kwargs): super().__init__(receiver, **kwargs) + self._params["valve_num"] = valve_num def execute(self) -> None: - self._result = CommandResult(*self._receiver.valve_closed()) + self._result = CommandResult( + *self._receiver.valve_closed(self._params["valve_num"]) + ) -class FestoOpenTimed(FestoParentCommand): - """Open the valve for a set time then close""" +class FestoCloseAll(FestoParentCommand): + """Close all solenoid valves""" - def __init__(self, receiver: FestoSolenoidValve, time: int, **kwargs): + def __init__(self, receiver: FestoSolenoidValve, **kwargs): super().__init__(receiver, **kwargs) - self._params["time"] = time def execute(self) -> None: - self._result = CommandResult(*self._receiver.open_timed(self._params["time"])) + self._result = CommandResult(*self._receiver.valve) diff --git a/commands/mfc_commands.py b/commands/mfc_commands.py new file mode 100644 index 0000000..1f7b532 --- /dev/null +++ b/commands/mfc_commands.py @@ -0,0 +1,92 @@ +from typing import Tuple, List +import asyncio +from devices.mfc import MassFlowController +from .command import Command, CommandResult + + +class MFCParentCommand(Command): + """Parent class for MKS instruments mass flow controller""" + + receiver_cls = MassFlowController + + def __init__(self, receiver: MassFlowController, **kwargs): + super().__init__(receiver, **kwargs) + + +class MFCInitialize(MFCParentCommand): + """Initialize the mass flow controller""" + + def __init__(self, receiver: MassFlowController, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class MFCDeinitialize(MFCParentCommand): + """Deinitialize the mass flow controller""" + + def __init__(self, receiver: MassFlowController, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult( + asyncio.run(self._receiver.deinitialize()), "Deinitialized the mfc" + ) + # self._result = CommandResult(*self._receiver.deinitialize()) + + +class MFCGetData(MFCParentCommand): + """Return data on mfc operation""" + + def __init__(self, receiver: MassFlowController, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + # asyncio.run(self._receiver.get()) + self._result = CommandResult(asyncio.run(self._receiver.get()), "Received Data") + # self._result = CommandResult(True, "got data") + + +class MFCSetGas(MFCParentCommand): + """Set the type of gas for operation""" + + """Gas instance must first be created within web browser""" + + def __init__(self, receiver: MassFlowController, gas: str, **kwargs): + super().__init__(receiver, **kwargs) + self._params["gas"] = gas + + def execute(self) -> None: + self._result = CommandResult( + asyncio.run(self._receiver.set_gas(self._params["gas"])), "Set the gas" + ) + # self._result = CommandResult(*self._receiver.set_gas(self._params['gas'])) + + +class MFCSet(MFCParentCommand): + """Set the desired flowrate in sccm""" + + def __init__(self, receiver: MassFlowController, setpoint: int, **kwargs): + super().__init__(receiver, **kwargs) + self._params["setpoint"] = setpoint + + def execute(self) -> None: + self._result = CommandResult( + asyncio.run(self._receiver.set(self._params["setpoint"])), + "Set the flowrate", + ) + # self._result = CommandResult(*self._receiver.set(self._params['setpoint'])) + + +class MFCOpen(MFCParentCommand): + """Open the mfc, set the flowrate to the maximum for the set gas""" + + def __init__(self, receiver: MassFlowController, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult( + asyncio.run(self._receiver.open()), "Opened the mfc, maximum flowrate" + ) + # self._result = CommandResult(*self._receiver.open()) diff --git a/commands/oxygen_sensor_commands.py b/commands/oxygen_sensor_commands.py new file mode 100644 index 0000000..d3131fe --- /dev/null +++ b/commands/oxygen_sensor_commands.py @@ -0,0 +1,54 @@ +from typing import Tuple, List +from devices.oxygen_sensor import OxygenSensor +from .command import Command, CommandResult + + +class OxygenParentCommand(Command): + """Parent class for DFRobot Oxygen Sensor Commands""" + + receiver_cls = OxygenSensor + + def __init__(self, receiver: OxygenSensor, **kwargs): + super().__init__(receiver, **kwargs) + + +class OxygenConnect(Command): + """Open the serial port to the oxygen sensor""" + + receiver_cls = OxygenSensor + + def __init__(self, receiver: OxygenSensor, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.start_serial()) + + +class OxygenInitialize(OxygenParentCommand): + """Initialize the oxygen sensor""" + + def __init__(self, receiver: OxygenSensor, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class OxygenDeinitialize(OxygenParentCommand): + """Deinitialize the oxygen sensor""" + + def __init__(self, receiver: OxygenSensor, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize()) + + +class OxygenGetOxygen(OxygenParentCommand): + """Read average oxygen data""" + + def __init__(self, receiver: OxygenSensor, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_oxygen()) diff --git a/commands/sht85_sensor_commands.py b/commands/sht85_sensor_commands.py new file mode 100644 index 0000000..c9921d2 --- /dev/null +++ b/commands/sht85_sensor_commands.py @@ -0,0 +1,64 @@ +from typing import Tuple, List +from devices.sht85_sensor import SHT85HumidityTempSensor +from .command import Command, CommandResult + + +class SHT85ParentCommand(Command): + """Parent class for SHT85 humidity and temperature sensor""" + + receiver_cls = SHT85HumidityTempSensor + + def __init__(self, receiver: SHT85HumidityTempSensor, **kwargs): + super().__init__(receiver, **kwargs) + + +class SHT85Connect(Command): + """Open the serial port to the SHT85 sensor.""" + + receiver_cls = SHT85HumidityTempSensor + + def __init__(self, receiver: SHT85HumidityTempSensor, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.start_serial()) + + +class SHT85Initialize(SHT85ParentCommand): + """Initialize the humidity and temperature sensor""" + + def __init__(self, receiver: SHT85HumidityTempSensor, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class SHT85Deinitialize(SHT85ParentCommand): + """Deinitialize the humidity and temperature sensor""" + + def __init__(self, receiver: SHT85HumidityTempSensor, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize()) + + +class SHT85GetHumidity(SHT85ParentCommand): + """Read humidity data""" + + def __init__(self, receiver: SHT85HumidityTempSensor, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_humidity()) + + +class SHT85GetTemp(SHT85ParentCommand): + """Read temperature data""" + + def __init__(self, receiver: SHT85HumidityTempSensor, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_temp()) diff --git a/devices/festo_solenoid_valve.py b/devices/festo_solenoid_valve.py index 83029ea..2df1444 100644 --- a/devices/festo_solenoid_valve.py +++ b/devices/festo_solenoid_valve.py @@ -1,69 +1,58 @@ -# modules for device as of commit 4123ed0 - -"""Requires 'StandardFirmata' basic example uploaded on Arduino Uno""" +"""Requires Festo Solenoid Valve flashed to Arduino Board""" +"""Valve 1: Pin 12, Valve 2: Pin 8, Valve 3: Pin 4""" from typing import Tuple, Optional import time -import pyfirmata +import serial from .device import ArduinoSerialDevice, check_initialized, check_serial class FestoSolenoidValve(ArduinoSerialDevice): def __init__( - self, - name: str, - numchannel: int, - port: str = "COM5", - baudrate: int = 9600, - timeout: float = 0.1, + self, name: str, port: str, baudrate: int = 9600, timeout: float = 0.1 ): super().__init__(name, port, baudrate, timeout) - self.board = pyfirmata.Arduino(self.port) - self.numchannel = numchannel - self.pin = self.board.get_pin(f"d:{numchannel}:o") - - def get_init_args(self) -> dict: - args_dict = { - "name": self.name, - "numchannel": self.numchannel, - "port": self.port, - "baudrate": self.baudrate, - "timeout": self.timeout, - } - return args_dict - - def update_init_args(self, args_dict: dict): - self.name = args_dict["name"] - self.numchannel = args_dict["numchannel"] - self.port = args_dict["port"] - self.pin = self.board.get_pin(f"d:{self.port}:o") # TODO: Check if this is necessary - self.baudrate = args_dict["baudrate"] - self.timeout = args_dict["timeout"] def initialize(self) -> Tuple[bool, str]: self._is_initialized = True - # TODO: solenoid valve initialize return (True, "Solenoid valve initialized") def deinitialize(self) -> Tuple[bool, str]: + self.ser.close() self._is_initialized = False - self.board.exit() return (True, "Solenoid valve deinitialized") @check_serial @check_initialized - def valve_open(self) -> Tuple[bool, str]: - self.pin.write(1) + def valve_open(self, valve_num: int) -> Tuple[bool, str]: + if valve_num == 1: + self.ser.write(b"A") + elif valve_num == 2: + self.ser.write(b"B") + elif valve_num == 3: + self.ser.write(b"C") + else: + return (False, "Incorrect solenoid valve number") return (True, "Solenoid valve is open") @check_serial - def valve_closed(self) -> Tuple[bool, str]: - self.pin.write(0) + def valve_closed(self, valve_num: int) -> Tuple[bool, str]: + if valve_num == 1: + self.ser.write(b"D") + elif valve_num == 2: + self.ser.write(b"E") + elif valve_num == 3: + self.ser.write(b"F") + else: + return (False, "Incorrect solenoid valve number") return (True, "Solenoid valve is closed") @check_serial - def open_timed(self, time: int) -> Tuple[bool, str]: - self.pin.write(1) - self.board.pass_time(time) - self.pin.write(0) - return (True, f"Solenoid valve was opened for {time} seconds") + def close_all(self) -> Tuple[bool, str]: + self.ser.write(b"D") + time.sleep(0.25) + self.ser.write(b"E") + time.sleep(0.25) + self.ser.write(b"F") + time.sleep(0.25) + return (True, "Closed all solenoid valves") diff --git a/devices/mfc.py b/devices/mfc.py new file mode 100644 index 0000000..cc670e9 --- /dev/null +++ b/devices/mfc.py @@ -0,0 +1,51 @@ +from typing import Tuple +import time +from mfc import FlowController +import asyncio +from .device import Device, check_initialized + + +# Default ip address is 192.168.2.155 +# Secondary mfc ip address should be set to 192.168.2.156 and so on +# Must pip install the correct edited version of mfc python library +# +24 VDC is pin 7, ground is pin 5 + + +class MassFlowController(Device): + def __init__(self, name: str, ip: str): + super().__init__(name) + self._ip = ip + + def initialize(self) -> Tuple[bool, str]: + self._is_initialized = True + return (True, "Initialized mass flow controller") + + async def deinitialize(self) -> Tuple[bool, str]: + async with FlowController(self._ip) as fc: + await fc.disconnect() + self._is_initialized = False + return (True, "Deinitialized mass flow controller") + + @check_initialized + async def get(self) -> Tuple[bool, dict]: + async with FlowController(self._ip) as fc: + info = await fc.get() + return (True, info) + + @check_initialized + async def set_gas(self, gas: str) -> Tuple[bool, str]: + """Gas instance must first be created within web browser interface""" + async with FlowController(self._ip) as fc: + await fc.set_gas(gas) + return (True, f"Set MFC gas to {gas}") + + @check_initialized + async def set(self, setpoint) -> Tuple[bool, str]: + async with FlowController(self._ip) as fc: + await fc.set(setpoint) + return (True, f"Set MFC flowrate to {setpoint} sccm") + + async def open(self) -> Tuple[bool, str]: + async with FlowController(self._ip) as fc: + await fc.open() + return (True, "Set the MFC flowrate to its maximum") diff --git a/devices/oxygen_sensor.py b/devices/oxygen_sensor.py new file mode 100644 index 0000000..1037538 --- /dev/null +++ b/devices/oxygen_sensor.py @@ -0,0 +1,33 @@ +from device import ArduinoSerialDevice, check_initialized, check_serial +import serial +from time import sleep +from typing import Optional, Tuple + + +class OxygenSensor(ArduinoSerialDevice): + def __init__( + self, name: str, port: str, baudrate: int = 9600, timeout: Optional[float] = 1.0 + ): + super().__init__(name, port, baudrate, timeout) + self.ser.bytesize = serial.EIGHTBITS + self.ser.parity = serial.PARITY_NONE + + def initialize(self) -> Tuple[bool, str]: + self.ser.setDTR(False) + self.ser.flushInput() + self.ser.setDTR(True) + self._is_initialized = True + return (True, "Initialized oxygen sensor") + + def deinitialize(self) -> Tuple[bool, str]: + self.ser.close() + self._is_initialized = False + return (True, "Deinitialized oxygen sensor") + + @check_serial + @check_initialized + def get_oxygen(self) -> Tuple[bool, float, str]: + self.ser.write(b"O") + sleep(0.75) + oxygen = float(self.ser.readline().strip().decode()) + return (True, f"Oxygen concentration is {oxygen} %vol") diff --git a/devices/sht85_sensor.py b/devices/sht85_sensor.py new file mode 100644 index 0000000..2c3d781 --- /dev/null +++ b/devices/sht85_sensor.py @@ -0,0 +1,45 @@ +from .device import ArduinoSerialDevice, SerialDevice, check_initialized, check_serial +import serial +from time import sleep +from typing import Optional, Tuple + + +class SHT85HumidityTempSensor(ArduinoSerialDevice): + """Humidity and Temperature Sensor""" + + def __init__( + self, name: str, port: str, baudrate: int = 9600, timeout: Optional[float] = 1.0 + ): + super().__init__(name, port, baudrate, timeout) + # self.ser = serial.Serial() + # self.ser.bytesize = serial.EIGHTBITS + # self.ser.parity = serial.PARITY_NONE + + def initialize(self) -> Tuple[bool, str]: + # self.ser.setDTR(False) + # self.ser.flushInput() + # self.ser.setDTR(True) + # self.ser.open() + self._is_initialized = True + return (True, "Initialized humidity and temperature sensor") + + def deinitialize(self) -> Tuple[bool, str]: + self.ser.close() + self._is_initialized = False + return (True, "Deinitialized humidity and temperature sensor") + + @check_serial + @check_initialized + def get_humidity(self) -> Tuple[bool, float, str]: + self.ser.write(b"H") + sleep(0.75) + humidity = float(self.ser.readline().strip().decode()) + return (True, f"The relative humidity is {humidity}%") + + @check_serial + @check_initialized + def get_temp(self) -> Tuple[bool, float, str]: + self.ser.write(b"T") + sleep(0.75) + temp = float(self.ser.readline().strip().decode()) + return (True, f"The temperature is {temp} degrees C") diff --git a/util.py b/util.py index a270318..ccb3c9e 100644 --- a/util.py +++ b/util.py @@ -80,7 +80,11 @@ def evaluate(eval_str): class Encoder(json.JSONEncoder): def default(self, obj): - if isinstance(obj, Device) or isinstance(obj, Command) or isinstance(obj, MiscDeviceClass): + if ( + isinstance(obj, Device) + or isinstance(obj, Command) + or isinstance(obj, MiscDeviceClass) + ): return obj.__dict__ elif isinstance(obj, np.ndarray): return obj.tolist() @@ -250,7 +254,7 @@ def default(self, obj): "FestoSolenoidValve": { "obj": FestoSolenoidValve, "serial": True, - "serial_sequence": ["FestoInitialize"], + "serial_sequence": ["FestoConnect", "FestoInitialize"], "import_device": "from devices.festo_solenoid_valve import FestoSolenoidValve", "import_commands": "from commands.festo_solenoid_valve_commands import *", "init": { @@ -285,6 +289,17 @@ def default(self, obj): }, }, "commands": { + "FestoConnect": { + "default_code": "FestoConnect(receiver= '')", + "args": { + "receiver": { + "default": "FestoSolenoidValve", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": FestoConnect, + }, "FestoInitialize": { "default_code": "FestoInitialize(receiver= '')", "args": { @@ -329,22 +344,33 @@ def default(self, obj): }, "obj": FestoValveClosed, }, - "FestoOpenTimed": { - "default_code": "FestoOpenTimed(receiver= '', time=0)", + "FestoCloseAll": { + "default_code": "FestoCloseAll(receiver= '')", "args": { "receiver": { "default": "FestoSolenoidValve", "type": str, "notes": "Name of the device", }, - "time": { - "default": 0, - "type": int, - "notes": "Time to keep the valve open", - }, }, - "obj": FestoOpenTimed, - }, + "obj": FestoCloseAll, + } + # "FestoOpenTimed": { + # "default_code": "FestoOpenTimed(receiver= '', time=0)", + # "args": { + # "receiver": { + # "default": "FestoSolenoidValve", + # "type": str, + # "notes": "Name of the device", + # }, + # "time": { + # "default": 0, + # "type": int, + # "notes": "Time to keep the valve open", + # }, + # }, + # "obj": FestoOpenTimed, + # }, }, }, "LinearStage150": { From 4fa4131180a090e8195e4e3a53aee702db4785a6 Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 14 Aug 2023 16:48:50 -0700 Subject: [PATCH 060/125] restructure --- {commands => aamp_app}/__init__.py | 0 app.py => aamp_app/app.py | 255 +++++++++++++----- .../command_invoker.py | 134 +++++---- .../command_sequence.py | 0 {devices => aamp_app/commands}/__init__.py | 0 {commands => aamp_app/commands}/command.py | 0 .../commands}/dummy_composite_commands.py | 0 .../commands}/dummy_heater_commands.py | 0 .../commands}/dummy_meter_commands.py | 0 .../commands}/dummy_motor_commands.py | 0 .../festo_solenoid_valve_commands.py | 0 .../commands}/generic_commands.py | 0 .../commands}/heating_stage_commands.py | 0 .../commands}/keithley_2450_commands.py | 0 .../commands}/kinova_arm_commands.py | 0 .../commands}/linear_stage_150_commands.py | 0 .../commands}/mfc_commands.py | 0 .../commands}/mts50_z8_commands.py | 0 .../commands}/multi_stepper_commands.py | 0 .../commands}/newport_esp301_commands.py | 0 .../commands}/oxygen_sensor_commands.py | 0 .../commands}/psd6_syringe_pump_commands.py | 0 .../commands}/sht85_sensor_commands.py | 0 .../stellarnet_spectrometer_commands.py | 0 .../commands}/utility_commands.py | 0 .../commands}/ximea_camera_commands.py | 0 .../console_interceptor.py | 0 {devices => aamp_app/devices}/.gitignore | 0 {devices => aamp_app/devices}/README.md | 0 aamp_app/devices/__init__.py | 0 {devices => aamp_app/devices}/device.py | 0 {devices => aamp_app/devices}/dummy_heater.py | 0 {devices => aamp_app/devices}/dummy_meter.py | 0 {devices => aamp_app/devices}/dummy_motor.py | 0 .../devices}/dummy_motor_source.py | 0 .../devices}/festo_solenoid_valve.py | 0 .../devices}/heating_stage.py | 0 .../devices}/keithley_2450.py | 0 {devices => aamp_app/devices}/kinova_arm.py | 0 .../devices}/linear_stage_150.py | 0 {devices => aamp_app/devices}/mfc.py | 0 {devices => aamp_app/devices}/mts50_z8.py | 0 .../devices}/multi_stepper.py | 0 .../devices}/newport_esp301.py | 0 .../devices}/oxygen_sensor.py | 0 .../devices}/psd6_syringe_pump.py | 0 {devices => aamp_app/devices}/sht85_sensor.py | 0 .../devices}/stellarnet_spectrometer.py | 0 .../devices}/utility_device.py | 0 {devices => aamp_app/devices}/ximea_camera.py | 0 .../mongodb_helper.py | 0 {pages => aamp_app/pages}/data.py | 0 {pages => aamp_app/pages}/database.py | 0 {pages => aamp_app/pages}/execute-recipe.py | 0 {pages => aamp_app/pages}/home.py | 0 {pages => aamp_app/pages}/load-recipe.py | 0 {pages => aamp_app/pages}/manual-control.py | 0 .../pages}/python-edit-recipe.py | 0 .../pages}/real-time-telemetry.py | 8 +- {pages => aamp_app/pages}/view-recipe.py | 0 util.py => aamp_app/util.py | 0 blank.yaml | 3 - 62 files changed, 268 insertions(+), 132 deletions(-) rename {commands => aamp_app}/__init__.py (100%) rename app.py => aamp_app/app.py (91%) rename command_invoker.py => aamp_app/command_invoker.py (75%) rename command_sequence.py => aamp_app/command_sequence.py (100%) rename {devices => aamp_app/commands}/__init__.py (100%) rename {commands => aamp_app/commands}/command.py (100%) rename {commands => aamp_app/commands}/dummy_composite_commands.py (100%) rename {commands => aamp_app/commands}/dummy_heater_commands.py (100%) rename {commands => aamp_app/commands}/dummy_meter_commands.py (100%) rename {commands => aamp_app/commands}/dummy_motor_commands.py (100%) rename {commands => aamp_app/commands}/festo_solenoid_valve_commands.py (100%) rename {commands => aamp_app/commands}/generic_commands.py (100%) rename {commands => aamp_app/commands}/heating_stage_commands.py (100%) rename {commands => aamp_app/commands}/keithley_2450_commands.py (100%) rename {commands => aamp_app/commands}/kinova_arm_commands.py (100%) rename {commands => aamp_app/commands}/linear_stage_150_commands.py (100%) rename {commands => aamp_app/commands}/mfc_commands.py (100%) rename {commands => aamp_app/commands}/mts50_z8_commands.py (100%) rename {commands => aamp_app/commands}/multi_stepper_commands.py (100%) rename {commands => aamp_app/commands}/newport_esp301_commands.py (100%) rename {commands => aamp_app/commands}/oxygen_sensor_commands.py (100%) rename {commands => aamp_app/commands}/psd6_syringe_pump_commands.py (100%) rename {commands => aamp_app/commands}/sht85_sensor_commands.py (100%) rename {commands => aamp_app/commands}/stellarnet_spectrometer_commands.py (100%) rename {commands => aamp_app/commands}/utility_commands.py (100%) rename {commands => aamp_app/commands}/ximea_camera_commands.py (100%) rename console_interceptor.py => aamp_app/console_interceptor.py (100%) rename {devices => aamp_app/devices}/.gitignore (100%) rename {devices => aamp_app/devices}/README.md (100%) create mode 100644 aamp_app/devices/__init__.py rename {devices => aamp_app/devices}/device.py (100%) rename {devices => aamp_app/devices}/dummy_heater.py (100%) rename {devices => aamp_app/devices}/dummy_meter.py (100%) rename {devices => aamp_app/devices}/dummy_motor.py (100%) rename {devices => aamp_app/devices}/dummy_motor_source.py (100%) rename {devices => aamp_app/devices}/festo_solenoid_valve.py (100%) rename {devices => aamp_app/devices}/heating_stage.py (100%) rename {devices => aamp_app/devices}/keithley_2450.py (100%) rename {devices => aamp_app/devices}/kinova_arm.py (100%) rename {devices => aamp_app/devices}/linear_stage_150.py (100%) rename {devices => aamp_app/devices}/mfc.py (100%) rename {devices => aamp_app/devices}/mts50_z8.py (100%) rename {devices => aamp_app/devices}/multi_stepper.py (100%) rename {devices => aamp_app/devices}/newport_esp301.py (100%) rename {devices => aamp_app/devices}/oxygen_sensor.py (100%) rename {devices => aamp_app/devices}/psd6_syringe_pump.py (100%) rename {devices => aamp_app/devices}/sht85_sensor.py (100%) rename {devices => aamp_app/devices}/stellarnet_spectrometer.py (100%) rename {devices => aamp_app/devices}/utility_device.py (100%) rename {devices => aamp_app/devices}/ximea_camera.py (100%) rename mongodb_helper.py => aamp_app/mongodb_helper.py (100%) rename {pages => aamp_app/pages}/data.py (100%) rename {pages => aamp_app/pages}/database.py (100%) rename {pages => aamp_app/pages}/execute-recipe.py (100%) rename {pages => aamp_app/pages}/home.py (100%) rename {pages => aamp_app/pages}/load-recipe.py (100%) rename {pages => aamp_app/pages}/manual-control.py (100%) rename {pages => aamp_app/pages}/python-edit-recipe.py (100%) rename {pages => aamp_app/pages}/real-time-telemetry.py (67%) rename {pages => aamp_app/pages}/view-recipe.py (100%) rename util.py => aamp_app/util.py (100%) delete mode 100644 blank.yaml diff --git a/commands/__init__.py b/aamp_app/__init__.py similarity index 100% rename from commands/__init__.py rename to aamp_app/__init__.py diff --git a/app.py b/aamp_app/app.py similarity index 91% rename from app.py rename to aamp_app/app.py index d094b2c..79fc7e5 100644 --- a/app.py +++ b/aamp_app/app.py @@ -12,7 +12,6 @@ import dash_bootstrap_components as dbc import os, signal import inspect -from pw import mongo_username, mongo_password from bson.objectid import ObjectId from console_interceptor import ConsoleInterceptor from gridfs import GridFS @@ -27,13 +26,29 @@ _has_serial = True import typing +if os.path.isfile("pw.txt"): + with open("pw.txt", "r") as f: + mongo_username, mongo_password = f.read().split("\n") +else: + mongo_username = input("Enter MongoDB username: ") + mongo_password = input("Enter MongoDB password: ") + with open("pw.txt", "w") as f: + f.write(mongo_username + "\n" + mongo_password) print("\nreset complete") com = CommandSequence() invoker = CommandInvoker(com, log_to_file=True, log_filename="mylog.log") invoker.clear_log_file() invoker.invoking = False -com.load_from_yaml("blank.yaml") +if os.path.isfile("blank.yaml"): + com.load_from_yaml("blank.yaml") +else: + with open("blank.yaml", "w") as f: + f.write( + """- [] +- [] +- ALL""" + ) mongo = MongoDBHelper( @@ -59,17 +74,25 @@ dbc.NavItem(dbc.NavLink("Load", href="/load-recipe", external_link=True)), dbc.NavItem(dbc.NavLink("View", href="/view-recipe", external_link=True)), dbc.NavItem(dbc.NavLink("Execute", href="/execute-recipe", external_link=True)), - dbc.NavItem(dbc.NavLink("Manual Control", href="/manual-control", external_link=True)), - dbc.NavItem(dbc.NavLink("Database Browser", href="/database", external_link=True)), + dbc.NavItem( + dbc.NavLink("Manual Control", href="/manual-control", external_link=True) + ), + dbc.NavItem( + dbc.NavLink("Database Browser", href="/database", external_link=True) + ), dbc.DropdownMenu( children=[ # dbc.DropdownMenuItem( # "Load Recipe", href="/load-recipe", external_link=True # ), dbc.DropdownMenuItem( - "Real Time Telemetry", href="/real-time-telemetry", external_link=True + "Real Time Telemetry", + href="/real-time-telemetry", + external_link=True, + ), + dbc.DropdownMenuItem( + "Edit Code", href="/edit-recipe", external_link=True ), - dbc.DropdownMenuItem("Edit Code", href="/edit-recipe", external_link=True), dbc.DropdownMenuItem("Document", href="/data", external_link=True), ], nav=True, @@ -104,7 +127,9 @@ def update_upstream_recipe_dict(): "commands": recipe_dict[1], "execution_options": recipe_dict[2], } - mongo.db["recipes"].update_one({"_id": com.document["_id"]}, {"$set": com.document}) + mongo.db["recipes"].update_one( + {"_id": com.document["_id"]}, {"$set": com.document} + ) print("successfully updated recipe_dict upstream") return True else: @@ -115,7 +140,9 @@ def update_upstream_recipe_dict(): def update_execution_upstream(execution): if "document" in list(com.__dict__.keys()): com.document["executions"].append(execution) - mongo.db["recipes"].update_one({"_id": com.document["_id"]}, {"$set": com.document}) + mongo.db["recipes"].update_one( + {"_id": com.document["_id"]}, {"$set": com.document} + ) print("successfully updated execution upstream") return True else: @@ -217,7 +244,10 @@ def get_document_from_db(n_clicks, filename): # homepage "danger", 8000, ] - if document.get("dash_friendly", "") == False or document.get("python_code", "") == "": + if ( + document.get("dash_friendly", "") == False + or document.get("python_code", "") == "" + ): yaml_content = document.get("yaml_data", "") # Update the YAML output with open("to_load.yaml", "w") as file: @@ -344,7 +374,9 @@ def update_device_table(n_clicks, data, table): # view-recipe page ) def save_command(n_clicks, active_cell, data, value): # view-recipe page print("save_command") - if active_cell is not None and data[active_cell["row"]]["params"] != str(json.loads(value)): + if active_cell is not None and data[active_cell["row"]]["params"] != str( + json.loads(value) + ): com.command_list[data[active_cell["row"]]["index"]][0]._params = eval(value) update_upstream_recipe_dict() return None @@ -369,7 +401,9 @@ def save_command(n_clicks, active_cell, data, value): # view-recipe page ) def save_device(n_clicks, active_cell, data, value): # view-recipe page print("save_device") - if active_cell is not None and data[active_cell["row"]]["params"] != str(json.loads(value)): + if active_cell is not None and data[active_cell["row"]]["params"] != str( + json.loads(value) + ): # data_row = data[active_cell["row"]] params = eval(value) com.device_by_name[params["name"]].update_init_args(params) @@ -421,7 +455,9 @@ def fill_device_add_json_editor(value, is_open): # view-recipe page # args_dict[arg] = value args_list = list(util.devices_ref_redundancy[value]["init"]["args"].keys()) for arg in args_list: - args_dict[arg] = util.devices_ref_redundancy[value]["init"]["args"][arg]["default"] + args_dict[arg] = util.devices_ref_redundancy[value]["init"]["args"][arg][ + "default" + ] return [(json.dumps(args_dict, indent=4))] @@ -446,7 +482,9 @@ def fill_device_json_editor(is_open, active_cell, data): # view-recipe page for port, desc, hwid in sorted(ports): str_ports += f"{port}: {desc} [{hwid}]\n" lines = str_ports.splitlines() - device_port_html = [html.Div(["COM Port Info:"], style={"fontWeight": "bold"})] + device_port_html = [ + html.Div(["COM Port Info:"], style={"fontWeight": "bold"}) + ] device_port_html.append(html.Div([html.Div(line) for line in lines])) else: device_port_html = "" @@ -606,7 +644,9 @@ def view_recipe_enable_delete_device_button(table_div_children): return True -@app.callback(Output("delete-command-button", "disabled"), Input("commands-table", "active_cell")) +@app.callback( + Output("delete-command-button", "disabled"), Input("commands-table", "active_cell") +) def view_recipe_enable_delete_command_button(table_div_children): active_cell = table_div_children if active_cell is not None: @@ -660,7 +700,9 @@ def update_commands_table(n_clicks, data, table): # view-recipe page command_params = [] for index, command in enumerate(command_list): temp_dict_command_params = {"command": type(command[0]).__name__} - temp_dict_command_params.update({"params": str(command[0].get_init_args()), "index": index}) + temp_dict_command_params.update( + {"params": str(command[0].get_init_args()), "index": index} + ) command_params.append((temp_dict_command_params)) # else: # command_params.append(command._params) @@ -751,7 +793,9 @@ def view_recipe_fill_add_command_json_editor(command_type, url, device_type): if command_type is None or command_type == "": return "" args_dict = {} - args_list = util.devices_ref_redundancy[device_type]["commands"][command_type]["args"] + args_list = util.devices_ref_redundancy[device_type]["commands"][command_type][ + "args" + ] for arg in args_list: args_dict[arg] = args_list[arg]["default"] args_dict["delay"] = 0.0 @@ -831,7 +875,9 @@ def view_recipe_fill_execution_options(url): filesToRet += item + "\n" return [ filesToRet, - com.document["recipe_dict"]["execution_options"]["default_execution_record_name"], + com.document["recipe_dict"]["execution_options"][ + "default_execution_record_name" + ], ] return ["", ""] @@ -849,10 +895,14 @@ def view_recipe_fill_execution_options(url): ], prevent_initial_call=True, ) -def view_recipe_save_execution_options(n, filenames, default_execution_record_name, url): +def view_recipe_save_execution_options( + n, filenames, default_execution_record_name, url +): if str(url) == "/view-recipe": com.execution_options["output_files"] = filenames.splitlines() - com.execution_options["default_execution_record_name"] = default_execution_record_name + com.execution_options[ + "default_execution_record_name" + ] = default_execution_record_name success = update_upstream_recipe_dict() if success: return ["Saved!", {"display": "block"}] @@ -994,7 +1044,9 @@ def enable_add_device_button_ace(value, is_openInp, is_open): # python-edit-rec State("command-add-modal-ace", "is_open"), prevent_initial_call=True, ) -def enable_add_command_button_ace(value, is_openInp, is_open): # python-edit-recipe page +def enable_add_command_button_ace( + value, is_openInp, is_open +): # python-edit-recipe page if value == "" or value is None: return True print("enable_add_command_button_ace") @@ -1051,19 +1103,25 @@ def add_device_to_recipe_ace(n_clicks, value, device_type): # python-edit-recip ], prevent_initial_call=True, ) -def add_commands_to_recipe_ace(n_clicks, value, command, device_type): # python-edit-recipe page +def add_commands_to_recipe_ace( + n_clicks, value, command, device_type +): # python-edit-recipe page print("add_commands_to_recipe_ace") og_value = str(value) if value == "" or value is None: return ["", True, "No code in editor", "warning", 3000] try: value = str(value) - command_line = util.devices_ref_redundancy[device_type]["commands"][command]["default_code"] + command_line = util.devices_ref_redundancy[device_type]["commands"][command][ + "default_code" + ] import_line = util.devices_ref_redundancy[device_type]["import_commands"] import_device_line = util.devices_ref_redundancy[device_type]["import_device"] if import_device_line not in value: raise Exception( - "Device (or its import '" + import_device_line + "') not found in recipe" + "Device (or its import '" + + import_device_line + + "') not found in recipe" ) if import_line not in value: value = import_line + "\n" + value @@ -1162,8 +1220,13 @@ def execute_recipe_load_document_viewer(n, url): toRet += "\n" for command in recipe_ec[1]: toRet += str(command) + "\n" - if "default_execution_record_name" in list(com.execution_options.keys()): - return [toRet, com.execution_options["default_execution_record_name"]] + if "default_execution_record_name" in list( + com.execution_options.keys() + ): + return [ + toRet, + com.execution_options["default_execution_record_name"], + ] return [toRet, "Execution"] return ["", "No Recipe Loaded"] @@ -1189,7 +1252,11 @@ def execute_recipe_upload_data( print("execute_recipe_upload_data") execution = {} execution["name"] = name - if isinstance(recipe_data, list) and recipe_data is not None and recipe_data != []: + if ( + isinstance(recipe_data, list) + and recipe_data is not None + and recipe_data != [] + ): execution["recipe"] = recipe_data[0].split("\n") elif recipe_data is not None and recipe_data != "": execution["recipe"] = recipe_data.split("\n") @@ -1200,7 +1267,9 @@ def execute_recipe_upload_data( for i, file in enumerate(files): # print(file) file_bytes = base64.b64decode(file + "==") - execution["files"].append(mongo_gridfs.put(file_bytes, filename=filenames[i])) + execution["files"].append( + mongo_gridfs.put(file_bytes, filename=filenames[i]) + ) exec_success = update_execution_upstream(execution) # exec_success = False if exec_success: @@ -1343,7 +1412,9 @@ def create_manual_control_device_form(value, url): toRet.append( dbc.Row( [ - dbc.Label([arg], html_for=str(value + "+" + arg), width=2), + dbc.Label( + [arg], html_for=str(value + "+" + arg), width=2 + ), dbc.Col( [ dbc.Input( @@ -1382,22 +1453,34 @@ def create_manual_control_command_form(command, device, url, device_form): toRet = [] if util.devices_ref_redundancy[device]["serial"] == True: seq_toRet = [] - for seq_command in util.devices_ref_redundancy[device]["serial_sequence"]: + for seq_command in util.devices_ref_redundancy[device][ + "serial_sequence" + ]: seq_toRet.append(dbc.Row([dbc.Label([seq_command])])) - args = util.devices_ref_redundancy[device]["commands"][seq_command]["args"] + args = util.devices_ref_redundancy[device]["commands"][seq_command][ + "args" + ] for arg in args: seq_toRet.append( dbc.Row( [ dbc.Label( [arg], - html_for=str(device + "+" + seq_command + "+" + arg), + html_for=str( + device + "+" + seq_command + "+" + arg + ), width=2, ), dbc.Col( [ dbc.Input( - id=str(device + "+" + seq_command + "+" + arg), + id=str( + device + + "+" + + seq_command + + "+" + + arg + ), value=args[arg]["default"], placeholder=args[arg]["notes"], ), @@ -1501,7 +1584,9 @@ def open_manual_control_execute_modal(n, url): Output("manual-control-alert", "children", allow_duplicate=True), Output("manual-control-alert", "color", allow_duplicate=True), Output("manual-control-alert", "duration", allow_duplicate=True), - Output("manual-control-execute-modal-body-code", "children", allow_duplicate=True), + Output( + "manual-control-execute-modal-body-code", "children", allow_duplicate=True + ), ], Input("manual-control-open-execute-modal-button", "n_clicks"), [ @@ -1514,7 +1599,9 @@ def open_manual_control_execute_modal(n, url): ], prevent_initial_call=True, ) -def manual_control_execute_fill_code(n, url, opt, device, command, device_form, command_form): +def manual_control_execute_fill_code( + n, url, opt, device, command, device_form, command_form +): if str(url) == "/manual-control": print("manual_control_execute_fill_code") if device is None or device == "" or command is None or command == "": @@ -1542,12 +1629,16 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, if util.devices_ref_redundancy[device]["init"]["args"][arg]["type"] == str: instantiate_code += "'" instantiate_code += str( - device_form[i]["props"]["children"][1]["props"]["children"][0]["props"]["value"] + device_form[i]["props"]["children"][1]["props"]["children"][0][ + "props" + ]["value"] ) instantiate_code += "'" else: instantiate_code += str( - device_form[i]["props"]["children"][1]["props"]["children"][0]["props"]["value"] + device_form[i]["props"]["children"][1]["props"]["children"][0][ + "props" + ]["value"] ) instantiate_code += ")" code += str(device) + "_seq" + " = CommandSequence()" @@ -1559,9 +1650,17 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, for i, serial_seq_command in enumerate( util.devices_ref_redundancy[device]["serial_sequence"] ): - code += str(device) + "_seq" + ".add_command(" + str(serial_seq_command) + "(" + code += ( + str(device) + + "_seq" + + ".add_command(" + + str(serial_seq_command) + + "(" + ) for ii, serial_seq_command_arg in enumerate( - util.devices_ref_redundancy[device]["commands"][serial_seq_command]["args"] + util.devices_ref_redundancy[device]["commands"][serial_seq_command][ + "args" + ] ): if ii != 0: code += ", " @@ -1572,16 +1671,18 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, + str(device) + "_seq.device_by_name['" + str( - command_form[0]["props"]["children"][(2 * ii) + 1]["props"][ - "children" - ][1]["props"]["children"][0]["props"]["value"] + command_form[0]["props"]["children"][(2 * ii) + 1][ + "props" + ]["children"][1]["props"]["children"][0]["props"][ + "value" + ] ) + "']" ) elif ( - util.devices_ref_redundancy[device]["commands"][serial_seq_command]["args"][ - serial_seq_command_arg - ]["type"] + util.devices_ref_redundancy[device]["commands"][ + serial_seq_command + ]["args"][serial_seq_command_arg]["type"] == str ): code += ( @@ -1589,9 +1690,11 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, + "=" + "'" + str( - command_form[0]["props"]["children"][(2 * ii) + 1]["props"][ - "children" - ][1]["props"]["children"][0]["props"]["value"] + command_form[0]["props"]["children"][(2 * ii) + 1][ + "props" + ]["children"][1]["props"]["children"][0]["props"][ + "value" + ] ) + "'" ) @@ -1600,9 +1703,11 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, serial_seq_command_arg + "=" + str( - command_form[0]["props"]["children"][(2 * ii) + 1]["props"][ - "children" - ][1]["props"]["children"][0]["props"]["value"] + command_form[0]["props"]["children"][(2 * ii) + 1][ + "props" + ]["children"][1]["props"]["children"][0]["props"][ + "value" + ] ) ) @@ -1620,9 +1725,9 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, + str(device) + "_seq.device_by_name['" + str( - command_form[2]["props"]["children"][ii]["props"]["children"][1][ - "props" - ]["children"][0]["props"]["value"] + command_form[2]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] ) + "']" ) @@ -1637,9 +1742,9 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, + "=" + "'" + str( - command_form[2]["props"]["children"][ii]["props"]["children"][1][ - "props" - ]["children"][0]["props"]["value"] + command_form[2]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] ) + "'" ) @@ -1648,9 +1753,9 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, seq_command_arg + "=" + str( - command_form[2]["props"]["children"][ii]["props"]["children"][1][ - "props" - ]["children"][0]["props"]["value"] + command_form[2]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] ) ) @@ -1669,9 +1774,9 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, + str(device) + "_seq.device_by_name['" + str( - command_form[1]["props"]["children"][ii]["props"]["children"][1][ - "props" - ]["children"][0]["props"]["value"] + command_form[1]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] ) + "']" ) @@ -1686,9 +1791,9 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, + "=" + "'" + str( - command_form[1]["props"]["children"][ii]["props"]["children"][1][ - "props" - ]["children"][0]["props"]["value"] + command_form[1]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] ) + "'" ) @@ -1697,9 +1802,9 @@ def manual_control_execute_fill_code(n, url, opt, device, command, device_form, seq_command_arg + "=" + str( - command_form[1]["props"]["children"][ii]["props"]["children"][1][ - "props" - ]["children"][0]["props"]["value"] + command_form[1]["props"]["children"][ii]["props"][ + "children" + ][1]["props"]["children"][0]["props"]["value"] ) ) @@ -1924,7 +2029,9 @@ def fill_database_collection_schema(collection, db, url): print("fill_database_collection_schema") if collection is not None and collection != "" and db is not None and db != "": try: - schema = mongo.client[db].get_collection(collection).options()["validator"] + schema = ( + mongo.client[db].get_collection(collection).options()["validator"] + ) schema = process_schema(schema) return render_dict(schema) except Exception as e: @@ -1968,12 +2075,14 @@ def fill_real_time_telemetry(n, device, url): if str(url) == "/real-time-telemetry" and device is not None and device != "": print("fill_real_time_telemetry") if "telemetry" in list(util.devices_ref_redundancy[device].keys()): - device_parameter_options = util.devices_ref_redundancy[device]["telemetry"]["options"] + device_parameter_options = util.devices_ref_redundancy[device]["telemetry"][ + "options" + ] for parameter in util.devices_ref_redundancy[device]["telemetry"]: print(parameter) - parameter_options = util.devices_ref_redundancy[device]["telemetry"]["parameters"][ - parameter - ] + parameter_options = util.devices_ref_redundancy[device]["telemetry"][ + "parameters" + ][parameter] return [""] return [""] diff --git a/command_invoker.py b/aamp_app/command_invoker.py similarity index 75% rename from command_invoker.py rename to aamp_app/command_invoker.py index 29c8c0e..e11ccb7 100644 --- a/command_invoker.py +++ b/aamp_app/command_invoker.py @@ -17,25 +17,26 @@ from commands.utility_commands import DelayPauseCommand -format = '[%(asctime)s] [%(levelname)-5s]: %(message)s' +format = "[%(asctime)s] [%(levelname)-5s]: %(message)s" log_formatter = logging.Formatter(format) logging.basicConfig(level=logging.INFO, format=format) class CommandInvoker: """Handles the execution and logging of a command sequence""" + log_directory = "" def __init__( - self, - command_seq: CommandSequence, - log_to_file: bool = True, - log_filename: Optional[str] = None, - alert_slack: bool = False) -> None: - + self, + command_seq: CommandSequence, + log_to_file: bool = True, + log_filename: Optional[str] = None, + alert_slack: bool = False, + ) -> None: if not _has_slack and alert_slack: raise ImportError("slackclient module is required to alert slack.") - + self._command_seq = command_seq self._log_to_file = log_to_file self._alert_slack = alert_slack @@ -44,21 +45,21 @@ def __init__( if self._log_to_file: if self._log_filename is None: - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - self._log_filename = "logs/" +timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + self._log_filename = "logs/" + timestamp + + if not self._log_filename[-4:] == ".log": + self._log_filename += ".log" - if not self._log_filename[-4:] == '.log': - self._log_filename += '.log' - # self._log_filename = self.log_directory + self._log_filename - + self._file_handler = logging.FileHandler(self._log_filename) self._file_handler.setFormatter(log_formatter) if not len(self.log.handlers): self.log.addHandler(self._file_handler) if self._alert_slack: - self._slack_token = os.environ.get('SLACK_BOT_TOKEN') + self._slack_token = os.environ.get("SLACK_BOT_TOKEN") self._slack_client = slack.WebClient(token=self._slack_token) def invoke_commands(self) -> bool: @@ -83,7 +84,7 @@ def invoke_commands(self) -> bool: self.log.info("") self.log_command_names(unloop=False) - self.log.info("="*20 + "BEGINNING OF COMMAND SEQUENCE EXECUTION" + "="*20) + self.log.info("=" * 20 + "BEGINNING OF COMMAND SEQUENCE EXECUTION" + "=" * 20) command_generator = self._command_seq.yield_next_command() @@ -100,19 +101,23 @@ def invoke_commands(self) -> bool: break # Process the command's start delay - delay = command._params['delay'] + delay = command._params["delay"] if isinstance(delay, float) or isinstance(delay, int): if delay > 0.0: self.log.info("DELAY -> " + str(delay)) time.sleep(delay) elif delay == "PAUSE" or delay == "P": self.log.info("PAUSE -> Waiting for user to press enter") - print('') - print("Press ENTER to continue, type 'quit' to terminate execution immediately:") + print("") + print( + "Press ENTER to continue, type 'quit' to terminate execution immediately:" + ) userinput = input() if userinput == "quit": - self.log.info("PAUSE -> User terminated execution early by entering 'quit'") - has_error = True # Not really an error but returning False since an early termination (even if intentional) may disrupt a higher workflow + self.log.info( + "PAUSE -> User terminated execution early by entering 'quit'" + ) + has_error = True # Not really an error but returning False since an early termination (even if intentional) may disrupt a higher workflow break else: self.log.info("PAUSE -> User continued command execution") @@ -127,10 +132,22 @@ def invoke_commands(self) -> bool: # Check result if command.result.was_successful: - self.log.info("RESULT -> " + str(command.result.was_successful) + ", " + command.result.message) + self.log.info( + "RESULT -> " + + str(command.result.was_successful) + + ", " + + command.result.message + ) else: - self.log.error("RESULT -> " + str(command.result.was_successful) + ", " + command.result.message) - self.log.error("Received False result. Terminating command execution early!") + self.log.error( + "RESULT -> " + + str(command.result.was_successful) + + ", " + + command.result.message + ) + self.log.error( + "Received False result. Terminating command execution early!" + ) if self._alert_slack: self.log.info("Sending command details to slack.") self.alert_slack_command(command) @@ -139,7 +156,7 @@ def invoke_commands(self) -> bool: break # Go to next command # Finished command list execution - self.log.info("="*20 + "END OF COMMAND SEQUENCE EXECUTION" + "="*20) + self.log.info("=" * 20 + "END OF COMMAND SEQUENCE EXECUTION" + "=" * 20) self.log.info("") if self._log_to_file: print("") @@ -166,13 +183,19 @@ def alert_slack_command(self, command: Command): try: response = self._slack_client.chat_postMessage( channel="printer-bot-test", - text=("Error in the following command execution:\n" - "COMMAND -> " + command.name + "\n" - "RESULT -> " + str(command.result.was_successful) + ", " + command.result.message + "\n" - "See log file \"" + str(self._log_filename) + "\" for more details.") - ) + text=( + "Error in the following command execution:\n" + "COMMAND -> " + command.name + "\n" + "RESULT -> " + + str(command.result.was_successful) + + ", " + + command.result.message + + "\n" + 'See log file "' + str(self._log_filename) + '" for more details.' + ), + ) except SlackApiError as inst: - self.log.error("Could not send message to slack: " + inst.response['error']) + self.log.error("Could not send message to slack: " + inst.response["error"]) def alert_slack_message(self, message: str): """Attempt to send a message to a designated slack channel. @@ -184,21 +207,21 @@ def alert_slack_message(self, message: str): """ try: response = self._slack_client.chat_postMessage( - channel="printer-bot-test", - text=message) + channel="printer-bot-test", text=message + ) except SlackApiError as inst: - self.log.error("Could not send message to slack: " + inst.response['error']) - + self.log.error("Could not send message to slack: " + inst.response["error"]) + def upload_log_slack(self): """Attempt to upload the invoker's log file to a designated slack channel.""" try: - response = self._slack_client.files_upload( + response = self._slack_client.files_upload( file=self._log_filename, - initial_comment='Uploading log file with error: ' + self._log_filename, - channels='printer-bot-test' + initial_comment="Uploading log file with error: " + self._log_filename, + channels="printer-bot-test", ) except SlackApiError as inst: - self.log.error("Could not upload log file: " + inst.response['error']) + self.log.error("Could not upload log file: " + inst.response["error"]) def log_command_names(self, unloop: bool = False): """Log the names of each command in the sequence @@ -208,12 +231,14 @@ def log_command_names(self, unloop: bool = False): unloop : bool, optional Whether to unloop the commands sequence or not, by default False """ - self.log.info("="*20 + "LIST OF COMMAND NAMES" + "="*20) + self.log.info("=" * 20 + "LIST OF COMMAND NAMES" + "=" * 20) for name in self._command_seq.get_command_names(unloop): self.log.info(name) self.log.info("") - self.log.info("(Number of iterations: " + str(self._command_seq.num_iterations) + ")") - self.log.info("="*20 + "END OF COMMAND NAMES" + "="*20) + self.log.info( + "(Number of iterations: " + str(self._command_seq.num_iterations) + ")" + ) + self.log.info("=" * 20 + "END OF COMMAND NAMES" + "=" * 20) def log_command_names_descriptions(self, unloop: bool = False): """Log the names and descriptions of each command in the sequence @@ -223,13 +248,15 @@ def log_command_names_descriptions(self, unloop: bool = False): unloop : bool, optional Whether to unloop the commands sequence or not, by default False """ - self.log.info("="*20 + "LIST OF COMMAND NAMES/DESCRIPTIONS" + "="*20) + self.log.info("=" * 20 + "LIST OF COMMAND NAMES/DESCRIPTIONS" + "=" * 20) for name_desc in self._command_seq.get_command_names_descriptions(unloop): self.log.info(name_desc[0]) self.log.info(name_desc[1]) self.log.info("") - self.log.info("(Number of iterations: " + str(self._command_seq.num_iterations) + ")") - self.log.info("="*20 + "END OF COMMAND NAMES/DESCRIPTIONS" + "="*20) + self.log.info( + "(Number of iterations: " + str(self._command_seq.num_iterations) + ")" + ) + self.log.info("=" * 20 + "END OF COMMAND NAMES/DESCRIPTIONS" + "=" * 20) def get_log_messages(self): """Get all log messages as a list. @@ -243,7 +270,7 @@ def get_log_messages(self): for handler in self.log.handlers: if isinstance(handler, logging.FileHandler): handler.flush() # Ensure all messages are written to the log file - with open(handler.baseFilename, 'r') as log_file: + with open(handler.baseFilename, "r") as log_file: log_messages.extend(log_file.readlines()) return log_messages @@ -251,7 +278,7 @@ def clear_log_file(self): """Clear the log file by truncating its content.""" if self._log_to_file: if os.path.exists(self._log_filename): - with open(self._log_filename, 'w') as log_file: + with open(self._log_filename, "w") as log_file: log_file.truncate(0) self.log.info("Log file cleared.") else: @@ -260,11 +287,10 @@ def clear_log_file(self): self.log.warning("Log file is not being used.") - # experiment name, id # is logging, log file, delay between commands, or delay list -# delay list can have a value like -1 or a str to indicate that -# we wait for user input before proceeding or type quit to terminate early +# delay list can have a value like -1 or a str to indicate that +# we wait for user input before proceeding or type quit to terminate early # this could also be implemented as a Command that waits for input and returns # consider also ctrl c exception termination, safely terminate and log # invoker observer/inspecter for more complex bookkeeping? @@ -273,7 +299,7 @@ def clear_log_file(self): # for parallel processes with threading consider branches and loop done with the follow command nodes: jump, conditional jump, label, end (ala assembly, exapunks) # example of conditional jump, prompt user for y/n to jump to a previous label to loop, or jump to a future label to skip some commands -# on loops we can either do the same thing or change the arguments to command (e.g. to explore how printing speed changes each loop). +# on loops we can either do the same thing or change the arguments to command (e.g. to explore how printing speed changes each loop). # This can be done with a preset that is passed (e.g. a list of parameters corresponding to each loop) # or this can be done live, e.g. based on equation/condition or based on user input or based on input from a machine learning optimizer # how to check against infinite loop @@ -283,14 +309,14 @@ def clear_log_file(self): # or an invoker manager or the client sees a new branch node and creates a new invoker and passes it the branch list # but what if a branch within a branch? need some sort of composite/recursive approach -# how to deal with commands with arguments that change every loop? +# how to deal with commands with arguments that change every loop? # Should we have command objects that persist throughout loops? if so, should they keep track of their own execution count? (this probably makes writing command classes more complicated) # should we generate new commands instead, cloning the loop section on each iteration with new commands? # whose job is it to change the command or generate new commands, the client or invoker # and how do we deal with commands with arguments that come from user input on each loop? # how can we make the user input argument and preset list argument methods as similar as possible? -# are commands changable after instantiation or should we just destroy and remake them? +# are commands changable after instantiation or should we just destroy and remake them? # if changable then we need update functions/getters to update name/description when arguments change which makes them more complicated -# if we should destroy and remake, we might as well make them when we are ready (after taking user input). +# if we should destroy and remake, we might as well make them when we are ready (after taking user input). # But this means the client doesnt have a singular invoker_command method call, and we should always push new commands to the stack/queue and have invoker invoke them # this means the invoker needs to wait when reaching the end of its current command list, meaning the client needs a definite way to terminate invoking on the invoker diff --git a/command_sequence.py b/aamp_app/command_sequence.py similarity index 100% rename from command_sequence.py rename to aamp_app/command_sequence.py diff --git a/devices/__init__.py b/aamp_app/commands/__init__.py similarity index 100% rename from devices/__init__.py rename to aamp_app/commands/__init__.py diff --git a/commands/command.py b/aamp_app/commands/command.py similarity index 100% rename from commands/command.py rename to aamp_app/commands/command.py diff --git a/commands/dummy_composite_commands.py b/aamp_app/commands/dummy_composite_commands.py similarity index 100% rename from commands/dummy_composite_commands.py rename to aamp_app/commands/dummy_composite_commands.py diff --git a/commands/dummy_heater_commands.py b/aamp_app/commands/dummy_heater_commands.py similarity index 100% rename from commands/dummy_heater_commands.py rename to aamp_app/commands/dummy_heater_commands.py diff --git a/commands/dummy_meter_commands.py b/aamp_app/commands/dummy_meter_commands.py similarity index 100% rename from commands/dummy_meter_commands.py rename to aamp_app/commands/dummy_meter_commands.py diff --git a/commands/dummy_motor_commands.py b/aamp_app/commands/dummy_motor_commands.py similarity index 100% rename from commands/dummy_motor_commands.py rename to aamp_app/commands/dummy_motor_commands.py diff --git a/commands/festo_solenoid_valve_commands.py b/aamp_app/commands/festo_solenoid_valve_commands.py similarity index 100% rename from commands/festo_solenoid_valve_commands.py rename to aamp_app/commands/festo_solenoid_valve_commands.py diff --git a/commands/generic_commands.py b/aamp_app/commands/generic_commands.py similarity index 100% rename from commands/generic_commands.py rename to aamp_app/commands/generic_commands.py diff --git a/commands/heating_stage_commands.py b/aamp_app/commands/heating_stage_commands.py similarity index 100% rename from commands/heating_stage_commands.py rename to aamp_app/commands/heating_stage_commands.py diff --git a/commands/keithley_2450_commands.py b/aamp_app/commands/keithley_2450_commands.py similarity index 100% rename from commands/keithley_2450_commands.py rename to aamp_app/commands/keithley_2450_commands.py diff --git a/commands/kinova_arm_commands.py b/aamp_app/commands/kinova_arm_commands.py similarity index 100% rename from commands/kinova_arm_commands.py rename to aamp_app/commands/kinova_arm_commands.py diff --git a/commands/linear_stage_150_commands.py b/aamp_app/commands/linear_stage_150_commands.py similarity index 100% rename from commands/linear_stage_150_commands.py rename to aamp_app/commands/linear_stage_150_commands.py diff --git a/commands/mfc_commands.py b/aamp_app/commands/mfc_commands.py similarity index 100% rename from commands/mfc_commands.py rename to aamp_app/commands/mfc_commands.py diff --git a/commands/mts50_z8_commands.py b/aamp_app/commands/mts50_z8_commands.py similarity index 100% rename from commands/mts50_z8_commands.py rename to aamp_app/commands/mts50_z8_commands.py diff --git a/commands/multi_stepper_commands.py b/aamp_app/commands/multi_stepper_commands.py similarity index 100% rename from commands/multi_stepper_commands.py rename to aamp_app/commands/multi_stepper_commands.py diff --git a/commands/newport_esp301_commands.py b/aamp_app/commands/newport_esp301_commands.py similarity index 100% rename from commands/newport_esp301_commands.py rename to aamp_app/commands/newport_esp301_commands.py diff --git a/commands/oxygen_sensor_commands.py b/aamp_app/commands/oxygen_sensor_commands.py similarity index 100% rename from commands/oxygen_sensor_commands.py rename to aamp_app/commands/oxygen_sensor_commands.py diff --git a/commands/psd6_syringe_pump_commands.py b/aamp_app/commands/psd6_syringe_pump_commands.py similarity index 100% rename from commands/psd6_syringe_pump_commands.py rename to aamp_app/commands/psd6_syringe_pump_commands.py diff --git a/commands/sht85_sensor_commands.py b/aamp_app/commands/sht85_sensor_commands.py similarity index 100% rename from commands/sht85_sensor_commands.py rename to aamp_app/commands/sht85_sensor_commands.py diff --git a/commands/stellarnet_spectrometer_commands.py b/aamp_app/commands/stellarnet_spectrometer_commands.py similarity index 100% rename from commands/stellarnet_spectrometer_commands.py rename to aamp_app/commands/stellarnet_spectrometer_commands.py diff --git a/commands/utility_commands.py b/aamp_app/commands/utility_commands.py similarity index 100% rename from commands/utility_commands.py rename to aamp_app/commands/utility_commands.py diff --git a/commands/ximea_camera_commands.py b/aamp_app/commands/ximea_camera_commands.py similarity index 100% rename from commands/ximea_camera_commands.py rename to aamp_app/commands/ximea_camera_commands.py diff --git a/console_interceptor.py b/aamp_app/console_interceptor.py similarity index 100% rename from console_interceptor.py rename to aamp_app/console_interceptor.py diff --git a/devices/.gitignore b/aamp_app/devices/.gitignore similarity index 100% rename from devices/.gitignore rename to aamp_app/devices/.gitignore diff --git a/devices/README.md b/aamp_app/devices/README.md similarity index 100% rename from devices/README.md rename to aamp_app/devices/README.md diff --git a/aamp_app/devices/__init__.py b/aamp_app/devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/devices/device.py b/aamp_app/devices/device.py similarity index 100% rename from devices/device.py rename to aamp_app/devices/device.py diff --git a/devices/dummy_heater.py b/aamp_app/devices/dummy_heater.py similarity index 100% rename from devices/dummy_heater.py rename to aamp_app/devices/dummy_heater.py diff --git a/devices/dummy_meter.py b/aamp_app/devices/dummy_meter.py similarity index 100% rename from devices/dummy_meter.py rename to aamp_app/devices/dummy_meter.py diff --git a/devices/dummy_motor.py b/aamp_app/devices/dummy_motor.py similarity index 100% rename from devices/dummy_motor.py rename to aamp_app/devices/dummy_motor.py diff --git a/devices/dummy_motor_source.py b/aamp_app/devices/dummy_motor_source.py similarity index 100% rename from devices/dummy_motor_source.py rename to aamp_app/devices/dummy_motor_source.py diff --git a/devices/festo_solenoid_valve.py b/aamp_app/devices/festo_solenoid_valve.py similarity index 100% rename from devices/festo_solenoid_valve.py rename to aamp_app/devices/festo_solenoid_valve.py diff --git a/devices/heating_stage.py b/aamp_app/devices/heating_stage.py similarity index 100% rename from devices/heating_stage.py rename to aamp_app/devices/heating_stage.py diff --git a/devices/keithley_2450.py b/aamp_app/devices/keithley_2450.py similarity index 100% rename from devices/keithley_2450.py rename to aamp_app/devices/keithley_2450.py diff --git a/devices/kinova_arm.py b/aamp_app/devices/kinova_arm.py similarity index 100% rename from devices/kinova_arm.py rename to aamp_app/devices/kinova_arm.py diff --git a/devices/linear_stage_150.py b/aamp_app/devices/linear_stage_150.py similarity index 100% rename from devices/linear_stage_150.py rename to aamp_app/devices/linear_stage_150.py diff --git a/devices/mfc.py b/aamp_app/devices/mfc.py similarity index 100% rename from devices/mfc.py rename to aamp_app/devices/mfc.py diff --git a/devices/mts50_z8.py b/aamp_app/devices/mts50_z8.py similarity index 100% rename from devices/mts50_z8.py rename to aamp_app/devices/mts50_z8.py diff --git a/devices/multi_stepper.py b/aamp_app/devices/multi_stepper.py similarity index 100% rename from devices/multi_stepper.py rename to aamp_app/devices/multi_stepper.py diff --git a/devices/newport_esp301.py b/aamp_app/devices/newport_esp301.py similarity index 100% rename from devices/newport_esp301.py rename to aamp_app/devices/newport_esp301.py diff --git a/devices/oxygen_sensor.py b/aamp_app/devices/oxygen_sensor.py similarity index 100% rename from devices/oxygen_sensor.py rename to aamp_app/devices/oxygen_sensor.py diff --git a/devices/psd6_syringe_pump.py b/aamp_app/devices/psd6_syringe_pump.py similarity index 100% rename from devices/psd6_syringe_pump.py rename to aamp_app/devices/psd6_syringe_pump.py diff --git a/devices/sht85_sensor.py b/aamp_app/devices/sht85_sensor.py similarity index 100% rename from devices/sht85_sensor.py rename to aamp_app/devices/sht85_sensor.py diff --git a/devices/stellarnet_spectrometer.py b/aamp_app/devices/stellarnet_spectrometer.py similarity index 100% rename from devices/stellarnet_spectrometer.py rename to aamp_app/devices/stellarnet_spectrometer.py diff --git a/devices/utility_device.py b/aamp_app/devices/utility_device.py similarity index 100% rename from devices/utility_device.py rename to aamp_app/devices/utility_device.py diff --git a/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py similarity index 100% rename from devices/ximea_camera.py rename to aamp_app/devices/ximea_camera.py diff --git a/mongodb_helper.py b/aamp_app/mongodb_helper.py similarity index 100% rename from mongodb_helper.py rename to aamp_app/mongodb_helper.py diff --git a/pages/data.py b/aamp_app/pages/data.py similarity index 100% rename from pages/data.py rename to aamp_app/pages/data.py diff --git a/pages/database.py b/aamp_app/pages/database.py similarity index 100% rename from pages/database.py rename to aamp_app/pages/database.py diff --git a/pages/execute-recipe.py b/aamp_app/pages/execute-recipe.py similarity index 100% rename from pages/execute-recipe.py rename to aamp_app/pages/execute-recipe.py diff --git a/pages/home.py b/aamp_app/pages/home.py similarity index 100% rename from pages/home.py rename to aamp_app/pages/home.py diff --git a/pages/load-recipe.py b/aamp_app/pages/load-recipe.py similarity index 100% rename from pages/load-recipe.py rename to aamp_app/pages/load-recipe.py diff --git a/pages/manual-control.py b/aamp_app/pages/manual-control.py similarity index 100% rename from pages/manual-control.py rename to aamp_app/pages/manual-control.py diff --git a/pages/python-edit-recipe.py b/aamp_app/pages/python-edit-recipe.py similarity index 100% rename from pages/python-edit-recipe.py rename to aamp_app/pages/python-edit-recipe.py diff --git a/pages/real-time-telemetry.py b/aamp_app/pages/real-time-telemetry.py similarity index 67% rename from pages/real-time-telemetry.py rename to aamp_app/pages/real-time-telemetry.py index f3dc5c6..48ecc69 100644 --- a/pages/real-time-telemetry.py +++ b/aamp_app/pages/real-time-telemetry.py @@ -4,14 +4,18 @@ from util import devices_ref_redundancy dash.register_page( - __name__, path="/real-time-telemetry", name="Real Time Telemetry", title="Real Time Telemetry" + __name__, + path="/real-time-telemetry", + name="Real Time Telemetry", + title="Real Time Telemetry", ) layout = html.Div( [ html.H1("Real Time Telemetry", className="mb-3"), dcc.Dropdown( - options=list(devices_ref_redundancy.keys()), id="real-time-telemetry-device-dropdown" + options=list(devices_ref_redundancy.keys()), + id="real-time-telemetry-device-dropdown", ), dcc.Interval(id="interval-real-time-telemetry", interval=500, n_intervals=0), html.Div(id="real-time-telemetry-div"), diff --git a/pages/view-recipe.py b/aamp_app/pages/view-recipe.py similarity index 100% rename from pages/view-recipe.py rename to aamp_app/pages/view-recipe.py diff --git a/util.py b/aamp_app/util.py similarity index 100% rename from util.py rename to aamp_app/util.py diff --git a/blank.yaml b/blank.yaml deleted file mode 100644 index c4e5ad8..0000000 --- a/blank.yaml +++ /dev/null @@ -1,3 +0,0 @@ -- [] -- [] -- ALL From 1135202aab8350398827d68c47117ea1407338e4 Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 14 Aug 2023 16:49:20 -0700 Subject: [PATCH 061/125] updated gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 3dbbb92..245e210 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,5 @@ e4mongo.yaml project_const.py pw.py .DS_Store +blank.yaml +pw.txt From 9e2b1b664229feda2235888aba23f3c940ab162b Mon Sep 17 00:00:00 2001 From: Piyush Date: Mon, 14 Aug 2023 17:28:52 -0700 Subject: [PATCH 062/125] added mfc to util --- aamp_app/util.py | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/aamp_app/util.py b/aamp_app/util.py index ccb3c9e..8320a46 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -10,6 +10,9 @@ from devices.linear_stage_150 import LinearStage150 from devices.mts50_z8 import MTS50_Z8 from devices.keithley_2450 import Keithley2450 +from devices.mfc import MassFlowController +from devices.oxygen_sensor import OxygenSensor +from devices.sht85_sensor import SHT85HumidityTempSensor from devices.device import Device, MiscDeviceClass from devices.utility_device import UtilityCommands @@ -24,6 +27,7 @@ from commands.utility_commands import * from commands.heating_stage_commands import * from commands.multi_stepper_commands import * +from commands.mfc_commands import * from commands.newport_esp301_commands import * from commands.utility_commands import * @@ -978,6 +982,107 @@ def default(self, obj): }, }, }, + "MassFlowController": { + "obj": MassFlowController, + "serial": True, + "serial_sequence": ["MassFlowControllerInitialize"], + "import_device": "from devices.mfc import MassFlowController", + "import_commands": "from commands.mfc_commands import *", + "init": { + "default_code": "MassFlowController(name='MassFlowController', ip='192.168.2.155')", + "obj_name": "MassFlowController", + "args": { + "name": { + "default": "MassFlowController", + "type": str, + "notes": "Name of the device", + }, + "ip": { + "default": "192.168.2.155", + "type": str, + "notes": "IP of the device", + }, + }, + }, + "commands": { + "MFCInitialize": { + "default_code": "MFCInitialize(receiver= '')", + "args": { + "receiver": { + "default": "MassFlowController", + "type": str, + "notes": "Name of the device", + } + }, + "obj": MFCInitialize, + }, + "MFCDeinitialize": { + "default_code": "MFCDeinitialize(receiver= '')", + "args": { + "receiver": { + "default": "MassFlowController", + "type": str, + "notes": "Name of the device", + }, + "obj": MFCDeinitialize, + }, + }, + "MFCGetData": { + "default_code": "MFCGetData(receiver= '')", + "args": { + "receiver": { + "default": "MassFlowController", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": MFCGetData, + }, + "MFCSetGas": { + "default_code": "MFCSetGas(receiver= '', gas= 'N2')", + "args": { + "receiver": { + "default": "MassFlowController", + "type": str, + "notes": "Name of the device", + }, + "gas": { + "default": "N2", + "type": str, + "notes": "Gas to set", + }, + }, + "obj": MFCSetGas, + }, + "MFCSet": { + "default_code": "MFCSet(receiver= '', setpoint= 0.0)", + "args": { + "receiver": { + "default": "MassFlowController", + "type": str, + "notes": "Name of the device", + }, + "setpoint": { + "default": 0.0, + "type": float, + "notes": "Setpoint to set", + }, + }, + "obj": MFCSet, + }, + "MFCOpen": { + "default_code": "MFCOpen(receiver= '')", + "args": { + "receiver": { + "default": "MassFlowController", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": MFCOpen, + }, + }, + }, "NewportESP301": { "obj": NewportESP301, "serial": True, From a2f50e6d604e97b1c51334eb45b545e5006542aa Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 15 Aug 2023 17:35:27 -0700 Subject: [PATCH 063/125] added sht85 to util --- aamp_app/util.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/aamp_app/util.py b/aamp_app/util.py index 8320a46..6682d24 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -28,6 +28,7 @@ from commands.heating_stage_commands import * from commands.multi_stepper_commands import * from commands.mfc_commands import * +from commands.sht85_sensor_commands import * from commands.newport_esp301_commands import * from commands.utility_commands import * @@ -982,6 +983,96 @@ def default(self, obj): }, }, }, + "SHT85HumidityTempSensor": { + "obj": SHT85HumidityTempSensor, + "serial": True, + "serial_sequence": ["SHT85Connect", "SHT85Initialize"], + "import_device": "from devices.sht85_sensor import SHT85HumidityTempSensor", + "import_commands": "from commands.sht85_sensor_commands import *", + "init": { + "default_code": "SHT85HumidityTempSensor(name='SHT85HumidityTempSensor', port='', baudrate=9600, timeout=1.0)", + "obj_name": "SHT85HumidityTempSensor", + "args": { + "name": { + "default": "SHT85HumidityTempSensor", + "type": str, + "notes": "Name of the device", + }, + "port": { + "default": "COM", + "type": str, + "notes": "Port of the device", + }, + "baudrate": { + "default": 9600, + "type": int, + "notes": "Baudrate of the device", + }, + "timeout": { + "default": 1.0, + "type": float, + "notes": "Timeout of the device", + }, + }, + }, + "commands": { + "SHT85Connect": { + "default_code": "SHT85Connect(receiver= '')", + "args": { + "receiver": { + "default": "SHT85HumidityTempSensor", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": SHT85Connect, + }, + "SHT85Initialize": { + "default_code": "SHT85Initialize(receiver= '')", + "args": { + "receiver": { + "default": "SHT85HumidityTempSensor", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": SHT85Initialize, + }, + "SHT85Deinitialize": { + "default_code": "SHT85Deinitialize(receiver= '')", + "args": { + "receiver": { + "default": "SHT85HumidityTempSensor", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": SHT85Deinitialize, + }, + "SHT85GetHumidity": { + "default_code": "SHT85GetHumidity(receiver= '')", + "args": { + "receiver": { + "default": "SHT85HumidityTempSensor", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": SHT85GetHumidity, + }, + "SHT85GetTemp": { + "default_code": "SHT85GetTemp(receiver= '')", + "args": { + "receiver": { + "default": "SHT85HumidityTempSensor", + "type": str, + "notes": "Name of the device", + }, + }, + "obj": SHT85GetTemp, + }, + }, + }, "MassFlowController": { "obj": MassFlowController, "serial": True, From 4cf87a63551f0849cc9b2d446098c1cc1c874019 Mon Sep 17 00:00:00 2001 From: Piyush Date: Wed, 16 Aug 2023 23:11:55 -0700 Subject: [PATCH 064/125] telemetry working, options page, home update --- aamp_app/app.py | 94 ++++++++++++++++++++------- aamp_app/devices/dummy_motor.py | 67 ++++++++++++++----- aamp_app/devices/linear_stage_150.py | 14 +++- aamp_app/devices/oxygen_sensor.py | 2 +- aamp_app/pages/home.py | 51 ++++++++++++++- aamp_app/pages/options.py | 11 ++++ aamp_app/pages/real-time-telemetry.py | 13 +++- aamp_app/util.py | 39 ++++++++--- 8 files changed, 236 insertions(+), 55 deletions(-) create mode 100644 aamp_app/pages/options.py diff --git a/aamp_app/app.py b/aamp_app/app.py index 79fc7e5..1c06522 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -73,32 +73,37 @@ children=[ dbc.NavItem(dbc.NavLink("Load", href="/load-recipe", external_link=True)), dbc.NavItem(dbc.NavLink("View", href="/view-recipe", external_link=True)), + dbc.NavItem(dbc.NavLink("Code", href="/edit-recipe", external_link=True)), + dbc.NavItem(dbc.NavLink("Doc", href="/data", external_link=True)), dbc.NavItem(dbc.NavLink("Execute", href="/execute-recipe", external_link=True)), dbc.NavItem( dbc.NavLink("Manual Control", href="/manual-control", external_link=True) ), + dbc.NavItem(dbc.NavLink("DB Browser", href="/database", external_link=True)), + # dbc.NavItem(dbc.NavLink("Options", href="/options", external_link=True)), dbc.NavItem( - dbc.NavLink("Database Browser", href="/database", external_link=True) - ), - dbc.DropdownMenu( - children=[ - # dbc.DropdownMenuItem( - # "Load Recipe", href="/load-recipe", external_link=True - # ), - dbc.DropdownMenuItem( - "Real Time Telemetry", - href="/real-time-telemetry", - external_link=True, - ), - dbc.DropdownMenuItem( - "Edit Code", href="/edit-recipe", external_link=True - ), - dbc.DropdownMenuItem("Document", href="/data", external_link=True), - ], - nav=True, - in_navbar=True, - label="Tools", + dbc.NavLink("Real Time", href="/real-time-telemetry", external_link=True) ), + # dbc.DropdownMenu( + # children=[ + # # dbc.DropdownMenuItem( + # # "Load Recipe", href="/load-recipe", external_link=True + # # ), + # dbc.DropdownMenuItem( + # "Real Time Telemetry", + # href="/real-time-telemetry", + # external_link=True, + # ), + # dbc.DropdownMenuItem( + # "Edit Code", href="/edit-recipe", external_link=True + # ), + # dbc.DropdownMenuItem("Document", href="/data", external_link=True), + # # dbc.DropdownMenuItem("Options", href="/options", external_link=True), + # ], + # nav=True, + # in_navbar=True, + # label="Tools", + # ), ], brand="AAMP", brand_href="/", @@ -2063,27 +2068,66 @@ def fill_database_document_viewer(document, collection, url, db): # --------------------------------------------------------------- # Real Time Telemetry page # --------------------------------------------------------------- +import random @app.callback( [Output("real-time-telemetry-div", "children")], - [Input("interval-real-time-telemetry", "n_intervals")], - [Input("real-time-telemetry-device-dropdown", "value"), State("url", "pathname")], + [ + Input("real-time-telemetry-device-dropdown", "value"), + Input("interval-real-time-telemetry", "n_intervals"), + ], + [State("url", "pathname")], prevent_initial_call=True, ) -def fill_real_time_telemetry(n, device, url): +def fill_real_time_telemetry(device, n, url): if str(url) == "/real-time-telemetry" and device is not None and device != "": print("fill_real_time_telemetry") if "telemetry" in list(util.devices_ref_redundancy[device].keys()): device_parameter_options = util.devices_ref_redundancy[device]["telemetry"][ "options" ] - for parameter in util.devices_ref_redundancy[device]["telemetry"]: - print(parameter) + telemetry_data = {} + for parameter in util.devices_ref_redundancy[device]["telemetry"][ + "parameters" + ]: parameter_options = util.devices_ref_redundancy[device]["telemetry"][ "parameters" ][parameter] + telemetry_data[parameter] = getattr( + util.devices_ref_redundancy[device]["default_obj"], + parameter_options["function_name"], + )() + telemetry_data["rand"] = random.randint(1, 10) + + metrics_data = telemetry_data + + metrics_cards = [] + for metric_name, metric_value in metrics_data.items(): + card = dbc.Card( + dbc.CardBody( + [ + html.H5(metric_name, className="card-title"), + html.P( + f"{metric_value}", + className="card-text", + style={"fontSize": "1.25rem"}, + ), + ] + ), + className="mb-3", + style={"width": "30%"}, + ) + metrics_cards.append(card) + return [ + dbc.Row( + children=metrics_cards, + id="metrics-row", + style={"justifyContent": "space-around"}, + ) + ] + return [str(telemetry_data)] return [""] return [""] diff --git a/aamp_app/devices/dummy_motor.py b/aamp_app/devices/dummy_motor.py index fd08eaa..acb44d8 100644 --- a/aamp_app/devices/dummy_motor.py +++ b/aamp_app/devices/dummy_motor.py @@ -18,7 +18,7 @@ def get_init_args(self) -> dict: "speed": self.motor._speed, } return args_dict - + def update_init_args(self, args_dict: dict): self.motor._speed = args_dict["speed"] self._name = args_dict["name"] @@ -26,17 +26,29 @@ def update_init_args(self, args_dict: dict): @property def position(self) -> float: return self.motor.position - + + def get_position(self) -> float: + return self.motor.position + @property def speed(self) -> float: return self.motor.speed + def get_speed(self) -> float: + return self.motor.speed + def initialize(self) -> Tuple[bool, str]: self.motor.home_motor() while self.motor._position != 0.0: - print("DummyMotor " + self.name + " position: " + str(self.motor._position), end='\r') + print( + "DummyMotor " + self.name + " position: " + str(self.motor._position), + end="\r", + ) time.sleep(0.1) - print("DummyMotor " + self.name + " position: " + str(self.motor._position), end='\r') + print( + "DummyMotor " + self.name + " position: " + str(self.motor._position), + end="\r", + ) self._is_initialized = True return (True, "Initialized DummyMotor by homing and setting position to zero") @@ -50,9 +62,16 @@ def set_speed(self, speed: float): self.motor.speed = speed # if out of range, then the setter did nothing if self.motor.speed == speed: - return (True, "DummyMotor speed was successfully set to " + str(self.motor.speed)) + return ( + True, + "DummyMotor speed was successfully set to " + str(self.motor.speed), + ) else: - return (False, "DummyMotor speed was not set. Speed is currently " + str(self.motor.speed)) + return ( + False, + "DummyMotor speed was not set. Speed is currently " + + str(self.motor.speed), + ) @check_initialized def move_absolute(self, position: float) -> Tuple[bool, str]: @@ -64,9 +83,15 @@ def move_absolute(self, position: float) -> Tuple[bool, str]: self.motor.move_absolute(position) while self.motor.position != position: - print("DummyMotor " + self.name + " position: " + str(self.motor.position), end='\r') - time.sleep(.1) - print("DummyMotor " + self.name + " position: " + str(self.motor.position), end='\r') + print( + "DummyMotor " + self.name + " position: " + str(self.motor.position), + end="\r", + ) + time.sleep(0.1) + print( + "DummyMotor " + self.name + " position: " + str(self.motor.position), + end="\r", + ) return (True, "DummyMotor has reached position " + str(position)) @@ -82,16 +107,26 @@ def move_relative(self, distance: float) -> Tuple[bool, str]: self.motor.move_relative(distance) while self.motor.position != position: - print("DummyMotor " + self.name + " position: " + str(self.motor.position), end='\r') - time.sleep(.1) - print("DummyMotor " + self.name + " position: " + str(self.motor.position), end='\r') - - return (True, "DummyMotor has moved by " + str(distance) + " and reached position " + str(position)) + print( + "DummyMotor " + self.name + " position: " + str(self.motor.position), + end="\r", + ) + time.sleep(0.1) + print( + "DummyMotor " + self.name + " position: " + str(self.motor.position), + end="\r", + ) + + return ( + True, + "DummyMotor has moved by " + + str(distance) + + " and reached position " + + str(position), + ) def is_valid_position(self, position) -> bool: if position >= self.motor.min_position and position <= self.motor.max_position: return True else: return False - - diff --git a/aamp_app/devices/linear_stage_150.py b/aamp_app/devices/linear_stage_150.py index 0aebb8a..5c4913b 100644 --- a/aamp_app/devices/linear_stage_150.py +++ b/aamp_app/devices/linear_stage_150.py @@ -49,7 +49,9 @@ def initialize(self) -> Tuple[bool, str]: # lts150: initialize, home it (if needed) # Home Stage; MGMSG_MOT_MOVE_HOME - self.ser.write(pack(" bool: def set_enabled_state(self, state: bool) -> Tuple[bool, str]: if state: - self.ser.write(pack(" Tuple[bool, str]: @check_serial def get_position(self) -> float: + if not self.ser.is_open: + self.start_serial() self._position = 0.0 Device_Unit_SF = 409600 # MGMSG_MOT_GET_POSCOUNTER - self.ser.write(pack(" Date: Thu, 17 Aug 2023 23:10:26 -0700 Subject: [PATCH 065/125] added auth rejection and optimized command --- aamp_app/app.py | 39 ++++++++++++++----- aamp_app/commands/command.py | 72 ++++++++++++++++++------------------ 2 files changed, 67 insertions(+), 44 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index 1c06522..4ae77fc 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -16,7 +16,6 @@ from console_interceptor import ConsoleInterceptor from gridfs import GridFS import base64 -import io try: import serial.tools.list_ports @@ -29,9 +28,31 @@ if os.path.isfile("pw.txt"): with open("pw.txt", "r") as f: mongo_username, mongo_password = f.read().split("\n") + mongo = MongoDBHelper( + "mongodb+srv://" + + mongo_username + + ":" + + mongo_password + + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "diaogroup", + ) else: mongo_username = input("Enter MongoDB username: ") mongo_password = input("Enter MongoDB password: ") + + mongo = MongoDBHelper( + "mongodb+srv://" + + mongo_username + + ":" + + mongo_password + + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", + "diaogroup", + ) + try: + db_list = mongo.client.list_database_names() + except Exception: + print("Connection Failed. Try Again.") + os.kill(os.getpid(), signal.SIGINT) with open("pw.txt", "w") as f: f.write(mongo_username + "\n" + mongo_password) @@ -51,14 +72,14 @@ ) -mongo = MongoDBHelper( - "mongodb+srv://" - + mongo_username - + ":" - + mongo_password - + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", - "diaogroup", -) +# mongo = MongoDBHelper( +# "mongodb+srv://" +# + mongo_username +# + ":" +# + mongo_password +# + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", +# "diaogroup", +# ) mongo_gridfs = GridFS(mongo.db, collection="recipes") app = dash.Dash( diff --git a/aamp_app/commands/command.py b/aamp_app/commands/command.py index c83f2b2..3e67e38 100644 --- a/aamp_app/commands/command.py +++ b/aamp_app/commands/command.py @@ -5,28 +5,28 @@ from devices.device import Device - # TODO # logging from within the composite command execution? # The adding of args into params dict can be done with locals() + class Command(ABC): """The Command abstract base class which acts as an interface for other objects that use commands.""" - receiver_cls = None # or Device? + receiver_cls = None # or Device? @abstractmethod def __init__(self, receiver: Device, delay: float = 0.0): # Child classes will still have 'receiver' in their signature (separate from **kwargs) because I want the IDE to hint at the specific receiver class - # delay will be passed up through **kwargs as it is an secondary parameter for all commands + # delay will be passed up through **kwargs as it is an secondary parameter for all commands self._receiver = receiver self._params = {} - self._params['receiver_name'] = receiver.name - self._params['delay'] = delay + self._params["receiver_name"] = receiver.name + self._params["delay"] = delay # self._was_successful = None - # self._result_message = None + # self._result_message = None - # this could also just be None, instead of a CommandResult with None attributes + # this could also just be None, instead of a CommandResult with None attributes # is there ever a time where the result would be accessed before the command is executed? # it might be better to just have it as a CommandResult with None's since whatever accessing it # is expecting _result to be a CommandResult object anyways @@ -51,14 +51,14 @@ def name(self) -> str: """ self._name = type(self).__name__ for key, value in self._params.items(): - if not key == 'delay': + if not key == "delay": self._name += " " + key + "=" + str(value) # I want delay to always be last when displayed and only if its not zero - if isinstance(self._params['delay'], str): - self._name += " " + 'delay=' + str(self._params['delay']) + if isinstance(self._params["delay"], str): + self._name += " " + "delay=" + str(self._params["delay"]) else: - if self._params['delay'] > 0.0: - self._name += " " + 'delay=' + str(self._params['delay']) + if self._params["delay"] > 0.0: + self._name += " " + "delay=" + str(self._params["delay"]) return self._name @property @@ -111,22 +111,25 @@ def get_init_args(self) -> dict: """ return self._params -# store info like name and description in here too? or leave separate? Leave separate, semantically i think it makes sense + +# store info like name and description in here too? or leave separate? Leave separate, semantically i think it makes sense # to retrive non-result info from the command instead of the commandresult object, potentially avoid conflicting info too # Mainly adding to future proof in case more info needs to be retrieved after execution -class CommandResult(): +class CommandResult: """Object which stores whether a command's execution was successful and other relevant information""" - def __init__(self, was_successful: Optional[bool] = None, message: Optional[str] = None): + def __init__( + self, was_successful: Optional[bool] = None, message: Optional[str] = None + ): # I considered whether to have a reset/update function instead and have init call it, this function could be used outside to reset/update the result attributes # The alternative is to just use the contructor to create a new reset object. I decided just using the constructor is better. - # Although it creates a new object in memory, it ensures that whenever a command executes if it uses the constructor then the results - # will be fully reset and not hold any old info if it is executed multiples times. In other cases, it may hold old info like for + # Although it creates a new object in memory, it ensures that whenever a command executes if it uses the constructor then the results + # will be fully reset and not hold any old info if it is executed multiples times. In other cases, it may hold old info like for # 1) explicitly tuple unpacking in the result attributes (old version) or 2) using an update function (which may not update all attrs) - + self._was_successful = was_successful self._message = message - + @property def was_successful(self) -> Optional[bool]: """Whether the command's execution was successful or not. @@ -147,20 +150,20 @@ def message(self) -> Optional[str]: Optional[str] Returns a string describing the result of the command's execution with details if it failed. """ - return self._message - + return self._message + # A composite command contains a command list but it behaves like a regular command to any other object using it. # This means a composite command can contain a composite command and it can have many levels arbitrarily deep # This also means it can potentially be recursive causing an infinite loop # This also means that the contents of a composite command (and all potential levels of composite commands it has) should ideally be checked # To make sure it does not cause problems for the overall recipe -# For now, CompositeCommands do not implement checks against adding other composite commands to itself as it can be useful to have at least 1-2 levels +# For now, CompositeCommands do not implement checks against adding other composite commands to itself as it can be useful to have at least 1-2 levels class CompositeCommand(Command): """A composite command which contains multiple commands but can act like a single command that executes all contained commands sequentially.""" # receiver_cls = None - + def __init__(self, delay: float = 0.0): # super().__init__(**kwargs) # self._name = "CompositeCommand" @@ -169,9 +172,9 @@ def __init__(self, delay: float = 0.0): # self._receiver = None self._params = {} # self._params['receiver_name'] = 'None' - self._params['delay'] = delay + self._params["delay"] = delay # self._was_successful = None - # self._result_message = None + # self._result_message = None self._result = CommandResult() @property @@ -211,7 +214,7 @@ def add_command(self, command: Command, index: Optional[int] = None): """ if index is None: self._command_list.append(command) - else: + else: self._command_list.insert(index, command) def remove_command(self, index: Optional[int] = None): @@ -228,10 +231,9 @@ def remove_command(self, index: Optional[int] = None): def execute(self) -> None: """Executes each command in the command list sequentially and returns early if a command's execution was not successful.""" - # LOGGING? + # LOGGING? for command in self._command_list: - - delay = command._params['delay'] + delay = command._params["delay"] if isinstance(delay, float) or isinstance(delay, int): if delay > 0.0: time.sleep(delay) @@ -243,19 +245,19 @@ def execute(self) -> None: # self._result._message = command.result_message self._result = command.result if not command.was_successful: - return + return - # store the success of the last command + # store the success of the last command # or write a new message specific to the composite command # self._result_message = "Successfully executed composite command " + type(self).__name__ -# for the receiver and concretecommand classes, I figured there were two ways of writing the code. Keep the receiver methods low level and employ logic -# in the commands, or put all low level methods, logic, and subsequent high level methods in the receiver and have the command simply call the +# for the receiver and concretecommand classes, I figured there were two ways of writing the code. Keep the receiver methods low level and employ logic +# in the commands, or put all low level methods, logic, and subsequent high level methods in the receiver and have the command simply call the # high level methods with minimal logic if at all. I decided the latter mainly because some devices/receivers will have many different commands # and I want to keep all the command classes streamlined and minimal. I also felt that having the logic and higher level methods separate from the lower level methods # would make it more difficult to write and maintain due to switching back and forth between the modules. -# If there is a need to separate low level and high level methods, then in my opinion it should take place between the receiver class and another class it extends/inherits +# If there is a need to separate low level and high level methods, then in my opinion it should take place between the receiver class and another class it extends/inherits # from or the actual device firmware. # nearly all attributes in Command and its child classes are protected because the parameters are not meant to be changed after construction. @@ -264,4 +266,4 @@ def execute(self) -> None: # This can be fixed by implementing the arg dictionary mentioned below and using the name and description getters to update the value by iterating the dict # This parallels the way CompositeCommands have names that update to reflect its command list when the name getter is used -# consider a command rank system to be able to check that some commands must come before/after others, e.g. initialize? \ No newline at end of file +# consider a command rank system to be able to check that some commands must come before/after others, e.g. initialize? From da46caa11d99ab536bdfd216e2c7bbfd3bb64b5a Mon Sep 17 00:00:00 2001 From: Piyush Date: Thu, 17 Aug 2023 23:18:42 -0700 Subject: [PATCH 066/125] updated recipe_tool --- recipe_tool.py | 190 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 141 insertions(+), 49 deletions(-) diff --git a/recipe_tool.py b/recipe_tool.py index 0df1d3d..fc1dd2a 100644 --- a/recipe_tool.py +++ b/recipe_tool.py @@ -17,20 +17,20 @@ init() -from command_sequence import CommandSequence -from command_invoker import CommandInvoker -from commands.command import Command -from commands.utility_commands import LoopStartCommand, LoopEndCommand -from devices.heating_stage import HeatingStage -from devices.multi_stepper import MultiStepper -from devices.newport_esp301 import NewportESP301 +from aamp_app.command_sequence import CommandSequence +from aamp_app.command_invoker import CommandInvoker +from aamp_app.commands.command import Command +from aamp_app.commands.utility_commands import LoopStartCommand, LoopEndCommand +from aamp_app.devices.heating_stage import HeatingStage +from aamp_app.devices.multi_stepper import MultiStepper +from aamp_app.devices.newport_esp301 import NewportESP301 # from devices.stellarnet_spectrometer import StellarNetSpectrometer # from devices.ximea_camera import XimeaCamera -from devices.dummy_heater import DummyHeater -from devices.dummy_motor import DummyMotor +from aamp_app.devices.dummy_heater import DummyHeater +from aamp_app.devices.dummy_motor import DummyMotor -import util +import aamp_app.util as util # TODO @@ -184,7 +184,9 @@ def display_menu(): "Back to Main Menu": main_menu, } prompt = questionary.select( - display_menu_prompt, choices=list(display_menu_options.keys()), style=custom_style + display_menu_prompt, + choices=list(display_menu_options.keys()), + style=custom_style, ) response = prompt.ask() response_function = display_menu_options[response] @@ -206,12 +208,19 @@ def display_device_menu(): def print_devices(): - device_names_classes = seq.get_device_names_classes() # [name, class name] of every device + device_names_classes = ( + seq.get_device_names_classes() + ) # [name, class name] of every device print("") print(Fore.WHITE + "List of Devices:") print(Fore.GREEN + " {:20.20s}".format("Name") + Fore.YELLOW + "Class") for name_class in device_names_classes: - print(Fore.GREEN + " {:20.20s}".format(name_class[0]) + Fore.YELLOW + name_class[1]) + print( + Fore.GREEN + + " {:20.20s}".format(name_class[0]) + + Fore.YELLOW + + name_class[1] + ) print("") @@ -265,7 +274,9 @@ def display_commands(): for param in name_parts: param_name = param.split("=")[0] param_value = param.split("=")[1] - print(Fore.WHITE + param_name + "=" + Fore.YELLOW + param_value, end=" ") + print( + Fore.WHITE + param_name + "=" + Fore.YELLOW + param_value, end=" " + ) print("") print("") @@ -300,7 +311,13 @@ def display_commands_hide_iterations(): print(Fore.RED + "{:7s}".format(str(index)), end="") index += 1 if "*" in name_parts[0]: - print(Fore.WHITE + "*" + Fore.CYAN + "{:39.39s}".format(name_parts.pop(0)[1:]), end="") + print( + Fore.WHITE + + "*" + + Fore.CYAN + + "{:39.39s}".format(name_parts.pop(0)[1:]), + end="", + ) else: print(Fore.CYAN + "{:40.40s}".format(name_parts.pop(0)), end="") if len(name_parts) == 0: @@ -309,7 +326,9 @@ def display_commands_hide_iterations(): for param in name_parts: param_name = param.split("=")[0] param_value = param.split("=")[1] - print(Fore.WHITE + param_name + "=" + Fore.YELLOW + param_value, end=" ") + print( + Fore.WHITE + param_name + "=" + Fore.YELLOW + param_value, end=" " + ) print("") print("") @@ -321,7 +340,10 @@ def display_commands_unlooped(): command_list = seq.get_unlooped_command_list() if len(command_list) == 0: - print(Fore.RED + "The command sequence and/or num_iterations is not valid, cannot unloop.") + print( + Fore.RED + + "The command sequence and/or num_iterations is not valid, cannot unloop." + ) return command_names = [] @@ -351,7 +373,9 @@ def display_commands_unlooped(): for param in name_parts: param_name = param.split("=")[0] param_value = param.split("=")[1] - print(Fore.WHITE + param_name + "=" + Fore.YELLOW + param_value, end=" ") + print( + Fore.WHITE + param_name + "=" + Fore.YELLOW + param_value, end=" " + ) print("") print("") @@ -391,7 +415,9 @@ def display_command_iterations(): for param in name_parts: param_name = param.split("=")[0] param_value = param.split("=")[1] - print(Fore.WHITE + param_name + "=" + Fore.YELLOW + param_value, end=" ") + print( + Fore.WHITE + param_name + "=" + Fore.YELLOW + param_value, end=" " + ) print("") print("") @@ -410,7 +436,9 @@ def device_menu(): "Back to Main Menu": main_menu, } prompt = questionary.select( - device_menu_prompt, choices=list(device_menu_options.keys()), style=custom_style + device_menu_prompt, + choices=list(device_menu_options.keys()), + style=custom_style, ) response = prompt.ask() response_function = device_menu_options[response] @@ -421,13 +449,18 @@ def add_device(): approved_devices = list(named_devices.keys()) approved_devices.append("Go back") response = questionary.select( - "Which approved device would you like to add?", choices=approved_devices, style=custom_style + "Which approved device would you like to add?", + choices=approved_devices, + style=custom_style, ).ask() if response == "Go back": return if response in seq.device_by_name: - print(Fore.RED + "Device is already added. To edit it, you must remove it and re-add it.") + print( + Fore.RED + + "Device is already added. To edit it, you must remove it and re-add it." + ) return device_cls = named_devices[response] arg_dict = prompt_signature_args(device_cls.__init__, ignored_args=["name"]) @@ -487,7 +520,9 @@ def command_menu(): "Back to Main Menu": main_menu, } prompt = questionary.select( - command_menu_prompt, choices=list(command_menu_options.keys()), style=custom_style + command_menu_prompt, + choices=list(command_menu_options.keys()), + style=custom_style, ) response = prompt.ask() response_function = command_menu_options[response] @@ -496,7 +531,10 @@ def command_menu(): def add_command(): if len(seq.device_list) == 0: - print(Fore.RED + "There are currently no devices. Add a device to create commands for it.") + print( + Fore.RED + + "There are currently no devices. Add a device to create commands for it." + ) return device_index = select_device("Choose a device to create a command for:") @@ -504,14 +542,18 @@ def add_command(): return device = seq.device_list[device_index] - valid_command_dict = get_all_commands_classes_for_receiver(command_directory, device.__class__) + valid_command_dict = get_all_commands_classes_for_receiver( + command_directory, device.__class__ + ) valid_command_names_desc = [] for name, cls in valid_command_dict.items(): valid_command_names_desc.append("{:30.30s}".format(name) + "- " + cls.__doc__) valid_command_names_desc.append("Go back") response = questionary.select( - "Choose the command to add:", choices=valid_command_names_desc, style=custom_style + "Choose the command to add:", + choices=valid_command_names_desc, + style=custom_style, ).ask() if response == "Go back": return @@ -541,7 +583,9 @@ def remove_commands(): print(Fore.RED + "There are currently no commands.") return - del_indices = select_multiple_commands("Select the command(s) you would like to remove:") + del_indices = select_multiple_commands( + "Select the command(s) you would like to remove:" + ) if len(del_indices) == 0: print(Fore.RED + "No commands were deleted (Select commands with Space Bar)") return @@ -579,14 +623,17 @@ def move_command(): def add_loop(): if seq.count_loop_commands() > 0: - print(Fore.RED + "Recipe already has loop commands. Remove them and re-add them.") + print( + Fore.RED + "Recipe already has loop commands. Remove them and re-add them." + ) return if len(seq.command_list) == 0: print(Fore.RED + "There are currently no commands.") return loop_indices = select_multiple_commands( - "Select the FIRST and LAST commands of the loop section:", validator_func=valid_loop_count + "Select the FIRST and LAST commands of the loop section:", + validator_func=valid_loop_count, ) if len(loop_indices) == 0: @@ -629,7 +676,11 @@ def get_all_command_classes(command_dir: str): # check if each file is a "regular file" with extension ".py" and neglecting the "__init__.py" file # then add to module_names str list for file in listdir(command_dir): - if isfile(join(command_dir, file)) and file != "__init__.py" and file.split(".")[1] == "py": + if ( + isfile(join(command_dir, file)) + and file != "__init__.py" + and file.split(".")[1] == "py" + ): # get rid of the file extension and replace / with . module_name = join(command_dir, file).split(".")[0].replace("/", ".") module_names.append(module_name) @@ -674,11 +725,15 @@ def iteration_menu(): "Add Command Iteration": add_command_iteration, "Remove Command Iteration(s)": remove_command_iterations, "Move Command Iteration": move_command_iteration, - "Set Number of Loop Iterations (currently=" + num_iter_str + ")": set_num_iterations, + "Set Number of Loop Iterations (currently=" + + num_iter_str + + ")": set_num_iterations, "Back to Main Menu": main_menu, } prompt = questionary.select( - command_menu_prompt, choices=list(command_menu_options.keys()), style=custom_style + command_menu_prompt, + choices=list(command_menu_options.keys()), + style=custom_style, ) response = prompt.ask() response_function = command_menu_options[response] @@ -695,7 +750,9 @@ def add_command_iteration(): return for iteration in seq.command_list[command_index]: - if isinstance(iteration, LoopStartCommand) or isinstance(iteration, LoopEndCommand): + if isinstance(iteration, LoopStartCommand) or isinstance( + iteration, LoopEndCommand + ): print(Fore.RED + "Cannot add iteration to a Loop Start/End Command") return @@ -845,7 +902,10 @@ def execute_recipe(): "No logging", ] response = questionary.select( - "Select logging option:", choices=log_options, default=log_options[0], style=custom_style + "Select logging option:", + choices=log_options, + default=log_options[0], + style=custom_style, ).ask() if response == log_options[0]: @@ -875,7 +935,10 @@ def execute_recipe(): invocation_successful = invoker.invoke_commands() if invocation_successful: print( - Fore.CYAN + "Recipe execution complete." + Fore.GREEN + " Execution was successful." + Fore.CYAN + + "Recipe execution complete." + + Fore.GREEN + + " Execution was successful." ) else: print( @@ -893,7 +956,10 @@ def execute_recipe(): def execute_manual(): global seq if len(seq.device_list) == 0: - print(Fore.RED + "There are currently no devices. Add a device to execute commands for it.") + print( + Fore.RED + + "There are currently no devices. Add a device to execute commands for it." + ) return # Warning! Manual command execution can alter the state of your devices which may or may not be desired! @@ -952,13 +1018,17 @@ def execute_manual(): ) valid_command_names_desc = [] for name, cls in valid_command_dict.items(): - valid_command_names_desc.append("{:30.30s}".format(name) + "- " + cls.__doc__) + valid_command_names_desc.append( + "{:30.30s}".format(name) + "- " + cls.__doc__ + ) valid_command_names_desc.append("Go back") # Choose command loop while True: response = questionary.select( - "Choose the command to add:", choices=valid_command_names_desc, style=custom_style + "Choose the command to add:", + choices=valid_command_names_desc, + style=custom_style, ).ask() if response == "Go back": break @@ -1060,7 +1130,9 @@ def quit_program(): # Useful Functions ################################################## def select_device(prompt_message, allow_backout=True): - device_names_classes = seq.get_device_names_classes() # [name, class name] of every device + device_names_classes = ( + seq.get_device_names_classes() + ) # [name, class name] of every device display_device_menu_options = [] for name_class in device_names_classes: display_device_menu_options.append( @@ -1092,13 +1164,17 @@ def select_command(prompt_message, allow_backout=True): # Format the string with spaces for ndx, name in enumerate(command_names): name_parts = name.strip().split(" ") - command_names[ndx] = "{:4s}".format(str(ndx)) + "{:30s}".format(name_parts.pop(0)) + command_names[ndx] = "{:4s}".format(str(ndx)) + "{:30s}".format( + name_parts.pop(0) + ) if len(name_parts) > 0: for param in name_parts: command_names[ndx] += " " + param if allow_backout: command_names.append("Go back") - prompt = questionary.select(prompt_message, choices=command_names, style=custom_style) + prompt = questionary.select( + prompt_message, choices=command_names, style=custom_style + ) response = prompt.ask() if response == "Go back": response = None @@ -1120,12 +1196,16 @@ def select_multiple_commands(prompt_message, validator_func=None): # Format the string with spaces for ndx, name in enumerate(command_names): name_parts = name.strip().split(" ") - command_names[ndx] = "{:4s}".format(str(ndx)) + "{:30s}".format(name_parts.pop(0)) + command_names[ndx] = "{:4s}".format(str(ndx)) + "{:30s}".format( + name_parts.pop(0) + ) if len(name_parts) > 0: for param in name_parts: command_names[ndx] += " " + param if validator_func is None: - prompt = questionary.checkbox(prompt_message, choices=command_names, style=custom_style) + prompt = questionary.checkbox( + prompt_message, choices=command_names, style=custom_style + ) else: prompt = questionary.checkbox( prompt_message, @@ -1147,14 +1227,18 @@ def select_command_iteration(command_index, prompt_message, allow_backout=True): for ndx, name in enumerate(iteration_names): name_parts = name.strip().split(" ") - iteration_names[ndx] = "{:4s}".format(str(ndx)) + "{:30s}".format(name_parts.pop(0)) + iteration_names[ndx] = "{:4s}".format(str(ndx)) + "{:30s}".format( + name_parts.pop(0) + ) if len(name_parts) > 0: for param in name_parts: iteration_names[ndx] += " " + param if allow_backout: iteration_names.append("Go back") - prompt = questionary.select(prompt_message, choices=iteration_names, style=custom_style) + prompt = questionary.select( + prompt_message, choices=iteration_names, style=custom_style + ) response = prompt.ask() if response == "Go back": response = None @@ -1170,12 +1254,16 @@ def select_multiple_command_iterations(command_index, prompt_message): for ndx, name in enumerate(iteration_names): name_parts = name.strip().split(" ") - iteration_names[ndx] = "{:4s}".format(str(ndx)) + "{:30s}".format(name_parts.pop(0)) + iteration_names[ndx] = "{:4s}".format(str(ndx)) + "{:30s}".format( + name_parts.pop(0) + ) if len(name_parts) > 0: for param in name_parts: iteration_names[ndx] += " " + param - prompt = questionary.checkbox(prompt_message, choices=iteration_names, style=custom_style) + prompt = questionary.checkbox( + prompt_message, choices=iteration_names, style=custom_style + ) response_list = prompt.ask() index_list = [] for response in response_list: @@ -1215,7 +1303,9 @@ def prompt_signature_args(func, ignored_args): if default == "N/A": response = questionary.text("Enter value for the parameter").ask() else: - response = questionary.text("Enter value for the parameter", default=default).ask() + response = questionary.text( + "Enter value for the parameter", default=default + ).ask() arg_dict[param.name] = eval(response) return arg_dict @@ -1309,7 +1399,9 @@ def rainbow_bg(): def valid_yml(file): if file.lower() == "quit": return True - if not isfile(file) or (file.split(".")[-1] != "yml" and file.split(".")[-1] != "yaml"): + if not isfile(file) or ( + file.split(".")[-1] != "yml" and file.split(".")[-1] != "yaml" + ): return "Enter a valid yml/yaml file" else: return True From d16951d4a2584fcc7b68f76438ca794291faf164 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Tue, 19 Nov 2024 23:33:24 -0800 Subject: [PATCH 067/125] Changed Mongo credentials to mine to access my cluster for testing --- aamp_app/app.py | 57 +++++++++++++++++--------------------- db/gridfs_local.py | 16 +++++++++-- db/validation/devices.py | 18 ++++++++++-- db/validation/films.py | 17 ++++++++++-- db/validation/solutions.py | 18 ++++++++++-- 5 files changed, 87 insertions(+), 39 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index 4ae77fc..79d5c3b 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -25,36 +25,31 @@ _has_serial = True import typing -if os.path.isfile("pw.txt"): - with open("pw.txt", "r") as f: - mongo_username, mongo_password = f.read().split("\n") - mongo = MongoDBHelper( - "mongodb+srv://" - + mongo_username - + ":" - + mongo_password - + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", - "diaogroup", - ) -else: - mongo_username = input("Enter MongoDB username: ") - mongo_password = input("Enter MongoDB password: ") - - mongo = MongoDBHelper( - "mongodb+srv://" - + mongo_username - + ":" - + mongo_password - + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", - "diaogroup", - ) - try: - db_list = mongo.client.list_database_names() - except Exception: - print("Connection Failed. Try Again.") - os.kill(os.getpid(), signal.SIGINT) - with open("pw.txt", "w") as f: - f.write(mongo_username + "\n" + mongo_password) +def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + +load_env() + +mongo_uri = os.environ.get("MONGO_URI") +mongo_db_name = os.environ.get("MONGO_DB_NAME") + +mongo = MongoDBHelper( + mongo_uri, + mongo_db_name, +) + +try: + db_list = mongo.client.list_database_names() +except Exception: + print("Connection Failed. Try Again.") + os.kill(os.getpid(), signal.SIGINT) +# with open("pw.txt", "w") as f: +# f.write(mongo_username + "\n" + mongo_password) print("\nreset complete") com = CommandSequence() @@ -77,7 +72,7 @@ # + mongo_username # + ":" # + mongo_password -# + "@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", +# + "@mp3-cluster.rf3cato.mongodb.net/?retryWrites=true&w=majority", # "diaogroup", # ) mongo_gridfs = GridFS(mongo.db, collection="recipes") diff --git a/db/gridfs_local.py b/db/gridfs_local.py index 44d3216..cdaed04 100644 --- a/db/gridfs_local.py +++ b/db/gridfs_local.py @@ -1,12 +1,24 @@ from mongodb_helper import MongoDBHelper from gridfs import GridFS from bson import ObjectId -from pw import mongo_password, mongo_username, mongo_uri +import os +def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + +load_env() + +mongo_uri = os.environ.get("MONGO_URI") +mongo_db_name = os.environ.get("MONGO_DB_NAME") mongo = MongoDBHelper( mongo_uri, - "diaogroup", + mongo_db_name, ) db = mongo.db diff --git a/db/validation/devices.py b/db/validation/devices.py index 1775a1a..ed441ae 100644 --- a/db/validation/devices.py +++ b/db/validation/devices.py @@ -1,8 +1,22 @@ from mongodb_helper import MongoDBHelper +import os + +def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + +load_env() + +mongo_uri = os.environ.get("MONGO_URI") +mongo_db_name = os.environ.get("MONGO_DB_NAME") mongo = MongoDBHelper( - "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", - "diaogroup", + mongo_uri, + mongo_db_name, ) db = mongo.db diff --git a/db/validation/films.py b/db/validation/films.py index b956e1a..1781938 100644 --- a/db/validation/films.py +++ b/db/validation/films.py @@ -1,9 +1,22 @@ from mongodb_helper import MongoDBHelper +import os +def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + +load_env() + +mongo_uri = os.environ.get("MONGO_URI") +mongo_db_name = os.environ.get("MONGO_DB_NAME") mongo = MongoDBHelper( - "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", - "diaogroup", + mongo_uri, + mongo_db_name, ) db = mongo.db diff --git a/db/validation/solutions.py b/db/validation/solutions.py index 9d36f23..f04da0a 100644 --- a/db/validation/solutions.py +++ b/db/validation/solutions.py @@ -1,8 +1,22 @@ from mongodb_helper import MongoDBHelper +import os + +def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + +load_env() + +mongo_uri = os.environ.get("MONGO_URI") +mongo_db_name = os.environ.get("MONGO_DB_NAME") mongo = MongoDBHelper( - "mongodb+srv://ppahuja2:s5eMFr1js8iEcMt8@diaogroup.nrcgqsq.mongodb.net/?retryWrites=true&w=majority", - "diaogroup", + mongo_uri, + mongo_db_name, ) db = mongo.db From 0b880d6f6f8a325cecdb06c4287e99350d19dede Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Tue, 14 Jan 2025 10:58:49 -0800 Subject: [PATCH 068/125] added the validation schema creation scripts, and made it add an empty document if one doesn't exist already, databse propogates almost completely correctly throughout the app now --- aamp_app/app.py | 42 +- aamp_app/command_sequence.py | 11 +- aamp_app/db/gridfs_local.py | 41 ++ aamp_app/db/validation/devices.py | 402 ++++++++++++++++++ aamp_app/db/validation/films.py | 94 ++++ aamp_app/db/validation/solutions.py | 85 ++++ db/validation/devices.py | 636 ++++++++++++++-------------- db/validation/films.py | 146 +++---- db/validation/solutions.py | 127 +++--- 9 files changed, 1120 insertions(+), 464 deletions(-) create mode 100644 aamp_app/db/gridfs_local.py create mode 100644 aamp_app/db/validation/devices.py create mode 100644 aamp_app/db/validation/films.py create mode 100644 aamp_app/db/validation/solutions.py diff --git a/aamp_app/app.py b/aamp_app/app.py index 79d5c3b..56cee8b 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -17,6 +17,7 @@ from gridfs import GridFS import base64 +from db.validation import solutions, films, devices try: import serial.tools.list_ports except ImportError: @@ -77,6 +78,10 @@ def load_env(file_path=".env"): # ) mongo_gridfs = GridFS(mongo.db, collection="recipes") +solutions.init_collection() +devices.init_collection() +films.init_collection() + app = dash.Dash( __name__, external_stylesheets=[dbc.themes.BOOTSTRAP], @@ -140,22 +145,29 @@ def print_pagename(url): # all pages def update_upstream_recipe_dict(): - print("update_upstream_recipe_dict") - if "document" in list(com.__dict__.keys()): - recipe_dict = com.get_recipe() - com.document["recipe_dict"] = { - "devices": recipe_dict[0], - "commands": recipe_dict[1], - "execution_options": recipe_dict[2], + if not hasattr(com, 'document'): + # create_new_recipe_doc(n, "/load-recipe", "testfile") + com.document = { + '_id': ObjectId(), + 'recipe_dict': { + 'devices': [], + 'commands': [], + 'execution_options': [] + } } - mongo.db["recipes"].update_one( - {"_id": com.document["_id"]}, {"$set": com.document} - ) - print("successfully updated recipe_dict upstream") - return True - else: - print("com.document not found") - return False + recipe_dict = com.get_recipe() + com.document['recipe_dict'] = { + 'devices': recipe_dict[0], + 'commands': recipe_dict[1], + 'execution_options': recipe_dict[2] + } + mongo.db['recipes'].update_one( + {'_id': com.document['_id']}, + {'$set': com.document}, + upsert=True + ) + return True + def update_execution_upstream(execution): diff --git a/aamp_app/command_sequence.py b/aamp_app/command_sequence.py index e8ff61f..c223d38 100644 --- a/aamp_app/command_sequence.py +++ b/aamp_app/command_sequence.py @@ -11,7 +11,7 @@ import inspect import util - +from bson.objectid import ObjectId # Representer.add_representer(ABCMeta, Representer.represent_name) # Should move loop interpretation to invoker? @@ -642,6 +642,15 @@ def load_from_dict(self, recipe_dict): self.execution_options = recipe_dict["execution_options"] def add_device_from_dict(self, device_type, device_dict): + if not hasattr(self, 'document'): + self.document = { + '_id': ObjectId(), + 'recipe_dict': { + 'devices': [], + 'commands': [], + 'execution_options': [] + } + } self.add_device(util.devices_ref_redundancy[device_type]["obj"](**device_dict)) self.update_device_by_name() diff --git a/aamp_app/db/gridfs_local.py b/aamp_app/db/gridfs_local.py new file mode 100644 index 0000000..cdaed04 --- /dev/null +++ b/aamp_app/db/gridfs_local.py @@ -0,0 +1,41 @@ +from mongodb_helper import MongoDBHelper +from gridfs import GridFS +from bson import ObjectId +import os + +def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + +load_env() + +mongo_uri = os.environ.get("MONGO_URI") +mongo_db_name = os.environ.get("MONGO_DB_NAME") + +mongo = MongoDBHelper( + mongo_uri, + mongo_db_name, +) + +db = mongo.db + +fs = GridFS(db, collection="recipes") + +# Open and store the CSV file using GridFS +with open("data.csv", "rb") as file: + file_id = fs.put(file, filename="data.csv") + # create ObjectId(file_id) in document to point to csv + +# Retrieve the CSV file from GridFS +gridfs_file = fs.find_one({"_id": ObjectId("64c2d215255f257ee66d30e9")}) + +# Read the CSV data from the file +csv_data = gridfs_file.read() + +# Print the CSV data +print(csv_data) +# print(csv_data.decode()) diff --git a/aamp_app/db/validation/devices.py b/aamp_app/db/validation/devices.py new file mode 100644 index 0000000..7ada7de --- /dev/null +++ b/aamp_app/db/validation/devices.py @@ -0,0 +1,402 @@ +from mongodb_helper import MongoDBHelper +from pymongo.errors import CollectionInvalid +import os + +def init_collection(): + def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + + load_env() + + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") + + mongo = MongoDBHelper( + mongo_uri, + mongo_db_name, + ) + + db = mongo.db + + device_dict = { + "device": { + "electrode_etl": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + "etl": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + "active_layer": { + "donor": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + "acceptor": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + }, + "htl": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + "electrode_htl": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + }, + "result": { + "jv_curve": ["object_id", "null"], + "t80": ["float", "null"], + }, + } + + device_dict_schema = { + "bsonType": "object", + "title": "Device Object Validation", + "required": ["device", "result"], + "properties": { + "device": { + "bsonType": "object", + "title": "Device Validation", + "required": ["electrode_etl", "active_layer", "electrode_htl"], + "properties": { + "electrode_etl": { + "bsonType": "object", + "title": "Electrode ETL Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": ["molecule", "concentration"], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "active_layer": { + "bsonType": "object", + "title": "Active Layer Validation", + "required": ["donor", "acceptor"], + "properties": { + "donor": { + "bsonType": "object", + "title": "Donor Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": [ + "molecule", + "concentration", + ], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "acceptor": { + "bsonType": "object", + "title": "Acceptor Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": [ + "molecule", + "concentration", + ], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "electrode_htl": { + "bsonType": "object", + "title": "Electrode HTL Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": ["molecule", "concentration"], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "result": { + "bsonType": "object", + "title": "Result Object Validation", + "required": ["jv_curve", "t80"], + "properties": { + "jv_curve": { + "bsonType": ["objectId", "null"], + "description": "'jv_curve' must be an objectId and is required", + }, + "t80": { + "bsonType": ["double", "null"], + "description": "'t80' must be a double and is required", + }, + }, + }, + }, + } + + + collection_name = "devices" + collection_options = {"validator": {"$jsonSchema": device_dict_schema}} + try: + db.create_collection(collection_name, **collection_options) + except CollectionInvalid: + print("Error with creating devices collection, it probably already exists") diff --git a/aamp_app/db/validation/films.py b/aamp_app/db/validation/films.py new file mode 100644 index 0000000..0eddb36 --- /dev/null +++ b/aamp_app/db/validation/films.py @@ -0,0 +1,94 @@ +from mongodb_helper import MongoDBHelper +from pymongo.errors import CollectionInvalid +import os + +def init_collection(): + def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + + load_env() + + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") + + mongo = MongoDBHelper( + mongo_uri, + mongo_db_name, + ) + + db = mongo.db + + + film_dict = { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + }, + "result": { + "uv_vis": ["object_id", "null"], + "t80": ["float", "null"], + }, + } + + + film_dict_schema = { + "bsonType": "object", + "title": "Film Object Validation", + "required": ["metadata", "result"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Object Validation", + "required": ["solvent", "concentration", "printing_speed", "printing_temperature"], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'printing_speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'printing_temperature' must be a double and is required", + }, + }, + }, + "result": { + "bsonType": "object", + "title": "Result Object Validation", + "required": ["uv_vis", "t80"], + "properties": { + "uv_vis": { + "bsonType": ["objectId", "null"], + "description": "'uv_vis' must be an objectId and is required", + }, + "t80": { + "bsonType": ["double", "null"], + "description": "'t80' must be a double and is required", + }, + }, + }, + }, + } + + + + collection_name = "film" + collection_options = {"validator": {"$jsonSchema": film_dict_schema}} + try: + db.create_collection(collection_name, **collection_options) + except CollectionInvalid: + print("Error with creating films collection, it probably already exists") \ No newline at end of file diff --git a/aamp_app/db/validation/solutions.py b/aamp_app/db/validation/solutions.py new file mode 100644 index 0000000..c35888c --- /dev/null +++ b/aamp_app/db/validation/solutions.py @@ -0,0 +1,85 @@ +from mongodb_helper import MongoDBHelper +from pymongo.errors import CollectionInvalid +import os + +def init_collection(): + def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + + load_env() + + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") + + mongo = MongoDBHelper( + mongo_uri, + mongo_db_name, + ) + + db = mongo.db + + + solution_dict = { + "metadata": { + "solvent": "string", + "concentration": 0.3, + }, + "result": { + "uv_vis": None, + "t80": 0.3, + }, + } + + + + json_schema = { + "bsonType": "object", + "title": "Solution Object Validation", + "required": ["metadata", "result"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Object Validation", + "required": ["solvent", "concentration"], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + }, + }, + "result": { + "bsonType": "object", + "title": "Result Object Validation", + "required": ["uv_vis", "t80"], + "properties": { + "uv_vis": { + "bsonType": ["objectId", "null"], + "description": "'uv_vis' must be an objectId and is required", + }, + "t80": { + "bsonType": ["double", "null"], + "description": "'t80' must be a double and is required", + }, + }, + }, + }, + } + + + + collection_name = "solutions" + collection_options = {"validator": {"$jsonSchema": json_schema}} + try: + db.create_collection(collection_name, **collection_options) + except CollectionInvalid: + print("Error with creating solutions collection, it probably already exists") \ No newline at end of file diff --git a/db/validation/devices.py b/db/validation/devices.py index ed441ae..b204603 100644 --- a/db/validation/devices.py +++ b/db/validation/devices.py @@ -1,58 +1,45 @@ from mongodb_helper import MongoDBHelper +from pymongo.errors import CollectionInvalid import os -def load_env(file_path=".env"): - if os.path.exists(file_path): - with open(file_path, "r") as f: - for line in f: - if "=" in line and not line.strip().startswith("#"): - key, value = line.strip().split("=", 1) - os.environ[key] = value +def init_collection(): + def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value -load_env() + load_env() -mongo_uri = os.environ.get("MONGO_URI") -mongo_db_name = os.environ.get("MONGO_DB_NAME") + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") -mongo = MongoDBHelper( - mongo_uri, - mongo_db_name, -) + mongo = MongoDBHelper( + mongo_uri, + mongo_db_name, + ) -db = mongo.db + db = mongo.db -device_dict = { - "device": { - "electrode_etl": { - "material": { - "metadata": { - "solvent": "string", - "concentration": "float", - "printing_speed": "float", - "printing_temperature": "float", - "additive": { - "molecule": "string", - "concentration": "float", - }, - } - } - }, - "etl": { - "material": { - "metadata": { - "solvent": "string", - "concentration": "float", - "printing_speed": "float", - "printing_temperature": "float", - "additive": { - "molecule": "string", + device_dict = { + "device": { + "electrode_etl": { + "material": { + "metadata": { + "solvent": "string", "concentration": "float", - }, + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } } - } - }, - "active_layer": { - "donor": { + }, + "etl": { "material": { "metadata": { "solvent": "string", @@ -66,7 +53,37 @@ def load_env(file_path=".env"): } } }, - "acceptor": { + "active_layer": { + "donor": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + "acceptor": { + "material": { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } + } + }, + }, + "htl": { "material": { "metadata": { "solvent": "string", @@ -80,101 +97,87 @@ def load_env(file_path=".env"): } } }, - }, - "htl": { - "material": { - "metadata": { - "solvent": "string", - "concentration": "float", - "printing_speed": "float", - "printing_temperature": "float", - "additive": { - "molecule": "string", + "electrode_htl": { + "material": { + "metadata": { + "solvent": "string", "concentration": "float", - }, + "printing_speed": "float", + "printing_temperature": "float", + "additive": { + "molecule": "string", + "concentration": "float", + }, + } } - } + }, }, - "electrode_htl": { - "material": { - "metadata": { - "solvent": "string", - "concentration": "float", - "printing_speed": "float", - "printing_temperature": "float", - "additive": { - "molecule": "string", - "concentration": "float", - }, - } - } + "result": { + "jv_curve": ["object_id", "null"], + "t80": ["float", "null"], }, - }, - "result": { - "jv_curve": ["object_id", "null"], - "t80": ["float", "null"], - }, -} + } -device_dict_schema = { - "bsonType": "object", - "title": "Device Object Validation", - "required": ["device", "result"], - "properties": { - "device": { - "bsonType": "object", - "title": "Device Validation", - "required": ["electrode_etl", "active_layer", "electrode_htl"], - "properties": { - "electrode_etl": { - "bsonType": "object", - "title": "Electrode ETL Validation", - "required": ["material"], - "properties": { - "material": { - "bsonType": "object", - "title": "Material Validation", - "required": ["metadata"], - "properties": { - "metadata": { - "bsonType": "object", - "title": "Metadata Validation", - "required": [ - "solvent", - "concentration", - "printing_speed", - "printing_temperature", - "additive", - ], - "properties": { - "solvent": { - "bsonType": "string", - "description": "'solvent' must be a string and is required", - }, - "concentration": { - "bsonType": "double", - "description": "'concentration' must be a double and is required", - }, - "printing_speed": { - "bsonType": "double", - "description": "'speed' must be a double and is required", - }, - "printing_temperature": { - "bsonType": "double", - "description": "'temperature' must be a double and is required", - }, - "additive": { - "bsonType": "object", - "title": "Additive Validation", - "required": ["molecule", "concentration"], - "properties": { - "molecule": { - "bsonType": "string", - "description": "'molecule' must be a string and is required", - }, - "concentration": { - "bsonType": "double", - "description": "'concentration' must be a double and is required", + device_dict_schema = { + "bsonType": "object", + "title": "Device Object Validation", + "required": ["device", "result"], + "properties": { + "device": { + "bsonType": "object", + "title": "Device Validation", + "required": ["electrode_etl", "active_layer", "electrode_htl"], + "properties": { + "electrode_etl": { + "bsonType": "object", + "title": "Electrode ETL Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": ["molecule", "concentration"], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, }, }, }, @@ -183,64 +186,64 @@ def load_env(file_path=".env"): }, }, }, - }, - "active_layer": { - "bsonType": "object", - "title": "Active Layer Validation", - "required": ["donor", "acceptor"], - "properties": { - "donor": { - "bsonType": "object", - "title": "Donor Validation", - "required": ["material"], - "properties": { - "material": { - "bsonType": "object", - "title": "Material Validation", - "required": ["metadata"], - "properties": { - "metadata": { - "bsonType": "object", - "title": "Metadata Validation", - "required": [ - "solvent", - "concentration", - "printing_speed", - "printing_temperature", - "additive", - ], - "properties": { - "solvent": { - "bsonType": "string", - "description": "'solvent' must be a string and is required", - }, - "concentration": { - "bsonType": "double", - "description": "'concentration' must be a double and is required", - }, - "printing_speed": { - "bsonType": "double", - "description": "'speed' must be a double and is required", - }, - "printing_temperature": { - "bsonType": "double", - "description": "'temperature' must be a double and is required", - }, - "additive": { - "bsonType": "object", - "title": "Additive Validation", - "required": [ - "molecule", - "concentration", - ], - "properties": { - "molecule": { - "bsonType": "string", - "description": "'molecule' must be a string and is required", - }, - "concentration": { - "bsonType": "double", - "description": "'concentration' must be a double and is required", + "active_layer": { + "bsonType": "object", + "title": "Active Layer Validation", + "required": ["donor", "acceptor"], + "properties": { + "donor": { + "bsonType": "object", + "title": "Donor Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": [ + "molecule", + "concentration", + ], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, }, }, }, @@ -249,59 +252,59 @@ def load_env(file_path=".env"): }, }, }, - }, - "acceptor": { - "bsonType": "object", - "title": "Acceptor Validation", - "required": ["material"], - "properties": { - "material": { - "bsonType": "object", - "title": "Material Validation", - "required": ["metadata"], - "properties": { - "metadata": { - "bsonType": "object", - "title": "Metadata Validation", - "required": [ - "solvent", - "concentration", - "printing_speed", - "printing_temperature", - "additive", - ], - "properties": { - "solvent": { - "bsonType": "string", - "description": "'solvent' must be a string and is required", - }, - "concentration": { - "bsonType": "double", - "description": "'concentration' must be a double and is required", - }, - "printing_speed": { - "bsonType": "double", - "description": "'speed' must be a double and is required", - }, - "printing_temperature": { - "bsonType": "double", - "description": "'temperature' must be a double and is required", - }, - "additive": { - "bsonType": "object", - "title": "Additive Validation", - "required": [ - "molecule", - "concentration", - ], - "properties": { - "molecule": { - "bsonType": "string", - "description": "'molecule' must be a string and is required", - }, - "concentration": { - "bsonType": "double", - "description": "'concentration' must be a double and is required", + "acceptor": { + "bsonType": "object", + "title": "Acceptor Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": [ + "molecule", + "concentration", + ], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, }, }, }, @@ -312,56 +315,56 @@ def load_env(file_path=".env"): }, }, }, - }, - "electrode_htl": { - "bsonType": "object", - "title": "Electrode HTL Validation", - "required": ["material"], - "properties": { - "material": { - "bsonType": "object", - "title": "Material Validation", - "required": ["metadata"], - "properties": { - "metadata": { - "bsonType": "object", - "title": "Metadata Validation", - "required": [ - "solvent", - "concentration", - "printing_speed", - "printing_temperature", - "additive", - ], - "properties": { - "solvent": { - "bsonType": "string", - "description": "'solvent' must be a string and is required", - }, - "concentration": { - "bsonType": "double", - "description": "'concentration' must be a double and is required", - }, - "printing_speed": { - "bsonType": "double", - "description": "'speed' must be a double and is required", - }, - "printing_temperature": { - "bsonType": "double", - "description": "'temperature' must be a double and is required", - }, - "additive": { - "bsonType": "object", - "title": "Additive Validation", - "required": ["molecule", "concentration"], - "properties": { - "molecule": { - "bsonType": "string", - "description": "'molecule' must be a string and is required", - }, - "concentration": { - "bsonType": "double", - "description": "'concentration' must be a double and is required", + "electrode_htl": { + "bsonType": "object", + "title": "Electrode HTL Validation", + "required": ["material"], + "properties": { + "material": { + "bsonType": "object", + "title": "Material Validation", + "required": ["metadata"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Validation", + "required": [ + "solvent", + "concentration", + "printing_speed", + "printing_temperature", + "additive", + ], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'temperature' must be a double and is required", + }, + "additive": { + "bsonType": "object", + "title": "Additive Validation", + "required": ["molecule", "concentration"], + "properties": { + "molecule": { + "bsonType": "string", + "description": "'molecule' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, }, }, }, @@ -372,27 +375,28 @@ def load_env(file_path=".env"): }, }, }, - }, - "result": { - "bsonType": "object", - "title": "Result Object Validation", - "required": ["jv_curve", "t80"], - "properties": { - "jv_curve": { - "bsonType": ["objectId", "null"], - "description": "'jv_curve' must be an objectId and is required", - }, - "t80": { - "bsonType": ["double", "null"], - "description": "'t80' must be a double and is required", + "result": { + "bsonType": "object", + "title": "Result Object Validation", + "required": ["jv_curve", "t80"], + "properties": { + "jv_curve": { + "bsonType": ["objectId", "null"], + "description": "'jv_curve' must be an objectId and is required", + }, + "t80": { + "bsonType": ["double", "null"], + "description": "'t80' must be a double and is required", + }, }, }, }, - }, -} + } -collection_name = "devices" -collection_options = {"validator": {"$jsonSchema": device_dict_schema}} -db.create_collection(collection_name, **collection_options) -quit() + collection_name = "devices" + collection_options = {"validator": {"$jsonSchema": device_dict_schema}} + try: + db.create_collection(collection_name, **collection_options) + except CollectionInvalid: + print("Error with creating solutions collection") diff --git a/db/validation/films.py b/db/validation/films.py index 1781938..6897c2b 100644 --- a/db/validation/films.py +++ b/db/validation/films.py @@ -1,90 +1,94 @@ from mongodb_helper import MongoDBHelper +from pymongo.errors import CollectionInvalid import os -def load_env(file_path=".env"): - if os.path.exists(file_path): - with open(file_path, "r") as f: - for line in f: - if "=" in line and not line.strip().startswith("#"): - key, value = line.strip().split("=", 1) - os.environ[key] = value +def init_collection(): + def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value -load_env() + load_env() -mongo_uri = os.environ.get("MONGO_URI") -mongo_db_name = os.environ.get("MONGO_DB_NAME") + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") -mongo = MongoDBHelper( - mongo_uri, - mongo_db_name, -) + mongo = MongoDBHelper( + mongo_uri, + mongo_db_name, + ) -db = mongo.db + db = mongo.db -film_dict = { - "metadata": { - "solvent": "string", - "concentration": "float", - "printing_speed": "float", - "printing_temperature": "float", - }, - "result": { - "uv_vis": ["object_id", "null"], - "t80": ["float", "null"], - }, -} - - -film_dict_schema = { - "bsonType": "object", - "title": "Film Object Validation", - "required": ["metadata", "result"], - "properties": { + film_dict = { "metadata": { - "bsonType": "object", - "title": "Metadata Object Validation", - "required": ["solvent", "concentration", "printing_speed", "printing_temperature"], - "properties": { - "solvent": { - "bsonType": "string", - "description": "'solvent' must be a string and is required", - }, - "concentration": { - "bsonType": "double", - "description": "'concentration' must be a double and is required", - }, - "printing_speed": { - "bsonType": "double", - "description": "'printing_speed' must be a double and is required", - }, - "printing_temperature": { - "bsonType": "double", - "description": "'printing_temperature' must be a double and is required", - }, - }, + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", }, "result": { - "bsonType": "object", - "title": "Result Object Validation", - "required": ["uv_vis", "t80"], - "properties": { - "uv_vis": { - "bsonType": ["objectId", "null"], - "description": "'uv_vis' must be an objectId and is required", + "uv_vis": ["object_id", "null"], + "t80": ["float", "null"], + }, + } + + + film_dict_schema = { + "bsonType": "object", + "title": "Film Object Validation", + "required": ["metadata", "result"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Object Validation", + "required": ["solvent", "concentration", "printing_speed", "printing_temperature"], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, + "printing_speed": { + "bsonType": "double", + "description": "'printing_speed' must be a double and is required", + }, + "printing_temperature": { + "bsonType": "double", + "description": "'printing_temperature' must be a double and is required", + }, }, - "t80": { - "bsonType": ["double", "null"], - "description": "'t80' must be a double and is required", + }, + "result": { + "bsonType": "object", + "title": "Result Object Validation", + "required": ["uv_vis", "t80"], + "properties": { + "uv_vis": { + "bsonType": ["objectId", "null"], + "description": "'uv_vis' must be an objectId and is required", + }, + "t80": { + "bsonType": ["double", "null"], + "description": "'t80' must be a double and is required", + }, }, }, }, - }, -} + } -collection_name = "film" -collection_options = {"validator": {"$jsonSchema": film_dict_schema}} -db.create_collection(collection_name, **collection_options) -quit() \ No newline at end of file + collection_name = "film" + collection_options = {"validator": {"$jsonSchema": film_dict_schema}} + try: + db.create_collection(collection_name, **collection_options) + except CollectionInvalid: + print("Error with creating solutions collection") \ No newline at end of file diff --git a/db/validation/solutions.py b/db/validation/solutions.py index f04da0a..c2944c4 100644 --- a/db/validation/solutions.py +++ b/db/validation/solutions.py @@ -1,81 +1,86 @@ from mongodb_helper import MongoDBHelper +from pymongo.errors import CollectionInvalid import os -def load_env(file_path=".env"): - if os.path.exists(file_path): - with open(file_path, "r") as f: - for line in f: - if "=" in line and not line.strip().startswith("#"): - key, value = line.strip().split("=", 1) - os.environ[key] = value +def init_collection(): + def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value -load_env() + load_env() -mongo_uri = os.environ.get("MONGO_URI") -mongo_db_name = os.environ.get("MONGO_DB_NAME") + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") -mongo = MongoDBHelper( - mongo_uri, - mongo_db_name, -) + mongo = MongoDBHelper( + mongo_uri, + mongo_db_name, + ) -db = mongo.db + db = mongo.db -solution_dict = { - "metadata": { - "solvent": "string", - "concentration": 0.3, - }, - "result": { - "uv_vis": None, - "t80": 0.3, - }, -} + solution_dict = { + "metadata": { + "solvent": "string", + "concentration": 0.3, + }, + "result": { + "uv_vis": None, + "t80": 0.3, + }, + } -json_schema = { - "bsonType": "object", - "title": "Solution Object Validation", - "required": ["metadata", "result"], - "properties": { - "metadata": { - "bsonType": "object", - "title": "Metadata Object Validation", - "required": ["solvent", "concentration"], - "properties": { - "solvent": { - "bsonType": "string", - "description": "'solvent' must be a string and is required", - }, - "concentration": { - "bsonType": "double", - "description": "'concentration' must be a double and is required", + json_schema = { + "bsonType": "object", + "title": "Solution Object Validation", + "required": ["metadata", "result"], + "properties": { + "metadata": { + "bsonType": "object", + "title": "Metadata Object Validation", + "required": ["solvent", "concentration"], + "properties": { + "solvent": { + "bsonType": "string", + "description": "'solvent' must be a string and is required", + }, + "concentration": { + "bsonType": "double", + "description": "'concentration' must be a double and is required", + }, }, }, - }, - "result": { - "bsonType": "object", - "title": "Result Object Validation", - "required": ["uv_vis", "t80"], - "properties": { - "uv_vis": { - "bsonType": ["objectId", "null"], - "description": "'uv_vis' must be an objectId and is required", - }, - "t80": { - "bsonType": ["double", "null"], - "description": "'t80' must be a double and is required", + "result": { + "bsonType": "object", + "title": "Result Object Validation", + "required": ["uv_vis", "t80"], + "properties": { + "uv_vis": { + "bsonType": ["objectId", "null"], + "description": "'uv_vis' must be an objectId and is required", + }, + "t80": { + "bsonType": ["double", "null"], + "description": "'t80' must be a double and is required", + }, }, }, }, - }, -} + } -collection_name = "solutions" -collection_options = {"validator": {"$jsonSchema": json_schema}} -db.create_collection(collection_name, **collection_options) -quit() + collection_name = "solutions" + collection_options = {"validator": {"$jsonSchema": json_schema}} + try: + db.create_collection(collection_name, **collection_options) + except CollectionInvalid: + print("Error with creating solutions collection") + quit() From 85cb96e6ab635388dd90e55729e4fbee30c539c8 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Mon, 27 Jan 2025 15:27:35 -0800 Subject: [PATCH 069/125] added random name generation with timestamp for recipes --- aamp_app/app.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index 56cee8b..0a950d4 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -1,3 +1,4 @@ +from datetime import datetime from command_sequence import CommandSequence from command_invoker import CommandInvoker import json @@ -149,11 +150,14 @@ def update_upstream_recipe_dict(): # create_new_recipe_doc(n, "/load-recipe", "testfile") com.document = { '_id': ObjectId(), + 'file_name': 'testfile', 'recipe_dict': { 'devices': [], 'commands': [], 'execution_options': [] - } + }, + 'dash_friendly': True, + 'executions': [] } recipe_dict = com.get_recipe() com.document['recipe_dict'] = { @@ -314,6 +318,7 @@ def get_document_from_db(n_clicks, filename): # homepage def create_new_recipe_doc(n, url, name): if str(url) == "/load-recipe": print("create_new_recipe_doc") + name = f"Recipe_{datetime.now().strftime('%Y%m%d_%H%M%S')}" mongo.db["recipes"].insert_one( { "file_name": name, @@ -1983,7 +1988,8 @@ def open_fill_manual_control_serial(n): def fill_database_db_dropdown(n, url): if str(url) == "/database": print("fill_database_db_dropdown") - return list(mongo.client.list_database_names()) + # temporary provision to not expose my other databases in demos + return [x for x in list(mongo.client.list_database_names()) if x == "aamp_test"] @app.callback( From d9566c67b0c6c492ee7ca3b1d921c6a0719b7c40 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Mon, 27 Jan 2025 16:16:19 -0800 Subject: [PATCH 070/125] fixed recipes schema validation, reordered recipe list --- aamp_app/app.py | 25 +++++--- aamp_app/db/validation/devices.py | 2 +- aamp_app/db/validation/films.py | 2 +- aamp_app/db/validation/recipes.py | 97 +++++++++++++++++++++++++++++ aamp_app/db/validation/solutions.py | 2 +- 5 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 aamp_app/db/validation/recipes.py diff --git a/aamp_app/app.py b/aamp_app/app.py index 0a950d4..283b2db 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -18,7 +18,7 @@ from gridfs import GridFS import base64 -from db.validation import solutions, films, devices +from db.validation import solutions, films, devices, recipes try: import serial.tools.list_ports except ImportError: @@ -82,6 +82,7 @@ def load_env(file_path=".env"): solutions.init_collection() devices.init_collection() films.init_collection() +recipes.init_collection() app = dash.Dash( __name__, @@ -211,7 +212,7 @@ def fetch_recipe_list(n_clicks): # homepage ) print("fetch_recipe_list") data = [] - for doc in docs: + for doc in reversed(list(docs)): file_name = doc.get("file_name", "") posix_friendly = doc.get("posix_friendly", True) dash_friendly = doc.get("dash_friendly", False) @@ -334,7 +335,7 @@ def create_new_recipe_doc(n, url, name): "executions": [], } ) - + fetch_recipe_list(1) return [True, "Recipe created", "success", 10000] @@ -1988,7 +1989,7 @@ def open_fill_manual_control_serial(n): def fill_database_db_dropdown(n, url): if str(url) == "/database": print("fill_database_db_dropdown") - # temporary provision to not expose my other databases in demos + # temporary provision to not expose other databases in demos return [x for x in list(mongo.client.list_database_names()) if x == "aamp_test"] @@ -2068,13 +2069,19 @@ def fill_database_collection_schema(collection, db, url): print("fill_database_collection_schema") if collection is not None and collection != "" and db is not None and db != "": try: - schema = ( - mongo.client[db].get_collection(collection).options()["validator"] - ) - schema = process_schema(schema) - return render_dict(schema) + print("Colln info:") + print(mongo.client[db].get_collection(collection)) + options = mongo.client[db].get_collection(collection).options() + if "validator" in options: + schema = options["validator"] + schema = process_schema(schema) + return render_dict(schema) + else: + return [""] except Exception as e: + print(f"Error retrieving schema: {e}") return ["Validation rules missing or something went wrong."] + return [] diff --git a/aamp_app/db/validation/devices.py b/aamp_app/db/validation/devices.py index 7ada7de..a79c88c 100644 --- a/aamp_app/db/validation/devices.py +++ b/aamp_app/db/validation/devices.py @@ -399,4 +399,4 @@ def load_env(file_path=".env"): try: db.create_collection(collection_name, **collection_options) except CollectionInvalid: - print("Error with creating devices collection, it probably already exists") + print("devices collection already exists") diff --git a/aamp_app/db/validation/films.py b/aamp_app/db/validation/films.py index 0eddb36..f4b88da 100644 --- a/aamp_app/db/validation/films.py +++ b/aamp_app/db/validation/films.py @@ -91,4 +91,4 @@ def load_env(file_path=".env"): try: db.create_collection(collection_name, **collection_options) except CollectionInvalid: - print("Error with creating films collection, it probably already exists") \ No newline at end of file + print("film collection already exists") \ No newline at end of file diff --git a/aamp_app/db/validation/recipes.py b/aamp_app/db/validation/recipes.py new file mode 100644 index 0000000..56b3bc9 --- /dev/null +++ b/aamp_app/db/validation/recipes.py @@ -0,0 +1,97 @@ +from mongodb_helper import MongoDBHelper +from pymongo.errors import CollectionInvalid +import os + +def init_collection(): + def load_env(file_path=".env"): + if os.path.exists(file_path): + with open(file_path, "r") as f: + for line in f: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + + load_env() + + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") + + mongo = MongoDBHelper( + mongo_uri, + mongo_db_name, + ) + + db = mongo.db + + + film_dict = { + "metadata": { + "solvent": "string", + "concentration": "float", + "printing_speed": "float", + "printing_temperature": "float", + }, + "result": { + "uv_vis": ["object_id", "null"], + "t80": ["float", "null"], + }, + } + + + recipe_dict_schema = { + "bsonType": "object", + "title": "Recipe Object Validation", + "required": ["file_name", "recipe_dict", "dash_friendly", "executions"], + "properties": { + "file_name": { + "bsonType": "string", + "description": "'file_name' must be a string and is required" + }, + "recipe_dict": { + "bsonType": "object", + "title": "Recipe Dictionary Validation", + "required": ["devices", "commands", "execution_options"], + "properties": { + "devices": { + "bsonType": "array", + "description": "'devices' must be an array and is required" + }, + "commands": { + "bsonType": "array", + "description": "'commands' must be an array and is required" + }, + "execution_options": { + "bsonType": "object", + "required": ["output_files", "default_execution_record_name"], + "properties": { + "output_files": { + "bsonType": "array", + "description": "'output_files' must be an array" + }, + "default_execution_record_name": { + "bsonType": "string", + "description": "'default_execution_record_name' must be a string" + } + } + } + } + }, + "dash_friendly": { + "bsonType": "bool", + "description": "'dash_friendly' must be a boolean and is required" + }, + "executions": { + "bsonType": "array", + "description": "'executions' must be an array and is required" + } + } + } + + + + collection_name = "recipes" + collection_options = {"validator": {"$jsonSchema": recipe_dict_schema}} + try: + db.create_collection(collection_name, **collection_options) + except CollectionInvalid: + print("recipes collection already exists") \ No newline at end of file diff --git a/aamp_app/db/validation/solutions.py b/aamp_app/db/validation/solutions.py index c35888c..b862852 100644 --- a/aamp_app/db/validation/solutions.py +++ b/aamp_app/db/validation/solutions.py @@ -82,4 +82,4 @@ def load_env(file_path=".env"): try: db.create_collection(collection_name, **collection_options) except CollectionInvalid: - print("Error with creating solutions collection, it probably already exists") \ No newline at end of file + print("solutions collection already exists") \ No newline at end of file From 1a7deef97c72ec8c83de4c3929d41d0791f80db8 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Tue, 28 Jan 2025 00:19:15 -0800 Subject: [PATCH 071/125] Changed validation level to warning, now the only devices that don't work are the abstract class ones and the one that needs a valid port to work --- aamp_app/db/validation/recipes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aamp_app/db/validation/recipes.py b/aamp_app/db/validation/recipes.py index 56b3bc9..1161b42 100644 --- a/aamp_app/db/validation/recipes.py +++ b/aamp_app/db/validation/recipes.py @@ -90,7 +90,9 @@ def load_env(file_path=".env"): collection_name = "recipes" - collection_options = {"validator": {"$jsonSchema": recipe_dict_schema}} + # keep validationLevel as "moderate" to allow for existing documents to be validated + # keep validationAction as "warning" so we can update the schema as we learn more about devices + collection_options = {"validator": {"$jsonSchema": recipe_dict_schema}, "validationLevel": "moderate", "validationAction": "warn"} try: db.create_collection(collection_name, **collection_options) except CollectionInvalid: From 3594d7a6795d86a0cc756d52074bf4709c9945f2 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Tue, 11 Feb 2025 14:55:48 -0800 Subject: [PATCH 072/125] Updated dash ui branch with Chang's changes so all devices work up until connection to the physical devices --- aamp_app/devices/festo_solenoid_valve.py | 15 +++++++++++++++ aamp_app/devices/mfc.py | 11 +++++++++++ aamp_app/devices/multi_stepper.py | 19 +++++++++++++++++++ aamp_app/devices/sht85_sensor.py | 15 +++++++++++++++ aamp_app/util.py | 18 +++++++++--------- 5 files changed, 69 insertions(+), 9 deletions(-) diff --git a/aamp_app/devices/festo_solenoid_valve.py b/aamp_app/devices/festo_solenoid_valve.py index 2df1444..e79abfd 100644 --- a/aamp_app/devices/festo_solenoid_valve.py +++ b/aamp_app/devices/festo_solenoid_valve.py @@ -13,6 +13,21 @@ def __init__( ): super().__init__(name, port, baudrate, timeout) + def get_init_args(self) -> dict: + args_dict = { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout + } + return args_dict + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + def initialize(self) -> Tuple[bool, str]: self._is_initialized = True return (True, "Solenoid valve initialized") diff --git a/aamp_app/devices/mfc.py b/aamp_app/devices/mfc.py index cc670e9..9b8151d 100644 --- a/aamp_app/devices/mfc.py +++ b/aamp_app/devices/mfc.py @@ -16,6 +16,17 @@ def __init__(self, name: str, ip: str): super().__init__(name) self._ip = ip + def get_init_args(self) -> dict: + args_dict = { + "name": self._name, + "ip": self._ip, + } + return args_dict + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._ip = args_dict["ip"] + def initialize(self) -> Tuple[bool, str]: self._is_initialized = True return (True, "Initialized mass flow controller") diff --git a/aamp_app/devices/multi_stepper.py b/aamp_app/devices/multi_stepper.py index 916f96b..34da031 100644 --- a/aamp_app/devices/multi_stepper.py +++ b/aamp_app/devices/multi_stepper.py @@ -34,6 +34,25 @@ def __init__( self._stepper_list = stepper_list self._move_timeout = move_timeout # max move timeout for ALL possible motors controlled by arduino + def get_init_args(self) -> dict: + args_dict = { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "stepper_list": self._stepper_list, + "move_timeout": self._move_timeout + } + return args_dict + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._stepper_list = args_dict["stepper_list"] + self._move_timeout = args_dict["move_timeout"] + def initialize(self): for stepper in self._stepper_list: was_homed, comment = self.home(stepper) diff --git a/aamp_app/devices/sht85_sensor.py b/aamp_app/devices/sht85_sensor.py index 2c3d781..eddf4f1 100644 --- a/aamp_app/devices/sht85_sensor.py +++ b/aamp_app/devices/sht85_sensor.py @@ -15,6 +15,21 @@ def __init__( # self.ser.bytesize = serial.EIGHTBITS # self.ser.parity = serial.PARITY_NONE + def get_init_args(self) -> dict: + args_dict = { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + } + return args_dict + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + def initialize(self) -> Tuple[bool, str]: # self.ser.setDTR(False) # self.ser.flushInput() diff --git a/aamp_app/util.py b/aamp_app/util.py index 547a6b8..cb89b63 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -271,11 +271,11 @@ def default(self, obj): "type": str, "notes": "Name of the device.", }, - "numchannel": { - "default": 1, - "type": int, - "notes": "", - }, + # "numchannel": { + # "default": 1, + # "type": int, + # "notes": "", + # }, "port": { "default": "COM5", "type": str, @@ -1263,14 +1263,14 @@ def default(self, obj): "obj": NewportESP301Deinitialize, }, "NewportESP301MoveSpeedAbsolute": { - "default_code": "NewportESP301MoveSpeedAbsolute(receiver= '', axis= 1, position= 0, speed= 20.0)", + "default_code": "NewportESP301MoveSpeedAbsolute(receiver= '', axis_number= 1, position= 0, speed= 20.0)", "args": { "receiver": { "default": "NewportESP301", "type": str, "notes": "Name of the device", }, - "axis": { + "axis_number": { "default": 1, "type": int, "notes": "Axis number", @@ -1289,14 +1289,14 @@ def default(self, obj): "obj": NewportESP301MoveSpeedAbsolute, }, "NewportESP301MoveSpeedRelative": { - "default_code": "NewportESP301MoveSpeedRelative(receiver= '', axis= 1, distance= 0, speed= 20.0)", + "default_code": "NewportESP301MoveSpeedRelative(receiver= '', axis_number= 1, distance= 0, speed= 20.0)", "args": { "receiver": { "default": "NewportESP301", "type": str, "notes": "Name of the device", }, - "axis": { + "axis_number": { "default": 1, "type": int, "notes": "Axis number", From df15f0c0e65ae1a48da010a02c2df163f082c073 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Wed, 12 Feb 2025 11:31:25 -0800 Subject: [PATCH 073/125] Added logs to database for manual control, simple schema --- aamp_app/app.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/aamp_app/app.py b/aamp_app/app.py index 283b2db..a464a05 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -187,6 +187,19 @@ def update_execution_upstream(execution): print("com.document not found") return False +def save_log_to_mongo(log_string): + """Save a log entry to the MongoDB logs collection.""" + try: + log_lines = log_string.strip().split("\n") + + log_entry = { + "timestamp": f"Recipe_{datetime.now().strftime('%Y%m%d_%H%M%S')}", + "log": log_lines, + } + mongo.db["logs"].insert_one(log_entry) # Save to 'logs' collection + print("Log saved to MongoDB successfully.") + except Exception as e: + print(f"Failed to save log to MongoDB: {e}") # --------------------------------------------------- # Home Page @@ -1878,6 +1891,9 @@ def manual_control_execute_fill_code( # else: # log_string += msg # print(messages) + + save_log_to_mongo(code_log_string) + return ( opt, True, From 59d93fe73ed0f54d61d59be4df1352c07f42ef30 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Tue, 25 Feb 2025 12:55:06 -0800 Subject: [PATCH 074/125] added initial image upload functionality --- aamp_app/app.py | 149 ++++++++++++++++++++++++++------------- aamp_app/pages/home.py | 1 + aamp_app/pages/images.py | 63 +++++++++++++++++ requirements.txt | Bin 360 -> 388 bytes 4 files changed, 163 insertions(+), 50 deletions(-) create mode 100644 aamp_app/pages/images.py diff --git a/aamp_app/app.py b/aamp_app/app.py index a464a05..e000c00 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -9,6 +9,7 @@ from dash import dcc from dash import html from dash import dash_table +import dash_ag_grid as dag from dash.dependencies import Input, Output, State import dash_bootstrap_components as dbc import os, signal @@ -78,6 +79,7 @@ def load_env(file_path=".env"): # "diaogroup", # ) mongo_gridfs = GridFS(mongo.db, collection="recipes") +fs = GridFS(mongo.db) solutions.init_collection() devices.init_collection() @@ -107,6 +109,7 @@ def load_env(file_path=".env"): dbc.NavItem( dbc.NavLink("Real Time", href="/real-time-telemetry", external_link=True) ), + dbc.NavItem(dbc.NavLink("Images", href="/images", external_link=True)), # dbc.DropdownMenu( # children=[ # # dbc.DropdownMenuItem( @@ -186,15 +189,14 @@ def update_execution_upstream(execution): else: print("com.document not found") return False - + +# TODO: include where the log was generated from def save_log_to_mongo(log_string): """Save a log entry to the MongoDB logs collection.""" try: - log_lines = log_string.strip().split("\n") - log_entry = { "timestamp": f"Recipe_{datetime.now().strftime('%Y%m%d_%H%M%S')}", - "log": log_lines, + "log": log_string, } mongo.db["logs"].insert_one(log_entry) # Save to 'logs' collection print("Log saved to MongoDB successfully.") @@ -356,62 +358,45 @@ def create_new_recipe_doc(n, url, name): # View Recipe Page # --------------------------------------------------- - @app.callback( Output("devices-table-div", "children"), [Input("refresh-button1", "n_clicks"), Input("devices-table", "data")], [State("devices-table-div", "children")], ) -def update_device_table(n_clicks, data, table): # view-recipe page +def update_device_table(n_clicks, data, table): print("update_device_table") - # table_data1 = dl5 + table_data1 = com.get_clean_device_list().copy() - # print(com.device_list[1].get_init_args()) - table_data1_new = [] - for index, list in enumerate(table_data1): - # table_data1[index][1].update({"device_type": table_data1[index][0]}) - # del table_data1[index][0] - table_data1_new.append( - { - "index": index, - "device_type": table_data1[index][0], - "params": str(table_data1[index][1]), - } - ) + table_data1_new = [ + { + "index": index, + "device_type": device[0], + "params": str(device[1]), + } + for index, device in enumerate(table_data1) + ] - table_data1 = table_data1_new + column_defs = [ + {"headerName": "Index", "field": "index"}, + {"headerName": "Device Type", "field": "device_type", "rowDrag": True}, + {"headerName": "Parameters", "field": "params", "flex": 1}, + ] - table = dash_table.DataTable( + grid = dag.AgGrid( id="devices-table", - data=table_data1, - columns=[ - {"name": "Index", "id": "index"}, - {"name": "Type", "id": "device_type"}, - {"name": "Parameters", "id": "params"}, - ], - style_cell={ - "overflow": "hidden", - "textOverflow": "ellipsis", - "maxWidth": 0, - "textAlign": "left", - "padding": "5px", + columnDefs=column_defs, + rowData=table_data1_new, + defaultColDef={"sortable": True, "filter": True, "resizable": True}, + dashGridOptions={ + "rowDragManaged": True, + "animateRows": True, + "rowSelection": "single", }, - style_cell_conditional=[ - {"if": {"column_id": "index"}, "width": "5%"}, - {"if": {"column_id": "device_type"}, "width": "20%"}, - {"if": {"column_id": "params"}, "width": "70%"}, - ], - # tooltip_data=[ - # { - # column: {"value": str(value), "type": "markdown"} - # for column, value in row.items() - # } - # for row in table_data1 - # ], - # tooltip_duration=None, - # editable = True, + style={"height": 400, "width": "100%"}, ) - return table + + return html.Div(grid) + @app.callback( @@ -443,7 +428,10 @@ def save_command(n_clicks, active_cell, data, value): # view-recipe page Output("view-recipe-alert", "color", allow_duplicate=True), Output("view-recipe-alert", "duration", allow_duplicate=True), ], - Input("save-device-editor", "n_clicks"), + [ + Input("save-device-editor", "n_clicks"), + Input("devices-table", "data") + ], [ State("devices-table", "active_cell"), State("devices-table", "data"), @@ -451,7 +439,7 @@ def save_command(n_clicks, active_cell, data, value): # view-recipe page ], prevent_initial_call=True, ) -def save_device(n_clicks, active_cell, data, value): # view-recipe page +def save_device(n_clicks, updated_data, active_cell, data, value): # view-recipe page print("save_device") if active_cell is not None and data[active_cell["row"]]["params"] != str( json.loads(value) @@ -2188,6 +2176,67 @@ def fill_real_time_telemetry(device, n, url): return [""] return [""] +# --------------------------------------------------------------- +# Images page +# --------------------------------------------------------------- + +@app.callback( + Output("images-alert", "children"), + Output("images-alert", "is_open"), + Output("images-alert", "color"), + Input("upload-image-button", "n_clicks"), + [ + State("sample-number", "value"), + State("motor-speed", "value"), + State("temperature", "value"), + State("concentration", "value"), + State("printing-gap", "value"), + State("precursor-volume", "value"), + State("solvent", "value"), + State("image-upload", "contents"), + ], + prevent_initial_call=True, +) +def upload_image(n_clicks, sample_number, motor_speed, temperature, concentration, printing_gap, precursor_volume, solvent, image_contents): + print("upload_image") + if not image_contents: + return ( + "No image uploaded. Please upload an image.", + True, + "danger", + ) + + try: + header, encoded = image_contents.split(",", 1) + image_data = base64.b64decode(encoded) + + image_id = fs.put(image_data, filename=f"sample_{sample_number}.png") + + metadata = { + "sample_number": sample_number, + "motor_speed": motor_speed, + "temperature": temperature, + "concentration": concentration, + "printing_gap": printing_gap, + "precursor_volume": precursor_volume, + "solvent": solvent, + "image_id": str(image_id), + "timestamp": datetime.utcnow(), + } + mongo.db["images"].insert_one(metadata) + + return ( + f"Image uploaded successfully with ID: {image_id}", + True, + "success", + ) + except Exception as e: + print(f"Error uploading image: {e}") + return ( + f"Failed to upload image. Error: {str(e)}", + True, + "danger", + ) if __name__ == "__main__": app.run(debug=True) diff --git a/aamp_app/pages/home.py b/aamp_app/pages/home.py index f72604c..5ba9893 100644 --- a/aamp_app/pages/home.py +++ b/aamp_app/pages/home.py @@ -33,6 +33,7 @@ "Manual Control": "/manual-control", "Database Browser": "/database-browser", "Real Time Telemetry": "/real-time-telemetry", + "Images": "/images", # "Options": "/options", } diff --git a/aamp_app/pages/images.py b/aamp_app/pages/images.py new file mode 100644 index 0000000..15a2e70 --- /dev/null +++ b/aamp_app/pages/images.py @@ -0,0 +1,63 @@ +from dash import html, dcc, callback, Input, Output, State +import dash_bootstrap_components as dbc +import dash + +dash.register_page(__name__, path="/images", name="Images", title="Upload Images") + +layout = html.Div( + [ + html.H1("Upload Images to MongoDB"), + dbc.Alert( + id="images-alert", + color="success", + is_open=False, + fade=True, + className="mb-3", + ), + dbc.Row( + [ + dbc.Col(dbc.Input(id="sample-number", type="number", placeholder="Sample Number"), width=4), + dbc.Col(dbc.Input(id="motor-speed", type="number", placeholder="Motor Speed"), width=4), + dbc.Col(dbc.Input(id="temperature", type="number", placeholder="Temperature"), width=4), + ], + className="mb-3", + ), + dbc.Row( + [ + dbc.Col(dbc.Input(id="concentration", type="number", placeholder="Concentration"), width=4), + dbc.Col(dbc.Input(id="printing-gap", type="number", placeholder="Printing Gap"), width=4), + dbc.Col(dbc.Input(id="precursor-volume", type="number", placeholder="Precursor Volume"), width=4), + ], + className="mb-3", + ), + dbc.Row( + [ + dbc.Col(dbc.Input(id="solvent", type="text", placeholder="Solvent"), width=6), + ], + className="mb-3", + ), + dbc.Row( + [ + dcc.Upload( + id="image-upload", + children=html.Div(["Drag and Drop or ", html.A("Select an Image")]), + style={ + "width": "100%", + "height": "60px", + "lineHeight": "60px", + "borderWidth": "1px", + "borderStyle": "dashed", + "borderRadius": "5px", + "textAlign": "center", + "margin": "10px", + }, + multiple=False, + ), + ], + className="mb-3", + ), + dbc.Button("Upload Image", id="upload-image-button", color="primary"), + html.Div(id="upload-status", style={"marginTop": "20px"}), + ], + className="container", +) diff --git a/requirements.txt b/requirements.txt index bedb147adc4c9f99296121324a712ee37aa2ec43..f0448e2511384ed5983c4e65fabb365dd28b85e6 100644 GIT binary patch delta 33 mcmaFC)WSSrns^FBB117l27@k;P6v|d3`Go?3@H;QUIze|MhOrA delta 9 QcmZo+e!( Date: Thu, 27 Feb 2025 11:41:23 -0800 Subject: [PATCH 075/125] Added the pump device and documentation for adding more devices in the future --- aamp_app/adding_devices.md | 304 +++++++++++++++++++++++++++++++++++++ aamp_app/app.py | 9 +- aamp_app/util.py | 155 ++++++++++++++++++- 3 files changed, 465 insertions(+), 3 deletions(-) create mode 100644 aamp_app/adding_devices.md diff --git a/aamp_app/adding_devices.md b/aamp_app/adding_devices.md new file mode 100644 index 0000000..68dc39d --- /dev/null +++ b/aamp_app/adding_devices.md @@ -0,0 +1,304 @@ +# Adding Devices to the Device Dropdown +This is a step-by-step guide for how to add devices and commands to the dropdowns in the View tab + +### Step 1: + +Ensure that you have the device_name.py file and the device_name_commands.py file for the device you want to add. device_name.py should be in aamp_app/devices and device_name_commands.py should be in aamp_app/commands. You will have to refer to both of these files later to populate utils.py correctly. + +### Step 2: + +Navigate to aamp_app/utils.py. This file contains the configurations for each devices and their respective commands as they show up in the UI. + +### Step 3: + +Import the device at the top of the file. For example: +```python +from devices.psd6_syringe_pump import PSD6SyringePump +``` + +Also import all functions from its respective commands file: +```python +from commands.psd6_syringe_pump_commands import * +``` + +### Step 4: + +Add the device to the devices_ref_redundancy object. The device specifications should match what is outlined in the device's python file. All parameters that don't already have a default value defined in the class and are not optional must be included. For example, here is the python file defining the PSD6SyringePump: +```python +class PSD6SyringePump(SerialDevice): + status_dict = { + '@': "Pump busy - no error", + '`': "Pump ready - no error", + 'a': "Initialization error - pump failed to initialize", + 'b': "Invalid command - unrecognized command is used.", + 'c': "Invalid operand - invalid parameter is given with a command.", + 'd': "Invalid command sequence - command communication protocol is incorrect", + 'f': "EEPROM failure - EEPROM is faulty", + 'g': "Syringe not initialized - syringe failed to initialize", + 'i': "Syringe overload - syringe encounters excessive back pressure", + 'j': "Valve overload - valve drive encounters excessive back pressure", + 'k': "Syringe move not allowed - valve is in the bypass or throughput position, syringe move commands are not allowed", + 'o': "Pump busy - command buffer is full" + } + + # volume_factor = {'ul': 1.0, 'ml': 1000.0} + # time_factor = {'s': 1.0, 'min': 1.0/60.0} + + def __init__( + self, + name: str, + port: str, + baudrate: int = 9600, + timeout: Optional[float] = 10.0, + stroke_volume: float = 5000.0, + stroke_steps: int = 6000, + default_flowrate: float = 1000.0, + # volume_unit: str = 'ul', + # time_unit: str = 's', + port_dead_volumes: List[float] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + poll_interval: float = 0.1): + + super().__init__(name, port, baudrate, timeout) + self._stroke_volume = stroke_volume + self._stroke_steps = stroke_steps + self._default_flowrate = default_flowrate + # self._volume_unit = volume_unit + # self._time_unit = time_unit + self._port_dead_volumes = port_dead_volumes + self._poll_interval = poll_interval + + # technically if I send a command that is out of range of these values + # the pump should be able to give me an error message anyways + # but if I store a default flowrate in this object I'd rather + # have it be correct, than let the pump handle it because + # setting a wrong default flowrate here does not yield an error until + # later when some other command is sent that uses it + self._max_steps_per_sec = 10000 + self._min_steps_per_sec = 2 + + # # this is ok to leave out since anything using a position immediately issues a command + # self._min_position = 0 + # self._max_position = 6000 + +``` + +And here is the corresponding device specification in JSON in utils.py: +```python +"PSD6SyringePump": { + "obj": PSD6SyringePump, + "serial": True, + "serial_sequence": ["PSD6SyringePumpInitialize"], + "import_device": "from devices.psd6_syringe_pump import PSD6SyringePump", + "import_commands": "from commands.psd6_syringe_pump_commands import *", + "init": { + "default_code": "PSD6SyringePump(name='PSD6SyringePump', port='COM5', baudrate=9600, timeout=10.0)", + "obj_name": "PSD6SyringePump", + "args": { + "name": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "port": { + "default": "COM5", + "type": str, + "notes": "Port", + }, + "baudrate": { + "default": 9600, + "type": int, + "notes": "Baudrate", + }, + "timeout": { + "default": 10.0, + "type": float, + "notes": "Timeout", + }, + }, + }, + # ... +} +``` + +### Step 5: + +Add a nested object to the device object conatining each command. Similarly to the previous step, this requires looking at what parameters each function takes and their defaults. Here are the functions in the commands file: + +```python +class PSD6SyringePumpConnect(PSD6SyringePumpParentCommand): + """Open the serial port to the PSD6 syringe pump.""" + + def __init__(self, receiver: PSD6SyringePump, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.start_serial()) + +class PSD6SyringePumpInitialize(PSD6SyringePumpParentCommand): + def __init__(self, receiver: PSD6SyringePump, **kwargs): + super().__init__(receiver, **kwargs) + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + +class PSD6SyringePumpMoveValve(PSD6SyringePumpParentCommand): + def __init__(self, receiver: PSD6SyringePump, valve_num: int, **kwargs): + super().__init__(receiver, **kwargs) + self._params['valve_num'] = valve_num + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_valve_position(self._params['valve_num'])) + +class PSD6SyringePumpMoveAbsolute(PSD6SyringePumpParentCommand): + def __init__(self, receiver: PSD6SyringePump, volume: float, valve_num: Optional[int] = None, flowrate: Optional[float] = None, **kwargs): + super().__init__(receiver, **kwargs) + self._params['volume'] = volume + self._params['valve_num'] = valve_num + self._params['flowrate'] = flowrate + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_syringe_absolute_volume(self._params['volume'], self._params['valve_num'], self._params['flowrate'])) + +class PSD6SyringePumpInfuse(PSD6SyringePumpParentCommand): + def __init__(self, receiver: PSD6SyringePump, volume: float, valve_num: Optional[int] = None, flowrate: Optional[float] = None, **kwargs): + super().__init__(receiver, **kwargs) + self._params['volume'] = volume + self._params['valve_num'] = valve_num + self._params['flowrate'] = flowrate + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.infuse_syringe_volume(self._params['volume'], self._params['valve_num'], self._params['flowrate'])) + +class PSD6SyringePumpWithdraw(PSD6SyringePumpParentCommand): + def __init__(self, receiver: PSD6SyringePump, volume: float, valve_num: Optional[int] = None, flowrate: Optional[float] = None, **kwargs): + super().__init__(receiver, **kwargs) + self._params['volume'] = volume + self._params['valve_num'] = valve_num + self._params['flowrate'] = flowrate + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.withdraw_syringe_volume(self._params['volume'], self._params['valve_num'], self._params['flowrate'])) +``` + +And here are the corresponding command objects in utils.py: +```python +# As a nested object within the previously defined device object... +"commands": { + "PSD6SyringePumpConnect": { + "default_code": "PSD6SyringePumpConnect(receiver= '')", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + } + }, + "obj": PSD6SyringePumpConnect, + }, + "PSD6SyringePumpInitialize": { + "default_code": "PSD6SyringePumpInitialize(receiver= '')", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + } + }, + "obj": PSD6SyringePumpInitialize, + }, + "PSD6SyringePumpMoveValve": { + "default_code": "PSD6SyringePumpMoveValve(receiver= '', valve_num=0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + }, + "obj": PSD6SyringePumpMoveValve, + }, + "PSD6SyringePumpMoveAbsolute": { + "default_code": "PSD6SyringePumpMoveAbsolute(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "volume": { + "default": 0.0, + "type": float, + "notes": "Volume.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + "flowrate": { + "default": 0.0, + "type": float, + "notes": "Flowrate.", + }, + }, + "obj": PSD6SyringePumpMoveAbsolute, + }, + "PSD6SyringePumpInfuse": { + "default_code": "PSD6SyringePumpInfuse(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "volume": { + "default": 0.0, + "type": float, + "notes": "Volume.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + "flowrate": { + "default": 0.0, + "type": float, + "notes": "Flowrate.", + }, + }, + "obj": PSD6SyringePumpInfuse, + }, + "PSD6SyringePumpWithdraw": { + "default_code": "PSD6SyringePumpWithdraw(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "volume": { + "default": 0.0, + "type": float, + "notes": "Volume.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + "flowrate": { + "default": 0.0, + "type": float, + "notes": "Flowrate.", + }, + }, + "obj": PSD6SyringePumpWithdraw, + }, + }, +``` \ No newline at end of file diff --git a/aamp_app/app.py b/aamp_app/app.py index e000c00..203a66f 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -909,7 +909,11 @@ def view_recipe_add_command(n, url, device_type, command_type, json_value): def view_recipe_fill_execution_options(url): if str(url) == "/view-recipe": if "document" in com.__dict__.keys(): - ls = com.document["recipe_dict"]["execution_options"]["output_files"] + try: + ls = com.document["recipe_dict"]["execution_options"]["output_files"] + except Exception as e: + print(str(e)) + ls = [] filesToRet = "" for item in ls: filesToRet += item + "\n" @@ -939,7 +943,8 @@ def view_recipe_save_execution_options( n, filenames, default_execution_record_name, url ): if str(url) == "/view-recipe": - com.execution_options["output_files"] = filenames.splitlines() + if filenames is not None: + com.execution_options["output_files"] = filenames.splitlines() com.execution_options[ "default_execution_record_name" ] = default_execution_record_name diff --git a/aamp_app/util.py b/aamp_app/util.py index cb89b63..c601f28 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -15,6 +15,7 @@ from devices.sht85_sensor import SHT85HumidityTempSensor from devices.device import Device, MiscDeviceClass from devices.utility_device import UtilityCommands +from devices.psd6_syringe_pump import PSD6SyringePump from commands.linear_stage_150_commands import * from commands.mts50_z8_commands import * @@ -31,6 +32,7 @@ from commands.sht85_sensor_commands import * from commands.newport_esp301_commands import * from commands.utility_commands import * +from commands.psd6_syringe_pump_commands import * import json import numpy as np @@ -1449,7 +1451,7 @@ def default(self, obj): }, }, # "Spectrometer": {"obj": StellarNetSpectrometer}, - # "XimeaCamera": {"obj": XimeaCamera}, + "XimeaCamera": {"obj": XimeaCamera}, "DummyHeater": { "obj": DummyHeater, "serial": True, @@ -1529,6 +1531,157 @@ def default(self, obj): }, }, }, + "PSD6SyringePump": { + "obj": PSD6SyringePump, + "serial": True, + "serial_sequence": ["PSD6SyringePumpInitialize"], + "import_device": "from devices.psd6_syringe_pump import PSD6SyringePump", + "import_commands": "from commands.psd6_syringe_pump_commands import *", + "init": { + "default_code": "PSD6SyringePump(name='PSD6SyringePump', port='COM5', baudrate=9600, timeout=10.0)", + "obj_name": "PSD6SyringePump", + "args": { + "name": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "port": { + "default": "COM5", + "type": str, + "notes": "Port", + }, + "baudrate": { + "default": 9600, + "type": int, + "notes": "Baudrate", + }, + "timeout": { + "default": 10.0, + "type": float, + "notes": "Timeout", + }, + }, + }, + "commands": { + "PSD6SyringePumpConnect": { + "default_code": "PSD6SyringePumpConnect(receiver= '')", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + } + }, + "obj": PSD6SyringePumpConnect, + }, + "PSD6SyringePumpInitialize": { + "default_code": "PSD6SyringePumpInitialize(receiver= '')", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + } + }, + "obj": PSD6SyringePumpInitialize, + }, + "PSD6SyringePumpMoveValve": { + "default_code": "PSD6SyringePumpMoveValve(receiver= '', valve_num=0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + }, + "obj": PSD6SyringePumpMoveValve, + }, + "PSD6SyringePumpMoveAbsolute": { + "default_code": "PSD6SyringePumpMoveAbsolute(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "volume": { + "default": 0.0, + "type": float, + "notes": "Volume.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + "flowrate": { + "default": 0.0, + "type": float, + "notes": "Flowrate.", + }, + }, + "obj": PSD6SyringePumpMoveAbsolute, + }, + "PSD6SyringePumpInfuse": { + "default_code": "PSD6SyringePumpInfuse(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "volume": { + "default": 0.0, + "type": float, + "notes": "Volume.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + "flowrate": { + "default": 0.0, + "type": float, + "notes": "Flowrate.", + }, + }, + "obj": PSD6SyringePumpInfuse, + }, + "PSD6SyringePumpWithdraw": { + "default_code": "PSD6SyringePumpWithdraw(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "volume": { + "default": 0.0, + "type": float, + "notes": "Volume.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + "flowrate": { + "default": 0.0, + "type": float, + "notes": "Flowrate.", + }, + }, + "obj": PSD6SyringePumpWithdraw, + }, + }, + }, } From 0a7d78a97e099aa39e14a5238251bc34c7ad0414 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Thu, 27 Feb 2025 11:45:25 -0800 Subject: [PATCH 076/125] added step 6 to documentation --- aamp_app/adding_devices.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/aamp_app/adding_devices.md b/aamp_app/adding_devices.md index 68dc39d..13fbc22 100644 --- a/aamp_app/adding_devices.md +++ b/aamp_app/adding_devices.md @@ -3,7 +3,7 @@ This is a step-by-step guide for how to add devices and commands to the dropdown ### Step 1: -Ensure that you have the device_name.py file and the device_name_commands.py file for the device you want to add. device_name.py should be in aamp_app/devices and device_name_commands.py should be in aamp_app/commands. You will have to refer to both of these files later to populate utils.py correctly. +Ensure that you have the device_name.py file and the device_name_commands.py file for the device you want to add. device_name.py should be in aamp_app/devices and device_name_commands.py should be in aamp_app/commands. You will have to refer to both of these files later to populate the utils file correctly. ### Step 2: @@ -122,7 +122,7 @@ And here is the corresponding device specification in JSON in utils.py: ### Step 5: -Add a nested object to the device object conatining each command. Similarly to the previous step, this requires looking at what parameters each function takes and their defaults. Here are the functions in the commands file: +Add a nested object to the device object containing each command. Similarly to the previous step, this requires looking at what parameters each function takes and their defaults. Here are the functions in the commands file: ```python class PSD6SyringePumpConnect(PSD6SyringePumpParentCommand): @@ -180,7 +180,7 @@ class PSD6SyringePumpWithdraw(PSD6SyringePumpParentCommand): self._result = CommandResult(*self._receiver.withdraw_syringe_volume(self._params['volume'], self._params['valve_num'], self._params['flowrate'])) ``` -And here are the corresponding command objects in utils.py: +And here are the corresponding command objects in the utils file: ```python # As a nested object within the previously defined device object... "commands": { @@ -301,4 +301,8 @@ And here are the corresponding command objects in utils.py: "obj": PSD6SyringePumpWithdraw, }, }, -``` \ No newline at end of file +``` + +### Step 6: + +To confirm that everything is working correctly, ensure all of the dropdowns are propagated correctly with the new device and commands, and that they can all be added to the database. \ No newline at end of file From 3ed10cf56fa58de8f79176b11e3bcb916f40733c Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Tue, 4 Mar 2025 20:49:16 -0800 Subject: [PATCH 077/125] Fixed typo in the adding device documentation --- aamp_app/adding_devices.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aamp_app/adding_devices.md b/aamp_app/adding_devices.md index 13fbc22..eeccde5 100644 --- a/aamp_app/adding_devices.md +++ b/aamp_app/adding_devices.md @@ -3,11 +3,11 @@ This is a step-by-step guide for how to add devices and commands to the dropdown ### Step 1: -Ensure that you have the device_name.py file and the device_name_commands.py file for the device you want to add. device_name.py should be in aamp_app/devices and device_name_commands.py should be in aamp_app/commands. You will have to refer to both of these files later to populate the utils file correctly. +Ensure that you have the device_name.py file and the device_name_commands.py file for the device you want to add. device_name.py should be in aamp_app/devices and device_name_commands.py should be in aamp_app/commands. You will have to refer to both of these files later to populate the util file correctly. ### Step 2: -Navigate to aamp_app/utils.py. This file contains the configurations for each devices and their respective commands as they show up in the UI. +Navigate to aamp_app/util.py. This file contains the configurations for each devices and their respective commands as they show up in the UI. ### Step 3: @@ -82,7 +82,7 @@ class PSD6SyringePump(SerialDevice): ``` -And here is the corresponding device specification in JSON in utils.py: +And here is the corresponding device specification in JSON in util.py: ```python "PSD6SyringePump": { "obj": PSD6SyringePump, @@ -180,7 +180,7 @@ class PSD6SyringePumpWithdraw(PSD6SyringePumpParentCommand): self._result = CommandResult(*self._receiver.withdraw_syringe_volume(self._params['volume'], self._params['valve_num'], self._params['flowrate'])) ``` -And here are the corresponding command objects in the utils file: +And here are the corresponding command objects in the util file: ```python # As a nested object within the previously defined device object... "commands": { From d8d65542459c384fc55bb7584726664f0d1ea859 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Wed, 5 Mar 2025 09:04:29 -0800 Subject: [PATCH 078/125] first ximea device add commit to test if it works on chang's side --- aamp_app/util.py | 139 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 1 deletion(-) diff --git a/aamp_app/util.py b/aamp_app/util.py index c601f28..3be07e5 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -16,6 +16,7 @@ from devices.device import Device, MiscDeviceClass from devices.utility_device import UtilityCommands from devices.psd6_syringe_pump import PSD6SyringePump +from devices.ximea_camera import XimeaCamera from commands.linear_stage_150_commands import * from commands.mts50_z8_commands import * @@ -33,6 +34,7 @@ from commands.newport_esp301_commands import * from commands.utility_commands import * from commands.psd6_syringe_pump_commands import * +from commands.ximea_camera_commands import * import json import numpy as np @@ -1451,7 +1453,6 @@ def default(self, obj): }, }, # "Spectrometer": {"obj": StellarNetSpectrometer}, - "XimeaCamera": {"obj": XimeaCamera}, "DummyHeater": { "obj": DummyHeater, "serial": True, @@ -1682,6 +1683,142 @@ def default(self, obj): }, }, }, + "XimeaCamera": { + "obj": XimeaCamera, + "serial": True, + "serial_sequence": ["XimeaCameraInitialize"], + "import_device": "from devices.ximea_camera import XimeaCamera", + "import_commands": "from commands.ximea_camera_commands import *", + "init": { + "default_code": "XimeaCamera(name='XimeaCamera')", + "obj_name": "XimeaCamera", + "args": { + "name": { + "default": "XimeaCamera", + "type": str, + "notes": "Name of the device.", + }, + }, + }, + "commands": { + "XimeaCameraInitialize": { + "default_code": "XimeaCameraInitialize(receiver= '')", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + } + }, + "obj": PSD6SyringePumpConnect, + }, + "PSD6SyringePumpInitialize": { + "default_code": "PSD6SyringePumpInitialize(receiver= '')", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + } + }, + "obj": PSD6SyringePumpInitialize, + }, + "PSD6SyringePumpMoveValve": { + "default_code": "PSD6SyringePumpMoveValve(receiver= '', valve_num=0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + }, + "obj": PSD6SyringePumpMoveValve, + }, + "PSD6SyringePumpMoveAbsolute": { + "default_code": "PSD6SyringePumpMoveAbsolute(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "volume": { + "default": 0.0, + "type": float, + "notes": "Volume.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + "flowrate": { + "default": 0.0, + "type": float, + "notes": "Flowrate.", + }, + }, + "obj": PSD6SyringePumpMoveAbsolute, + }, + "PSD6SyringePumpInfuse": { + "default_code": "PSD6SyringePumpInfuse(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "volume": { + "default": 0.0, + "type": float, + "notes": "Volume.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + "flowrate": { + "default": 0.0, + "type": float, + "notes": "Flowrate.", + }, + }, + "obj": PSD6SyringePumpInfuse, + }, + "PSD6SyringePumpWithdraw": { + "default_code": "PSD6SyringePumpWithdraw(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "args": { + "receiver": { + "default": "PSD6SyringePump", + "type": str, + "notes": "Name of the device.", + }, + "volume": { + "default": 0.0, + "type": float, + "notes": "Volume.", + }, + "valve_num": { + "default": 0, + "type": int, + "notes": "Valve number.", + }, + "flowrate": { + "default": 0.0, + "type": float, + "notes": "Flowrate.", + }, + }, + "obj": PSD6SyringePumpWithdraw, + }, + }, + }, } From 2e6bb736d27650efad4c96c9b4f6faf003c286a5 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Thu, 6 Mar 2025 11:42:20 -0800 Subject: [PATCH 079/125] added the ximea camera and all commands to util.py, still have to test with a working installation --- aamp_app/util.py | 151 +++++++++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 57 deletions(-) diff --git a/aamp_app/util.py b/aamp_app/util.py index 3be07e5..35aebcc 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -1705,117 +1705,154 @@ def default(self, obj): "default_code": "XimeaCameraInitialize(receiver= '')", "args": { "receiver": { - "default": "PSD6SyringePump", + "default": "XimeaCamera", "type": str, "notes": "Name of the device.", } }, - "obj": PSD6SyringePumpConnect, + "obj": XimeaCameraInitialize, }, - "PSD6SyringePumpInitialize": { - "default_code": "PSD6SyringePumpInitialize(receiver= '')", + "XimeaCameraDeinitialize": { + "default_code": "XimeaCameraDeinitialize(receiver= '', reset_init_flag=True)", "args": { "receiver": { - "default": "PSD6SyringePump", + "default": "XimeaCamera", "type": str, "notes": "Name of the device.", + }, + "reset_init_flag": { + "default": True, + "type": str, + "notes": "Reset init flag.", } }, - "obj": PSD6SyringePumpInitialize, + "obj": XimeaCameraDeinitialize, }, - "PSD6SyringePumpMoveValve": { - "default_code": "PSD6SyringePumpMoveValve(receiver= '', valve_num=0)", + "XimeaCameraGetImage": { + "default_code": "XimeaCameraGetImage(receiver= '', save_to_file=True, filename=None, exposure_time=None, gain=None, show_pop_up=False)", "args": { "receiver": { - "default": "PSD6SyringePump", + "default": "XimeaCamera", "type": str, "notes": "Name of the device.", }, - "valve_num": { - "default": 0, + "save_to_file": { + "default": True, + "type": bool, + "notes": "Whether the image should be saved to file or not.", + }, + "filename": { + "default": None, + "type": str, + "notes": "Filename to save the image to.", + }, + "exposure_time": { + "default": None, "type": int, - "notes": "Valve number.", + "notes": "Exposure time of the camera.", + }, + "gain": { + "default": None, + "type": float, + "notes": "Gain of the camera.", + }, + "show_pop_up": { + "default": False, + "type": bool, + "notes": "Whether to show a pop up of the image.", }, }, - "obj": PSD6SyringePumpMoveValve, + "obj": XimeaCameraGetImage, }, - "PSD6SyringePumpMoveAbsolute": { - "default_code": "PSD6SyringePumpMoveAbsolute(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "XimeaCameraSetDefaultExposure": { + "default_code": "XimeaCameraSetDefaultExposure(receiver= '', exposure_time=None)", "args": { "receiver": { - "default": "PSD6SyringePump", + "default": "XimeaCamera", "type": str, "notes": "Name of the device.", }, - "volume": { - "default": 0.0, - "type": float, - "notes": "Volume.", - }, - "valve_num": { - "default": 0, + "exposure_time": { + "default": None, "type": int, - "notes": "Valve number.", - }, - "flowrate": { - "default": 0.0, - "type": float, - "notes": "Flowrate.", + "notes": "Updated exposure time.", }, }, - "obj": PSD6SyringePumpMoveAbsolute, + "obj": XimeaCameraSetDefaultExposure, }, - "PSD6SyringePumpInfuse": { - "default_code": "PSD6SyringePumpInfuse(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "XimeaCameraSetDefaultGain": { + "default_code": "XimeaCameraSetDefaultGain(receiver= '', gain=None)", "args": { "receiver": { - "default": "PSD6SyringePump", + "default": "XimeaCamera", "type": str, "notes": "Name of the device.", }, - "volume": { - "default": 0.0, + "gain": { + "default": None, "type": float, - "notes": "Volume.", + "notes": "Updated gain.", }, - "valve_num": { - "default": 0, + }, + "obj": XimeaCameraSetDefaultGain, + }, + "XimeaCameraUpdateWhiteBal": { + "default_code": "XimeaCameraUpdateWhiteBal(receiver= '', exposure_time=None, gain=None)", + "args": { + "receiver": { + "default": "XimeaCamera", + "type": str, + "notes": "Name of the device.", + }, + "exposure_time": { + "default": None, "type": int, - "notes": "Valve number.", + "notes": "Updated exposure time.", }, - "flowrate": { - "default": 0.0, + "gain": { + "default": None, "type": float, - "notes": "Flowrate.", + "notes": "Updated gain.", }, }, - "obj": PSD6SyringePumpInfuse, + "obj": XimeaCameraUpdateWhiteBal, }, - "PSD6SyringePumpWithdraw": { - "default_code": "PSD6SyringePumpWithdraw(receiver= '', volume=0.0, valve_num= 0, flowrate=0.0)", + "XimeaCameraSetManualWhiteBal": { + "default_code": "XimeaCameraSetManualWhiteBal(receiver= '', wb_kr=None, wb_kg=None, wb_kb=None)", "args": { "receiver": { - "default": "PSD6SyringePump", + "default": "XimeaCamera", "type": str, "notes": "Name of the device.", }, - "volume": { - "default": 0.0, + "wb_kr": { + "default": None, "type": float, - "notes": "Volume.", + "notes": "White balance red.", }, - "valve_num": { - "default": 0, - "type": int, - "notes": "Valve number.", + "wb_kg": { + "default": None, + "type": float, + "notes": "White balance green.", }, - "flowrate": { - "default": 0.0, + "wb_kb": { + "default": None, "type": float, - "notes": "Flowrate.", + "notes": "White balance blue.", }, }, - "obj": PSD6SyringePumpWithdraw, + "obj": XimeaCameraSetManualWhiteBal, + }, + "XimeaCameraResetWhiteBal": { + "default_code": "XimeaCameraResetWhiteBal(receiver= '')", + "args": { + "receiver": { + "default": "XimeaCamera", + "type": str, + "notes": "Name of the device.", + } + }, + "obj": XimeaCameraResetWhiteBal, }, }, }, From 785837dd10a49e24cbb9e3acd3132db48090c0d8 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Thu, 6 Mar 2025 12:38:24 -0800 Subject: [PATCH 080/125] changed xiapi import statement and removed it from requirements.txt, Chang is on Windows and was able to successfully install by manually putting the xiapi installation in the lib folder --- aamp_app/devices/ximea_camera.py | 2 +- requirements.txt | Bin 388 -> 364 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/aamp_app/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py index 9cc543a..bff71af 100644 --- a/aamp_app/devices/ximea_camera.py +++ b/aamp_app/devices/ximea_camera.py @@ -2,7 +2,7 @@ from typing import Optional, Tuple, List from PIL import Image -from ximea import * +from ximea import xiapi from .device import Device, check_initialized diff --git a/requirements.txt b/requirements.txt index f0448e2511384ed5983c4e65fabb365dd28b85e6..da576af9666786007d52758aad484df798b46bac 100644 GIT binary patch delta 7 OcmZo+e#5jOhY Date: Sun, 23 Mar 2025 19:16:49 -0700 Subject: [PATCH 081/125] added visualizations, working sampling, normalization --- aamp_app/app.py | 218 +++++++++++++++++++++++++++++++++++++- aamp_app/pages/home.py | 3 +- aamp_app/pages/sampler.py | 211 ++++++++++++++++++++++++++++++++++++ requirements.txt | Bin 364 -> 474 bytes 4 files changed, 430 insertions(+), 2 deletions(-) create mode 100644 aamp_app/pages/sampler.py diff --git a/aamp_app/app.py b/aamp_app/app.py index 203a66f..eded6e3 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -18,6 +18,14 @@ from console_interceptor import ConsoleInterceptor from gridfs import GridFS import base64 +import math +import numpy as np +import pandas as pd +from sklearn.decomposition import PCA +from sklearn.preprocessing import StandardScaler +import umap +import plotly.express as px +import plotly.graph_objects as go from db.validation import solutions, films, devices, recipes try: @@ -109,7 +117,8 @@ def load_env(file_path=".env"): dbc.NavItem( dbc.NavLink("Real Time", href="/real-time-telemetry", external_link=True) ), - dbc.NavItem(dbc.NavLink("Images", href="/images", external_link=True)), + # dbc.NavItem(dbc.NavLink("Images", href="/images", external_link=True)), + dbc.NavItem(dbc.NavLink("Sampler", href="/sampler", external_link=True)), # dbc.DropdownMenu( # children=[ # # dbc.DropdownMenuItem( @@ -2243,6 +2252,213 @@ def upload_image(n_clicks, sample_number, motor_speed, temperature, concentratio "danger", ) + +# --------------------------------------------------------------- +# Sampler page +# --------------------------------------------------------------- + +# Add these constants from initial_point_generator.py +SOLV_NAMES = ["CF", "CB", "CB9:A1", "CB8:A2", "CB7:A3"] +CONCEN_D = [2, 5, 10, 15, 20] +PRINT_GAP_D = [50, 100] +PREC_VOL_D = [6, 9, 12] +MOTOR_SPEEDS_D = [0.01, 0.0355, 0.126, 0.4472, 1.587, 5.635, 20] +SPEED_C = (0.01, 20.0) +PREC_VOL_C = (6.0, 12.0) +CONCEN_C = (1, 5) + +TEMP_CHOICES_D = { + "CF": [25, 41.3], + "CB": [25, 47.3, 62.9, 87.6, 107.4], + "CB9:A1": [25, 47.3, 62.9, 87.6, 107.4], + "CB8:A2": [25, 47.3, 62.9, 87.6, 107.4], + "CB7:A3": [25, 47.3, 62.9, 87.6, 107.4] +} + +@app.callback( + Output("sampler-temperature-options", "children"), + Input("sampler-solvent-dropdown", "value"), +) +def update_temperature_options(selected_solvents): + if not selected_solvents: + return [html.P("Temperature options will appear after selecting solvent(s)")] + + temp_options = [] + for solvent in selected_solvents: + temp_options.append( + html.Div([ + html.H6(f"Temperature options for {solvent}:"), + dcc.Dropdown( + id=f"sampler-temp-{solvent}", + options=[{"label": temp, "value": temp} for temp in TEMP_CHOICES_D[solvent]], + value=TEMP_CHOICES_D[solvent], + multi=True, + ), + ]) + ) + + return temp_options + +@app.callback( + Output("sampler-results-table", "children"), + Output("sampler-alert", "children"), + Output("sampler-alert", "is_open"), + Output("sampler-alert", "color"), + Output("sampler-save-button", "disabled"), + Input("sampler-generate-button", "n_clicks"), + [ + State("sampler-campaign-name", "value"), + State("sampler-polymer-name", "value"), + State("sampler-smile-string", "value"), + State("sampler-mw", "value"), + State("sampler-pdi", "value"), + State("sampler-solvent-dropdown", "value"), + State("sampler-concentration-range", "value"), + State("sampler-motor-speed-min", "value"), + State("sampler-motor-speed-max", "value"), + State("sampler-printing-gap-dropdown", "value"), + State("sampler-precursor-volume", "value"), + State("sampler-method-dropdown", "value"), + State("sampler-num-samples", "value"), + ], + prevent_initial_call=True, +) +def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smile_string, mw, pdi, + solvents, concentration_range, motor_speed_min, motor_speed_max, + printing_gaps, precursor_vol, sampling_method, num_samples): + print("generate_parameter_sets") + # print the parameters inputted for debugging purposes + print(f"Solvents: {solvents}") + print(f"Concentration Range: {concentration_range}") + print(f"Motor Speed Min: {motor_speed_min}") + print(f"Motor Speed Max: {motor_speed_max}") + print(f"Printing Gaps: {printing_gaps}") + print(f"Precursor Volume: {precursor_vol}") + print(f"Sampling Method: {sampling_method}") + print(f"Number of Samples: {num_samples}") + + if not solvents: + return None, "Please select at least one solvent.", True, "danger", True + + try: + parameter_sets = [] + sample_count = 1 + + for solvent in solvents: + temp_options = TEMP_CHOICES_D.get(solvent, [25]) + + for _ in range(num_samples): + log_speed_min = math.log10(motor_speed_min) + log_speed_max = math.log10(motor_speed_max) + log_speed = log_speed_min + random.random() * (log_speed_max - log_speed_min) + motor_speed = round(10 ** log_speed, 2) + + temperature = round(random.choice(temp_options)) + concentration = random.choice(concentration_range) if concentration_range else random.choice(CONCEN_D) + printing_gap = random.choice(printing_gaps) if printing_gaps else 50 + precursor_volume = random.choice(precursor_vol) if precursor_vol else random.choice(PREC_VOL_D) + + # make all the parameters normalized between 0 and 1 using min-max normalization + motor_speed_norm = (math.log10(motor_speed) - log_speed_min) / (log_speed_max - log_speed_min) + temperature_norm = (temperature - min(temp_options)) / (max(temp_options) - min(temp_options)) + concentration_norm = (concentration - min(concentration_range)) / (max(concentration_range) - min(concentration_range)) + printing_gap_norm = (printing_gap - min(printing_gaps)) / (max(printing_gaps) - min(printing_gaps)) + precursor_volume_norm = (precursor_volume - min(precursor_vol)) / (max(precursor_vol) - min(precursor_vol)) + + parameter_set = { + "sample_no": sample_count, + "campaign_name": campaign_name, + "polymer_name": polymer_name, + "smile_string": smile_string, + "mw": mw, + "pdi": pdi, + "motor_speed": motor_speed, + "temperature": temperature, + "concentration": concentration, + "printing_gap": printing_gap, + "precursor_volume": precursor_volume, + "solvent": solvent, + "motor_speed_norm": motor_speed_norm, + "temperature_norm": temperature_norm, + "concentration_norm": concentration_norm, + "printing_gap_norm": printing_gap_norm, + "precursor_volume_norm": precursor_volume_norm + } + + parameter_sets.append(parameter_set) + sample_count += 1 + + df = pd.DataFrame(parameter_sets) + + table = dash_table.DataTable( + id="sampler-results", + columns=[ + {"name": "Sample No", "id": "sample_no"}, + {"name": "Motor Speed", "id": "motor_speed"}, + {"name": "Temperature", "id": "temperature"}, + {"name": "Concentration", "id": "concentration"}, + {"name": "Printing Gap", "id": "printing_gap"}, + {"name": "Precursor Volume", "id": "precursor_volume"}, + {"name": "Solvent", "id": "solvent"}, + ], + data=parameter_sets, + style_table={"overflowX": "auto"}, + ) + + if len(df) >= 2: + X = df[['motor_speed_norm', 'temperature_norm', 'concentration_norm', + 'printing_gap_norm', 'precursor_volume_norm']].values + + pca = PCA(n_components=2) + pca_result = pca.fit_transform(X) + + reducer = umap.UMAP(random_state=42, n_neighbors=min(5, len(df)-1)) # Adjust n_neighbors + umap_result = reducer.fit_transform(X) + + pca_fig = px.scatter( + x=pca_result[:, 0], + y=pca_result[:, 1], + color=df['solvent'], + labels={'x': 'PCA Component 1', 'y': 'PCA Component 2'}, + title='PCA Visualization of Parameter Sets' + ) + + umap_fig = px.scatter( + x=umap_result[:, 0], + y=umap_result[:, 1], + color=df['solvent'], + labels={'x': 'UMAP Component 1', 'y': 'UMAP Component 2'}, + title='UMAP Visualization of Parameter Sets' + ) + + res = html.Div([ + html.H3("Parameter Space Visualizations"), + html.Div([ + html.Div([ + dcc.Graph(figure=pca_fig) + ], className="col-md-6"), + html.Div([ + dcc.Graph(figure=umap_fig) + ], className="col-md-6"), + ], className="row"), + table + ]) + else: + res = html.Div([ + html.H3("Parameter Space Visualizations"), + html.P("Not enough data points for visualization. Generate more samples."), + html.H3("Generated Parameter Sets"), + table + ]) + + + return res, f"Generated {len(parameter_sets)} parameter sets using simple random sampling.", True, "success", False + except Exception as e: + print(f"Error generating parameter sets: {e}") + return None, f"Failed to generate parameter sets: {str(e)}", True, "danger", True + + + if __name__ == "__main__": app.run(debug=True) diff --git a/aamp_app/pages/home.py b/aamp_app/pages/home.py index 5ba9893..d157510 100644 --- a/aamp_app/pages/home.py +++ b/aamp_app/pages/home.py @@ -33,7 +33,8 @@ "Manual Control": "/manual-control", "Database Browser": "/database-browser", "Real Time Telemetry": "/real-time-telemetry", - "Images": "/images", + # "Images": "/images", + "Sampler": "/sampler", # "Options": "/options", } diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py new file mode 100644 index 0000000..ad3b575 --- /dev/null +++ b/aamp_app/pages/sampler.py @@ -0,0 +1,211 @@ +from dash import Dash, html, dcc, dash_table, callback, Input, Output, State +import dash_bootstrap_components as dbc +import dash + +dash.register_page(__name__, path="/sampler", name="Sampler", title="Sampler") + +SOLV_NAMES = ["CF", "CB", "CB9:A1", "CB8:A2", "CB7:A3"] +TEMP_CHOICES_D = { + "CF": [25, 41.3], + "CB": [25, 47.3, 62.9, 87.6, 107.4], + "CB9:A1": [25, 47.3, 62.9, 87.6, 107.4], + "CB8:A2": [25, 47.3, 62.9, 87.6, 107.4], + "CB7:A3": [25, 47.3, 62.9, 87.6, 107.4] +} +CONCEN_D = [2, 5, 10, 15, 20] +PRINT_GAP_D = [50, 100] +PREC_VOL_D = [6, 9, 12] +MOTOR_SPEEDS_D = [0.01, 0.0355, 0.126, 0.4472, 1.587, 5.635, 20] +SPEED_C = (0.01, 20.0) +PREC_VOL_C = (6.0, 12.0) +CONCEN_C = (1, 5) + +layout = html.Div( + [ + html.H1("Sampler"), + dbc.Alert( + id="sampler-alert", + color="success", + is_open=False, + fade=True, + className="mb-3", + ), + html.Div( + [ + html.H2("Campaign Metadata", className="mt-4"), + dbc.Row( + [ + dbc.Col( + [ + html.H5("Campaign Name"), + dbc.Input(id="sampler-campaign-name", type="text", placeholder="Enter campaign name"), + ], + width=3, + ), + dbc.Col( + [ + html.H5("Polymer Name"), + dbc.Input(id="sampler-polymer-name", type="text", placeholder="Enter polymer name"), + ], + width=3, + ), + dbc.Col( + [ + html.H5("SMILE String"), + dbc.Input(id="sampler-smile-string", type="text", placeholder="Enter SMILE string"), + ], + width=3, + ), + dbc.Col( + [ + html.H5("Molecular Weight (MW)"), + dbc.Input(id="sampler-mw", type="number", placeholder="Enter MW", min=0), + ], + width=3, + ), + dbc.Col( + [ + html.H5("Polydispersity Index (PDI)"), + dbc.Input(id="sampler-pdi", type="number", placeholder="Enter PDI", min=1, step=0.01), + ], + width=3, + ), + ], + className="mb-3", + ), + html.H2("Dependent Variables"), + dbc.Row( + [ + dbc.Col( + [ + html.H5("Solvent"), + dcc.Dropdown( + id="sampler-solvent-dropdown", + options=[{"label": solv, "value": solv} for solv in SOLV_NAMES], + multi=True, + ), + ], + width=6, + ), + dbc.Col( + [ + html.H5("Temperature"), + html.Div(id="sampler-temperature-options", children=[]), + ], + width=6, + ), + ], + className="mb-3", + ), + dbc.Row( + [ + dbc.Col( + [ + html.H5("Concentration Range"), + dcc.Dropdown( + id="sampler-concentration-range", + options=[{"label": str(concen), "value": concen} for concen in CONCEN_D], + multi=True, + value=CONCEN_D, + ), + ], + width=12, + ), + ], + className="mb-3", + ), + + html.H2("Independent Variables"), + dbc.Row( + [ + dbc.Col( + [ + html.H5("Motor Speed Range"), + dbc.InputGroup( + [ + dbc.Input(id="sampler-motor-speed-min", type="number", placeholder="Min", value=SPEED_C[0]), + dbc.Input(id="sampler-motor-speed-max", type="number", placeholder="Max", value=SPEED_C[1]), + ] + ), + ], + width=4, + ), + dbc.Col( + [ + html.H5("Printing Gap"), + dcc.Dropdown( + id="sampler-printing-gap-dropdown", + options=[{"label": str(gap), "value": gap} for gap in PRINT_GAP_D], + multi=True, + value=PRINT_GAP_D, + ), + ], + width=4, + ), + dbc.Col( + [ + html.H5("Precursor Volume"), + dcc.Dropdown( + id="sampler-precursor-volume", + options=[{"label": str(vol), "value": vol} for vol in PREC_VOL_D], + multi=True, + value=PREC_VOL_D, + ), + ], + width=4, + ), + ], + className="mb-3", + ), + + html.H2("Sampling Configuration"), + dbc.Row( + [ + dbc.Col( + [ + html.H5("Sampling Method"), + dcc.Dropdown( + id="sampler-method-dropdown", + options=[ + {"label": "Sobol Sequence", "value": "sobol"}, + {"label": "Latin Hypercube Sampling", "value": "lhs"}, + ], + value="sobol", + ), + ], + width=6, + ), + dbc.Col( + [ + html.H5("Number of Samples per Solvent"), + dbc.Input(id="sampler-num-samples", type="number", value=5, min=1), + ], + width=6, + ), + ], + className="mb-3", + ), + + dbc.Button( + "Generate Parameter Sets", + id="sampler-generate-button", + color="primary", + className="mb-3", + ), + + html.H2("Generated Parameter Sets"), + html.Div(id="sampler-results-table", className="mb-3"), + + dbc.Button( + "Save Parameter Sets", + id="sampler-save-button", + color="success", + className="mb-3", + disabled=True, + ), + ], + className="container", + ), + ], + className="container", +) diff --git a/requirements.txt b/requirements.txt index da576af9666786007d52758aad484df798b46bac..49b9b9731f177195bed84bbc36e1002db35bc830 100644 GIT binary patch delta 118 zcmaFEbc=aI4r3lK0~bRvLo!1qLpBhXFz7PmFr+dhG88f7F@WSs8FGQV0u1p2pt^jZ c`W%Kzu(~|3$^s}4q^AU^3#KNMAqlJs0Dp=Vt^fc4 delta 7 Ocmcb`{Dx^m4kG{!oC4_p From 600916d5af4622fb14f822364349bdcf949feb42 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Mon, 24 Mar 2025 14:55:30 -0700 Subject: [PATCH 082/125] Added contour plot for parameter space visualization and more solvent and constraints --- aamp_app/app.py | 151 +++++++++++++++++++++++++++++++++----- aamp_app/pages/sampler.py | 47 ++++++++++-- 2 files changed, 173 insertions(+), 25 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index eded6e3..64a4048 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -26,6 +26,7 @@ import umap import plotly.express as px import plotly.graph_objects as go +from scipy.stats import gaussian_kde from db.validation import solutions, films, devices, recipes try: @@ -2257,8 +2258,24 @@ def upload_image(n_clicks, sample_number, motor_speed, temperature, concentratio # Sampler page # --------------------------------------------------------------- +# add the following to the temp choices d and c +# when adding to d, make sure to add three or four options evenly spaced between the min and max +# when adding to c, make sure to just add the min and max values + +# "1,4-Dichlorobenzene",C1=CC(=CC=C1Cl)Cl��,25,153.548 +# "1,2,4-Trihlorobenzene",C1=CC(=C(C=C1Cl)Cl)Cl,25,184.946 +# o-xylene,CC1=CC=CC=C1C��,25,119.5683 +# m-xylene,CC1=CC(=CC=C1)C,25,114.5912 +# p-xylene,CC1=CC=C(C=C1)C,25,113.775 +# mesitylene,CC1=CC(=CC(=C1)C)C,25,139.1875 +# toluene,CC1=CC=CC=C1�,25,87.7163 +# 1-Chloronaphthalene,C1=CC=C2C(=C1)C=CC=C2Cl,25,227.995 +# anisole,COC1=CC=CC=C1�,25,129.1316 +# Tetrahydrofuran,C1CCOC1��,25,45.7105 +# decane,CCCCCCCCCC,25,148.3072 + # Add these constants from initial_point_generator.py -SOLV_NAMES = ["CF", "CB", "CB9:A1", "CB8:A2", "CB7:A3"] +SOLV_NAMES = ["CF", "CB", "CB9:A1", "CB8:A2", "CB7:A3", "1,4-Dichlorobenzene", "1,2,4-Trihlorobenzene", "o-xylene", "m-xylene", "p-xylene", "mesitylene", "toluene", "1-Chloronaphthalene", "anisole", "Tetrahydrofuran", "decane"] CONCEN_D = [2, 5, 10, 15, 20] PRINT_GAP_D = [50, 100] PREC_VOL_D = [6, 9, 12] @@ -2272,7 +2289,37 @@ def upload_image(n_clicks, sample_number, motor_speed, temperature, concentratio "CB": [25, 47.3, 62.9, 87.6, 107.4], "CB9:A1": [25, 47.3, 62.9, 87.6, 107.4], "CB8:A2": [25, 47.3, 62.9, 87.6, 107.4], - "CB7:A3": [25, 47.3, 62.9, 87.6, 107.4] + "CB7:A3": [25, 47.3, 62.9, 87.6, 107.4], + "1,4-Dichlorobenzene": [25, 47.3, 62.9, 87.6, 107.4, 135], + "1,2,4-Trihlorobenzene": [25, 47.3, 62.9, 87.6, 107.4, 135], + "o-xylene": [25, 47.3, 62.9, 87.6, 107.4, 119.6], + "m-xylene": [25, 47.3, 62.9, 87.6, 107.4, 114.6], + "p-xylene": [25, 47.3, 62.9, 87.6, 107.4, 113.8], + "mesitylene": [25, 47.3, 62.9, 87.6, 107.4, 135], + "toluene": [25, 47.3, 55.1, 62.9, 75.1, 87.7], + "1-Chloronaphthalene": [25, 47.3, 62.9, 87.6, 107.4, 135], + "anisole": [25, 47.3, 62.9, 87.6, 107.4, 129.1], + "Tetrahydrofuran": [25, 30.1, 35.4, 40, 45.7], + "decane": [25, 47.3, 62.9, 87.6, 107.4, 135], +} + +TEMP_CHOICES_C = { + "CF": (25, 41.3), + "CB": (25, 107.4), + "CB9:A1": (25, 107.4), + "CB8:A2": (25, 107.4), + "CB7:A3": (25, 107.4), + "1,4-Dichlorobenzene": (25, 135), + "1,2,4-Trihlorobenzene": (25, 135), + "o-xylene": (25, 119.6), + "m-xylene": (25, 114.6), + "p-xylene": (25, 113.8), + "mesitylene": (25, 135), + "toluene": (25, 87.7), + "1-Chloronaphthalene": (25, 135), + "anisole": (25, 129.1), + "Tetrahydrofuran": (25, 45.7), + "decane": (25, 135), } @app.callback( @@ -2407,30 +2454,100 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smile_string, if len(df) >= 2: X = df[['motor_speed_norm', 'temperature_norm', 'concentration_norm', - 'printing_gap_norm', 'precursor_volume_norm']].values + 'printing_gap_norm', 'precursor_volume_norm']].values pca = PCA(n_components=2) pca_result = pca.fit_transform(X) - reducer = umap.UMAP(random_state=42, n_neighbors=min(5, len(df)-1)) # Adjust n_neighbors + reducer = umap.UMAP(random_state=42, n_neighbors=min(5, len(df)-1)) umap_result = reducer.fit_transform(X) - pca_fig = px.scatter( - x=pca_result[:, 0], - y=pca_result[:, 1], - color=df['solvent'], - labels={'x': 'PCA Component 1', 'y': 'PCA Component 2'}, - title='PCA Visualization of Parameter Sets' - ) + n_background = 1000 + background_points = np.random.rand(n_background, X.shape[1]) + + background_pca = pca.transform(background_points) + background_umap = reducer.transform(background_points) + + pca_fig = go.Figure() + + x_min, x_max = background_pca[:,0].min(), background_pca[:,0].max() + y_min, y_max = background_pca[:,1].min(), background_pca[:,1].max() + + xi = np.linspace(x_min, x_max, 100) + yi = np.linspace(y_min, y_max, 100) + xi, yi = np.meshgrid(xi, yi) + + positions = np.vstack([xi.ravel(), yi.ravel()]) + values = np.vstack([background_pca[:,0], background_pca[:,1]]) + kernel = gaussian_kde(values) + z = np.reshape(kernel(positions).T, xi.shape) + + pca_fig.add_trace(go.Contour( + z=z, + x=xi[0], + y=yi[:,0], + colorscale='Blues', + showscale=False, + opacity=0.5, + name='Parameter Space Density' + )) - umap_fig = px.scatter( - x=umap_result[:, 0], - y=umap_result[:, 1], - color=df['solvent'], - labels={'x': 'UMAP Component 1', 'y': 'UMAP Component 2'}, - title='UMAP Visualization of Parameter Sets' + for solvent in df['solvent'].unique(): + mask = df['solvent'] == solvent + pca_fig.add_trace(go.Scatter( + x=pca_result[mask, 0], + y=pca_result[mask, 1], + mode='markers', + marker=dict(size=8), + name=solvent + )) + + pca_fig.update_layout( + title='PCA Visualization of Parameter Sets', + xaxis_title='PCA Component 1', + yaxis_title='PCA Component 2' ) + umap_fig = go.Figure() + + x_min, x_max = background_umap[:,0].min(), background_umap[:,0].max() + y_min, y_max = background_umap[:,1].min(), background_umap[:,1].max() + + xi = np.linspace(x_min, x_max, 100) + yi = np.linspace(y_min, y_max, 100) + xi, yi = np.meshgrid(xi, yi) + + positions = np.vstack([xi.ravel(), yi.ravel()]) + values = np.vstack([background_umap[:,0], background_umap[:,1]]) + kernel = gaussian_kde(values) + z = np.reshape(kernel(positions).T, xi.shape) + + umap_fig.add_trace(go.Contour( + z=z, + x=xi[0], + y=yi[:,0], + colorscale='Blues', + showscale=False, + opacity=0.5, + name='Parameter Space Density' + )) + + for solvent in df['solvent'].unique(): + mask = df['solvent'] == solvent + umap_fig.add_trace(go.Scatter( + x=umap_result[mask, 0], + y=umap_result[mask, 1], + mode='markers', + marker=dict(size=8), + name=solvent + )) + + umap_fig.update_layout( + title='UMAP Visualization of Parameter Sets', + xaxis_title='UMAP Component 1', + yaxis_title='UMAP Component 2' + ) + res = html.Div([ html.H3("Parameter Space Visualizations"), html.Div([ diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py index ad3b575..a6cc9af 100644 --- a/aamp_app/pages/sampler.py +++ b/aamp_app/pages/sampler.py @@ -4,14 +4,7 @@ dash.register_page(__name__, path="/sampler", name="Sampler", title="Sampler") -SOLV_NAMES = ["CF", "CB", "CB9:A1", "CB8:A2", "CB7:A3"] -TEMP_CHOICES_D = { - "CF": [25, 41.3], - "CB": [25, 47.3, 62.9, 87.6, 107.4], - "CB9:A1": [25, 47.3, 62.9, 87.6, 107.4], - "CB8:A2": [25, 47.3, 62.9, 87.6, 107.4], - "CB7:A3": [25, 47.3, 62.9, 87.6, 107.4] -} +SOLV_NAMES = ["CF", "CB", "CB9:A1", "CB8:A2", "CB7:A3", "1,4-Dichlorobenzene", "1,2,4-Trihlorobenzene", "o-xylene", "m-xylene", "p-xylene", "mesitylene", "toluene", "1-Chloronaphthalene", "anisole", "Tetrahydrofuran", "decane"] CONCEN_D = [2, 5, 10, 15, 20] PRINT_GAP_D = [50, 100] PREC_VOL_D = [6, 9, 12] @@ -20,6 +13,44 @@ PREC_VOL_C = (6.0, 12.0) CONCEN_C = (1, 5) +TEMP_CHOICES_D = { + "CF": [25, 41.3], + "CB": [25, 47.3, 62.9, 87.6, 107.4], + "CB9:A1": [25, 47.3, 62.9, 87.6, 107.4], + "CB8:A2": [25, 47.3, 62.9, 87.6, 107.4], + "CB7:A3": [25, 47.3, 62.9, 87.6, 107.4], + "1,4-Dichlorobenzene": [25, 47.3, 62.9, 87.6, 107.4, 135], + "1,2,4-Trihlorobenzene": [25, 47.3, 62.9, 87.6, 107.4, 135], + "o-xylene": [25, 47.3, 62.9, 87.6, 107.4, 119.6], + "m-xylene": [25, 47.3, 62.9, 87.6, 107.4, 114.6], + "p-xylene": [25, 47.3, 62.9, 87.6, 107.4, 113.8], + "mesitylene": [25, 47.3, 62.9, 87.6, 107.4, 135], + "toluene": [25, 47.3, 55.1, 62.9, 75.1, 87.7], + "1-Chloronaphthalene": [25, 47.3, 62.9, 87.6, 107.4, 135], + "anisole": [25, 47.3, 62.9, 87.6, 107.4, 129.1], + "Tetrahydrofuran": [25, 30.1, 35.4, 40, 45.7], + "decane": [25, 47.3, 62.9, 87.6, 107.4, 135], +} + +TEMP_CHOICES_C = { + "CF": (25, 41.3), + "CB": (25, 107.4), + "CB9:A1": (25, 107.4), + "CB8:A2": (25, 107.4), + "CB7:A3": (25, 107.4), + "1,4-Dichlorobenzene": (25, 135), + "1,2,4-Trihlorobenzene": (25, 135), + "o-xylene": (25, 119.6), + "m-xylene": (25, 114.6), + "p-xylene": (25, 113.8), + "mesitylene": (25, 135), + "toluene": (25, 87.7), + "1-Chloronaphthalene": (25, 135), + "anisole": (25, 129.1), + "Tetrahydrofuran": (25, 45.7), + "decane": (25, 135), +} + layout = html.Div( [ html.H1("Sampler"), From b5017f2647e70aeabe725dbb6cf70bdc42aa3155 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Thu, 27 Mar 2025 00:45:35 -0700 Subject: [PATCH 083/125] added the option to include custom values for all dropdowns --- aamp_app/app.py | 110 ++++++++++++++++++++++++++------------ aamp_app/pages/sampler.py | 97 +++++++++++++++++++++------------ 2 files changed, 139 insertions(+), 68 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index 64a4048..c7b7791 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -10,7 +10,7 @@ from dash import html from dash import dash_table import dash_ag_grid as dag -from dash.dependencies import Input, Output, State +from dash.dependencies import Input, Output, State, MATCH, ALL import dash_bootstrap_components as dbc import os, signal import inspect @@ -2323,28 +2323,80 @@ def upload_image(n_clicks, sample_number, motor_speed, temperature, concentratio } @app.callback( + Output({"type": "sampler-dropdown", "id": MATCH}, "options"), + Output({"type": "sampler-dropdown", "id": MATCH}, "value"), + Input({"type": "add-custom-button", "id": MATCH}, "n_clicks"), + State({"type": "custom-input", "id": MATCH}, "value"), + State({"type": "sampler-dropdown", "id": MATCH}, "options"), + State({"type": "sampler-dropdown", "id": MATCH}, "value"), + prevent_initial_call=True +) +def add_custom_value(n_clicks, custom_value, current_options, current_value): + if custom_value is not None: + new_option = {"label": str(custom_value), "value": custom_value} + if new_option not in current_options: + current_options.append(new_option) + current_value = current_value + [custom_value] if current_value else [custom_value] + return current_options, current_value + +@app.callback( + Output({"type": "sampler-temp-dropdown", "index": MATCH}, "options"), + Output({"type": "sampler-temp-dropdown", "index": MATCH}, "value"), + Input({"type": "add-custom-temp", "index": MATCH}, "n_clicks"), + State({"type": "custom-temp-input", "index": MATCH}, "value"), + State({"type": "sampler-temp-dropdown", "index": MATCH}, "options"), + State({"type": "sampler-temp-dropdown", "index": MATCH}, "value"), + prevent_initial_call=True +) +def add_custom_temperature(n_clicks, custom_temp, current_options, current_value): + if custom_temp is not None: + new_option = {"label": str(custom_temp), "value": custom_temp} + if new_option not in current_options: + current_options.append(new_option) + current_value = current_value + [custom_temp] if current_value else [custom_temp] + return current_options, current_value + +@app.callback( + Output("temperature-container", "style"), Output("sampler-temperature-options", "children"), - Input("sampler-solvent-dropdown", "value"), + Input("sampler-solvent-dropdown", "value") ) def update_temperature_options(selected_solvents): if not selected_solvents: - return [html.P("Temperature options will appear after selecting solvent(s)")] + return {'display': 'none'}, [] temp_options = [] for solvent in selected_solvents: temp_options.append( html.Div([ html.H6(f"Temperature options for {solvent}:"), - dcc.Dropdown( - id=f"sampler-temp-{solvent}", - options=[{"label": temp, "value": temp} for temp in TEMP_CHOICES_D[solvent]], - value=TEMP_CHOICES_D[solvent], - multi=True, + dbc.Row( + [ + dbc.Col( + [ + dcc.Dropdown( + id={"type": "sampler-temp-dropdown", "index": solvent}, + options=[{"label": str(temp), "value": temp} for temp in TEMP_CHOICES_D[solvent]], + value=TEMP_CHOICES_D[solvent], + multi=True, + ), + ], + width=8, + ), + dbc.Col( + dbc.InputGroup([ + dbc.Input(id={"type": "custom-temp-input", "index": solvent}, type="number"), + dbc.Button("Add", id={"type": "add-custom-temp", "index": solvent}, size="sm"), + ]), + width=4, + ) + ], + className="mb-3", ), ]) ) - return temp_options + return {'display': 'block'}, temp_options @app.callback( Output("sampler-results-table", "children"), @@ -2360,30 +2412,22 @@ def update_temperature_options(selected_solvents): State("sampler-mw", "value"), State("sampler-pdi", "value"), State("sampler-solvent-dropdown", "value"), - State("sampler-concentration-range", "value"), + State({"type": "sampler-temp-dropdown", "index": ALL}, "value"), + State({"type": "sampler-temp-dropdown", "index": ALL}, "id"), + State({"type": "sampler-dropdown", "id": "concentration"}, "value"), State("sampler-motor-speed-min", "value"), State("sampler-motor-speed-max", "value"), - State("sampler-printing-gap-dropdown", "value"), - State("sampler-precursor-volume", "value"), + State({"type": "sampler-dropdown", "id": "printing-gap"}, "value"), + State({"type": "sampler-dropdown", "id": "precursor-volume"}, "value"), State("sampler-method-dropdown", "value"), State("sampler-num-samples", "value"), ], prevent_initial_call=True, ) def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smile_string, mw, pdi, - solvents, concentration_range, motor_speed_min, motor_speed_max, - printing_gaps, precursor_vol, sampling_method, num_samples): - print("generate_parameter_sets") - # print the parameters inputted for debugging purposes - print(f"Solvents: {solvents}") - print(f"Concentration Range: {concentration_range}") - print(f"Motor Speed Min: {motor_speed_min}") - print(f"Motor Speed Max: {motor_speed_max}") - print(f"Printing Gaps: {printing_gaps}") - print(f"Precursor Volume: {precursor_vol}") - print(f"Sampling Method: {sampling_method}") - print(f"Number of Samples: {num_samples}") - + solvents, temperatures, temp_ids, concentration_range, + motor_speed_min, motor_speed_max, printing_gaps, precursor_vol, + sampling_method, num_samples): if not solvents: return None, "Please select at least one solvent.", True, "danger", True @@ -2391,8 +2435,10 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smile_string, parameter_sets = [] sample_count = 1 + solvent_temps = {temp_id['index']: temps for temp_id, temps in zip(temp_ids, temperatures)} + print(solvent_temps) for solvent in solvents: - temp_options = TEMP_CHOICES_D.get(solvent, [25]) + temp_options = solvent_temps.get(solvent, [25]) for _ in range(num_samples): log_speed_min = math.log10(motor_speed_min) @@ -2406,11 +2452,11 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smile_string, precursor_volume = random.choice(precursor_vol) if precursor_vol else random.choice(PREC_VOL_D) # make all the parameters normalized between 0 and 1 using min-max normalization - motor_speed_norm = (math.log10(motor_speed) - log_speed_min) / (log_speed_max - log_speed_min) - temperature_norm = (temperature - min(temp_options)) / (max(temp_options) - min(temp_options)) - concentration_norm = (concentration - min(concentration_range)) / (max(concentration_range) - min(concentration_range)) - printing_gap_norm = (printing_gap - min(printing_gaps)) / (max(printing_gaps) - min(printing_gaps)) - precursor_volume_norm = (precursor_volume - min(precursor_vol)) / (max(precursor_vol) - min(precursor_vol)) + motor_speed_norm = (math.log10(motor_speed) - log_speed_min) / (log_speed_max - log_speed_min) if log_speed_max - log_speed_min != 0 else 0 + temperature_norm = (temperature - min(temp_options)) / (max(temp_options) - min(temp_options)) if max(temp_options) - min(temp_options) != 0 else 0 + concentration_norm = (concentration - min(concentration_range)) / (max(concentration_range) - min(concentration_range)) if max(concentration_range) - min(concentration_range) != 0 else 0 + printing_gap_norm = (printing_gap - min(printing_gaps)) / (max(printing_gaps) - min(printing_gaps)) if max(printing_gaps) - min(printing_gaps) != 0 else 0 + precursor_volume_norm = (precursor_volume - min(precursor_vol)) / (max(precursor_vol) - min(precursor_vol)) if max(precursor_vol) - min(precursor_vol) != 0 else 0 parameter_set = { "sample_no": sample_count, @@ -2549,7 +2595,6 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smile_string, ) res = html.Div([ - html.H3("Parameter Space Visualizations"), html.Div([ html.Div([ dcc.Graph(figure=pca_fig) @@ -2562,7 +2607,6 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smile_string, ]) else: res = html.Div([ - html.H3("Parameter Space Visualizations"), html.P("Not enough data points for visualization. Generate more samples."), html.H3("Generated Parameter Sets"), table diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py index a6cc9af..70d10e7 100644 --- a/aamp_app/pages/sampler.py +++ b/aamp_app/pages/sampler.py @@ -63,7 +63,6 @@ ), html.Div( [ - html.H2("Campaign Metadata", className="mt-4"), dbc.Row( [ dbc.Col( @@ -71,7 +70,7 @@ html.H5("Campaign Name"), dbc.Input(id="sampler-campaign-name", type="text", placeholder="Enter campaign name"), ], - width=3, + width=2, ), dbc.Col( [ @@ -89,22 +88,21 @@ ), dbc.Col( [ - html.H5("Molecular Weight (MW)"), + html.H5("Molecular Weight"), dbc.Input(id="sampler-mw", type="number", placeholder="Enter MW", min=0), ], - width=3, + width=2, ), dbc.Col( [ - html.H5("Polydispersity Index (PDI)"), + html.H5("Polydispersity Index"), dbc.Input(id="sampler-pdi", type="number", placeholder="Enter PDI", min=1, step=0.01), ], - width=3, + width=2, ), ], className="mb-3", ), - html.H2("Dependent Variables"), dbc.Row( [ dbc.Col( @@ -119,53 +117,52 @@ width=6, ), dbc.Col( - [ - html.H5("Temperature"), - html.Div(id="sampler-temperature-options", children=[]), - ], + html.Div( + [ + html.H5("Temperature"), + html.Div(id="sampler-temperature-options", children=[]), + ], + id="temperature-container", + style={'display': 'none'} + ), width=6, ), ], className="mb-3", ), + html.H5("Concentration Range"), dbc.Row( [ dbc.Col( [ - html.H5("Concentration Range"), dcc.Dropdown( - id="sampler-concentration-range", + id={"type": "sampler-dropdown", "id": "concentration"}, options=[{"label": str(concen), "value": concen} for concen in CONCEN_D], multi=True, value=CONCEN_D, ), ], - width=12, + width=4, ), + dbc.Col( + dbc.InputGroup([ + dbc.Input(id={"type": "custom-input", "id": "concentration"}, type="number", placeholder="Custom concentration"), + dbc.Button("Add", id={"type": "add-custom-button", "id": "concentration"}, size="sm"), + ]), + width=4, + ) ], className="mb-3", ), - - html.H2("Independent Variables"), + + # Modify the printing gap section + html.H5("Printing Gap"), dbc.Row( [ dbc.Col( [ - html.H5("Motor Speed Range"), - dbc.InputGroup( - [ - dbc.Input(id="sampler-motor-speed-min", type="number", placeholder="Min", value=SPEED_C[0]), - dbc.Input(id="sampler-motor-speed-max", type="number", placeholder="Max", value=SPEED_C[1]), - ] - ), - ], - width=4, - ), - dbc.Col( - [ - html.H5("Printing Gap"), dcc.Dropdown( - id="sampler-printing-gap-dropdown", + id={"type": "sampler-dropdown", "id": "printing-gap"}, options=[{"label": str(gap), "value": gap} for gap in PRINT_GAP_D], multi=True, value=PRINT_GAP_D, @@ -173,11 +170,25 @@ ], width=4, ), + dbc.Col( + dbc.InputGroup([ + dbc.Input(id={"type": "custom-input", "id": "printing-gap"}, type="number", placeholder="Custom printing gap"), + dbc.Button("Add", id={"type": "add-custom-button", "id": "printing-gap"}, size="sm"), + ]), + width=4, + ) + ], + className="mb-3", + ), + + # Modify the precursor volume section + html.H5("Precursor Volume"), + dbc.Row( + [ dbc.Col( [ - html.H5("Precursor Volume"), dcc.Dropdown( - id="sampler-precursor-volume", + id={"type": "sampler-dropdown", "id": "precursor-volume"}, options=[{"label": str(vol), "value": vol} for vol in PREC_VOL_D], multi=True, value=PREC_VOL_D, @@ -185,11 +196,28 @@ ], width=4, ), + dbc.Col( + dbc.InputGroup([ + dbc.Input(id={"type": "custom-input", "id": "precursor-volume"}, type="number", placeholder="Custom precursor volume"), + dbc.Button("Add", id={"type": "add-custom-button", "id": "precursor-volume"}, size="sm"), + ]), + width=4, + ) ], className="mb-3", ), - - html.H2("Sampling Configuration"), + dbc.Col( + [ + html.H5("Motor Speed Range"), + dbc.InputGroup( + [ + dbc.Input(id="sampler-motor-speed-min", type="number", placeholder="Min", value=SPEED_C[0]), + dbc.Input(id="sampler-motor-speed-max", type="number", placeholder="Max", value=SPEED_C[1]), + ] + ), + ], + width=4, + ), dbc.Row( [ dbc.Col( @@ -224,7 +252,6 @@ className="mb-3", ), - html.H2("Generated Parameter Sets"), html.Div(id="sampler-results-table", className="mb-3"), dbc.Button( From 0ab2d738a2480b6b1cfd666a201b90351fe6bd61 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Fri, 28 Mar 2025 12:01:54 -0700 Subject: [PATCH 084/125] added suggestions for campaign metadata inputs and image input --- aamp_app/app.py | 21 ++++++++++-- aamp_app/pages/sampler.py | 68 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index c7b7791..ad91418 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -2356,6 +2356,21 @@ def add_custom_temperature(n_clicks, custom_temp, current_options, current_value current_value = current_value + [custom_temp] if current_value else [custom_temp] return current_options, current_value +@app.callback( + Output("sampler-polymer-image-preview", "children"), + Input("sampler-polymer-image", "contents"), + State("sampler-polymer-image", "filename"), + prevent_initial_call=True +) +def update_image_preview(contents, filename): + if contents is None: + return [] + + return html.Div([ + html.Img(src=contents, style={'maxHeight': '200px', 'maxWidth': '100%'}), + html.P(filename) + ]) + @app.callback( Output("temperature-container", "style"), Output("sampler-temperature-options", "children"), @@ -2408,7 +2423,7 @@ def update_temperature_options(selected_solvents): [ State("sampler-campaign-name", "value"), State("sampler-polymer-name", "value"), - State("sampler-smile-string", "value"), + State("sampler-smiles-string", "value"), State("sampler-mw", "value"), State("sampler-pdi", "value"), State("sampler-solvent-dropdown", "value"), @@ -2424,7 +2439,7 @@ def update_temperature_options(selected_solvents): ], prevent_initial_call=True, ) -def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smile_string, mw, pdi, +def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string, mw, pdi, solvents, temperatures, temp_ids, concentration_range, motor_speed_min, motor_speed_max, printing_gaps, precursor_vol, sampling_method, num_samples): @@ -2462,7 +2477,7 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smile_string, "sample_no": sample_count, "campaign_name": campaign_name, "polymer_name": polymer_name, - "smile_string": smile_string, + "smiles_string": smiles_string, "mw": mw, "pdi": pdi, "motor_speed": motor_speed, diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py index 70d10e7..c46c83f 100644 --- a/aamp_app/pages/sampler.py +++ b/aamp_app/pages/sampler.py @@ -53,6 +53,33 @@ layout = html.Div( [ + html.Datalist( + id="smiles-suggestions", + children=[ + html.Option(value="{O=C(OCC(CCCC)[*]CCCCC)C1=C(C2=CC=CS2)SC(C(S3)=CC(C(OCC(CCCCCC)CCCC)=O)=C3C4=CC=[*]S4)=C1}"), + html.Option(value="{COCCOCCOCCOC1=C(C2=C(OCCOCCOCCOC)C=C(S2)[*])SC(C3=CC4=C(S3)C=C(S4)[*])=C1}") + ] + ), + html.Datalist( + id="polymer-suggestions", + children=[ + html.Option(value="PDCBT"), + html.Option(value="P(g42T-TT)") + ] + ), + html.Datalist( + id="mw-suggestions", + children=[ + html.Option(value="60000"), + html.Option(value="40000") + ] + ), + html.Datalist( + id="pdi-suggestions", + children=[ + html.Option(value="2.5") + ] + ), html.H1("Sampler"), dbc.Alert( id="sampler-alert", @@ -75,34 +102,65 @@ dbc.Col( [ html.H5("Polymer Name"), - dbc.Input(id="sampler-polymer-name", type="text", placeholder="Enter polymer name"), + dbc.Input(id="sampler-polymer-name", type="text", placeholder="Enter polymer name", list="polymer-suggestions"), ], width=3, ), dbc.Col( [ - html.H5("SMILE String"), - dbc.Input(id="sampler-smile-string", type="text", placeholder="Enter SMILE string"), + html.H5("SMILES String"), + dbc.Input(id="sampler-smiles-string", type="text", placeholder="Enter SMILES string", list="smiles-suggestions"), ], width=3, ), dbc.Col( [ html.H5("Molecular Weight"), - dbc.Input(id="sampler-mw", type="number", placeholder="Enter MW", min=0), + dbc.Input(id="sampler-mw", type="number", placeholder="Enter MW", min=0, list="mw-suggestions"), ], width=2, ), dbc.Col( [ html.H5("Polydispersity Index"), - dbc.Input(id="sampler-pdi", type="number", placeholder="Enter PDI", min=1, step=0.01), + dbc.Input(id="sampler-pdi", type="number", placeholder="Enter PDI", min=1, step=0.01, list="pdi-suggestions"), ], width=2, ), ], className="mb-3", ), + dbc.Row( + [ + dbc.Col( + [ + html.H5("Upload Polymer Image"), + dcc.Upload( + id="sampler-polymer-image", + children=html.Div([ + 'Drag and Drop or ', + html.A('Select an Image') + ]), + style={ + 'width': '100%', + 'height': '60px', + 'lineHeight': '60px', + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center', + 'margin': '10px 0' + }, + multiple=False, + accept='image/*' + ), + html.Div(id="sampler-polymer-image-preview"), + ], + width=12, + ), + ], + className="mb-3", + ), dbc.Row( [ dbc.Col( From 4810f4873b5d7a95ac8382e5f4d9637badf0d46d Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Fri, 28 Mar 2025 14:32:18 -0700 Subject: [PATCH 085/125] Added continuous options for every parameter, including individual temperature options for each solvent --- aamp_app/app.py | 222 ++++++++++++++++++++++++----- aamp_app/pages/sampler.py | 286 ++++++++++++++++++++++++++++++-------- 2 files changed, 417 insertions(+), 91 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index ad91418..e7e3f5d 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -2322,6 +2322,7 @@ def upload_image(n_clicks, sample_number, motor_speed, temperature, concentratio "decane": (25, 135), } +# adding custom discrete parameter options @app.callback( Output({"type": "sampler-dropdown", "id": MATCH}, "options"), Output({"type": "sampler-dropdown", "id": MATCH}, "value"), @@ -2356,6 +2357,18 @@ def add_custom_temperature(n_clicks, custom_temp, current_options, current_value current_value = current_value + [custom_temp] if current_value else [custom_temp] return current_options, current_value +# updating based on selections and inputs +@app.callback( + Output({"type": "discrete-container", "param": MATCH}, "style"), + Output({"type": "continuous-container", "param": MATCH}, "style"), + Input({"type": "toggle", "param": MATCH}, "value") +) +def toggle_input_type(is_continuous): + if is_continuous: + return {"display": "none"}, {"display": "block"} + else: + return {"display": "block"}, {"display": "none"} + @app.callback( Output("sampler-polymer-image-preview", "children"), Input("sampler-polymer-image", "contents"), @@ -2384,29 +2397,71 @@ def update_temperature_options(selected_solvents): for solvent in selected_solvents: temp_options.append( html.Div([ - html.H6(f"Temperature options for {solvent}:"), dbc.Row( [ + dbc.Col([html.H6(f"Temperature for {solvent}:")], width=8), dbc.Col( [ - dcc.Dropdown( - id={"type": "sampler-temp-dropdown", "index": solvent}, - options=[{"label": str(temp), "value": temp} for temp in TEMP_CHOICES_D[solvent]], - value=TEMP_CHOICES_D[solvent], - multi=True, + dbc.Switch( + id={"type": "toggle", "param": f"temp-{solvent}"}, + label="Continuous", + value=False, ), ], - width=8, + width=2, ), - dbc.Col( - dbc.InputGroup([ - dbc.Input(id={"type": "custom-temp-input", "index": solvent}, type="number"), - dbc.Button("Add", id={"type": "add-custom-temp", "index": solvent}, size="sm"), - ]), - width=4, - ) ], - className="mb-3", + className="mb-2", + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + dcc.Dropdown( + id={"type": "sampler-temp-dropdown", "index": solvent}, + options=[{"label": str(temp), "value": temp} for temp in TEMP_CHOICES_D[solvent]], + multi=True, + value=TEMP_CHOICES_D[solvent], + ), + ], + width=8, + ), + dbc.Col( + dbc.InputGroup([ + dbc.Input(id={"type": "custom-temp-input", "index": solvent}, type="number", placeholder="Custom temperature"), + dbc.Button("Add", id={"type": "add-custom-temp", "index": solvent}, size="sm"), + ]), + width=4, + ) + ], + className="mb-3", + ), + ], + id={"type": "discrete-container", "param": f"temp-{solvent}"}, + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + dbc.InputGroup( + [ + dbc.Input(id={"type": "temp-min", "index": solvent}, type="number", placeholder="Min", value=TEMP_CHOICES_C[solvent][0]), + dbc.Input(id={"type": "temp-max", "index": solvent}, type="number", placeholder="Max", value=TEMP_CHOICES_C[solvent][1]), + ] + ), + ], + width=4, + ), + ], + className="mb-3", + ), + ], + id={"type": "continuous-container", "param": f"temp-{solvent}"}, + style={"display": "none"}, ), ]) ) @@ -2427,51 +2482,146 @@ def update_temperature_options(selected_solvents): State("sampler-mw", "value"), State("sampler-pdi", "value"), State("sampler-solvent-dropdown", "value"), + State({"type": "toggle", "param": ALL}, "value"), + State({"type": "toggle", "param": ALL}, "id"), State({"type": "sampler-temp-dropdown", "index": ALL}, "value"), State({"type": "sampler-temp-dropdown", "index": ALL}, "id"), + State({"type": "temp-min", "index": ALL}, "value"), + State({"type": "temp-min", "index": ALL}, "id"), + State({"type": "temp-max", "index": ALL}, "value"), + State({"type": "temp-max", "index": ALL}, "id"), State({"type": "sampler-dropdown", "id": "concentration"}, "value"), - State("sampler-motor-speed-min", "value"), - State("sampler-motor-speed-max", "value"), + State({"type": "toggle", "param": "concentration"}, "value"), + State("concentration-min", "value"), + State("concentration-max", "value"), State({"type": "sampler-dropdown", "id": "printing-gap"}, "value"), + State({"type": "toggle", "param": "printing-gap"}, "value"), + State("printing-gap-min", "value"), + State("printing-gap-max", "value"), State({"type": "sampler-dropdown", "id": "precursor-volume"}, "value"), + State({"type": "toggle", "param": "precursor-volume"}, "value"), + State("precursor-volume-min", "value"), + State("precursor-volume-max", "value"), + State({"type": "sampler-dropdown", "id": "motor-speed"}, "value"), + State({"type": "toggle", "param": "motor-speed"}, "value"), + State("motor-speed-min", "value"), + State("motor-speed-max", "value"), State("sampler-method-dropdown", "value"), State("sampler-num-samples", "value"), ], prevent_initial_call=True, ) def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string, mw, pdi, - solvents, temperatures, temp_ids, concentration_range, - motor_speed_min, motor_speed_max, printing_gaps, precursor_vol, + solvents, toggle_values, toggle_ids, temp_dropdown_values, + temp_dropdown_ids, temp_min_values, temp_min_ids, + temp_max_values, temp_max_ids, concentration_range, + concentration_toggle, concentration_min, concentration_max, + printing_gaps, printing_gap_toggle, printing_gap_min, + printing_gap_max, precursor_vol, precursor_vol_toggle, + precursor_vol_min, precursor_vol_max, motor_speeds, + motor_speed_toggle, motor_speed_min, motor_speed_max, sampling_method, num_samples): if not solvents: return None, "Please select at least one solvent.", True, "danger", True try: + temp_toggles = {} + temp_discrete_values = {} + temp_min = {} + temp_max = {} + + for toggle_value, toggle_id in zip(toggle_values, toggle_ids): + param = toggle_id["param"] + if param.startswith("temp-"): + solvent = param.replace("temp-", "") + temp_toggles[solvent] = toggle_value + + for dropdown_value, dropdown_id in zip(temp_dropdown_values, temp_dropdown_ids): + solvent = dropdown_id["index"] + temp_discrete_values[solvent] = dropdown_value + + for min_value, min_id in zip(temp_min_values, temp_min_ids): + solvent = min_id["index"] + temp_min[solvent] = min_value + + for max_value, max_id in zip(temp_max_values, temp_max_ids): + solvent = max_id["index"] + temp_max[solvent] = max_value + parameter_sets = [] sample_count = 1 - solvent_temps = {temp_id['index']: temps for temp_id, temps in zip(temp_ids, temperatures)} - print(solvent_temps) for solvent in solvents: - temp_options = solvent_temps.get(solvent, [25]) + is_temp_continuous = temp_toggles.get(solvent, False) for _ in range(num_samples): - log_speed_min = math.log10(motor_speed_min) - log_speed_max = math.log10(motor_speed_max) - log_speed = log_speed_min + random.random() * (log_speed_max - log_speed_min) - motor_speed = round(10 ** log_speed, 2) - - temperature = round(random.choice(temp_options)) - concentration = random.choice(concentration_range) if concentration_range else random.choice(CONCEN_D) - printing_gap = random.choice(printing_gaps) if printing_gaps else 50 - precursor_volume = random.choice(precursor_vol) if precursor_vol else random.choice(PREC_VOL_D) + if is_temp_continuous: + min_temp = temp_min.get(solvent, TEMP_CHOICES_C[solvent][0]) + max_temp = temp_max.get(solvent, TEMP_CHOICES_C[solvent][1]) + temperature = round(random.uniform(min_temp, max_temp)) + else: + temp_options = temp_discrete_values.get(solvent, TEMP_CHOICES_D[solvent]) + temperature = round(random.choice(temp_options)) + + if concentration_toggle: + concentration = round(random.uniform(concentration_min, concentration_max), 2) + else: + concentration = random.choice(concentration_range) if concentration_range else random.choice(CONCEN_D) + + if printing_gap_toggle: + printing_gap = round(random.uniform(printing_gap_min, printing_gap_max)) + else: + printing_gap = random.choice(printing_gaps) if printing_gaps else random.choice(PRINT_GAP_D) + + if precursor_vol_toggle: + precursor_volume = round(random.uniform(precursor_vol_min, precursor_vol_max), 1) + else: + precursor_volume = random.choice(precursor_vol) if precursor_vol else random.choice(PREC_VOL_D) + + if motor_speed_toggle: + log_speed_min = math.log10(motor_speed_min) + log_speed_max = math.log10(motor_speed_max) + log_speed = log_speed_min + random.random() * (log_speed_max - log_speed_min) + motor_speed = round(10 ** log_speed, 2) + else: + motor_speed = random.choice(motor_speeds) if motor_speeds else random.choice(MOTOR_SPEEDS_D) # make all the parameters normalized between 0 and 1 using min-max normalization - motor_speed_norm = (math.log10(motor_speed) - log_speed_min) / (log_speed_max - log_speed_min) if log_speed_max - log_speed_min != 0 else 0 - temperature_norm = (temperature - min(temp_options)) / (max(temp_options) - min(temp_options)) if max(temp_options) - min(temp_options) != 0 else 0 - concentration_norm = (concentration - min(concentration_range)) / (max(concentration_range) - min(concentration_range)) if max(concentration_range) - min(concentration_range) != 0 else 0 - printing_gap_norm = (printing_gap - min(printing_gaps)) / (max(printing_gaps) - min(printing_gaps)) if max(printing_gaps) - min(printing_gaps) != 0 else 0 - precursor_volume_norm = (precursor_volume - min(precursor_vol)) / (max(precursor_vol) - min(precursor_vol)) if max(precursor_vol) - min(precursor_vol) != 0 else 0 + if motor_speed_toggle: + log_speed_min = math.log10(motor_speed_min) + log_speed_max = math.log10(motor_speed_max) + motor_speed_norm = (math.log10(motor_speed) - log_speed_min) / (log_speed_max - log_speed_min) if log_speed_max - log_speed_min != 0 else 0 + else: + motor_speeds_list = motor_speeds if motor_speeds else MOTOR_SPEEDS_D + log_min = math.log10(min(motor_speeds_list)) + log_max = math.log10(max(motor_speeds_list)) + motor_speed_norm = (math.log10(motor_speed) - log_min) / (log_max - log_min) if log_max - log_min != 0 else 0 + + if is_temp_continuous: + min_temp = temp_min.get(solvent, TEMP_CHOICES_C[solvent][0]) + max_temp = temp_max.get(solvent, TEMP_CHOICES_C[solvent][1]) + temperature_norm = (temperature - min_temp) / (max_temp - min_temp) + else: + temp_options = temp_discrete_values.get(solvent, TEMP_CHOICES_D[solvent]) + temperature_norm = (temperature - min(temp_options)) / (max(temp_options) - min(temp_options)) + + if concentration_toggle: + concentration_norm = (concentration - concentration_min) / (concentration_max - concentration_min) if concentration_max - concentration_min != 0 else 0 + else: + concentration_list = concentration_range if concentration_range else CONCEN_D + concentration_norm = (concentration - min(concentration_list)) / (max(concentration_list) - min(concentration_list)) if max(concentration_list) - min(concentration_list) != 0 else 0 + + if printing_gap_toggle: + printing_gap_norm = (printing_gap - printing_gap_min) / (printing_gap_max - printing_gap_min) if printing_gap_max - printing_gap_min != 0 else 0 + else: + printing_gaps_list = printing_gaps if printing_gaps else PRINT_GAP_D + printing_gap_norm = (printing_gap - min(printing_gaps_list)) / (max(printing_gaps_list) - min(printing_gaps_list)) if max(printing_gaps_list) - min(printing_gaps_list) != 0 else 0 + + if precursor_vol_toggle: + precursor_volume_norm = (precursor_volume - precursor_vol_min) / (precursor_vol_max - precursor_vol_min) if precursor_vol_max - precursor_vol_min != 0 else 0 + else: + precursor_vol_list = precursor_vol if precursor_vol else PREC_VOL_D + precursor_volume_norm = (precursor_volume - min(precursor_vol_list)) / (max(precursor_vol_list) - min(precursor_vol_list)) if max(precursor_vol_list) - min(precursor_vol_list) != 0 else 0 parameter_set = { "sample_no": sample_count, diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py index c46c83f..765c1da 100644 --- a/aamp_app/pages/sampler.py +++ b/aamp_app/pages/sampler.py @@ -188,93 +188,269 @@ ], className="mb-3", ), - html.H5("Concentration Range"), dbc.Row( [ + dbc.Col([html.H5("Concentration Range")], width=2), dbc.Col( [ - dcc.Dropdown( - id={"type": "sampler-dropdown", "id": "concentration"}, - options=[{"label": str(concen), "value": concen} for concen in CONCEN_D], - multi=True, - value=CONCEN_D, + dbc.Switch( + id={"type": "toggle", "param": "concentration"}, + label="Continuous", + value=False, ), ], - width=4, + width=2, ), - dbc.Col( - dbc.InputGroup([ - dbc.Input(id={"type": "custom-input", "id": "concentration"}, type="number", placeholder="Custom concentration"), - dbc.Button("Add", id={"type": "add-custom-button", "id": "concentration"}, size="sm"), - ]), - width=4, - ) ], - className="mb-3", + className="mb-2", + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + dcc.Dropdown( + id={"type": "sampler-dropdown", "id": "concentration"}, + options=[{"label": str(concen), "value": concen} for concen in CONCEN_D], + multi=True, + value=CONCEN_D, + ), + ], + width=4, + ), + dbc.Col( + dbc.InputGroup([ + dbc.Input(id="custom-concentration-input", type="number", placeholder="Custom concentration"), + dbc.Button("Add", id="add-custom-concentration", size="sm"), + ]), + width=4, + ) + ], + className="mb-3", + ), + ], + id={"type": "discrete-container", "param": "concentration"}, + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + dbc.InputGroup( + [ + dbc.Input(id="concentration-min", type="number", placeholder="Min", value=CONCEN_C[0]), + dbc.Input(id="concentration-max", type="number", placeholder="Max", value=CONCEN_C[1]), + ] + ), + ], + width=4, + ), + ], + className="mb-3", + ), + ], + id={"type": "continuous-container", "param": "concentration"}, + style={"display": "none"}, ), - - # Modify the printing gap section - html.H5("Printing Gap"), dbc.Row( [ + dbc.Col([html.H5("Printing Gap")], width=2), dbc.Col( [ - dcc.Dropdown( - id={"type": "sampler-dropdown", "id": "printing-gap"}, - options=[{"label": str(gap), "value": gap} for gap in PRINT_GAP_D], - multi=True, - value=PRINT_GAP_D, + dbc.Switch( + id={"type": "toggle", "param": "printing-gap"}, + label="Continuous", + value=False, ), ], - width=4, + width=2, ), - dbc.Col( - dbc.InputGroup([ - dbc.Input(id={"type": "custom-input", "id": "printing-gap"}, type="number", placeholder="Custom printing gap"), - dbc.Button("Add", id={"type": "add-custom-button", "id": "printing-gap"}, size="sm"), - ]), - width=4, - ) ], - className="mb-3", + className="mb-2", + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + dcc.Dropdown( + id={"type": "sampler-dropdown", "id": "printing-gap"}, + options=[{"label": str(gap), "value": gap} for gap in PRINT_GAP_D], + multi=True, + value=PRINT_GAP_D, + ), + ], + width=4, + ), + dbc.Col( + dbc.InputGroup([ + dbc.Input(id={"type": "custom-input", "id": "printing-gap"}, type="number", placeholder="Custom printing gap"), + dbc.Button("Add", id={"type": "add-custom-button", "id": "printing-gap"}, size="sm"), + ]), + width=4, + ) + ], + className="mb-3", + ), + ], + id={"type": "discrete-container", "param": "printing-gap"}, + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + dbc.InputGroup( + [ + dbc.Input(id="printing-gap-min", type="number", placeholder="Min", value=PRINT_GAP_D[0]), + dbc.Input(id="printing-gap-max", type="number", placeholder="Max", value=PRINT_GAP_D[-1]), + ] + ), + ], + width=4, + ), + ], + className="mb-3", + ), + ], + id={"type": "continuous-container", "param": "printing-gap"}, + style={"display": "none"}, ), - - # Modify the precursor volume section - html.H5("Precursor Volume"), dbc.Row( [ + dbc.Col([html.H5("Precursor Volume")], width=2), dbc.Col( [ - dcc.Dropdown( - id={"type": "sampler-dropdown", "id": "precursor-volume"}, - options=[{"label": str(vol), "value": vol} for vol in PREC_VOL_D], - multi=True, - value=PREC_VOL_D, + dbc.Switch( + id={"type": "toggle", "param": "precursor-volume"}, + label="Continuous", + value=False, + ), + ], + width=2, + ), + ], + className="mb-2", + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + dcc.Dropdown( + id={"type": "sampler-dropdown", "id": "precursor-volume"}, + options=[{"label": str(vol), "value": vol} for vol in PREC_VOL_D], + multi=True, + value=PREC_VOL_D, + ), + ], + width=4, + ), + dbc.Col( + dbc.InputGroup([ + dbc.Input(id={"type": "custom-input", "id": "precursor-volume"}, type="number", placeholder="Custom precursor volume"), + dbc.Button("Add", id={"type": "add-custom-button", "id": "precursor-volume"}, size="sm"), + ]), + width=4, + ) + ], + className="mb-3", + ), + ], + id={"type": "discrete-container", "param": "precursor-volume"}, + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + dbc.InputGroup( + [ + dbc.Input(id="precursor-volume-min", type="number", placeholder="Min", value=PREC_VOL_C[0]), + dbc.Input(id="precursor-volume-max", type="number", placeholder="Max", value=PREC_VOL_C[1]), + ] + ), + ], + width=4, ), ], - width=4, + className="mb-3", ), + ], + id={"type": "continuous-container", "param": "precursor-volume"}, + style={"display": "none"}, + ), + dbc.Row( + [ + dbc.Col([html.H5("Motor Speed Range")], width=2), dbc.Col( - dbc.InputGroup([ - dbc.Input(id={"type": "custom-input", "id": "precursor-volume"}, type="number", placeholder="Custom precursor volume"), - dbc.Button("Add", id={"type": "add-custom-button", "id": "precursor-volume"}, size="sm"), - ]), - width=4, - ) + [ + dbc.Switch( + id={"type": "toggle", "param": "motor-speed"}, + label="Continuous", + value=False, + ), + ], + width=2, + ), ], - className="mb-3", + className="mb-2", + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + dcc.Dropdown( + id={"type": "sampler-dropdown", "id": "motor-speed"}, + options=[{"label": str(speed), "value": speed} for speed in MOTOR_SPEEDS_D], + multi=True, + value=MOTOR_SPEEDS_D, + ), + ], + width=4, + ), + dbc.Col( + dbc.InputGroup([ + dbc.Input(id={"type": "custom-input", "id": "motor-speed"}, type="number", placeholder="Custom motor speed"), + dbc.Button("Add", id={"type": "add-custom-button", "id": "motor-speed"}, size="sm"), + ]), + width=4, + ) + ], + className="mb-3", + ), + ], + id={"type": "discrete-container", "param": "motor-speed"}, ), - dbc.Col( + html.Div( [ - html.H5("Motor Speed Range"), - dbc.InputGroup( + dbc.Row( [ - dbc.Input(id="sampler-motor-speed-min", type="number", placeholder="Min", value=SPEED_C[0]), - dbc.Input(id="sampler-motor-speed-max", type="number", placeholder="Max", value=SPEED_C[1]), - ] + dbc.Col( + [ + dbc.InputGroup( + [ + dbc.Input(id="motor-speed-min", type="number", placeholder="Min", value=SPEED_C[0]), + dbc.Input(id="motor-speed-max", type="number", placeholder="Max", value=SPEED_C[1]), + ] + ), + ], + width=4, + ), + ], + className="mb-3", ), ], - width=4, + id={"type": "continuous-container", "param": "motor-speed"}, + style={"display": "none"}, ), dbc.Row( [ From 4421e38cf0be5968ff8e2b39a78b0ef98ed8b282 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Thu, 3 Apr 2025 12:58:03 -0700 Subject: [PATCH 086/125] ordered temperature in the outputted parameter sets --- aamp_app/app.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index e7e3f5d..38fd80d 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -90,10 +90,10 @@ def load_env(file_path=".env"): mongo_gridfs = GridFS(mongo.db, collection="recipes") fs = GridFS(mongo.db) -solutions.init_collection() -devices.init_collection() -films.init_collection() -recipes.init_collection() +# solutions.init_collection() +# devices.init_collection() +# films.init_collection() +# recipes.init_collection() app = dash.Dash( __name__, @@ -2647,6 +2647,8 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string sample_count += 1 df = pd.DataFrame(parameter_sets) + df = df.sort_values(by=["solvent", "temperature"], ascending=[True, True]) + parameter_sets = df.to_dict(orient="records") table = dash_table.DataTable( id="sampler-results", @@ -2784,6 +2786,26 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string return None, f"Failed to generate parameter sets: {str(e)}", True, "danger", True +# @app.callback( +# Output("sampler-save-alert", "children"), +# Output("sampler-save-alert", "is_open"), +# Output("sampler-save-alert", "color"), +# Input("sampler-save-button", "n_clicks"), +# State("sampler-results", "data"), +# prevent_initial_call=True +# ) +# def save_parameter_sets_to_mongo(n_clicks, parameter_sets): +# if not parameter_sets: +# return "No parameter sets to save.", True, "warning" + +# try: +# result = mongo.db["parameters"].insert_many(parameter_sets) + +# return f"Successfully saved {len(result.inserted_ids)} parameter sets to MongoDB.", True, "success" +# except Exception as e: +# print(f"Failed to save parameter sets to MongoDB: {e}") +# return f"Failed to save parameter sets: {str(e)}", True, "danger" + if __name__ == "__main__": app.run(debug=True) From 78b4c19ef6ec3dcfd7dbeaab749ca1af5dfdd765 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Thu, 10 Apr 2025 12:40:43 -0700 Subject: [PATCH 087/125] fixed upload to database and added gpc data --- aamp_app/app.py | 124 +++++++++++++++++++++++++++++--------- aamp_app/pages/sampler.py | 38 +++++++++++- requirements.txt | Bin 474 -> 490 bytes 3 files changed, 132 insertions(+), 30 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index 38fd80d..a08ae4d 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -23,6 +23,8 @@ import pandas as pd from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler +import base64 +import io import umap import plotly.express as px import plotly.graph_objects as go @@ -100,6 +102,7 @@ def load_env(file_path=".env"): external_stylesheets=[dbc.themes.BOOTSTRAP], use_pages=True, prevent_initial_callbacks="initial_duplicate", + suppress_callback_exceptions=True, ) server = app.server @@ -2368,21 +2371,46 @@ def toggle_input_type(is_continuous): return {"display": "none"}, {"display": "block"} else: return {"display": "block"}, {"display": "none"} - + @app.callback( - Output("sampler-polymer-image-preview", "children"), - Input("sampler-polymer-image", "contents"), - State("sampler-polymer-image", "filename"), + Output("gpc-data-output", "children"), + Output("gpc-data-store", "data"), + Input("gpc-data-upload", "contents"), + State("gpc-data-upload", "filename"), prevent_initial_call=True ) -def update_image_preview(contents, filename): +def process_gpc_data(contents, filename): if contents is None: - return [] + return html.Div("No file uploaded yet."), None + + content_type, content_string = contents.split(',') + decoded = base64.b64decode(content_string) + + try: + if filename.endswith('.csv'): + df = pd.read_csv(io.StringIO(decoded.decode('utf-8'))) + elif filename.endswith(('.xlsx', '.xls')): + df = pd.read_excel(io.BytesIO(decoded)) + else: + return html.Div("Unsupported file type."), None + + gpc_data = df.to_dict('records') + + preview = html.Div([ + html.H5(f"GPC Data from {filename}"), + html.P(f"Successfully loaded {len(df)} rows"), + dash_table.DataTable( + data=df.head(5).to_dict('records'), + columns=[{'name': i, 'id': i} for i in df.columns], + style_table={'overflowX': 'auto'}, + ), + ]) + return preview, gpc_data + + except Exception as e: + return html.Div(f"Error processing file: {str(e)}"), None + - return html.Div([ - html.Img(src=contents, style={'maxHeight': '200px', 'maxWidth': '100%'}), - html.P(filename) - ]) @app.callback( Output("temperature-container", "style"), @@ -2786,25 +2814,67 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string return None, f"Failed to generate parameter sets: {str(e)}", True, "danger", True -# @app.callback( -# Output("sampler-save-alert", "children"), -# Output("sampler-save-alert", "is_open"), -# Output("sampler-save-alert", "color"), -# Input("sampler-save-button", "n_clicks"), -# State("sampler-results", "data"), -# prevent_initial_call=True -# ) -# def save_parameter_sets_to_mongo(n_clicks, parameter_sets): -# if not parameter_sets: -# return "No parameter sets to save.", True, "warning" +@app.callback( + Output("sampler-alert", "children", allow_duplicate=True), + Output("sampler-alert", "is_open", allow_duplicate=True), + Output("sampler-alert", "color", allow_duplicate=True), + Output("sampler-save-button", "disabled", allow_duplicate=True), + Input("sampler-save-button", "n_clicks"), + State("sampler-results", "data"), + State("gpc-data-store", "data"), + State("sampler-campaign-name", "value"), + State("sampler-polymer-name", "value"), + State("sampler-smiles-string", "value"), + State("sampler-mw", "value"), + State("sampler-pdi", "value"), + prevent_initial_call=True +) +def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_name, polymer_name, + smiles_string, mw, pdi): + if not parameter_sets: + return "No parameter sets to save.", True, "warning", True -# try: -# result = mongo.db["parameters"].insert_many(parameter_sets) + try: + campaign_doc = { + "campaign_name": campaign_name, + "polymer_name": polymer_name, + "smiles_string": smiles_string, + "mw": mw, + "pdi": pdi + } -# return f"Successfully saved {len(result.inserted_ids)} parameter sets to MongoDB.", True, "success" -# except Exception as e: -# print(f"Failed to save parameter sets to MongoDB: {e}") -# return f"Failed to save parameter sets: {str(e)}", True, "danger" + if gpc_data: + campaign_doc["gpc"] = gpc_data + + campaign_result = mongo.db["campaigns"].insert_one(campaign_doc) + campaign_id = campaign_result.inserted_id + + sets_to_insert = [] + for param_set in parameter_sets: + set_doc = { + "campaign_id": campaign_id, # Reference to the campaign + "sample_no": param_set["sample_no"], + "motor_speed": param_set["motor_speed"], + "temperature": param_set["temperature"], + "concentration": param_set["concentration"], + "printing_gap": param_set["printing_gap"], + "precursor_volume": param_set["precursor_volume"], + "solvent": param_set["solvent"], + "motor_speed_norm": param_set["motor_speed_norm"], + "temperature_norm": param_set["temperature_norm"], + "concentration_norm": param_set["concentration_norm"], + "printing_gap_norm": param_set["printing_gap_norm"], + "precursor_volume_norm": param_set["precursor_volume_norm"] + } + sets_to_insert.append(set_doc) + + sets_result = mongo.db["sets"].insert_many(sets_to_insert) + + return (f"Successfully saved campaign information and {len(sets_result.inserted_ids)} parameter sets to MongoDB.", + True, "success", False) + except Exception as e: + print(f"Failed to save parameter sets to MongoDB: {e}") + return f"Failed to save parameter sets: {str(e)}", True, "danger", True if __name__ == "__main__": diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py index 765c1da..e97ad05 100644 --- a/aamp_app/pages/sampler.py +++ b/aamp_app/pages/sampler.py @@ -115,14 +115,14 @@ ), dbc.Col( [ - html.H5("Molecular Weight"), - dbc.Input(id="sampler-mw", type="number", placeholder="Enter MW", min=0, list="mw-suggestions"), + html.H5("Number-Averaged Molecular Weight (Mn)"), + dbc.Input(id="sampler-mw", type="number", placeholder="Enter Mn", min=0, list="mw-suggestions"), ], width=2, ), dbc.Col( [ - html.H5("Polydispersity Index"), + html.H5("Polydispersity Index (PDI)"), dbc.Input(id="sampler-pdi", type="number", placeholder="Enter PDI", min=1, step=0.01, list="pdi-suggestions"), ], width=2, @@ -130,6 +130,38 @@ ], className="mb-3", ), + dbc.Row( + [ + dbc.Col( + [ + html.H5("Upload GPC Data"), + dcc.Upload( + id="gpc-data-upload", + children=html.Div([ + 'Drag and Drop or ', + html.A('Select a CSV or Excel File') + ]), + style={ + 'width': '100%', + 'height': '60px', + 'lineHeight': '60px', + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center', + 'margin': '10px 0' + }, + multiple=False, + accept='.csv, .xlsx, .xls' + ), + html.Div(id="gpc-data-output") + ], + width=12, + ), + ], + className="mb-3", + ), + dcc.Store(id="gpc-data-store", storage_type="memory"), dbc.Row( [ dbc.Col( diff --git a/requirements.txt b/requirements.txt index 49b9b9731f177195bed84bbc36e1002db35bc830..e2b7613e524e8f506622c8c859bfb06048476633 100644 GIT binary patch delta 24 ecmcb`{EB(QEk=QSh609EhCCoy$xy+N!vFwhUIyg= delta 7 OcmaFGe2aO*Ek*zik^@!% From 7ff9f640a97713bf019d2aa9b50dd9dba0e6a782 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh <> Date: Thu, 17 Apr 2025 12:11:58 -0700 Subject: [PATCH 088/125] added image to campaign document, handled duplicate campaign, image preview autofill --- aamp_app/app.py | 173 ++++++++++++++++++++++++------- aamp_app/devices/ximea_camera.py | 2 +- aamp_app/pages/sampler.py | 22 ++-- 3 files changed, 148 insertions(+), 49 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index a08ae4d..7fb640b 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -2325,6 +2325,9 @@ def upload_image(n_clicks, sample_number, motor_speed, temperature, concentratio "decane": (25, 135), } +from bson import ObjectId +import base64 + # adding custom discrete parameter options @app.callback( Output({"type": "sampler-dropdown", "id": MATCH}, "options"), @@ -2410,6 +2413,60 @@ def process_gpc_data(contents, filename): except Exception as e: return html.Div(f"Error processing file: {str(e)}"), None + +@app.callback( + Output("sampler-smiles-string", "value"), + Output("sampler-polymer-image-preview", "children"), + Input("sampler-polymer-name", "value"), + Input("sampler-polymer-image", "contents"), + State("sampler-polymer-image", "filename"), + prevent_initial_call=True +) +def handle_polymer_inputs(polymer_name, image_contents, image_filename): + ctx = dash.callback_context + triggered_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else None + + smiles = "" + image_preview = [] + + if triggered_id == "sampler-polymer-name" and polymer_name: + try: + campaign = mongo.db.campaigns.find_one({"polymer_name": polymer_name}) + if campaign: + smiles = campaign.get("smiles_string", "") + image_id = campaign.get("image_id") + if image_id: + try: + grid_out = fs.get(ObjectId(image_id)) + image_bytes = grid_out.read() + encoded = base64.b64encode(image_bytes).decode() + mime_type = grid_out.content_type if hasattr(grid_out, "content_type") else "image/png" + src = f"data:{mime_type};base64,{encoded}" + image_preview = [ + html.Img(src=src, style={'maxHeight': '200px', 'maxWidth': '100%'}), + html.P(grid_out.filename) + ] + except Exception as e: + print(f"Error retrieving image: {e}") + + except Exception as e: + print(f"Error in polymer name lookup: {e}") + + elif triggered_id == "sampler-polymer-image" and image_contents: + try: + content_type, content_string = image_contents.split(',') + decoded = base64.b64decode(content_string) + + image_preview = [ + html.Img(src=image_contents, style={'maxHeight': '200px', 'maxWidth': '100%'}), + html.P(image_filename) + ] + + except Exception as e: + print(f"Error processing image: {e}") + image_preview = [html.P("Invalid image file")] + + return smiles, image_preview @app.callback( @@ -2827,53 +2884,95 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string State("sampler-smiles-string", "value"), State("sampler-mw", "value"), State("sampler-pdi", "value"), + State("sampler-polymer-image", "contents"), + State("sampler-polymer-image", "filename"), prevent_initial_call=True ) def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_name, polymer_name, - smiles_string, mw, pdi): + smiles_string, mw, pdi, image_contents, image_filename): if not parameter_sets: return "No parameter sets to save.", True, "warning", True try: - campaign_doc = { - "campaign_name": campaign_name, - "polymer_name": polymer_name, - "smiles_string": smiles_string, - "mw": mw, - "pdi": pdi - } - - if gpc_data: - campaign_doc["gpc"] = gpc_data - - campaign_result = mongo.db["campaigns"].insert_one(campaign_doc) - campaign_id = campaign_result.inserted_id + image_id = None + if image_contents and image_filename: + content_type, content_string = image_contents.split(',') + decoded = base64.b64decode(content_string) + image_id = fs.put(decoded, filename=image_filename) + + existing_campaign = mongo.db.campaigns.find_one({"campaign_name": campaign_name}) - sets_to_insert = [] - for param_set in parameter_sets: - set_doc = { - "campaign_id": campaign_id, # Reference to the campaign - "sample_no": param_set["sample_no"], - "motor_speed": param_set["motor_speed"], - "temperature": param_set["temperature"], - "concentration": param_set["concentration"], - "printing_gap": param_set["printing_gap"], - "precursor_volume": param_set["precursor_volume"], - "solvent": param_set["solvent"], - "motor_speed_norm": param_set["motor_speed_norm"], - "temperature_norm": param_set["temperature_norm"], - "concentration_norm": param_set["concentration_norm"], - "printing_gap_norm": param_set["printing_gap_norm"], - "precursor_volume_norm": param_set["precursor_volume_norm"] + if existing_campaign: + campaign_id = existing_campaign["_id"] + update_data = {} + + if gpc_data: + update_data["gpc"] = gpc_data + if image_id: + update_data["image_id"] = image_id + + if update_data: + mongo.db.campaigns.update_one( + {"_id": campaign_id}, + {"$set": update_data} + ) + + sets_to_insert = [{ + "campaign_id": campaign_id, + "sample_no": p_set["sample_no"], + "motor_speed": p_set["motor_speed"], + "temperature": p_set["temperature"], + "concentration": p_set["concentration"], + "printing_gap": p_set["printing_gap"], + "precursor_volume": p_set["precursor_volume"], + "solvent": p_set["solvent"], + **{k: p_set[k] for k in p_set if k.endswith('_norm')} + } for p_set in parameter_sets] + + sets_result = mongo.db.sets.insert_many(sets_to_insert) + msg = f"Added {len(sets_result.inserted_ids)} parameter sets to existing campaign '{campaign_name}'" + if image_id: + msg += " with updated polymer image" + return msg, True, "success", False + + else: + campaign_doc = { + "campaign_name": campaign_name, + "polymer_name": polymer_name, + "smiles_string": smiles_string, + "mw": mw, + "pdi": pdi, + "created_at": datetime.now() } - sets_to_insert.append(set_doc) - - sets_result = mongo.db["sets"].insert_many(sets_to_insert) - - return (f"Successfully saved campaign information and {len(sets_result.inserted_ids)} parameter sets to MongoDB.", - True, "success", False) + + if gpc_data: + campaign_doc["gpc"] = gpc_data + if image_id: + campaign_doc["image_id"] = image_id + + campaign_result = mongo.db.campaigns.insert_one(campaign_doc) + campaign_id = campaign_result.inserted_id + + sets_to_insert = [{ + "campaign_id": campaign_id, + "sample_no": p_set["sample_no"], + "motor_speed": p_set["motor_speed"], + "temperature": p_set["temperature"], + "concentration": p_set["concentration"], + "printing_gap": p_set["printing_gap"], + "precursor_volume": p_set["precursor_volume"], + "solvent": p_set["solvent"], + **{k: p_set[k] for k in p_set if k.endswith('_norm')} + } for p_set in parameter_sets] + + sets_result = mongo.db.sets.insert_many(sets_to_insert) + msg = f"Created new campaign '{campaign_name}' with {len(sets_result.inserted_ids)} parameter sets" + if image_id: + msg += " and polymer image" + return msg, True, "success", False + except Exception as e: - print(f"Failed to save parameter sets to MongoDB: {e}") + print(f"Failed to save parameter sets: {str(e)}") return f"Failed to save parameter sets: {str(e)}", True, "danger", True diff --git a/aamp_app/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py index bff71af..0d6bdc4 100644 --- a/aamp_app/devices/ximea_camera.py +++ b/aamp_app/devices/ximea_camera.py @@ -9,7 +9,7 @@ # Unsure how cooperative xiapi.Camera is so did not use multiple inheritance # will need to figure out the method resolution order if using multiple inheritance class XimeaCamera(Device): - save_directory = 'data/imaging/' + save_directory = '../data/imaging/' def __init__(self, name: str): super().__init__(name) diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py index e97ad05..cd37f07 100644 --- a/aamp_app/pages/sampler.py +++ b/aamp_app/pages/sampler.py @@ -134,12 +134,12 @@ [ dbc.Col( [ - html.H5("Upload GPC Data"), + html.H5("Upload Polymer Image"), dcc.Upload( - id="gpc-data-upload", + id="sampler-polymer-image", children=html.Div([ 'Drag and Drop or ', - html.A('Select a CSV or Excel File') + html.A('Select an Image') ]), style={ 'width': '100%', @@ -152,26 +152,25 @@ 'margin': '10px 0' }, multiple=False, - accept='.csv, .xlsx, .xls' + accept='image/*' ), - html.Div(id="gpc-data-output") + html.Div(id="sampler-polymer-image-preview"), ], width=12, ), ], className="mb-3", ), - dcc.Store(id="gpc-data-store", storage_type="memory"), dbc.Row( [ dbc.Col( [ - html.H5("Upload Polymer Image"), + html.H5("Upload GPC Data"), dcc.Upload( - id="sampler-polymer-image", + id="gpc-data-upload", children=html.Div([ 'Drag and Drop or ', - html.A('Select an Image') + html.A('Select a CSV or Excel File') ]), style={ 'width': '100%', @@ -184,15 +183,16 @@ 'margin': '10px 0' }, multiple=False, - accept='image/*' + accept='.csv, .xlsx, .xls' ), - html.Div(id="sampler-polymer-image-preview"), + html.Div(id="gpc-data-output") ], width=12, ), ], className="mb-3", ), + dcc.Store(id="gpc-data-store", storage_type="memory"), dbc.Row( [ dbc.Col( From 6d8ef43482b74dd87fd986bbe884774d23174036 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Mon, 28 Apr 2025 18:45:16 +0000 Subject: [PATCH 089/125] Made recipe page --- aamp_app/app.py | 1 + aamp_app/devices/ximea_camera.py | 2 +- aamp_app/pages/home.py | 1 + aamp_app/pages/recipe-builder.py | 64 ++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 aamp_app/pages/recipe-builder.py diff --git a/aamp_app/app.py b/aamp_app/app.py index 7fb640b..3b2652d 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -123,6 +123,7 @@ def load_env(file_path=".env"): ), # dbc.NavItem(dbc.NavLink("Images", href="/images", external_link=True)), dbc.NavItem(dbc.NavLink("Sampler", href="/sampler", external_link=True)), + dbc.NavItem(dbc.NavLink("Recipe Builder", href="/recipe-builder", external_link=True)), # dbc.DropdownMenu( # children=[ # # dbc.DropdownMenuItem( diff --git a/aamp_app/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py index 0d6bdc4..e26bfbf 100644 --- a/aamp_app/devices/ximea_camera.py +++ b/aamp_app/devices/ximea_camera.py @@ -2,7 +2,7 @@ from typing import Optional, Tuple, List from PIL import Image -from ximea import xiapi +# from ximea import xiapi from .device import Device, check_initialized diff --git a/aamp_app/pages/home.py b/aamp_app/pages/home.py index d157510..b391049 100644 --- a/aamp_app/pages/home.py +++ b/aamp_app/pages/home.py @@ -35,6 +35,7 @@ "Real Time Telemetry": "/real-time-telemetry", # "Images": "/images", "Sampler": "/sampler", + "Recipe Builder": "/recipe-builder" # "Options": "/options", } diff --git a/aamp_app/pages/recipe-builder.py b/aamp_app/pages/recipe-builder.py new file mode 100644 index 0000000..a2e59f5 --- /dev/null +++ b/aamp_app/pages/recipe-builder.py @@ -0,0 +1,64 @@ +from dash import Dash, html, dcc, Input, Output, State, callback +import dash_bootstrap_components as dbc +import dash + +dash.register_page(__name__, path="/recipe-builder", name="Recipe Builder", title="Recipe Builder") + +layout = html.Div( + [ + html.H1("Recipe Builder"), + dbc.Alert( + id="recipe-alert", + color="success", + is_open=False, + fade=True, + className="mb-3", + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + html.H5("Select Campaign"), + dcc.Dropdown( + id="recipe-campaign-select", + options=[], + placeholder="Select a campaign..." + ), + ], + width=12, + ), + ], + className="mb-3", + ), + dash.dash_table.DataTable( + id='parameter-sets-table', + columns=[ + {'name': 'Sample No', 'id': 'sample_no'}, + {'name': 'Motor Speed', 'id': 'motor_speed'}, + {'name': 'Temperature', 'id': 'temperature'}, + {'name': 'Concentration', 'id': 'concentration'}, + {'name': 'Printing Gap', 'id': 'printing_gap'}, + {'name': 'Precursor Volume', 'id': 'precursor_volume'}, + {'name': 'Solvent', 'id': 'solvent'} + ], + page_size=10, + style_table={'overflowX': 'auto'}, + row_selectable='multi', + selected_rows=[] + ), + html.Div(id="recipe-output", className="mb-3"), + dcc.Store(id='recipe-parameter-sets'), + dbc.Button( + "Generate Selected Recipes", + id="recipe-generate-button", + color="primary", + className="mb-3", + ), + ], + className="container", + ), + ], + className="container", +) \ No newline at end of file From 5b328ef4edda8204c0f85fe1b49f43e129effc6c Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Mon, 28 Apr 2025 21:36:17 +0000 Subject: [PATCH 090/125] populated campaign dropdown in recipe builder from the database --- aamp_app/app.py | 25 +++++++++++++++++++++++++ aamp_app/devices/ximea_camera.py | 2 +- aamp_app/pages/recipe-builder.py | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index 3b2652d..9c781fa 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -2977,6 +2977,31 @@ def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_na return f"Failed to save parameter sets: {str(e)}", True, "danger", True +# --------------------------------------------------------------- +# Recipe Builder page +# --------------------------------------------------------------- + + +@app.callback( + Output('recipe-builder-campaign-dropdown', 'options'), + Input('recipe-builder-campaign-dropdown', 'search_value') +) +def update_campaign_options(search_value): + try: + campaigns = list(mongo.db.campaigns.find({}, {"campaign_name": 1})) + campaign_options = [{"label": camp["campaign_name"], "value": camp["campaign_name"]} + for camp in campaigns] + if search_value: + campaign_options = [c for c in campaign_options + if search_value.lower() in c["label"].lower()] + + return campaign_options + + except Exception as e: + print(f"Error fetching campaigns: {e}") + return [] + + if __name__ == "__main__": app.run(debug=True) diff --git a/aamp_app/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py index e26bfbf..0d6bdc4 100644 --- a/aamp_app/devices/ximea_camera.py +++ b/aamp_app/devices/ximea_camera.py @@ -2,7 +2,7 @@ from typing import Optional, Tuple, List from PIL import Image -# from ximea import xiapi +from ximea import xiapi from .device import Device, check_initialized diff --git a/aamp_app/pages/recipe-builder.py b/aamp_app/pages/recipe-builder.py index a2e59f5..18694db 100644 --- a/aamp_app/pages/recipe-builder.py +++ b/aamp_app/pages/recipe-builder.py @@ -22,7 +22,7 @@ [ html.H5("Select Campaign"), dcc.Dropdown( - id="recipe-campaign-select", + id="recipe-builder-campaign-dropdown", options=[], placeholder="Select a campaign..." ), From 17a121934610ebaa86feae815dba04da9331b47a Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Mon, 28 Apr 2025 21:52:41 +0000 Subject: [PATCH 091/125] opens the sets when a campaign is selected --- aamp_app/app.py | 38 ++++++++++++++++++++++++++++++++ aamp_app/devices/ximea_camera.py | 2 +- aamp_app/pages/recipe-builder.py | 19 +++++----------- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index 9c781fa..e1c215e 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -3002,6 +3002,44 @@ def update_campaign_options(search_value): return [] +@app.callback( + Output("recipe-builder-sets", "children"), + Input("recipe-builder-campaign-dropdown", "value"), + prevent_initial_call=True +) +def display_campaign_sets(selected_campaign): + if not selected_campaign: + return html.Div("Select a campaign to view its parameter sets") + + try: + campaign = mongo.db.campaigns.find_one({"campaign_name": selected_campaign}) + if not campaign: + return html.Div(f"Campaign '{selected_campaign}' not found") + + sets = list(mongo.db.sets.find({"campaign_id": campaign["_id"]})) + if not sets: + return html.Div(f"No parameter sets found for campaign '{selected_campaign}'") + + for s in sets: + s["_id"] = str(s["_id"]) + s["campaign_id"] = str(s["campaign_id"]) + + columns = [{"name": k.title().replace("_", " "), "id": k} + for k in sets[0].keys() if k not in ["_id", "campaign_id"]] + + return dash_table.DataTable( + id='campaign-sets-table', + columns=columns, + data=sets, + style_table={'overflowX': 'auto'}, + page_size=10 + ) + + except Exception as e: + print(f"Error loading parameter sets: {e}") + return html.Div("Error loading parameter sets", style={'color': 'red'}) + + if __name__ == "__main__": app.run(debug=True) diff --git a/aamp_app/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py index 0d6bdc4..e26bfbf 100644 --- a/aamp_app/devices/ximea_camera.py +++ b/aamp_app/devices/ximea_camera.py @@ -2,7 +2,7 @@ from typing import Optional, Tuple, List from PIL import Image -from ximea import xiapi +# from ximea import xiapi from .device import Device, check_initialized diff --git a/aamp_app/pages/recipe-builder.py b/aamp_app/pages/recipe-builder.py index 18694db..01942ff 100644 --- a/aamp_app/pages/recipe-builder.py +++ b/aamp_app/pages/recipe-builder.py @@ -32,21 +32,12 @@ ], className="mb-3", ), - dash.dash_table.DataTable( - id='parameter-sets-table', - columns=[ - {'name': 'Sample No', 'id': 'sample_no'}, - {'name': 'Motor Speed', 'id': 'motor_speed'}, - {'name': 'Temperature', 'id': 'temperature'}, - {'name': 'Concentration', 'id': 'concentration'}, - {'name': 'Printing Gap', 'id': 'printing_gap'}, - {'name': 'Precursor Volume', 'id': 'precursor_volume'}, - {'name': 'Solvent', 'id': 'solvent'} + html.Div( + [ + html.H4("Parameter Sets for Selected Campaign"), + html.Div(id="recipe-builder-sets") ], - page_size=10, - style_table={'overflowX': 'auto'}, - row_selectable='multi', - selected_rows=[] + className="mb-3" ), html.Div(id="recipe-output", className="mb-3"), dcc.Store(id='recipe-parameter-sets'), From 1cc7db3228abe6a1200398cd7a94fc941653b7dc Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Mon, 28 Apr 2025 22:36:57 +0000 Subject: [PATCH 092/125] working on selecting parameter sets for recipe creation --- aamp_app/app.py | 66 +++++++++++++++++++++++++++- aamp_app/pages/recipe-builder.py | 74 ++++++++++++++++---------------- 2 files changed, 101 insertions(+), 39 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index e1c215e..c13b2b9 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -29,6 +29,7 @@ import plotly.express as px import plotly.graph_objects as go from scipy.stats import gaussian_kde +from string import Template from db.validation import solutions, films, devices, recipes try: @@ -3027,19 +3028,80 @@ def display_campaign_sets(selected_campaign): columns = [{"name": k.title().replace("_", " "), "id": k} for k in sets[0].keys() if k not in ["_id", "campaign_id"]] + selected_rows = list(range(len(sets))) + return dash_table.DataTable( - id='campaign-sets-table', + id='recipe-builder-sets-table', columns=columns, data=sets, style_table={'overflowX': 'auto'}, - page_size=10 + page_size=10, + row_selectable='multi', + selected_rows=selected_rows ) + except Exception as e: print(f"Error loading parameter sets: {e}") return html.Div("Error loading parameter sets", style={'color': 'red'}) +with open('../recipes/recipe_sample.py', 'r') as f: + RECIPE_TEMPLATE = Template(f.read()) + +@app.callback( + Output("recipe-builder-output", "children"), + Input("recipe-builder-generate-button", "n_clicks"), + State("recipe-builder-sets-table", "selected_rows"), + State("recipe-builder-parameter-sets", "data"), + prevent_initial_call=True +) +def generate_recipes(n_clicks, selected_rows, parameter_sets): + if not parameter_sets or not selected_rows: + return html.Div("Please select parameter sets to generate recipes") + + generated_scripts = [] + + for idx in selected_rows: + params = parameter_sets[idx] + + sub_dict = { + 'sample_no': params['sample_no'], + 'polymer': params.get('polymer_name', 'Unknown Polymer'), + 'solvent': params['solvent'], + 'concentration': params['concentration'], + 'motor_speed': params['motor_speed'], + 'temperature': params['temperature'], + 'printing_gap': params['printing_gap'], + 'precursor_volume': params['precursor_volume'] + } + + try: + script = RECIPE_TEMPLATE.safe_substitute(sub_dict) + generated_scripts.append({ + "sample_no": params["sample_no"], + "script": script + }) + except Exception as e: + return html.Div(f"Error generating recipe: {str(e)}", style={'color': 'red'}) + + return html.Div([ + html.H4("Generated Recipes"), + html.Ul([ + html.Li([ + html.P(f"Sample {script['sample_no']}"), + dcc.Markdown(f'''``````'''), + html.A( + "Download Script", + href=f"data:text/plain;charset=utf-8,{script['script']}", + download=f"recipe_sample_{script['sample_no']}.py", + className="btn btn-primary mt-2" + ) + ]) for script in generated_scripts + ]) + ]) + + if __name__ == "__main__": app.run(debug=True) diff --git a/aamp_app/pages/recipe-builder.py b/aamp_app/pages/recipe-builder.py index 01942ff..1f3674d 100644 --- a/aamp_app/pages/recipe-builder.py +++ b/aamp_app/pages/recipe-builder.py @@ -15,41 +15,41 @@ className="mb-3", ), html.Div( - [ - dbc.Row( - [ - dbc.Col( - [ - html.H5("Select Campaign"), - dcc.Dropdown( - id="recipe-builder-campaign-dropdown", - options=[], - placeholder="Select a campaign..." - ), - ], - width=12, - ), - ], - className="mb-3", - ), - html.Div( - [ - html.H4("Parameter Sets for Selected Campaign"), - html.Div(id="recipe-builder-sets") - ], - className="mb-3" - ), - html.Div(id="recipe-output", className="mb-3"), - dcc.Store(id='recipe-parameter-sets'), - dbc.Button( - "Generate Selected Recipes", - id="recipe-generate-button", - color="primary", - className="mb-3", - ), - ], - className="container", - ), - ], - className="container", + [ + dbc.Row( + [ + dbc.Col( + [ + html.H5("Select Campaign"), + dcc.Dropdown( + id="recipe-builder-campaign-dropdown", + options=[], + placeholder="Select a campaign..." + ), + ], + width=12, + ), + ], + className="mb-3", + ), + html.Div( + [ + html.H4("Parameter Sets for Selected Campaign"), + html.Div(id="recipe-builder-sets") + ], + className="mb-3" + ), + html.Div(id="recipe-builder-output", className="mb-3"), + dcc.Store(id='recipe-builder-parameter-sets'), + dbc.Button( + "Generate Selected Recipes", + id="recipe-builder-generate-button", + color="primary", + className="mb-3", + ), + ], + className="container", + ) +], +className="container", ) \ No newline at end of file From 89a7322ec6740623e144541cde6a1093936489d2 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Mon, 5 May 2025 07:00:53 +0000 Subject: [PATCH 093/125] added solution position as a manual entry field, got recipe template creation working --- aamp_app/app.py | 46 ++++++++++++++++++++++--------- aamp_app/pages/recipe-builder.py | 47 +++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index c13b2b9..ec62626 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -3002,9 +3002,21 @@ def update_campaign_options(search_value): print(f"Error fetching campaigns: {e}") return [] +@app.callback( + Output("recipe-builder-run-button", "children"), + Input("recipe-builder-output", "children"), + State("recipe-builder-parameter-sets", "data"), + prevent_initial_call=True +) +def update_run_button_label(_, parameter_sets): + num_recipes = len(parameter_sets) if parameter_sets else 0 + return f"Run {num_recipes} recipe{'s' if num_recipes != 1 else ''}" + @app.callback( Output("recipe-builder-sets", "children"), + Output("recipe-builder-parameter-sets", "data"), + Output("recipe-builder-polymer-name", "data"), Input("recipe-builder-campaign-dropdown", "value"), prevent_initial_call=True ) @@ -3016,7 +3028,7 @@ def display_campaign_sets(selected_campaign): campaign = mongo.db.campaigns.find_one({"campaign_name": selected_campaign}) if not campaign: return html.Div(f"Campaign '{selected_campaign}' not found") - + polymer_name = campaign.get('polymer_name') sets = list(mongo.db.sets.find({"campaign_id": campaign["_id"]})) if not sets: return html.Div(f"No parameter sets found for campaign '{selected_campaign}'") @@ -3029,8 +3041,8 @@ def display_campaign_sets(selected_campaign): for k in sets[0].keys() if k not in ["_id", "campaign_id"]] selected_rows = list(range(len(sets))) - - return dash_table.DataTable( + + table = dash_table.DataTable( id='recipe-builder-sets-table', columns=columns, data=sets, @@ -3040,6 +3052,7 @@ def display_campaign_sets(selected_campaign): selected_rows=selected_rows ) + return table, sets, polymer_name except Exception as e: print(f"Error loading parameter sets: {e}") @@ -3054,20 +3067,21 @@ def display_campaign_sets(selected_campaign): Input("recipe-builder-generate-button", "n_clicks"), State("recipe-builder-sets-table", "selected_rows"), State("recipe-builder-parameter-sets", "data"), + State("recipe-builder-polymer-name", "data"), prevent_initial_call=True ) -def generate_recipes(n_clicks, selected_rows, parameter_sets): +def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name): if not parameter_sets or not selected_rows: return html.Div("Please select parameter sets to generate recipes") generated_scripts = [] - + for idx in selected_rows: params = parameter_sets[idx] sub_dict = { 'sample_no': params['sample_no'], - 'polymer': params.get('polymer_name', 'Unknown Polymer'), + 'polymer': polymer_name, 'solvent': params['solvent'], 'concentration': params['concentration'], 'motor_speed': params['motor_speed'], @@ -3085,18 +3099,24 @@ def generate_recipes(n_clicks, selected_rows, parameter_sets): except Exception as e: return html.Div(f"Error generating recipe: {str(e)}", style={'color': 'red'}) + print(script) return html.Div([ html.H4("Generated Recipes"), html.Ul([ html.Li([ html.P(f"Sample {script['sample_no']}"), - dcc.Markdown(f'''``````'''), - html.A( - "Download Script", - href=f"data:text/plain;charset=utf-8,{script['script']}", - download=f"recipe_sample_{script['sample_no']}.py", - className="btn btn-primary mt-2" - ) + html.Div([ + html.Pre( + script['script'], + style={ + 'height': '400px', + 'overflowY': 'auto', + 'backgroundColor': '#f8f9fa', + 'padding': '10px', + 'border': '1px solid #dee2e6' + } + ), + ]) ]) for script in generated_scripts ]) ]) diff --git a/aamp_app/pages/recipe-builder.py b/aamp_app/pages/recipe-builder.py index 1f3674d..4bc4b5d 100644 --- a/aamp_app/pages/recipe-builder.py +++ b/aamp_app/pages/recipe-builder.py @@ -27,7 +27,45 @@ placeholder="Select a campaign..." ), ], - width=12, + width=8, + ), + dbc.Col( + [ + html.H5("Enter Solution Position"), + dcc.Input( + id="recipe-builder-solution-position", + type="text", + placeholder="A1-D5" + ), + ], + width=4, + ), + ], + className="mb-3", + ), + dbc.Row( + [ + dbc.Col( + [ + dbc.Button( + "Generate Selected Recipes", + id="recipe-builder-generate-button", + color="primary", + className="mb-3", + ), + ], + width=4, + ), + dbc.Col( + [ + dbc.Button( + "Run 0 recipes", + id="recipe-builder-run-button", + n_clicks=0, + className="btn btn-success mt-2" + ), + ], + width=4, ), ], className="mb-3", @@ -40,13 +78,8 @@ className="mb-3" ), html.Div(id="recipe-builder-output", className="mb-3"), + dcc.Store(id='recipe-builder-polymer-name'), dcc.Store(id='recipe-builder-parameter-sets'), - dbc.Button( - "Generate Selected Recipes", - id="recipe-builder-generate-button", - color="primary", - className="mb-3", - ), ], className="container", ) From 1f8bd38224271d58fdfdd1e165f770d8ef3de2be Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Mon, 5 May 2025 07:44:33 +0000 Subject: [PATCH 094/125] added the manual entry of solution position and run all recipes button --- aamp_app/app.py | 64 ++++++++++++++++++++++++++++++-- aamp_app/devices/ximea_camera.py | 2 +- aamp_app/pages/recipe-builder.py | 1 + 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index ec62626..28769ff 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -3064,13 +3064,15 @@ def display_campaign_sets(selected_campaign): @app.callback( Output("recipe-builder-output", "children"), + Output("recipe-builder-generated-scripts", "data"), Input("recipe-builder-generate-button", "n_clicks"), State("recipe-builder-sets-table", "selected_rows"), State("recipe-builder-parameter-sets", "data"), State("recipe-builder-polymer-name", "data"), + State("recipe-builder-solution-position", "value"), prevent_initial_call=True ) -def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name): +def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name, solution_position): if not parameter_sets or not selected_rows: return html.Div("Please select parameter sets to generate recipes") @@ -3087,7 +3089,8 @@ def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name): 'motor_speed': params['motor_speed'], 'temperature': params['temperature'], 'printing_gap': params['printing_gap'], - 'precursor_volume': params['precursor_volume'] + 'precursor_volume': params['precursor_volume'], + 'solution_position': solution_position } try: @@ -3119,7 +3122,62 @@ def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name): ]) ]) for script in generated_scripts ]) - ]) + ]), generated_scripts + + +@app.callback( + Output("recipe-builder-run-log", "children"), + Input("recipe-builder-run-button", "n_clicks"), + State("recipe-builder-generated-scripts", "data"), + prevent_initial_call=True +) +def run_recipes_sequentially(n_clicks, generated_scripts): + if not generated_scripts: + raise PreventUpdate + + log_components = [] + interceptor = ConsoleInterceptor() + + try: + for script in generated_scripts: + code = script['script'] + log_components.append(html.H5(f"Running Sample {script['sample_no']}")) + + interceptor.start_interception() + try: + exec(code) + status = "Success" + alert_color = "success" + except Exception as e: + status = f"Error: {str(e)}" + alert_color = "danger" + finally: + interceptor.stop_interception() + + output = interceptor.get_intercepted_messages() + + log_components.extend([ + dbc.Alert(status, color=alert_color), + html.Pre( + f"Output:\n{output}", + style={ + 'backgroundColor': '#f8f9fa', + 'padding': '10px', + 'border': '1px solid #dee2e6', + 'maxHeight': '300px', + 'overflowY': 'auto' + } + ), + html.Hr() + ]) + + return log_components + + except Exception as e: + return html.Div(f"Critical error: {str(e)}", style={'color': 'red'}) + + finally: + del interceptor if __name__ == "__main__": diff --git a/aamp_app/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py index e26bfbf..0d6bdc4 100644 --- a/aamp_app/devices/ximea_camera.py +++ b/aamp_app/devices/ximea_camera.py @@ -2,7 +2,7 @@ from typing import Optional, Tuple, List from PIL import Image -# from ximea import xiapi +from ximea import xiapi from .device import Device, check_initialized diff --git a/aamp_app/pages/recipe-builder.py b/aamp_app/pages/recipe-builder.py index 4bc4b5d..b1e463a 100644 --- a/aamp_app/pages/recipe-builder.py +++ b/aamp_app/pages/recipe-builder.py @@ -80,6 +80,7 @@ html.Div(id="recipe-builder-output", className="mb-3"), dcc.Store(id='recipe-builder-polymer-name'), dcc.Store(id='recipe-builder-parameter-sets'), + dcc.Store(id="recipe-builder-generated-scripts"), ], className="container", ) From ed812c7fe1ed2ae162b03ebac1cec98af3254e15 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Mon, 12 May 2025 18:20:48 +0000 Subject: [PATCH 095/125] created solution map functionality --- aamp_app/app.py | 95 ++++++++++++++++++++++-- aamp_app/pages/home.py | 3 +- aamp_app/pages/recipe-builder.py | 4 +- aamp_app/pages/solution-map.py | 21 ++++++ aamp_app/solution_map.json | 122 +++++++++++++++++++++++++++++++ 5 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 aamp_app/pages/solution-map.py create mode 100644 aamp_app/solution_map.json diff --git a/aamp_app/app.py b/aamp_app/app.py index 28769ff..ab7f79e 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -9,6 +9,7 @@ from dash import dcc from dash import html from dash import dash_table +from dash import ctx import dash_ag_grid as dag from dash.dependencies import Input, Output, State, MATCH, ALL import dash_bootstrap_components as dbc @@ -125,6 +126,7 @@ def load_env(file_path=".env"): # dbc.NavItem(dbc.NavLink("Images", href="/images", external_link=True)), dbc.NavItem(dbc.NavLink("Sampler", href="/sampler", external_link=True)), dbc.NavItem(dbc.NavLink("Recipe Builder", href="/recipe-builder", external_link=True)), + dbc.NavItem(dbc.NavLink("Solution Map", href="/solution-map", external_link=True)), # dbc.DropdownMenu( # children=[ # # dbc.DropdownMenuItem( @@ -3062,6 +3064,55 @@ def display_campaign_sets(selected_campaign): with open('../recipes/recipe_sample.py', 'r') as f: RECIPE_TEMPLATE = Template(f.read()) +@app.callback( + Output("recipe-builder-solution-position", "value"), + Output("recipe-alert", "children"), + Output("recipe-alert", "is_open"), + Input("recipe-builder-parameter-sets", "data"), + Input("recipe-builder-sets-table", "selected_rows"), + State("recipe-builder-polymer-name", "data"), + prevent_initial_call=True +) +def auto_find_solution_positions(parameter_sets, selected_rows, polymer_name): + if not parameter_sets or not selected_rows: + return dash.no_update, dash.no_update, dash.no_update + + try: + solution_map = mongo.db.solution_map.find_one({}) + if not solution_map: + raise Exception("No solution map found in database") + + solution_map.pop('_id', None) + + positions = [] + for idx in selected_rows: + params = parameter_sets[idx] + + best_match = None + closest_diff = float('inf') + + for cell_id, cell in solution_map.items(): + if (cell['status'] == 'occupied' and + cell['polymer'] == polymer_name and + cell['solvent'] == params['solvent']): + + current_diff = abs(cell['concentration'] - params['concentration']) + if current_diff < closest_diff: + closest_diff = current_diff + best_match = cell_id + + if best_match: + positions.append(best_match) + else: + positions.append("A1") + # return "", f"No solution found for {polymer_name}/{params['solvent']}/{params['concentration']}%", True + + return ", ".join(positions), "Auto-filled solution positions", True + + except Exception as e: + return "", f"Error finding solution positions: {str(e)}", True + + @app.callback( Output("recipe-builder-output", "children"), Output("recipe-builder-generated-scripts", "data"), @@ -3072,13 +3123,11 @@ def display_campaign_sets(selected_campaign): State("recipe-builder-solution-position", "value"), prevent_initial_call=True ) -def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name, solution_position): - if not parameter_sets or not selected_rows: - return html.Div("Please select parameter sets to generate recipes") +def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name, solution_positions): + positions = [pos.strip() for pos in solution_positions.split(",")] if solution_positions else [] generated_scripts = [] - - for idx in selected_rows: + for idx, pos in zip(selected_rows, positions): params = parameter_sets[idx] sub_dict = { @@ -3090,7 +3139,7 @@ def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name, solu 'temperature': params['temperature'], 'printing_gap': params['printing_gap'], 'precursor_volume': params['precursor_volume'], - 'solution_position': solution_position + 'solution_position': pos } try: @@ -3102,7 +3151,6 @@ def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name, solu except Exception as e: return html.Div(f"Error generating recipe: {str(e)}", style={'color': 'red'}) - print(script) return html.Div([ html.H4("Generated Recipes"), html.Ul([ @@ -3180,6 +3228,39 @@ def run_recipes_sequentially(n_clicks, generated_scripts): del interceptor +# --------------------------------------------------- +# Solution Map Page +# --------------------------------------------------- + + +@app.callback( + Output("solution-map-json", "value"), + Input("solution-map-json", "id"), +) +def load_solution_map(_): + doc = mongo.db.solution_map.find_one({}) + if doc: + doc.pop("_id", None) + return json.dumps(doc, indent=2) + else: + return "{}" + +@app.callback( + Output("solution-map-alert", "children"), + Output("solution-map-alert", "is_open"), + Input("solution-map-save", "n_clicks"), + State("solution-map-json", "value"), + prevent_initial_call=True, +) +def save_solution_map(n_clicks, json_text): + try: + data = json.loads(json_text) + mongo.db.solution_map.replace_one({}, data, upsert=True) + return "Solution map saved!", True + except Exception as e: + return f"Error: {e}", True + + if __name__ == "__main__": app.run(debug=True) diff --git a/aamp_app/pages/home.py b/aamp_app/pages/home.py index b391049..44353db 100644 --- a/aamp_app/pages/home.py +++ b/aamp_app/pages/home.py @@ -35,7 +35,8 @@ "Real Time Telemetry": "/real-time-telemetry", # "Images": "/images", "Sampler": "/sampler", - "Recipe Builder": "/recipe-builder" + "Recipe Builder": "/recipe-builder", + "Solution Map": "/solution-map" # "Options": "/options", } diff --git a/aamp_app/pages/recipe-builder.py b/aamp_app/pages/recipe-builder.py index b1e463a..fa64c23 100644 --- a/aamp_app/pages/recipe-builder.py +++ b/aamp_app/pages/recipe-builder.py @@ -54,7 +54,7 @@ className="mb-3", ), ], - width=4, + width=3, ), dbc.Col( [ @@ -65,7 +65,7 @@ className="btn btn-success mt-2" ), ], - width=4, + width=2, ), ], className="mb-3", diff --git a/aamp_app/pages/solution-map.py b/aamp_app/pages/solution-map.py new file mode 100644 index 0000000..1727d36 --- /dev/null +++ b/aamp_app/pages/solution-map.py @@ -0,0 +1,21 @@ +from dash import html, dcc +import dash +import dash_bootstrap_components as dbc + +dash.register_page( + __name__, + path="/solution-map", + name="Solution Map", + title="Solution Map" +) + +layout = html.Div([ + html.H1("Solution Map"), + dbc.Alert(id="solution-map-alert", is_open=False, color="danger", className="mb-3"), + dcc.Textarea( + id="solution-map-json", + style={"width": "100%", "height": "400px", "fontFamily": "monospace"}, + spellCheck=False, + ), + dbc.Button("Save", id="solution-map-save", color="primary", className="mt-2"), +], className="container") diff --git a/aamp_app/solution_map.json b/aamp_app/solution_map.json new file mode 100644 index 0000000..8797320 --- /dev/null +++ b/aamp_app/solution_map.json @@ -0,0 +1,122 @@ +{ + "A1": { + "polymer": "P3MEEMT", + "solvent": "CB", + "concentration": 40, + "status": "occupied" + }, + "A2": { + "polymer": "P3MEEMT", + "solvent": "CB", + "concentration": 30, + "status": "occupied" + }, + "A3": { + "polymer": "P3MEEMT", + "solvent": "CB", + "concentration": 20, + "status": "occupied" + }, + "A4": { + "polymer": "P3MEEMT", + "solvent": "CB", + "concentration": 10, + "status": "occupied" + }, + "A5": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "B1": { + "polymer": "P3MEEMT", + "solvent": "CB", + "concentration": 15, + "status": "occupied" + }, + "B2": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "B3": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "B4": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "B5": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "C1": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "C2": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "C3": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "C4": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "C5": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "D1": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "D2": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "D3": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "D4": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + }, + "D5": { + "polymer": null, + "solvent": null, + "concentration": null, + "status": "empty" + } +} \ No newline at end of file From 2d68ca817d1721f392584c4ea1586093a6bfebea Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Mon, 12 May 2025 18:59:43 +0000 Subject: [PATCH 096/125] added help tooltips for my new pages --- aamp_app/pages/recipe-builder.py | 23 ++++++++++++++++++++++- aamp_app/pages/sampler.py | 23 ++++++++++++++++++++++- aamp_app/pages/solution-map.py | 23 ++++++++++++++++++++++- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/aamp_app/pages/recipe-builder.py b/aamp_app/pages/recipe-builder.py index fa64c23..6349c3f 100644 --- a/aamp_app/pages/recipe-builder.py +++ b/aamp_app/pages/recipe-builder.py @@ -6,7 +6,28 @@ layout = html.Div( [ - html.H1("Recipe Builder"), + html.Div([ + html.H1([ + "Recipe Builder", + html.Span( + " ?", + id="recipe-builder-help", + style={ + "cursor": "pointer", + "color": "gray", + "fontWeight": "bold", + "fontSize": "0.7em", + "marginLeft": "10px" + } + ), + ], style={"display": "inline-block"}), + dbc.Tooltip( + "This page uses parameter sets generated in the sampler to create recipe templates to run on the devices. Selecting a campaign brings up all the parameter sets associated with it, and uses the solution map to determine each set's corresponding solution position. Generating recipes uses string template matching to fill in the templates with the set information and lets the user preview the results. You can also run all of the recipes sequentially.", + target="recipe-builder-help", + placement="right", + style={"maxWidth": "350px"} + ), + ]), dbc.Alert( id="recipe-alert", color="success", diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py index cd37f07..bcc908e 100644 --- a/aamp_app/pages/sampler.py +++ b/aamp_app/pages/sampler.py @@ -80,7 +80,28 @@ html.Option(value="2.5") ] ), - html.H1("Sampler"), + html.Div([ + html.H1([ + "Sampler", + html.Span( + " ?", + id="sampler-help", + style={ + "cursor": "pointer", + "color": "gray", + "fontWeight": "bold", + "fontSize": "0.7em", + "marginLeft": "10px" + } + ), + ], style={"display": "inline-block"}), + dbc.Tooltip( + "This page allows the user to generate starting parameter sets for a campaign, covering as much as the parameter space as possible so the optimizer can narrow down on particular runs that where successful. You can enter campaign-specific information, and discrete or continuous parameter values for the sampler to explore, and it will generate PCA and uMAP visualizations, the generated parameter values, and their min-max normalized value for the optimizer. This data can then be saved to the database and used in other pages of the app, namely the recipe builder, to start a campaign.", + target="sampler-help", + placement="right", + style={"maxWidth": "350px"} + ), + ]), dbc.Alert( id="sampler-alert", color="success", diff --git a/aamp_app/pages/solution-map.py b/aamp_app/pages/solution-map.py index 1727d36..da1ab80 100644 --- a/aamp_app/pages/solution-map.py +++ b/aamp_app/pages/solution-map.py @@ -10,7 +10,28 @@ ) layout = html.Div([ - html.H1("Solution Map"), + html.Div([ + html.H1([ + "Solution Map", + html.Span( + " ?", + id="solution-map-help", + style={ + "cursor": "pointer", + "color": "gray", + "fontWeight": "bold", + "fontSize": "0.7em", + "marginLeft": "10px" + } + ), + ], style={"display": "inline-block"}), + dbc.Tooltip( + "This page allows you to view and edit the solution map. Each nested object in the JSON represents a cell with a polymer, solvent, and concentration. You can update the map and save changes to the database. Use this to manage which solutions are available in each position (A1-D5). When the recipe builder generates recipe templates, it uses the information from each parameter set and this solution map to decide where to move the handler arm.", + target="solution-map-help", + placement="right", + style={"maxWidth": "350px"} + ), + ]), dbc.Alert(id="solution-map-alert", is_open=False, color="danger", className="mb-3"), dcc.Textarea( id="solution-map-json", From 066902ba4888a763365615f74a19e48afdabb14d Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Thu, 15 May 2025 18:01:42 +0000 Subject: [PATCH 097/125] testing access --- README.md | 58 ++++++++++++++++++++++++++++++++ aamp_app/devices/ximea_camera.py | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 53b9bd0..a86edef 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,64 @@ Example 6 emulates optimization of a material processing experiment. It uses a f You will need some more packages to run example 6 (listed above). Run example 6 as stated above from root. +## Dash UI +The entire functionality of the app can also be accessed through a user-friendly Dash app interface. This includes creating recipes, running them, granulated manual control, and new features involved in automated process optimization, such as parameter set sampling, the recipe builder, and the solution mapper. + +### UI Setup Instructions + +1. Clone this repository. +``` +git clone https://github.com/moleculemaker/Lab_Automation +``` +2. Enter the Lab Automation folder. +``` +cd Lab_Automation +``` +3. Check out the dash-ui branch. +``` +git checkout dash-ui +``` +4. Create a virtual environment. +``` +python3 -m venv venv +``` +5. Activate the virtual environment. + +Mac: +``` +source venv/bin/activate +``` +Windows: +``` +venv\Scripts\activate +``` +6. Install the requirements. +``` +pip install -r requirements.txt +``` +7. Add a .env file to the aamp_app folder and aamp_app/db/validation containing your MongoDB credentials. The files should be in the following format: +``` +MONGO_URI=mongodb+srv://: +MONGO_DB_NAME=diaogroup +``` +8. Enter the aamp_app folder. +``` +cd aamp_app +``` +9. When running the app for the first time with an empty database, the correct collections with validation schemas need to be created. Uncomment the following lines in app.py: +``` +# solutions.init_collection() +# devices.init_collection() +# films.init_collection() +# recipes.init_collection() +``` +They can be commented again once the collections are created in the database. + +10. Run the app +``` +python -m app +``` + ## Further Details ### Creating Device Modules Please see the devices folder for examples of implementing device modules diff --git a/aamp_app/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py index 0d6bdc4..e26bfbf 100644 --- a/aamp_app/devices/ximea_camera.py +++ b/aamp_app/devices/ximea_camera.py @@ -2,7 +2,7 @@ from typing import Optional, Tuple, List from PIL import Image -from ximea import xiapi +# from ximea import xiapi from .device import Device, check_initialized From 58d4382b779fda375251b44893d4ed1bcd5ff8f1 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Thu, 15 May 2025 19:02:25 +0000 Subject: [PATCH 098/125] Added documentation, fixed a bug in the sampler, made it so that recipe builder halts execution if there is no perfect match in the solution map --- aamp_app/app.py | 28 ++++++++++++---------------- aamp_app/devices/ximea_camera.py | 2 +- aamp_app/pages/sampler.py | 4 ++-- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index ab7f79e..759de5b 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -3081,36 +3081,32 @@ def auto_find_solution_positions(parameter_sets, selected_rows, polymer_name): solution_map = mongo.db.solution_map.find_one({}) if not solution_map: raise Exception("No solution map found in database") - solution_map.pop('_id', None) positions = [] for idx in selected_rows: params = parameter_sets[idx] - - best_match = None - closest_diff = float('inf') + found = False for cell_id, cell in solution_map.items(): if (cell['status'] == 'occupied' and cell['polymer'] == polymer_name and - cell['solvent'] == params['solvent']): + cell['solvent'] == params['solvent'] and + cell['concentration'] == params['concentration']): - current_diff = abs(cell['concentration'] - params['concentration']) - if current_diff < closest_diff: - closest_diff = current_diff - best_match = cell_id + positions.append(cell_id) + found = True + break - if best_match: - positions.append(best_match) - else: - positions.append("A1") - # return "", f"No solution found for {polymer_name}/{params['solvent']}/{params['concentration']}%", True + if not found: + err_msg = (f"No exact match found for {polymer_name}/" + f"{params['solvent']}/{params['concentration']}%") + return "", err_msg, True - return ", ".join(positions), "Auto-filled solution positions", True + return ", ".join(positions), "Exact solution positions found", True except Exception as e: - return "", f"Error finding solution positions: {str(e)}", True + return "", f"Error: {str(e)}", True @app.callback( diff --git a/aamp_app/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py index e26bfbf..0d6bdc4 100644 --- a/aamp_app/devices/ximea_camera.py +++ b/aamp_app/devices/ximea_camera.py @@ -2,7 +2,7 @@ from typing import Optional, Tuple, List from PIL import Image -# from ximea import xiapi +from ximea import xiapi from .device import Device, check_initialized diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py index bcc908e..80cab80 100644 --- a/aamp_app/pages/sampler.py +++ b/aamp_app/pages/sampler.py @@ -274,8 +274,8 @@ ), dbc.Col( dbc.InputGroup([ - dbc.Input(id="custom-concentration-input", type="number", placeholder="Custom concentration"), - dbc.Button("Add", id="add-custom-concentration", size="sm"), + dbc.Input(id={"type": "custom-input", "id": "concentration"}, type="number", placeholder="Custom concentration"), + dbc.Button("Add", id={"type": "add-custom-button", "id": "concentration"}, size="sm"), ]), width=4, ) From d13dc86fea1c5517598937246e7375646663ebd0 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Fri, 16 May 2025 20:49:16 +0000 Subject: [PATCH 099/125] changed gitignore to include recipe sample --- README.md | 15 +++- recipes/.gitignore | 3 +- recipes/recipe_sample.py | 182 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 recipes/recipe_sample.py diff --git a/README.md b/README.md index a86edef..63bdfe2 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,20 @@ They can be commented again once the collections are created in the database. ``` python -m app ``` - +### Next steps + +There are a number of pending tasks required for the Dash UI and automated optimization workflow to be fully operational: +- Send experimental results to the Bayesian optimizer +- Receive parameters from the optimizer in the recipe builder, either through database triggers or directly in the app +- Validate custom inputs in the sampler +- Add different sampling methods to the sampler (currently uses simple random sampling) +- Validate the solution map JSON +- Add a way to update or include more recipe templates +- Get the Ximea Camera module working on Mac and Linux devices +- Test running recipes sequentially in the recipe builder +- Visualize parameter space exploration over the course of a campaign +- Visualize optimal parameter ranges over the course of a campaign +- Track campaign progress in the UI, along with logging and error tracking ## Further Details ### Creating Device Modules Please see the devices folder for examples of implementing device modules diff --git a/recipes/.gitignore b/recipes/.gitignore index 5398972..2e40254 100644 --- a/recipes/.gitignore +++ b/recipes/.gitignore @@ -7,4 +7,5 @@ !example4.yaml !example5.yaml !example6.yaml -!ui_recipe_example.yaml \ No newline at end of file +!ui_recipe_example.yaml +!recipe_sample.py \ No newline at end of file diff --git a/recipes/recipe_sample.py b/recipes/recipe_sample.py new file mode 100644 index 0000000..8baba53 --- /dev/null +++ b/recipes/recipe_sample.py @@ -0,0 +1,182 @@ +import time +import os +from command_invoker import CommandInvoker +from command_sequence import CommandSequence +from utilities.solution_map import SolutionMap +from commands.solution_map_commands import FindAndStoreSolutionPosition, GetSolutionWithStoredPosition + +from devices.ximea_camera import XimeaCamera +from devices.newport_esp301 import NewportESP301 +from devices.kinova_arm import KinovaArm +from devices.heating_stage import HeatingStage +from devices.polarizer_servo_motor import PolarizerServoMotor +from devices.psd6_syringe_pump import PSD6SyringePump + +from commands.ximea_camera_commands import * +from commands.newport_esp301_commands import * +from commands.kinova_arm_commands import * +from commands.heating_stage_commands import * +from commands.psd6_syringe_pump_commands import * +from commands.polarizer_servo_motor_commands import * + +def format_speed(speed): + if speed >= 1: + return f"{int(speed)}" + else: + # Convert to exponential notation + # Example: 0.2 -> 2E-1, 0.02 -> 2E-2 + exponent = 0 + normalized = speed + while normalized < 1: + normalized *= 10 + exponent -= 1 + return f"{int(normalized)}E{exponent}" + +# Sample Metadata +round_num = 0 # x +sample_num = $sample_no # y +polymer = "$polymer" +solvent = "$solvent" +concentration = $concentration # mg/ml +speed = $motor_speed # a: mm/s +speed_str = format_speed(speed) +temperature = $temperature # b: °C +gap = $printing_gap # c: um +volume = $precursor_volume # d: ul + +# configure devices +polarizer = PolarizerServoMotor('polarizer', 'COM22') +printer = NewportESP301('printer', 'COM6') +arm = KinovaArm('kinova') +heating_stage = HeatingStage('heating_stage', 'COM16',115200) +xi = XimeaCamera('xi') +pump1 = PSD6SyringePump('pump1', 'COM4') +pump2 = PSD6SyringePump('pump2', 'COM5') +solution_map = SolutionMap() + +# add devices +seq = CommandSequence() +seq.add_device(printer) +seq.add_device(arm) +seq.add_device(heating_stage) +seq.add_device(polarizer) +seq.add_device(xi) +seq.add_device(pump1) +seq.add_device(pump2) +seq.add_device(solution_map) + +# initialize commands +seq.add_command(NewportESP301Connect(printer)) +seq.add_command(NewportESP301Initialize(printer)) +seq.add_command(KinovaArmConnect(arm)) +seq.add_command(KinovaArmInitialize(arm)) +seq.add_command(HeatingStageConnect(heating_stage)) +seq.add_command(HeatingStageInitialize(heating_stage)) +seq.add_command(PolarizerConnect(polarizer)) +seq.add_command(PolarizerInitialize(polarizer)) +seq.add_command(PSD6SyringePumpConnect(pump1)) +seq.add_command(PSD6SyringePumpConnect(pump2)) +seq.add_command(PSD6SyringePumpInitialize(pump1)) +seq.add_command(PSD6SyringePumpInitialize(pump2)) +seq.add_command(XimeaCameraInitialize(xi)) + +# get substrate to the printing stage +seq.add_command(KinovaArmExecuteAction(arm, action_name='Home')) +seq.add_command(KinovaArmOpenGripper(arm)) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Sub_Handler_Up')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Sub_Handler_Down')) +seq.add_command(KinovaArmCloseGripper(arm)) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Sub_Handler_Up')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Substrate_Hotel_Down')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Substrate_1_Down')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Substrate_1_Up')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Substrate_Hotel_Up')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Sub_Handler_Up')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Home')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Printer_Up_Out')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Printer_Up_In')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Printer_Down_In')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Printer_Down_Out')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Home')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Sub_Handler_Up')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Sub_Handler_Down')) +seq.add_command(KinovaArmOpenGripper(arm)) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Sub_Handler_Up')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Home')) + +# set printing temperature +seq.add_command(HeatingStageSetSetPoint(heating_stage, temperature)) + +# get solution + +# Sahas - commenting this out for now because the user can manually enter solution position in the recipe builder UI +# find_solution_cmd = FindAndStoreSolutionPosition(solution_map, polymer, solvent, concentration) +# seq.add_command(find_solution_cmd) + +seq.add_command(KinovaArmExecuteAction(arm, action_name='Liq_Handler_Up')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Liq_Handler_Down')) +seq.add_command(KinovaArmCloseGripper(arm)) +seq.add_command(KinovaArmExecuteAction(arm, action_name='Liq_Handler_Up')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='$solution_position' + '_Up')) +seq.add_command(KinovaArmExecuteAction(arm, action_name='$solution_position' + '_Down')) + +# Set Exposure Time +CROSSPOL_EXPOSURE = 50000 +NORMAL_EXPOSURE = 12000 + +# Set Save Path (data/imaging is excluded) +BASE_DIR = polymer # P42gTTT +CROSSPOL_DIR = os.path.join(BASE_DIR, 'crosspol') # P42gTTT/crosspol +NORMAL_DIR = os.path.join(BASE_DIR, 'normal') # P42gTTT/normal + +# Set Base Sample Name +base_sample_name = f"R{round_num}S{sample_num}_{polymer}_{solvent}_{concentration}mgml_{speed_str}mms_{temperature}C_{gap}um_{volume}ul" + + + +# Take Cross-pol Images (polarizer 0 degree) +seq.add_command(RotatePolarizerAbsolute(polarizer, angle=0)) + +sample_angles = [90, 60, 45, 30, 0] +for angle in sample_angles: + seq.add_command(RotateSampleAbsolute(polarizer, angle=angle)) + filename = os.path.join(CROSSPOL_DIR, f"{base_sample_name}_crosspol_{angle}deg") + seq.add_command( + XimeaCameraGetImage(xi, + filename=filename, + y_upper=10, + y_length=1000, + x_upper=500, + x_length=1000, + exposure_time=CROSSPOL_EXPOSURE, + check_uniform=False) + ) + +# Take Normal Images (polarizer 90 degree) +seq.add_command(RotatePolarizerAbsolute(polarizer, angle=90)) + +for angle in sample_angles: + seq.add_command(RotateSampleAbsolute(polarizer, angle=angle)) + filename = os.path.join(NORMAL_DIR, f"{base_sample_name}_normal_{angle}deg") + seq.add_command( + XimeaCameraGetImage(xi, + filename=filename, + y_upper=10, + y_length=1000, + x_upper=500, + x_length=1000, + exposure_time=NORMAL_EXPOSURE, + check_uniform=False) + ) + +# Return to initial position +seq.add_command(RotatePolarizerAbsolute(polarizer, angle=0)) +seq.add_command(RotateSampleAbsolute(polarizer, angle=0)) + +# Create necessary folders (include full path) +os.makedirs(os.path.join('data', 'imaging', CROSSPOL_DIR), exist_ok=True) +os.makedirs(os.path.join('data', 'imaging', NORMAL_DIR), exist_ok=True) + +invoker = CommandInvoker(seq, False) +res = invoker.invoke_commands() +print(res) From 2077f20a73bac6235595cd1e61871a1854e5e979 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Fri, 16 May 2025 23:51:04 +0000 Subject: [PATCH 100/125] updated README with docs on how to add new recipes for recipe builder and more todo items --- README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 63bdfe2..4a3533e 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,16 @@ Lab Automation is a modular framework for automating multiple devices or instrum 4. [Usage](#usage) - [Automation](#automation) - [Optimization](#autonomous-process-optimization) -5. [Further Details](#further-details) +5. [Dash UI](#dash-ui) + - [UI Setup Instructions](#ui-setup-instructions) + - [Adding Recipes for the Recipe Builder](#adding-recipes-for-the-recipe-builder) + - [Next Steps](#next-steps) +6. [Further Details](#further-details) - [Creating Device Modules](#creating-device-modules) - [Creating Command Modules](#creating-command-modules) - [Composite Commands](#composite-commands) - [Program Scheme](#program-scheme) -6. [License](#license) +7. [License](#license) ## Description Lab Automation was developed for the purpose of automating laboratory experiments to enable high-throughput data collection and process optimization. It can be used to automate experimental procedures that involve instruments from different vendors, with different communication protocols, and also home-built equipment. The automated procedures can then be tied into sequential model-based optimization algorithms (e.g. Bayesian optimization) to enable self-driving, autonomous lab experiments. @@ -168,13 +172,24 @@ They can be commented again once the collections are created in the database. ``` python -m app ``` -### Next steps + +### Adding Recipes for the Recipe Builder +Right now the recipe builder is formatted to work with one recipe template, and that is recipe_sample.py. A keyword search for this in the app.py file will reveal the place where it is being read in and saved as a constant string literal. To add a new recipe: +1. Include it as a Python file in the recipes directory. +2. Replace all literals for template variables where appropriate (concentration, printing gap, arm position, etc.). You can refer to recipe_sample.py for an example implementation of this. Adding additional template variables requires modification of the generate_recipes callback in app.py. +3. Change the filename being read into app.py from recipe_sample.py to your new recipe's filename. +4. Ensure that the generate_recipes callback runs without error with the new recipe. +5. By default, all recipes are ignored. If you want git to track the new recipe, add !\.py to the .gitignore file in the recipes folder. + +### Next Steps There are a number of pending tasks required for the Dash UI and automated optimization workflow to be fully operational: - Send experimental results to the Bayesian optimizer - Receive parameters from the optimizer in the recipe builder, either through database triggers or directly in the app - Validate custom inputs in the sampler - Add different sampling methods to the sampler (currently uses simple random sampling) +- Allow a campaign to have more than one polymer +- Attach polymer information to each parameter set rather than to campaign metadata - Validate the solution map JSON - Add a way to update or include more recipe templates - Get the Ximea Camera module working on Mac and Linux devices @@ -182,6 +197,8 @@ There are a number of pending tasks required for the Dash UI and automated optim - Visualize parameter space exploration over the course of a campaign - Visualize optimal parameter ranges over the course of a campaign - Track campaign progress in the UI, along with logging and error tracking +- Merge the dash-ui branch to master once a full campaign can be run through the UI (and most of the above objectives are complete) + ## Further Details ### Creating Device Modules Please see the devices folder for examples of implementing device modules From dd7f1528a19e32eae679bec2952243fc5452a204 Mon Sep 17 00:00:00 2001 From: Sahas Ramesh Date: Sat, 17 May 2025 00:43:04 +0000 Subject: [PATCH 101/125] added batch number and polymer to parameter sets, now each batch in a campaign can have a different polymer, updated README todos accordingly --- aamp_app/app.py | 80 +++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index 759de5b..9f12792 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -2905,7 +2905,18 @@ def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_na image_id = fs.put(decoded, filename=image_filename) existing_campaign = mongo.db.campaigns.find_one({"campaign_name": campaign_name}) + pipeline = [ + {"$group": {"_id": None, "max_value": {"$max": "$batch_no"}}} + ] + + try: + result = list(mongo.db.sets.aggregate(pipeline)) + max_value = result[0]["max_value"] if result else None + except (IndexError, KeyError): + max_value = None + batch_no = max_value + 1 if max_value is not None else 1 + if existing_campaign: campaign_id = existing_campaign["_id"] update_data = {} @@ -2923,6 +2934,8 @@ def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_na sets_to_insert = [{ "campaign_id": campaign_id, + "polymer_name": polymer_name, + "batch_no": batch_no, "sample_no": p_set["sample_no"], "motor_speed": p_set["motor_speed"], "temperature": p_set["temperature"], @@ -2942,7 +2955,6 @@ def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_na else: campaign_doc = { "campaign_name": campaign_name, - "polymer_name": polymer_name, "smiles_string": smiles_string, "mw": mw, "pdi": pdi, @@ -2959,6 +2971,8 @@ def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_na sets_to_insert = [{ "campaign_id": campaign_id, + "polymer_name": polymer_name, + "batch_no": batch_no, "sample_no": p_set["sample_no"], "motor_speed": p_set["motor_speed"], "temperature": p_set["temperature"], @@ -3018,7 +3032,6 @@ def update_run_button_label(_, parameter_sets): @app.callback( Output("recipe-builder-sets", "children"), Output("recipe-builder-parameter-sets", "data"), - Output("recipe-builder-polymer-name", "data"), Input("recipe-builder-campaign-dropdown", "value"), prevent_initial_call=True ) @@ -3030,8 +3043,8 @@ def display_campaign_sets(selected_campaign): campaign = mongo.db.campaigns.find_one({"campaign_name": selected_campaign}) if not campaign: return html.Div(f"Campaign '{selected_campaign}' not found") - polymer_name = campaign.get('polymer_name') sets = list(mongo.db.sets.find({"campaign_id": campaign["_id"]})) + if not sets: return html.Div(f"No parameter sets found for campaign '{selected_campaign}'") @@ -3054,7 +3067,7 @@ def display_campaign_sets(selected_campaign): selected_rows=selected_rows ) - return table, sets, polymer_name + return table, sets except Exception as e: print(f"Error loading parameter sets: {e}") @@ -3070,43 +3083,39 @@ def display_campaign_sets(selected_campaign): Output("recipe-alert", "is_open"), Input("recipe-builder-parameter-sets", "data"), Input("recipe-builder-sets-table", "selected_rows"), - State("recipe-builder-polymer-name", "data"), prevent_initial_call=True ) -def auto_find_solution_positions(parameter_sets, selected_rows, polymer_name): +def auto_find_solution_positions(parameter_sets, selected_rows): if not parameter_sets or not selected_rows: return dash.no_update, dash.no_update, dash.no_update - try: - solution_map = mongo.db.solution_map.find_one({}) - if not solution_map: - raise Exception("No solution map found in database") - solution_map.pop('_id', None) + solution_map = mongo.db.solution_map.find_one({}) + if not solution_map: + raise Exception("No solution map found in database") + solution_map.pop('_id', None) + + positions = [] + for idx in selected_rows: + params = parameter_sets[idx] + found = False - positions = [] - for idx in selected_rows: - params = parameter_sets[idx] - found = False - - for cell_id, cell in solution_map.items(): - if (cell['status'] == 'occupied' and - cell['polymer'] == polymer_name and - cell['solvent'] == params['solvent'] and - cell['concentration'] == params['concentration']): - - positions.append(cell_id) - found = True - break - - if not found: - err_msg = (f"No exact match found for {polymer_name}/" - f"{params['solvent']}/{params['concentration']}%") - return "", err_msg, True + for cell_id, cell in solution_map.items(): + if (cell['status'] == 'occupied' and + cell['polymer'] == params['polymer_name'] and + cell['solvent'] == params['solvent'] and + cell['concentration'] == params['concentration']): + + positions.append(cell_id) + found = True + break - return ", ".join(positions), "Exact solution positions found", True + if not found: + err_msg = ( + f"No exact match found for {params['polymer_name']}/{params['solvent']}/{params['concentration']}%" + ) + return "", err_msg, True - except Exception as e: - return "", f"Error: {str(e)}", True + return ", ".join(positions), "Exact solution positions found", True @app.callback( @@ -3115,11 +3124,10 @@ def auto_find_solution_positions(parameter_sets, selected_rows, polymer_name): Input("recipe-builder-generate-button", "n_clicks"), State("recipe-builder-sets-table", "selected_rows"), State("recipe-builder-parameter-sets", "data"), - State("recipe-builder-polymer-name", "data"), State("recipe-builder-solution-position", "value"), prevent_initial_call=True ) -def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name, solution_positions): +def generate_recipes(n_clicks, selected_rows, parameter_sets, solution_positions): positions = [pos.strip() for pos in solution_positions.split(",")] if solution_positions else [] generated_scripts = [] @@ -3128,7 +3136,7 @@ def generate_recipes(n_clicks, selected_rows, parameter_sets, polymer_name, solu sub_dict = { 'sample_no': params['sample_no'], - 'polymer': polymer_name, + 'polymer': params['polymer_name'], 'solvent': params['solvent'], 'concentration': params['concentration'], 'motor_speed': params['motor_speed'], From 35e05cdc97c5cac1087a7a7475216b1bd03db58e Mon Sep 17 00:00:00 2001 From: Weiqi Zhang Date: Wed, 6 Aug 2025 11:59:53 -0500 Subject: [PATCH 102/125] add optimizer page --- aamp_app/app.py | 56 ++++- aamp_app/pages/bayesian-optimization.py | 314 ++++++++++++++++++++++++ aamp_app/pages/home.py | 3 +- 3 files changed, 370 insertions(+), 3 deletions(-) create mode 100644 aamp_app/pages/bayesian-optimization.py diff --git a/aamp_app/app.py b/aamp_app/app.py index 9f12792..b678217 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -127,6 +127,7 @@ def load_env(file_path=".env"): dbc.NavItem(dbc.NavLink("Sampler", href="/sampler", external_link=True)), dbc.NavItem(dbc.NavLink("Recipe Builder", href="/recipe-builder", external_link=True)), dbc.NavItem(dbc.NavLink("Solution Map", href="/solution-map", external_link=True)), + dbc.NavItem(dbc.NavLink("Optimization", href="/bayesian-optimization", external_link=True)), # dbc.DropdownMenu( # children=[ # # dbc.DropdownMenuItem( @@ -2890,10 +2891,15 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string State("sampler-pdi", "value"), State("sampler-polymer-image", "contents"), State("sampler-polymer-image", "filename"), + State({"type": "sampler-dropdown", "id": "concentration"}, "value"), + State({"type": "sampler-dropdown", "id": "printing-gap"}, "value"), + State({"type": "sampler-dropdown", "id": "precursor-volume"}, "value"), + State({"type": "sampler-dropdown", "id": "motor-speed"}, "value"), prevent_initial_call=True ) def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_name, polymer_name, - smiles_string, mw, pdi, image_contents, image_filename): + smiles_string, mw, pdi, image_contents, image_filename, + concentration_range, printing_gap, precursor_vol, motor_speed): if not parameter_sets: return "No parameter sets to save.", True, "warning", True @@ -2958,7 +2964,11 @@ def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_na "smiles_string": smiles_string, "mw": mw, "pdi": pdi, - "created_at": datetime.now() + "created_at": datetime.now(), + "concentration_range": concentration_range, + "printing_gap": printing_gap, + "precursor_volume": precursor_vol, + "motor_speed": motor_speed, } if gpc_data: @@ -3425,3 +3435,45 @@ def save_solution_map(n_clicks, json_text): # columns=[{"name": "Name", "id": "Name"}, {"name": "Value", "id": "Value"}], # ) # return table +@app.callback( + Output('optimization-status', 'children'), + Output('optimization-graph', 'figure'), + Output('results-table', 'data'), + Input('start-optimization', 'n_clicks'), + State('input-function', 'value') +) +def start_optimization(n_clicks, function_str): + if n_clicks is None or not function_str: + return "Click the button to start optimization.", {}, [] + + # Simulate Bayesian optimization process + iterations = 10 + results = [] + best_value = float('inf') + best_params = None + + for i in range(iterations): + # Simulate a random parameter and its evaluation + params = np.random.rand(2) + value = eval(function_str.replace('x', str(params[0])).replace('y', str(params[1]))) + results.append({ + "iteration": i + 1, + "best_value": value, + "parameters": f"x: {params[0]:.2f}, y: {params[1]:.2f}" + }) + if value < best_value: + best_value = value + best_params = params + # Create a simple graph to show the optimization process + fig = { + 'data': [ + {'x': [i + 1 for i in range(iterations)], 'y': [result['best_value'] for result in results], 'type': 'line', 'name': 'Best Value'} + ], + 'layout': { + 'title': 'Optimization Progress', + 'xaxis': {'title': 'Iteration'}, + 'yaxis': {'title': 'Best Value'}, + 'showlegend': True + } + } + return f"Optimization completed. Best value: {best_value:.2f} at parameters {best_params}.", fig, results \ No newline at end of file diff --git a/aamp_app/pages/bayesian-optimization.py b/aamp_app/pages/bayesian-optimization.py new file mode 100644 index 0000000..5dcb781 --- /dev/null +++ b/aamp_app/pages/bayesian-optimization.py @@ -0,0 +1,314 @@ +from dash import html, dcc, dash_table, callback, Input, Output, State +import dash_bootstrap_components as dbc +import dash +import numpy as np +import pandas as pd +from datetime import datetime, timedelta +import random + +dash.register_page( + __name__, + path="/bayesian-optimization", + name="Optimization", + title="Optimization" +) + +layout = html.Div( + [ + html.Div([ + html.H1([ + "Optimization", + html.Span( + " ?", + id="recipe-builder-help", + style={ + "cursor": "pointer", + "color": "gray", + "fontWeight": "bold", + "fontSize": "0.7em", + "marginLeft": "10px" + } + ), + ], style={"display": "inline-block"}), + dbc.Tooltip( + "This page uses parameter sets generated in the sampler to create recipe templates to run on the devices. Selecting a campaign brings up all the parameter sets associated with it, and uses the solution map to determine each set's corresponding solution position. Generating recipes uses string template matching to fill in the templates with the set information and lets the user preview the results. You can also run all of the recipes sequentially.", + target="recipe-builder-help", + placement="right", + style={"maxWidth": "350px"} + ), + ]), + dbc.Alert( + id="recipe-alert", + color="success", + is_open=False, + fade=True, + className="mb-3", + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + html.H5("Select Campaign"), + dcc.Dropdown( + id="recipe-builder-campaign-dropdown", + options=[], + placeholder="Select a campaign..." + ), + ], + width=8, + ), + ], + className="mb-3", + ), + ], + className="container", + ), + # html.Div([ + # html.H4("Campaign Progress"), + # dash_table.DataTable( + # id="recipe-builder-campaign-progress", + # columns=[ + # {"name": "Set ID", "id": "set_id"}, + # {"name": "Status", "id": "status"}, + # {"name": "Timestamp", "id": "timestamp"}, + # {"name": "Solution Pos", "id": "solution_pos"}, + # ], + # data=[], # Filled in via callback when a campaign is selected + # style_table={"overflowX": "auto"}, + # style_cell={"textAlign": "center"}, + # style_header={"backgroundColor": "#f8f9fa", "fontWeight": "bold"}, + # ) + # ], className="mb-4"), + + html.Hr(), + html.H4("Optimizer Parameter Generation"), + + html.Div(id="bo-hyperparam-fields"), + + dbc.Row([ + dbc.Col([ + html.Label("Number of Batches"), + dbc.Input(id="bo-num-batches", type="number", value=3, min=1) + ], width=4), + dcc.Interval(id="bo-generator-timer", interval=1000, n_intervals=0, disabled=True), + dcc.Store(id="bo-generated-data", data=[]), + + dbc.Col([ + html.Label("Stopping Criterion"), + dcc.Dropdown( + id="bo-stopping-criterion", + options=[ + {"label": "Max Iterations", "value": "max_iter"}, + # {"label": "Convergence (No Improvement)", "value": "no_improve"}, + {"label": "Time Limit (minutes)", "value": "time_limit"}, + # {"label": "Target Objective Reached", "value": "target_value"}, + ], + placeholder="Select stopping rule" + ) + ], width=4), + + dbc.Col([ + html.Label("Stopping Threshold"), + dbc.Input(id="bo-stopping-value", type="number", placeholder="Enter threshold") + ], width=4), + ], className="mb-3"), + + dbc.Button("Generate Optimizer Parameters", id="bo-generate-btn", color="primary", className="mb-3"), + html.Div(id="optimizer-output"), + + html.Div([ + dbc.Row([ + dbc.Col([ + dbc.Button("Pause", id="bo-pause-btn", color="warning", className="me-2"), + html.Span("Status: ", style={"fontWeight": "bold"}), + html.Span(id="bo-status-label", children="Idle") + ]) + ], className="mb-3") + ]), + + dcc.Store(id="bo-status-store", data="idle"), + + html.Div(id="bo-generated-table"), + + dbc.Button("Save Optimizer Parameters", id="bo-save-btn", color="success", className="mt-3", disabled=True), + + dbc.Alert(id="bo-save-alert", is_open=False, color="success", className="mt-3") + + ], + className="container", +) + +@callback( + Output("optimizer-output", "children"), + Input("bo-generate-btn", "n_clicks"), + State("bo-num-batches", "value"), + State("bo-stopping-criterion", "value"), + State("bo-stopping-value", "value"), + prevent_initial_call=True +) +def generate_optimizer_parameters(n_clicks, num_batches, stopping_criterion, stopping_value): + import torch + from optimizer import BayesianOptimizer, MockObjectiveFunction + + # Define search space bounds + if n_clicks > 0: + # bounds = torch.tensor([ + # [0.1, 1.0], # concentration + # [10.0, 100.0], # print_speed + # [0.05, 0.5], # gap_size + # [5.0, 25.0] # volume + # ]).T + bounds = torch.tensor([ + [2, 20], # concentration + [50.0, 100.0], # print_speed + [0.01, 20], # gap_size + [6.0, 12.0] # volume + ]).T + + # Create optimizer and objective function + optimizer = BayesianOptimizer(bounds=bounds, batch_size=num_batches) + objective = MockObjectiveFunction() + + # Run optimization + best_params, best_score = optimizer.optimize( + objective_function=objective, + n_iterations=stopping_value, + n_initial_points=10 + ) + return html.Div([ + html.H3("Optimization Results:"), + html.P(f"Best concentration: {best_params[0]:.4f}"), + html.P(f"Best print speed: {best_params[1]:.4f}"), + html.P(f"Best gap size: {best_params[2]:.4f}"), + html.P(f"Best volume: {best_params[3]:.4f}"), + html.P(f"Best score: {best_score:.4f}") + ]) + +# @callback( +# Output("bo-generator-timer", "disabled"), +# Output("bo-generated-data", "data"), +# Output("bo-generated-table", "children"), +# Output("bo-save-btn", "disabled"), +# Input("bo-generate-btn", "n_clicks"), +# Input("bo-generator-timer", "n_intervals"), +# State("bo-status-store", "data"), # 🆕 check if paused +# State("bo-generated-data", "data"), +# State("bo-num-batches", "value"), +# prevent_initial_call=True +# ) +# def manage_bo_generation(n_clicks, n_intervals, status, current_data, num_batches): +# import numpy as np +# import pandas as pd +# from dash import dash_table, callback_context + +# if current_data is None: +# current_data = [] + +# triggered = callback_context.triggered_id + +# if triggered == "bo-generate-btn": +# # Reset state and begin generation +# return False, [], dash.no_update, True + +# if triggered == "bo-generator-timer": +# if status != "running": +# # 🧊 paused or idle — stop timer activity +# return dash.no_update, dash.no_update, dash.no_update, dash.no_update + +# if len(current_data) >= num_batches: +# df = pd.DataFrame(current_data) +# return True, current_data, dash_table.DataTable( +# columns=[{"name": i, "id": i} for i in df.columns], +# data=df.to_dict("records"), +# style_table={"overflowX": "auto"}, +# page_size=10 +# ), False + +# # ➕ Generate one new row +# new_row = { +# "concentration": round(np.random.uniform(1, 5), 2), +# "motor_speed": round(np.random.uniform(0.01, 20.0), 2), +# "precursor_volume": round(np.random.uniform(6.0, 12.0), 2), +# } + +# updated_data = current_data + [new_row] +# df = pd.DataFrame(updated_data) + +# return False, updated_data, dash_table.DataTable( +# columns=[{"name": i, "id": i} for i in df.columns], +# data=df.to_dict("records"), +# style_table={"overflowX": "auto"}, +# page_size=10 +# ), len(updated_data) < num_batches + +# return dash.no_update, dash.no_update, dash.no_update, dash.no_update + +@callback( + Output("bo-save-alert", "children"), + Output("bo-save-alert", "is_open"), + Input("bo-save-btn", "n_clicks"), + prevent_initial_call=True +) +def save_bo_params(n): + return "BO parameter sets saved successfully!", True +@callback( + Output("bo-stopping-value", "placeholder"), + Input("bo-stopping-criterion", "value") +) +def update_bo_stopping_placeholder(mode): + if mode == "max_iter": + return "e.g., 10 iterations" + elif mode == "no_improve": + return "e.g., 3 stagnant batches" + elif mode == "time_limit": + return "e.g., 30 minutes" + return "Enter threshold" + +# @callback( +# Output("bo-status-store", "data"), +# Output("bo-pause-btn", "children"), +# Output("bo-status-label", "children"), +# Input("bo-pause-btn", "n_clicks"), +# Input("bo-generate-btn", "n_clicks"), +# State("bo-status-store", "data"), +# prevent_initial_call=True +# ) +# def update_bo_status(pause_clicks, generate_clicks, current_status): +# from dash import callback_context + +# triggered = callback_context.triggered_id + +# if triggered == "bo-generate-btn": +# return "running", "Pause", "Running" + +# if triggered == "bo-pause-btn": +# if current_status == "running": +# return "paused", "Resume", "Paused" +# elif current_status == "paused": +# return "running", "Pause", "Running" + +# # fallback +# return "idle", "Pause", "Idle" + +# @callback( +# Output("recipe-builder-campaign-progress", "data"), +# Input("recipe-builder-campaign-dropdown", "value") +# ) +# def update_campaign_progress(campaign_name): +# if not campaign_name: +# return [] +# return get_param_sets_for_campaign(campaign_name) +# def get_param_sets_for_campaign(campaign): +# now = datetime.now() +# statuses = ["Pending", "Generated", "Executed"] +# return [ +# { +# "set_id": f"PS{i+1}", +# "status": 'Pending', +# "timestamp": (now - timedelta(minutes=i*5)).strftime("%Y-%m-%d %H:%M"), +# "solution_pos": 'A1', +# } +# for i in range(5) +# ] \ No newline at end of file diff --git a/aamp_app/pages/home.py b/aamp_app/pages/home.py index 44353db..bd50e74 100644 --- a/aamp_app/pages/home.py +++ b/aamp_app/pages/home.py @@ -36,7 +36,8 @@ # "Images": "/images", "Sampler": "/sampler", "Recipe Builder": "/recipe-builder", - "Solution Map": "/solution-map" + "Solution Map": "/solution-map", + "Optimization": "/bayesian-optimization", # "Options": "/options", } From e4f8455f0294c34515238af4dfb207c9c4321502 Mon Sep 17 00:00:00 2001 From: Weiqi Zhang Date: Fri, 19 Sep 2025 03:03:31 -0500 Subject: [PATCH 103/125] Integrate optimizer and add plots --- aamp_app/optimizer/QUICKSTART.md | 169 ++++ aamp_app/optimizer/README.md | 255 ++++++ aamp_app/optimizer/__init__.py | 15 + aamp_app/optimizer/bayesian_optimizer.py | 286 +++++++ aamp_app/optimizer/demo/demo.py | 312 +++++++ aamp_app/optimizer/demo/example_usage.py | 282 +++++++ .../demo/model_integration_example.py | 156 ++++ aamp_app/optimizer/demo/run_demo.py | 55 ++ aamp_app/optimizer/model_objective_factory.py | 313 +++++++ .../optimizer/notebooks/demo_notebook.ipynb | 462 +++++++++++ .../notebooks/interactive_notebook.ipynb | 299 +++++++ .../notebooks/model_integration_demo.ipynb | 775 ++++++++++++++++++ .../notebooks/tutorial_notebook.ipynb | 70 ++ aamp_app/optimizer/objective_function.py | 199 +++++ aamp_app/optimizer/optimization_progress.png | Bin 0 -> 228134 bytes aamp_app/optimizer/requirements.txt | 17 + aamp_app/optimizer/setup.py | 53 ++ aamp_app/pages/bayesian-optimization.py | 365 ++++++--- 18 files changed, 3983 insertions(+), 100 deletions(-) create mode 100644 aamp_app/optimizer/QUICKSTART.md create mode 100644 aamp_app/optimizer/README.md create mode 100644 aamp_app/optimizer/__init__.py create mode 100644 aamp_app/optimizer/bayesian_optimizer.py create mode 100644 aamp_app/optimizer/demo/demo.py create mode 100644 aamp_app/optimizer/demo/example_usage.py create mode 100644 aamp_app/optimizer/demo/model_integration_example.py create mode 100644 aamp_app/optimizer/demo/run_demo.py create mode 100644 aamp_app/optimizer/model_objective_factory.py create mode 100644 aamp_app/optimizer/notebooks/demo_notebook.ipynb create mode 100644 aamp_app/optimizer/notebooks/interactive_notebook.ipynb create mode 100644 aamp_app/optimizer/notebooks/model_integration_demo.ipynb create mode 100644 aamp_app/optimizer/notebooks/tutorial_notebook.ipynb create mode 100644 aamp_app/optimizer/objective_function.py create mode 100644 aamp_app/optimizer/optimization_progress.png create mode 100644 aamp_app/optimizer/requirements.txt create mode 100644 aamp_app/optimizer/setup.py diff --git a/aamp_app/optimizer/QUICKSTART.md b/aamp_app/optimizer/QUICKSTART.md new file mode 100644 index 0000000..0adc1d2 --- /dev/null +++ b/aamp_app/optimizer/QUICKSTART.md @@ -0,0 +1,169 @@ +# Quick Start Guide + +Get up and running with the Bayesian Optimization package in minutes! + +## Installation + +1. **Install dependencies:** +```bash +pip install -r requirements.txt +``` + +2. **Verify installation:** +```bash +python -c "import torch, botorch, gpytorch; print('All dependencies installed successfully')" +``` + +## Run the Demo + +### Option 1: Simple Demo +```bash +python run_demo.py +``` + +### Option 2: From Python +```python +from optimizer import run_optimization_demo + +# Run with default settings +best_params, best_score = run_optimization_demo() +print(f"Best parameters: {best_params}") +print(f"Best score: {best_score:.4f}") +``` + +## Basic Usage + +### Simple Optimization +```python +import torch +from optimizer import BayesianOptimizer, MockObjectiveFunction + +# 1. Define parameter bounds +bounds = torch.tensor([ + [0.1, 1.0], # concentration + [10.0, 100.0], # print_speed + [0.05, 0.5], # gap_size + [5.0, 25.0] # volume +]).T + +# 2. Create optimizer and objective +optimizer = BayesianOptimizer(bounds=bounds, batch_size=8) +objective = MockObjectiveFunction(noise_std=0.1) + +# 3. Run optimization +best_params, best_score = optimizer.optimize( + objective_function=objective, + n_iterations=20, + n_initial_points=10 +) + +print(f"Best found: {best_params} with score {best_score:.4f}") +``` + +### Custom Objective Function +```python +def my_objective(X): + """Custom objective function""" + # Your optimization logic here + # X is a tensor of shape (batch_size, 4) + # Return tensor of shape (batch_size, 1) + + # Example: simple quadratic function + result = -(X - 0.5).pow(2).sum(dim=1, keepdim=True) + return result + +# Use with optimizer +optimizer = BayesianOptimizer(bounds=bounds, batch_size=8) +best_params, best_score = optimizer.optimize( + objective_function=my_objective, + n_iterations=15 +) +``` + +## Key Parameters + +| Parameter | Description | Default | Tips | +|-----------|-------------|---------|------| +| `bounds` | Parameter bounds tensor (2, n_dims) | Required | Define your search space | +| `batch_size` | Candidates per iteration | 8 | Larger = more parallel evaluation | +| `n_iterations` | Optimization iterations | 20 | More iterations = better results | +| `n_initial_points` | Initial random samples | 10 | Should be ≥ 2 * dimensions | +| `noise_std` | Objective function noise | 0.1 | Match your actual noise level | + +## Common Patterns + +### Pattern 1: Quick Optimization +```python +# For quick tests and experimentation +best_params, best_score = optimizer.optimize( + objective_function=objective, + n_iterations=10, + n_initial_points=5, + verbose=True +) +``` + +### Pattern 2: Production Optimization +```python +# For production use with more iterations +best_params, best_score = optimizer.optimize( + objective_function=objective, + n_iterations=50, + n_initial_points=20, + verbose=True +) +``` + +### Pattern 3: Manual Control +```python +# For custom control over the optimization loop +optimizer.generate_initial_data(10, objective) + +for i in range(20): + optimizer.fit_model() + candidates = optimizer.optimize_acquisition() + values = objective(candidates) + optimizer.update_training_data(candidates, values) + print(f"Iteration {i+1}: Best = {optimizer.best_observed_value:.4f}") +``` + +## Expected Output + +The demo will show: +- Parameter space information +- Optimization progress for each iteration +- Best parameters found +- Comparison with true optimum +- Convergence analysis +- Visualization plots (if matplotlib available) + +## Troubleshooting + +### Import Errors +```bash +# Install missing packages +pip install torch botorch gpytorch numpy matplotlib +``` + +### Slow Performance +- Reduce `batch_size` (e.g., 4 instead of 8) +- Reduce `n_iterations` for testing +- Use smaller `n_initial_points` + +### Memory Issues +- Reduce `batch_size` +- Use CPU instead of GPU: `torch.set_default_device('cpu')` + +## Next Steps + +1. **Read the full README.md** for detailed documentation +2. **Check example_usage.py** for advanced usage patterns +3. **Modify the objective function** for your specific problem +4. **Adjust bounds and parameters** for your optimization problem + +## Need Help? + +- Check the full documentation in `README.md` +- Look at examples in `example_usage.py` +- Review the source code for detailed comments +- The package is designed to be self-contained and well-documented \ No newline at end of file diff --git a/aamp_app/optimizer/README.md b/aamp_app/optimizer/README.md new file mode 100644 index 0000000..dbc16b5 --- /dev/null +++ b/aamp_app/optimizer/README.md @@ -0,0 +1,255 @@ +# Bayesian Optimization Package + +A complete, self-contained Python package for batch Bayesian Optimization using BoTorch and GPyTorch libraries. This package demonstrates how to optimize a noisy, black-box objective function with 4 continuous parameters using batch optimization. + +## Features + +- **Batch Optimization**: Suggests 8 new candidates per iteration for parallel evaluation +- **Noisy Objective Handling**: Uses qLogNoisyExpectedImprovement acquisition function (numerically stable) +- **Gaussian Process Surrogate**: SingleTaskGP model for function approximation +- **Real-world Simulation**: Mock objective function with realistic noise and single maximum +- **Well-documented**: Comprehensive comments and easy-to-understand code + +## Problem Definition + +### Objective +Maximize a noisy, black-box objective function representing a printing process optimization. + +### Parameters +- `concentration`: [0.1, 1.0] - Material concentration +- `print_speed`: [10.0, 100.0] - Printing speed (mm/s) +- `gap_size`: [0.05, 0.5] - Gap size between layers (mm) +- `volume`: [5.0, 25.0] - Volume per drop (μL) + +### Configuration +- **Batch Size**: 8 candidates per iteration +- **Iterations**: 20 optimization rounds +- **Initial Points**: 10 random samples + +## Installation + +1. Install the required dependencies: +```bash +pip install -r requirements.txt +``` + +2. Import and use the package: +```python +from optimizer import run_optimization_demo + +# Run the complete optimization workflow +best_params, best_score = run_optimization_demo() +``` + +## Usage Example + +```python +import torch +from optimizer import BayesianOptimizer, MockObjectiveFunction + +# Define search space bounds +bounds = torch.tensor([ + [0.1, 1.0], # concentration + [10.0, 100.0], # print_speed + [0.05, 0.5], # gap_size + [5.0, 25.0] # volume +]).T + +# Create optimizer and objective function +optimizer = BayesianOptimizer(bounds=bounds, batch_size=8) +objective = MockObjectiveFunction() + +# Run optimization +best_params, best_score = optimizer.optimize( + objective_function=objective, + n_iterations=20, + n_initial_points=10 +) + +print(f"Best parameters: {best_params}") +print(f"Best score: {best_score:.4f}") +``` + +## 📓 Jupyter Notebooks + +The package includes three interactive Jupyter notebooks: + +### 1. **Tutorial Notebook** (`tutorial_notebook.ipynb`) +- **Perfect for beginners** - step-by-step explanation of Bayesian optimization +- Covers theory, implementation, and best practices +- Interactive examples and visualizations + +### 2. **Demo Notebook** (`demo_notebook.ipynb`) +- **Complete demonstration** of the optimization workflow +- Comprehensive analysis and visualization +- Real-world manufacturing example + +### 3. **Interactive Notebook** (`interactive_notebook.ipynb`) +- **Experimentation platform** with easy parameter adjustment +- Compare different optimization settings +- Real-time visualization of results + +To use the notebooks: +```bash +# Install Jupyter if not already installed +pip install jupyter + +# Launch Jupyter +jupyter notebook + +# Open any of the .ipynb files +``` + +## Architecture + +### Core Components + +1. **BayesianOptimizer**: Main optimization class handling the GP model and acquisition function +2. **MockObjectiveFunction**: Simulates a realistic black-box objective with noise +3. **Demo Script**: Complete workflow demonstration + +### Workflow + +1. **Initialization**: Generate initial training dataset (10 random points) +2. **Optimization Loop** (20 iterations): + - Fit SingleTaskGP model to current data + - Define qLogNoisyExpectedImprovement acquisition function + - Optimize acquisition function to find 8 new candidates + - Evaluate candidates and update training data +3. **Results**: Report best parameters and score + +## Key Libraries Used + +- **BoTorch**: State-of-the-art Bayesian optimization library +- **GPyTorch**: Gaussian process library for surrogate modeling +- **PyTorch**: Tensor operations and automatic differentiation + +## Files Structure + +``` +optimizer/ +├── __init__.py # Package initialization +├── bayesian_optimizer.py # Core optimization class +├── objective_function.py # Mock objective function +├── demo.py # Complete demo script +├── run_demo.py # Simple executable script +├── example_usage.py # Advanced usage examples +├── setup.py # Package setup +├── requirements.txt # Dependencies +├── README.md # This file +├── QUICKSTART.md # Quick start guide +├── demo_notebook.ipynb # Complete demonstration notebook +├── interactive_notebook.ipynb # Interactive experimentation +└── tutorial_notebook.ipynb # Step-by-step tutorial +``` + +## Performance Notes + +- The mock objective function simulates realistic noise levels +- qLogNEI acquisition function is optimized for noisy objectives with improved numerical stability +- Batch optimization allows for parallel evaluation of candidates +- GPU acceleration available through PyTorch/BoTorch + +## License + +This package is part of the Polyprint project and serves as an educational example for Bayesian optimization in manufacturing processes. + +## Model Integration with Scikit-Learn + +The optimizer now supports direct integration with trained scikit-learn models through the `model_objective_factory` module. This allows you to use your trained models as objective functions for Bayesian optimization. + +### Using Your Trained Model + +#### 1. Create Objective Function from Model + +```python +from model_objective_factory import create_objective_from_model +import joblib + +# Load your trained model and scaler +model = joblib.load('your_trained_model.pkl') +scaler = joblib.load('your_trained_scaler.pkl') + +# Create BoTorch-compatible objective function +objective_fn = create_objective_from_model(model, scaler) +``` + +#### 2. Run Optimization + +```python +from bayesian_optimizer import BayesianOptimizer +import torch + +# Define parameter bounds +bounds = torch.tensor([ + [0.1, 10.0, 0.05, 5.0], # Lower bounds: [concentration, print_speed, gap_size, volume] + [1.0, 100.0, 0.5, 25.0] # Upper bounds: [concentration, print_speed, gap_size, volume] +], dtype=torch.float64) + +# Initialize optimizer +optimizer = BayesianOptimizer(bounds=bounds, batch_size=8, seed=42) + +# Run optimization +best_params, best_score = optimizer.optimize( + objective_function=objective_fn, + n_iterations=20, + n_initial_points=10, + verbose=True +) + +print(f"Best parameters: {best_params}") +print(f"Best success probability: {best_score:.4f}") +``` + +### How It Works + +The `create_objective_from_model` function creates a wrapper that: + +1. **Accepts BoTorch tensors**: Input shape `(batch_size, 4)` with parameters `[concentration, print_speed, gap_size, volume]` + +2. **Performs feature engineering**: Automatically creates the 11 features your model expects: + - `gap_size_squared` + - `solvent_CF` (fixed at 0.5) + - `print_speed_squared` + - `concentration_squared` + - `concentration` + - `gap_size` + - `print_speed_gap_size` + - `print_speed` + - `concentration_print_speed` + - `print_speed_volume` + - `volume` + +3. **Applies scaling**: Uses your trained `StandardScaler` to normalize features + +4. **Returns probabilities**: Uses `model.predict_proba()` to get success probabilities + +5. **Outputs BoTorch tensors**: Returns shape `(batch_size, 1)` tensor of success probabilities + +### Running the Demo + +```bash +# Test the model factory +python model_objective_factory.py + +# Test full integration with Bayesian optimization +python model_integration_example.py +``` + +### Key Benefits + +- **Seamless Integration**: Direct compatibility between scikit-learn models and BoTorch +- **Automatic Feature Engineering**: No need to manually recreate feature transformations +- **Batch Evaluation**: Efficient evaluation of multiple parameter combinations +- **Clean Architecture**: Factory pattern separates model logic from optimization logic +- **Robust**: Handles tensor conversions, scaling, and error checking automatically + +### Requirements + +Your scikit-learn model must: +- Be a trained `LogisticRegression` or similar classifier +- Have a `predict_proba()` method +- Expect exactly 11 features in the specified order +- Be paired with a fitted `StandardScaler` + +The factory pattern makes it easy to extend support for other model types in the future. \ No newline at end of file diff --git a/aamp_app/optimizer/__init__.py b/aamp_app/optimizer/__init__.py new file mode 100644 index 0000000..a318b02 --- /dev/null +++ b/aamp_app/optimizer/__init__.py @@ -0,0 +1,15 @@ +""" +Bayesian Optimization Package + +A complete, self-contained Python package for batch Bayesian Optimization +using BoTorch and GPyTorch libraries. +""" + +__version__ = "1.0.0" +__author__ = "Polyprint Project" + +from .bayesian_optimizer import BayesianOptimizer +from .objective_function import MockObjectiveFunction +from .demo.demo import run_optimization_demo + +__all__ = ["BayesianOptimizer", "MockObjectiveFunction", "run_optimization_demo"] \ No newline at end of file diff --git a/aamp_app/optimizer/bayesian_optimizer.py b/aamp_app/optimizer/bayesian_optimizer.py new file mode 100644 index 0000000..064b797 --- /dev/null +++ b/aamp_app/optimizer/bayesian_optimizer.py @@ -0,0 +1,286 @@ +import torch +import numpy as np +from typing import Callable, Tuple, Optional, List + +# BoTorch imports for Bayesian optimization +from botorch.models import SingleTaskGP +from botorch.fit import fit_gpytorch_mll +from botorch.acquisition import qLogNoisyExpectedImprovement +from botorch.optim import optimize_acqf +from botorch.utils.transforms import normalize, unnormalize + +# GPyTorch imports for Gaussian process components +from gpytorch.mlls import ExactMarginalLogLikelihood +from gpytorch.kernels import ScaleKernel, RBFKernel +from gpytorch.priors import GammaPrior + +def plot_optimization_results(optimizer, objective): + """Create selective plots of the optimization results (1, 3, and 4 only).""" + + import matplotlib.pyplot as plt + import numpy as np + import base64 + import io + + history = optimizer.get_optimization_history() + train_X, train_Y = optimizer.get_training_data() + true_params, _ = objective.get_optimal_parameters() + true_score = objective.evaluate_at_optimal() + + # Create a 1x3 subplot layout for the three selected plots + fig, axes = plt.subplots(1, 3, figsize=(21, 6)) # Wider layout + + ### 1. Optimization Progress (axes[0]) + iterations = [h['iteration'] for h in history] + best_values = [h['best_value'] for h in history] + + axes[0].plot(iterations, best_values, 'b-o', linewidth=2, markersize=6) + axes[0].axhline(y=true_score, color='r', linestyle='--', alpha=0.7, + label=f'True optimum: {true_score:.3f}') + axes[0].set_xlabel('Iteration') + axes[0].set_ylabel('Best Observed Value') + axes[0].set_title('Optimization Progress') + axes[0].legend() + axes[0].grid(True, alpha=0.3) + + ### 3. Parameter Space Exploration (axes[1]) + scatter = axes[1].scatter(train_X[:, 0], train_X[:, 1], c=train_Y.squeeze(), + cmap='viridis', alpha=0.6, s=50) + axes[1].scatter(optimizer.best_parameters[0], optimizer.best_parameters[1], + c='red', s=200, marker='*', label='Best found', + edgecolor='black', linewidth=2) + axes[1].scatter(true_params[0], true_params[1], c='orange', s=200, marker='*', + label='True optimum', edgecolor='black', linewidth=2) + axes[1].set_xlabel('Concentration') + axes[1].set_ylabel('Print Speed (mm/s)') + axes[1].set_title('Parameter Space Exploration') + axes[1].legend() + plt.colorbar(scatter, ax=axes[1], label='Objective Value') + + ### 4. Gap Size vs Volume (axes[2]) + scatter2 = axes[2].scatter(train_X[:, 2], train_X[:, 3], c=train_Y.squeeze(), + cmap='viridis', alpha=0.6, s=50) + axes[2].scatter(optimizer.best_parameters[2], optimizer.best_parameters[3], + c='red', s=200, marker='*', label='Best found', + edgecolor='black', linewidth=2) + axes[2].scatter(true_params[2], true_params[3], c='orange', s=200, marker='*', + label='True optimum', edgecolor='black', linewidth=2) + axes[2].set_xlabel('Gap Size (mm)') + axes[2].set_ylabel('Volume (μL)') + axes[2].set_title('🔍 Gap Size vs Volume') + axes[2].legend() + plt.colorbar(scatter2, ax=axes[2], label='Objective Value') + + plt.tight_layout() + plt.show() + + buf = io.BytesIO() + fig.savefig(buf, format="png", bbox_inches='tight') + buf.seek(0) + encoded_image = base64.b64encode(buf.read()).decode("utf-8") + buf.close() + plt.close(fig) + + return encoded_image + +class BayesianOptimizer: + + def __init__( + self, + bounds: torch.Tensor, + batch_size: int = 8, + noise_variance: float = 0.01, + seed: int = 42 + ): + self.bounds = bounds.double() + self.batch_size = batch_size + self.noise_variance = noise_variance + self.seed = seed + + torch.manual_seed(seed) + np.random.seed(seed) + + assert bounds.shape[0] == 2, "Bounds must have shape (2, n_dims)" + assert (bounds[1] > bounds[0]).all(), "Upper bounds must be greater than lower bounds" + + self.n_dims = bounds.shape[1] + + self.train_X = None + self.train_Y = None + self.model = None + + self.iteration_history = [] + self.best_observed_value = -float('inf') + self.best_parameters = None + + def generate_initial_data(self, n_points: int, objective_function: Callable) -> None: + print(f"Generating {n_points} initial training points...") + + initial_X = self._generate_random_points(n_points) + initial_Y = objective_function(initial_X) + + self.train_X = initial_X + self.train_Y = initial_Y + + # Print all initial parameter sets and their scores + # print("Initial parameter sets and scores:") # <-- ADDED PRINT + # for i in range(n_points): + # print(f" [{i+1}] Params: {initial_X[i].tolist()}, Score: {initial_Y[i].item():.4f}") # <-- ADDED PRINT + + best_idx = torch.argmax(initial_Y) + self.best_observed_value = initial_Y[best_idx].item() + self.best_parameters = initial_X[best_idx] + + print(f"Initial best score: {self.best_observed_value:.4f}") + print(f"Initial best parameters: {self.best_parameters}") + + def _generate_random_points(self, n_points: int) -> torch.Tensor: + unit_points = torch.rand(n_points, self.n_dims, dtype=torch.float64) + lower_bounds = self.bounds[0] + upper_bounds = self.bounds[1] + scaled_points = lower_bounds + (upper_bounds - lower_bounds) * unit_points + return scaled_points + + def fit_model(self) -> None: + if self.train_X is None or self.train_Y is None: + raise ValueError("No training data available. Call generate_initial_data first.") + + train_X_double = self.train_X.double() + train_Y_double = self.train_Y.double() + + train_X_normalized = normalize(train_X_double, self.bounds.double()) + train_Y_normalized = (train_Y_double - train_Y_double.mean()) / train_Y_double.std() + + self.model = SingleTaskGP( + train_X_normalized, + train_Y_normalized, + covar_module=ScaleKernel( + RBFKernel( + lengthscale_prior=GammaPrior(2.0, 0.5), + ard_num_dims=self.n_dims + ), + outputscale_prior=GammaPrior(2.0, 0.5) + ) + ) + + self.model.train() + mll = ExactMarginalLogLikelihood(self.model.likelihood, self.model) + fit_gpytorch_mll(mll) + self.model.eval() + + def optimize_acquisition(self) -> torch.Tensor: + if self.model is None: + raise ValueError("Model not fitted. Call fit_model first.") + + train_X_double = self.train_X.double() + train_X_normalized = normalize(train_X_double, self.bounds.double()) + + acquisition_function = qLogNoisyExpectedImprovement( + model=self.model, + X_baseline=train_X_normalized, + prune_baseline=True, + cache_root=True + ) + + unit_bounds = torch.stack([torch.zeros(self.n_dims), torch.ones(self.n_dims)]).double() + candidates, _ = optimize_acqf( + acq_function=acquisition_function, + bounds=unit_bounds, + q=self.batch_size, + num_restarts=20, + raw_samples=200, + options={"batch_limit": 5, "maxiter": 200} + ) + + candidates_unnormalized = unnormalize(candidates, self.bounds.double()) + return candidates_unnormalized.float() + + def update_training_data(self, new_X: torch.Tensor, new_Y: torch.Tensor) -> None: + self.train_X = torch.cat([self.train_X, new_X], dim=0) + self.train_Y = torch.cat([self.train_Y, new_Y], dim=0) + + current_best_idx = torch.argmax(new_Y) + current_best_value = new_Y[current_best_idx].item() + + if current_best_value > self.best_observed_value: + self.best_observed_value = current_best_value + self.best_parameters = new_X[current_best_idx] + + def optimize( + self, + objective_function: Callable, + n_iterations: int = 20, + n_initial_points: int = 10, + verbose: bool = True, + target: float = 0.95 + ) -> Tuple[torch.Tensor, float]: + + img = [] + + if verbose: + print("=" * 60) + print("BAYESIAN OPTIMIZATION STARTING") + print("=" * 60) + print(f"Parameters: {self.n_dims} dimensions") + print(f"Batch size: {self.batch_size}") + print(f"Iterations: {n_iterations}") + print(f"Initial points: {n_initial_points}") + print("=" * 60) + + self.generate_initial_data(n_initial_points, objective_function) + + for iteration in range(n_iterations): + if verbose: + print(f"\nIteration {iteration + 1}/{n_iterations}") + print("-" * 40) + print("Fitting GP model...") + + self.fit_model() + + if verbose: + print("Optimizing acquisition function...") + candidates = self.optimize_acquisition() + + if verbose: + print(f"Evaluating {self.batch_size} candidates...") + candidate_values = objective_function(candidates) + + # Print all evaluated candidates and their scores + print("New candidate parameter sets and scores:") # <-- ADDED PRINT + for i in range(self.batch_size): + print(f" [{i+1}] Params: {candidates[i].tolist()}, Score: {candidate_values[i].item():.4f}") # <-- ADDED PRINT + + self.update_training_data(candidates, candidate_values) + + iteration_info = { + 'iteration': iteration + 1, + 'best_value': self.best_observed_value, + 'best_params': self.best_parameters.clone() + } + self.iteration_history.append(iteration_info) + + if verbose: + print(f"Current best score: {self.best_observed_value:.4f}") + print(f"Current best parameters: {self.best_parameters}") + img.append(plot_optimization_results(self, objective_function)) + + if self.best_observed_value >= target: + if verbose: + print(f"Target objective {target} reached. Stopping optimization.") + break + + if verbose: + print("\n" + "=" * 60) + print("OPTIMIZATION COMPLETED") + print("=" * 60) + print(f"Final best score: {self.best_observed_value:.4f}") + print(f"Final best parameters: {self.best_parameters}") + print(f"Total evaluations: {len(self.train_X)}") + + return self.best_parameters, self.best_observed_value, img + + def get_optimization_history(self) -> List[dict]: + return self.iteration_history + + def get_training_data(self) -> Tuple[torch.Tensor, torch.Tensor]: + return self.train_X, self.train_Y diff --git a/aamp_app/optimizer/demo/demo.py b/aamp_app/optimizer/demo/demo.py new file mode 100644 index 0000000..79a3526 --- /dev/null +++ b/aamp_app/optimizer/demo/demo.py @@ -0,0 +1,312 @@ +""" +Bayesian Optimization Demo Script + +This script demonstrates a complete Bayesian optimization workflow for +optimizing a printing process with 4 continuous parameters. + +The script includes: +- Parameter space definition +- Mock objective function evaluation +- Bayesian optimization with batch candidates +- Results visualization and analysis +""" + +import torch +import numpy as np +import matplotlib.pyplot as plt +from typing import Tuple, List +import time + +# Import our custom modules +from ..bayesian_optimizer import BayesianOptimizer +from ..objective_function import MockObjectiveFunction + + +def print_header(title: str, width: int = 80) -> None: + """Print a formatted header for console output.""" + print("\n" + "=" * width) + print(f"{title:^{width}}") + print("=" * width) + + +def print_parameter_info() -> None: + """Print information about the optimization parameters.""" + print("\nPARAMETER SPACE:") + print("-" * 50) + print("Parameter | Range | Description") + print("-" * 50) + print("concentration | [0.1, 1.0] | Material concentration") + print("print_speed | [10.0, 100.0] | Printing speed (mm/s)") + print("gap_size | [0.05, 0.5] | Gap size between layers (mm)") + print("volume | [5.0, 25.0] | Volume per drop (μL)") + print("-" * 50) + + +def format_parameters(params: torch.Tensor) -> str: + """Format parameters for display.""" + if params.dim() == 1: + concentration, print_speed, gap_size, volume = params + return (f"concentration={concentration:.3f}, " + f"print_speed={print_speed:.1f}, " + f"gap_size={gap_size:.3f}, " + f"volume={volume:.1f}") + else: + return f"Tensor of shape {params.shape}" + + +def visualize_optimization_progress( + iteration_history: List[dict], + true_optimum: float, + save_plot: bool = False +) -> None: + """ + Visualize the optimization progress over iterations. + + Args: + iteration_history: List of iteration information + true_optimum: True optimal value for comparison + save_plot: Whether to save the plot to file + """ + try: + iterations = [info['iteration'] for info in iteration_history] + best_values = [info['best_value'] for info in iteration_history] + + plt.figure(figsize=(12, 8)) + + # Plot best observed value over iterations + plt.subplot(2, 1, 1) + plt.plot(iterations, best_values, 'b-o', linewidth=2, markersize=6) + plt.axhline(y=true_optimum, color='r', linestyle='--', + label=f'True optimum: {true_optimum:.3f}') + plt.xlabel('Iteration') + plt.ylabel('Best Observed Value') + plt.title('Optimization Progress: Best Value vs Iteration') + plt.legend() + plt.grid(True, alpha=0.3) + + # Plot improvement over iterations + plt.subplot(2, 1, 2) + improvements = [best_values[i] - best_values[0] for i in range(len(best_values))] + plt.plot(iterations, improvements, 'g-o', linewidth=2, markersize=6) + plt.xlabel('Iteration') + plt.ylabel('Improvement from Initial Best') + plt.title('Cumulative Improvement Over Iterations') + plt.grid(True, alpha=0.3) + + plt.tight_layout() + + if save_plot: + plt.savefig('optimization_progress.png', dpi=300, bbox_inches='tight') + print("Plot saved as 'optimization_progress.png'") + + plt.show() + + except ImportError: + print("Matplotlib not available. Skipping visualization.") + except Exception as e: + print(f"Error creating visualization: {e}") + + +def compare_with_true_optimum( + best_params: torch.Tensor, + best_score: float, + objective_function: MockObjectiveFunction +) -> None: + """ + Compare the optimization results with the true optimum. + + Args: + best_params: Best parameters found by optimization + best_score: Best score achieved + objective_function: The objective function to get true optimum + """ + print_header("COMPARISON WITH TRUE OPTIMUM") + + # Get true optimal parameters and score + true_params, true_max_score = objective_function.get_optimal_parameters() + true_score_noiseless = objective_function.evaluate_at_optimal() + + print(f"\nTRUE OPTIMUM:") + print(f"Parameters: {format_parameters(true_params)}") + print(f"Score (noiseless): {true_score_noiseless:.4f}") + print(f"Max possible score: {true_max_score:.4f}") + + print(f"\nOPTIMIZED RESULT:") + print(f"Parameters: {format_parameters(best_params)}") + print(f"Score (noisy): {best_score:.4f}") + + # Calculate parameter differences + param_diff = torch.norm(best_params - true_params).item() + score_diff = abs(best_score - true_score_noiseless) + + print(f"\nCOMPARISON:") + print(f"Parameter L2 distance: {param_diff:.4f}") + print(f"Score difference: {score_diff:.4f}") + print(f"Score gap from true optimum: {(true_score_noiseless - best_score):.4f}") + + # Performance assessment + if param_diff < 5.0: # Reasonable threshold for parameter space + print("✓ Parameters are close to true optimum") + else: + print("⚠ Parameters are far from true optimum") + + if score_diff < 0.2: # Reasonable threshold considering noise + print("✓ Score is close to true optimum") + else: + print("⚠ Score is far from true optimum") + + +def analyze_convergence(iteration_history: List[dict]) -> None: + """ + Analyze convergence properties of the optimization. + + Args: + iteration_history: List of iteration information + """ + print_header("CONVERGENCE ANALYSIS") + + best_values = [info['best_value'] for info in iteration_history] + + # Find when best improvements occurred + improvements = [] + for i in range(1, len(best_values)): + improvement = best_values[i] - best_values[i-1] + if improvement > 0.01: # Significant improvement threshold + improvements.append((i+1, improvement)) + + print(f"\nSIGNIFICANT IMPROVEMENTS (> 0.01):") + if improvements: + for iteration, improvement in improvements: + print(f"Iteration {iteration}: +{improvement:.4f}") + else: + print("No significant improvements found") + + # Calculate convergence metrics + final_best = best_values[-1] + initial_best = best_values[0] + total_improvement = final_best - initial_best + + print(f"\nCONVERGENCE METRICS:") + print(f"Initial best: {initial_best:.4f}") + print(f"Final best: {final_best:.4f}") + print(f"Total improvement: {total_improvement:.4f}") + + # Check for convergence in last 5 iterations + if len(best_values) >= 5: + last_5_values = best_values[-5:] + convergence_variance = np.var(last_5_values) + print(f"Variance in last 5 iterations: {convergence_variance:.6f}") + + if convergence_variance < 0.001: + print("✓ Optimization appears to have converged") + else: + print("⚠ Optimization may not have fully converged") + + +def run_optimization_demo( + n_iterations: int = 20, + n_initial_points: int = 10, + batch_size: int = 8, + noise_std: float = 0.1, + visualize: bool = True, + seed: int = 42 +) -> Tuple[torch.Tensor, float]: + """ + Run a complete Bayesian optimization demonstration. + + Args: + n_iterations: Number of optimization iterations + n_initial_points: Number of initial random points + batch_size: Batch size for candidate generation + noise_std: Standard deviation of noise in objective function + visualize: Whether to create visualization plots + seed: Random seed for reproducibility + + Returns: + Tuple of (best_parameters, best_score) + """ + print_header("BAYESIAN OPTIMIZATION DEMO") + print(f"Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S')}") + print_parameter_info() + + # Define parameter bounds (using float32 for consistency) + bounds = torch.tensor([ + [0.1, 1.0], # concentration + [10.0, 100.0], # print_speed + [0.05, 0.5], # gap_size + [5.0, 25.0] # volume + ], dtype=torch.float32).T # Transpose to get shape (2, 4) + + print(f"\nOPTIMIZATION CONFIGURATION:") + print(f"Dimensions: {bounds.shape[1]}") + print(f"Batch size: {batch_size}") + print(f"Iterations: {n_iterations}") + print(f"Initial points: {n_initial_points}") + print(f"Noise std: {noise_std}") + print(f"Random seed: {seed}") + + # Create objective function and optimizer + objective_function = MockObjectiveFunction(noise_std=noise_std, seed=seed) + optimizer = BayesianOptimizer( + bounds=bounds, + batch_size=batch_size, + seed=seed + ) + + # Run optimization + print_header("RUNNING OPTIMIZATION") + start_time = time.time() + + best_params, best_score = optimizer.optimize( + objective_function=objective_function, + n_iterations=n_iterations, + n_initial_points=n_initial_points, + verbose=True + ) + + end_time = time.time() + optimization_time = end_time - start_time + + print_header("OPTIMIZATION RESULTS") + print(f"Optimization completed in {optimization_time:.2f} seconds") + print(f"Total function evaluations: {len(optimizer.train_X)}") + print(f"Best score: {best_score:.4f}") + print(f"Best parameters: {format_parameters(best_params)}") + + # Detailed analysis + compare_with_true_optimum(best_params, best_score, objective_function) + analyze_convergence(optimizer.get_optimization_history()) + + # Visualization + if visualize: + print_header("VISUALIZATION") + true_optimum = objective_function.evaluate_at_optimal() + visualize_optimization_progress( + optimizer.get_optimization_history(), + true_optimum, + save_plot=True + ) + + print_header("DEMO COMPLETED") + return best_params, best_score + + +def main(): + """Main function to run the demo.""" + # Example 1: Standard optimization + print("Running standard optimization demo...") + best_params, best_score = run_optimization_demo() + + # Example 2: Quick optimization with fewer iterations + print("\n" + "="*80) + print("Running quick optimization demo (10 iterations)...") + run_optimization_demo( + n_iterations=10, + n_initial_points=5, + batch_size=4, + visualize=False + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/aamp_app/optimizer/demo/example_usage.py b/aamp_app/optimizer/demo/example_usage.py new file mode 100644 index 0000000..e1798be --- /dev/null +++ b/aamp_app/optimizer/demo/example_usage.py @@ -0,0 +1,282 @@ +""" +Example Usage Scripts for Bayesian Optimization Package + +This script demonstrates various ways to use the Bayesian optimization package +for different optimization scenarios. +""" + +import torch +import numpy as np +from optimizer import BayesianOptimizer, MockObjectiveFunction + + +def example_1_basic_usage(): + """ + Example 1: Basic usage with default parameters + """ + print("="*60) + print("EXAMPLE 1: Basic Usage") + print("="*60) + + # Define parameter bounds + bounds = torch.tensor([ + [0.1, 1.0], # concentration + [10.0, 100.0], # print_speed + [0.05, 0.5], # gap_size + [5.0, 25.0] # volume + ]).T + + # Create optimizer and objective function + optimizer = BayesianOptimizer(bounds=bounds, batch_size=8) + objective = MockObjectiveFunction(noise_std=0.1) + + # Run optimization + best_params, best_score = optimizer.optimize( + objective_function=objective, + n_iterations=10, + n_initial_points=5, + verbose=True + ) + + print(f"Best parameters: {best_params}") + print(f"Best score: {best_score:.4f}") + return best_params, best_score + + +def example_2_custom_parameters(): + """ + Example 2: Custom optimization parameters + """ + print("\n" + "="*60) + print("EXAMPLE 2: Custom Parameters") + print("="*60) + + # Define parameter bounds + bounds = torch.tensor([ + [0.1, 1.0], # concentration + [10.0, 100.0], # print_speed + [0.05, 0.5], # gap_size + [5.0, 25.0] # volume + ]).T + + # Create optimizer with custom parameters + optimizer = BayesianOptimizer( + bounds=bounds, + batch_size=4, # Smaller batch size + noise_variance=0.05, # Lower noise assumption + seed=123 # Different random seed + ) + + # Create objective with different noise level + objective = MockObjectiveFunction(noise_std=0.05, seed=123) + + # Run optimization with custom settings + best_params, best_score = optimizer.optimize( + objective_function=objective, + n_iterations=15, + n_initial_points=8, + verbose=True + ) + + print(f"Best parameters: {best_params}") + print(f"Best score: {best_score:.4f}") + return best_params, best_score + + +def example_3_step_by_step(): + """ + Example 3: Step-by-step optimization (manual control) + """ + print("\n" + "="*60) + print("EXAMPLE 3: Step-by-Step Optimization") + print("="*60) + + # Define parameter bounds + bounds = torch.tensor([ + [0.1, 1.0], # concentration + [10.0, 100.0], # print_speed + [0.05, 0.5], # gap_size + [5.0, 25.0] # volume + ]).T + + # Create optimizer and objective function + optimizer = BayesianOptimizer(bounds=bounds, batch_size=6) + objective = MockObjectiveFunction(noise_std=0.1) + + # Step 1: Generate initial data + print("Step 1: Generating initial data...") + optimizer.generate_initial_data(8, objective) + + # Step 2: Manual optimization loop + n_iterations = 5 + for i in range(n_iterations): + print(f"\nIteration {i+1}/{n_iterations}") + + # Fit model + print(" Fitting GP model...") + optimizer.fit_model() + + # Get next candidates + print(" Getting next candidates...") + candidates = optimizer.optimize_acquisition() + + # Evaluate candidates + print(" Evaluating candidates...") + values = objective(candidates) + + # Update training data + optimizer.update_training_data(candidates, values) + + print(f" Current best: {optimizer.best_observed_value:.4f}") + + print(f"\nFinal best parameters: {optimizer.best_parameters}") + print(f"Final best score: {optimizer.best_observed_value:.4f}") + + return optimizer.best_parameters, optimizer.best_observed_value + + +def example_4_analysis(): + """ + Example 4: Detailed analysis of optimization results + """ + print("\n" + "="*60) + print("EXAMPLE 4: Detailed Analysis") + print("="*60) + + # Define parameter bounds + bounds = torch.tensor([ + [0.1, 1.0], # concentration + [10.0, 100.0], # print_speed + [0.05, 0.5], # gap_size + [5.0, 25.0] # volume + ]).T + + # Create optimizer and objective function + optimizer = BayesianOptimizer(bounds=bounds, batch_size=8) + objective = MockObjectiveFunction(noise_std=0.1) + + # Run optimization + best_params, best_score = optimizer.optimize( + objective_function=objective, + n_iterations=8, + n_initial_points=6, + verbose=False + ) + + # Get optimization history + history = optimizer.get_optimization_history() + train_X, train_Y = optimizer.get_training_data() + + print(f"Optimization Summary:") + print(f"- Total evaluations: {len(train_X)}") + print(f"- Best score: {best_score:.4f}") + print(f"- Best parameters: {best_params}") + + # Analyze convergence + print(f"\nConvergence Analysis:") + best_values = [h['best_value'] for h in history] + improvements = [best_values[i] - best_values[i-1] for i in range(1, len(best_values))] + + print(f"- Initial best: {best_values[0]:.4f}") + print(f"- Final best: {best_values[-1]:.4f}") + print(f"- Total improvement: {best_values[-1] - best_values[0]:.4f}") + print(f"- Average improvement per iteration: {np.mean(improvements):.4f}") + + # Compare with true optimum + true_params, _ = objective.get_optimal_parameters() + true_score = objective.evaluate_at_optimal() + + param_distance = torch.norm(best_params - true_params).item() + score_gap = true_score - best_score + + print(f"\nComparison with True Optimum:") + print(f"- True optimal score: {true_score:.4f}") + print(f"- Found score: {best_score:.4f}") + print(f"- Score gap: {score_gap:.4f}") + print(f"- Parameter distance: {param_distance:.4f}") + + return best_params, best_score + + +def example_5_custom_objective(): + """ + Example 5: Using a custom objective function + """ + print("\n" + "="*60) + print("EXAMPLE 5: Custom Objective Function") + print("="*60) + + def custom_objective(X): + """ + Custom objective function example. + This function has a different optimal point than the mock function. + """ + # Ensure X is 2D + if X.dim() == 1: + X = X.unsqueeze(0) + + # Extract parameters + concentration = X[:, 0] + print_speed = X[:, 1] + gap_size = X[:, 2] + volume = X[:, 3] + + # Custom objective: minimize print time while maintaining quality + # Quality decreases if parameters are too far from optimal ranges + quality = 1.0 - 0.5 * ((concentration - 0.8)**2 + + (print_speed - 60.0)**2 / 1000.0 + + (gap_size - 0.3)**2 * 4.0 + + (volume - 12.0)**2 / 100.0) + + # Add noise + noise = torch.randn(X.shape[0], 1) * 0.05 + + return quality.unsqueeze(1) + noise + + # Define parameter bounds + bounds = torch.tensor([ + [0.1, 1.0], # concentration + [10.0, 100.0], # print_speed + [0.05, 0.5], # gap_size + [5.0, 25.0] # volume + ]).T + + # Create optimizer + optimizer = BayesianOptimizer(bounds=bounds, batch_size=6) + + # Run optimization with custom objective + best_params, best_score = optimizer.optimize( + objective_function=custom_objective, + n_iterations=10, + n_initial_points=6, + verbose=True + ) + + print(f"Best parameters: {best_params}") + print(f"Best score: {best_score:.4f}") + + return best_params, best_score + + +def main(): + """ + Run all examples + """ + print("BAYESIAN OPTIMIZATION EXAMPLES") + print("These examples demonstrate different ways to use the package.") + print("Each example shows a different aspect of the optimization workflow.") + + # Run examples + example_1_basic_usage() + example_2_custom_parameters() + example_3_step_by_step() + example_4_analysis() + example_5_custom_objective() + + print("\n" + "="*60) + print("ALL EXAMPLES COMPLETED") + print("="*60) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/aamp_app/optimizer/demo/model_integration_example.py b/aamp_app/optimizer/demo/model_integration_example.py new file mode 100644 index 0000000..53b19ce --- /dev/null +++ b/aamp_app/optimizer/demo/model_integration_example.py @@ -0,0 +1,156 @@ +import torch +import numpy as np +from model_objective_factory import create_objective_from_model, create_mock_model_and_scaler +from bayesian_optimizer import BayesianOptimizer + +def demonstrate_model_integration(): + """ + Demonstrate how to integrate a scikit-learn model with the Bayesian optimization framework. + """ + + print("=" * 60) + print("Model Integration with Bayesian Optimization") + print("=" * 60) + + # Step 1: Create or load a trained scikit-learn model + print("\n1. Creating trained scikit-learn model...") + model, scaler = create_mock_model_and_scaler() + print(f"Model created with {model.coef_.shape[1]} features") + + # Step 2: Create objective function from the model + print("\n2. Creating objective function from model...") + objective_fn = create_objective_from_model(model, scaler) + print("Objective function created successfully!") + + # Step 3: Set up Bayesian optimization + print("\n3. Setting up Bayesian optimization...") + + # Define parameter bounds (same as in the demo) + # BayesianOptimizer expects bounds in shape (2, n_dims) where first row is lower bounds + bounds = torch.tensor([ + [0.1, 10.0, 0.05, 5.0], # Lower bounds: [concentration, print_speed, gap_size, volume] + [1.0, 100.0, 0.5, 25.0] # Upper bounds: [concentration, print_speed, gap_size, volume] + ], dtype=torch.float64) + + # Initialize optimizer + optimizer = BayesianOptimizer( + bounds=bounds, + batch_size=4, # Smaller batch for demonstration + seed=42 + ) + + print(f"Optimizer initialized with bounds:") + print(f" Concentration: [{bounds[0,0]:.1f}, {bounds[1,0]:.1f}]") + print(f" Print Speed: [{bounds[0,1]:.1f}, {bounds[1,1]:.1f}]") + print(f" Gap Size: [{bounds[0,2]:.3f}, {bounds[1,2]:.3f}]") + print(f" Volume: [{bounds[0,3]:.1f}, {bounds[1,3]:.1f}]") + + # Step 4: Run optimization + print("\n4. Running Bayesian optimization...") + + # Run optimization + best_params, best_score = optimizer.optimize( + objective_function=objective_fn, + n_iterations=5, # Fewer iterations for demonstration + n_initial_points=6, + verbose=True + ) + + # Display results + print(f"\nOptimization completed!") + print(f"Best parameters found:") + print(f" Concentration: {best_params[0]:.3f}") + print(f" Print Speed: {best_params[1]:.1f}") + print(f" Gap Size: {best_params[2]:.3f}") + print(f" Volume: {best_params[3]:.1f}") + print(f"Best success probability: {best_score:.4f}") + + # Step 5: Analyze results + print("\n5. Analyzing optimization results...") + + # Get training data (all evaluated points) + train_X, train_Y = optimizer.get_training_data() + + print(f"Total evaluations: {len(train_Y)}") + print(f"Best score: {train_Y.max().item():.4f}") + print(f"Mean score: {train_Y.mean().item():.4f}") + print(f"Score improvement: {(train_Y.max() - train_Y.min()).item():.4f}") + + # Show top 3 parameter combinations + print("\nTop 3 parameter combinations:") + sorted_indices = torch.argsort(train_Y.flatten(), descending=True) + + for i, idx in enumerate(sorted_indices[:3]): + params = train_X[idx] + score = train_Y[idx].item() + print(f" {i+1}. Score: {score:.4f} | " + f"Conc: {params[0]:.3f}, Speed: {params[1]:.1f}, " + f"Gap: {params[2]:.3f}, Vol: {params[3]:.1f}") + + return optimizer, best_params, best_score + +def test_with_real_model_workflow(): + """ + Example of how to use this with a real trained model. + This shows the workflow you would follow with your actual model. + """ + + print("\n" + "=" * 60) + print("Real Model Integration Workflow") + print("=" * 60) + + print("\nExample workflow for using your trained model:") + print("1. Load your trained model:") + print(" import joblib") + print(" model = joblib.load('your_trained_model.pkl')") + print(" scaler = joblib.load('your_trained_scaler.pkl')") + + print("\n2. Create objective function:") + print(" from model_objective_factory import create_objective_from_model") + print(" objective_fn = create_objective_from_model(model, scaler)") + + print("\n3. Set up and run optimization:") + print(" from bayesian_optimizer import BayesianOptimizer") + print(" optimizer = BayesianOptimizer(objective_function=objective_fn, ...)") + print(" best_params, best_score, results = optimizer.optimize()") + + print("\n4. The optimization will:") + print(" - Handle batch evaluation of parameter combinations") + print(" - Automatically perform feature engineering for each combination") + print(" - Use your model's predict_proba to get success probabilities") + print(" - Find optimal parameters that maximize success probability") + + print("\nFeature Engineering Details:") + print("- Input: [concentration, print_speed, gap_size, volume]") + print("- Automatically creates 11 features in the correct order:") + print(" 1. gap_size_squared") + print(" 2. solvent_CF (fixed at 0.5)") + print(" 3. print_speed_squared") + print(" 4. concentration_squared") + print(" 5. concentration") + print(" 6. gap_size") + print(" 7. print_speed_gap_size") + print(" 8. print_speed") + print(" 9. concentration_print_speed") + print(" 10. print_speed_volume") + print(" 11. volume") + print("- Applies scaling using your trained StandardScaler") + print("- Returns success probabilities from your model") + +if __name__ == "__main__": + # Run the demonstration + optimizer, best_params, best_score = demonstrate_model_integration() + + # Show the real model workflow + test_with_real_model_workflow() + + print("\n" + "=" * 60) + print("Integration demonstration completed!") + print("=" * 60) + + print("\nKey Benefits:") + print("- Seamless integration between scikit-learn models and BoTorch") + print("- Automatic feature engineering and scaling") + print("- Batch evaluation for efficient optimization") + print("- Clean separation of concerns using factory pattern") + print("- Compatible with existing Bayesian optimization framework") \ No newline at end of file diff --git a/aamp_app/optimizer/demo/run_demo.py b/aamp_app/optimizer/demo/run_demo.py new file mode 100644 index 0000000..c47f293 --- /dev/null +++ b/aamp_app/optimizer/demo/run_demo.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +""" +Simple script to run the Bayesian Optimization demo. + +This script can be run directly to see the complete optimization workflow. +It handles imports and provides a clean interface for users. +""" + +import sys +import os + +# Add the parent directory to the Python path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +def main(): + """Main function to run the demo.""" + try: + # Import and run the demo + from optimizer.demo.demo import run_optimization_demo + + print("Starting Bayesian Optimization Demo...") + print("This may take a few minutes to complete.") + print("Press Ctrl+C to interrupt if needed.\n") + + # Run the demo with default parameters + best_params, best_score = run_optimization_demo( + n_iterations=20, + n_initial_points=10, + batch_size=8, + noise_std=0.1, + visualize=True, + seed=42 + ) + + print(f"\nDemo completed successfully!") + print(f"Best parameters found: {best_params}") + print(f"Best score achieved: {best_score:.4f}") + + except ImportError as e: + print(f"Import error: {e}") + print("Please ensure all required packages are installed:") + print("pip install -r requirements.txt") + sys.exit(1) + + except KeyboardInterrupt: + print("\nDemo interrupted by user.") + sys.exit(0) + + except Exception as e: + print(f"Error running demo: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/aamp_app/optimizer/model_objective_factory.py b/aamp_app/optimizer/model_objective_factory.py new file mode 100644 index 0000000..6a73b66 --- /dev/null +++ b/aamp_app/optimizer/model_objective_factory.py @@ -0,0 +1,313 @@ +import numpy as np +import torch +from sklearn.linear_model import LogisticRegression +from sklearn.preprocessing import StandardScaler +from typing import Callable, Tuple +import warnings +warnings.filterwarnings('ignore') + +def create_objective_from_model(model: LogisticRegression, scaler: StandardScaler = None) -> Callable: + """ + Factory function that creates a BoTorch-compatible objective function from a trained scikit-learn model. + + Parameters: + ----------- + model : LogisticRegression + A trained scikit-learn LogisticRegression model + scaler : StandardScaler, optional + A fitted StandardScaler to normalize features. If None, no scaling is applied. + + Returns: + -------- + Callable + An objective function that accepts (batch_size, 4) tensor of base parameters + and returns (batch_size, 1) tensor of success probabilities + """ + + # Determine expected number of features from the model or scaler + if scaler is not None: + n_features_expected = scaler.n_features_in_ + else: + n_features_expected = model.coef_.shape[1] + + print(f"Model expects {n_features_expected} features") + + def objective_function(X: torch.Tensor) -> torch.Tensor: + """ + Objective function that evaluates parameter combinations using the trained model. + + Parameters: + ----------- + X : torch.Tensor + Input tensor of shape (batch_size, 4) containing base parameters: + [concentration, print_speed, gap_size, volume] + + Returns: + -------- + torch.Tensor + Output tensor of shape (batch_size, 1) containing success probabilities + """ + + # Convert to numpy for feature engineering + X_np = X.detach().cpu().numpy() + batch_size = X_np.shape[0] + + # Extract base parameters + concentration = X_np[:, 0] # Column 0: concentration + print_speed = X_np[:, 1] # Column 1: print_speed + gap_size = X_np[:, 2] # Column 2: gap_size + volume = X_np[:, 3] # Column 3: volume + + # Create comprehensive feature matrix based on typical polymer printing feature engineering + features = create_feature_matrix(concentration, print_speed, gap_size, volume, n_features_expected) + + # Apply feature scaling if provided + if scaler is not None: + features = scaler.transform(features) + + # Get success probabilities using the trained model + # predict_proba returns [P(class=0), P(class=1)], we want P(class=1) + success_probabilities = model.predict_proba(features)[:, 1] + + # Convert back to PyTorch tensor and reshape for BoTorch + result = torch.tensor(success_probabilities, dtype=torch.float32, device=X.device) + result = result.unsqueeze(-1) # Shape: (batch_size, 1) + + return result + + return objective_function + +def create_feature_matrix(concentration, print_speed, gap_size, volume, n_features_expected): + """ + Create feature matrix with comprehensive feature engineering to match trained model. + """ + batch_size = len(concentration) + + if n_features_expected == 11: + # Original 11-feature structure + features = np.zeros((batch_size, 11)) + features[:, 0] = gap_size ** 2 # gap_size_squared + features[:, 1] = 0.5 # solvent_CF (fixed constant) + features[:, 2] = print_speed ** 2 # print_speed_squared + features[:, 3] = concentration ** 2 # concentration_squared + features[:, 4] = concentration # concentration + features[:, 5] = gap_size # gap_size + features[:, 6] = print_speed * gap_size # print_speed_gap_size + features[:, 7] = print_speed # print_speed + features[:, 8] = concentration * print_speed # concentration_print_speed + features[:, 9] = print_speed * volume # print_speed_volume + features[:, 10] = volume # volume + + else: + # Extended feature structure for larger models + features = np.zeros((batch_size, n_features_expected)) + + # Base parameters + features[:, 0] = concentration + features[:, 1] = print_speed + features[:, 2] = gap_size + features[:, 3] = volume + + # Interaction terms + features[:, 4] = concentration * print_speed + features[:, 5] = concentration * gap_size + features[:, 6] = concentration * volume + features[:, 7] = print_speed * gap_size + features[:, 8] = print_speed * volume + features[:, 9] = gap_size * volume + + # Squared terms + features[:, 10] = concentration ** 2 + features[:, 11] = print_speed ** 2 + features[:, 12] = gap_size ** 2 + features[:, 13] = volume ** 2 + + # Solvent features (assuming multiple solvents were used in training) + if n_features_expected >= 18: + # Add solvent dummy variables + features[:, 14] = 0.5 # solvent_CB (fixed) + features[:, 15] = 0.0 # solvent_CF + features[:, 16] = 0.0 # solvent_anisole + features[:, 17] = 0.0 # solvent_p-xylene + + # If even more features, add higher-order interactions + if n_features_expected > 18: + for i in range(18, min(n_features_expected, 25)): + # Add more complex interactions or polynomial terms + if i == 18: + features[:, i] = concentration * print_speed * gap_size + elif i == 19: + features[:, i] = concentration * print_speed * volume + elif i == 20: + features[:, i] = concentration * gap_size * volume + elif i == 21: + features[:, i] = print_speed * gap_size * volume + elif i == 22: + features[:, i] = concentration ** 3 + elif i == 23: + features[:, i] = print_speed ** 3 + elif i == 24: + features[:, i] = gap_size ** 3 + else: + features[:, i] = 1.0 # constant term + + return features + +def create_mock_model_and_scaler() -> Tuple[LogisticRegression, StandardScaler]: + """ + Create a mock LogisticRegression model and StandardScaler for demonstration purposes. + + Returns: + -------- + Tuple[LogisticRegression, StandardScaler] + A tuple containing the trained model and fitted scaler + """ + + # Set random seed for reproducibility + np.random.seed(42) + + # Create dummy training data + n_samples = 1000 + n_features = 11 + + # Generate realistic parameter ranges for the dummy data + # These approximate the ranges from the actual polymer printing experiments + X_dummy = np.random.rand(n_samples, n_features) + + # Scale features to realistic ranges based on the feature definitions + X_dummy[:, 0] = X_dummy[:, 0] * 0.25 # gap_size_squared (0 to 0.25) + X_dummy[:, 1] = 0.5 # solvent_CF (constant) + X_dummy[:, 2] = X_dummy[:, 2] * 10000 # print_speed_squared (0 to 10000) + X_dummy[:, 3] = X_dummy[:, 3] * 1.0 # concentration_squared (0 to 1.0) + X_dummy[:, 4] = X_dummy[:, 4] * 1.0 # concentration (0 to 1.0) + X_dummy[:, 5] = X_dummy[:, 5] * 0.5 # gap_size (0 to 0.5) + X_dummy[:, 6] = X_dummy[:, 6] * 50 # print_speed_gap_size (0 to 50) + X_dummy[:, 7] = X_dummy[:, 7] * 100 # print_speed (0 to 100) + X_dummy[:, 8] = X_dummy[:, 8] * 100 # concentration_print_speed (0 to 100) + X_dummy[:, 9] = X_dummy[:, 9] * 2500 # print_speed_volume (0 to 2500) + X_dummy[:, 10] = X_dummy[:, 10] * 25 # volume (0 to 25) + + # Create synthetic target variable with some realistic patterns + # Higher success probability for moderate concentration, speed, and gap_size + concentration = X_dummy[:, 4] + print_speed = X_dummy[:, 7] + gap_size = X_dummy[:, 5] + volume = X_dummy[:, 10] + + # Create a synthetic success probability based on realistic patterns + # Success is higher for moderate values of key parameters + success_prob = ( + 0.3 + # Base probability + 0.4 * np.exp(-((concentration - 0.6) ** 2) / 0.2) + # Optimal concentration around 0.6 + 0.2 * np.exp(-((print_speed - 45) ** 2) / 800) + # Optimal print_speed around 45 + 0.1 * np.exp(-((gap_size - 0.2) ** 2) / 0.08) # Optimal gap_size around 0.2 + ) + + # Add some noise + success_prob += np.random.normal(0, 0.1, n_samples) + success_prob = np.clip(success_prob, 0, 1) + + # Convert to binary outcomes + y_dummy = (success_prob > 0.5).astype(int) + + # Fit the scaler + scaler = StandardScaler() + X_dummy_scaled = scaler.fit_transform(X_dummy) + + # Train the logistic regression model + model = LogisticRegression(random_state=42, max_iter=1000) + model.fit(X_dummy_scaled, y_dummy) + + # Print model performance for verification + accuracy = model.score(X_dummy_scaled, y_dummy) + print(f"Mock model training accuracy: {accuracy:.3f}") + + return model, scaler + +def test_objective_function(): + """ + Test the objective function with sample inputs to verify correct operation. + """ + + # Create mock model and scaler + model, scaler = create_mock_model_and_scaler() + + # Create objective function + objective_fn = create_objective_from_model(model, scaler) + + # Test with a batch of parameter combinations + # Parameters: [concentration, print_speed, gap_size, volume] + test_params = torch.tensor([ + [0.5, 50.0, 0.2, 15.0], # Near optimal values + [0.1, 10.0, 0.05, 5.0], # Lower values + [0.9, 90.0, 0.45, 25.0], # Higher values + [0.6, 45.0, 0.2, 15.0] # Optimal values + ], dtype=torch.float32) + + print(f"Input tensor shape: {test_params.shape}") + print(f"Input parameters:\n{test_params}") + + # Call objective function + results = objective_fn(test_params) + + print(f"Output tensor shape: {results.shape}") + print(f"Success probabilities:\n{results}") + + # Verify that results are valid probabilities + assert torch.all(results >= 0), "All probabilities should be non-negative" + assert torch.all(results <= 1), "All probabilities should be <= 1" + assert results.shape == (4, 1), f"Expected shape (4, 1), got {results.shape}" + + print("All tests passed!") + + return objective_fn, test_params, results + +if __name__ == "__main__": + print("=" * 60) + print("Model Objective Factory Demonstration") + print("=" * 60) + + print("\n1. Creating and training mock LogisticRegression model...") + model, scaler = create_mock_model_and_scaler() + + print(f"\nModel coefficients shape: {model.coef_.shape}") + print(f"Model intercept: {model.intercept_[0]:.3f}") + print(f"Number of features expected: {model.coef_.shape[1]}") + + print("\n2. Creating objective function from model...") + objective_fn = create_objective_from_model(model, scaler) + print("Objective function created successfully!") + + print("\n3. Testing objective function with sample parameter combinations...") + + # Create sample input tensor (batch_size=2, 4 parameters) + sample_params = torch.tensor([ + [0.6, 45.0, 0.2, 15.0], # Near optimal combination + [0.2, 80.0, 0.4, 10.0] # Suboptimal combination + ], dtype=torch.float32) + + print(f"Sample input tensor shape: {sample_params.shape}") + print(f"Sample parameters:") + print(f" Batch 1: concentration={sample_params[0,0]:.1f}, print_speed={sample_params[0,1]:.1f}, gap_size={sample_params[0,2]:.1f}, volume={sample_params[0,3]:.1f}") + print(f" Batch 2: concentration={sample_params[1,0]:.1f}, print_speed={sample_params[1,1]:.1f}, gap_size={sample_params[1,2]:.1f}, volume={sample_params[1,3]:.1f}") + + # Call objective function + results = objective_fn(sample_params) + + print(f"\nResults tensor shape: {results.shape}") + print(f"Success probabilities:") + print(f" Batch 1: {results[0,0]:.4f}") + print(f" Batch 2: {results[1,0]:.4f}") + + print("\n4. Running comprehensive test...") + test_objective_function() + + print("\n" + "=" * 60) + print("Demonstration completed successfully!") + print("=" * 60) + + print("\nUsage Summary:") + print("- Use create_objective_from_model(model, scaler) to create an objective function") + print("- The returned function accepts (batch_size, 4) tensors of [concentration, print_speed, gap_size, volume]") + print("- It returns (batch_size, 1) tensors of success probabilities") + print("- The function handles all necessary feature engineering and scaling internally") \ No newline at end of file diff --git a/aamp_app/optimizer/notebooks/demo_notebook.ipynb b/aamp_app/optimizer/notebooks/demo_notebook.ipynb new file mode 100644 index 0000000..4172631 --- /dev/null +++ b/aamp_app/optimizer/notebooks/demo_notebook.ipynb @@ -0,0 +1,462 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "# Bayesian Optimization Demo\n", + "\n", + "This notebook demonstrates the complete Bayesian optimization workflow for optimizing a printing process with 4 continuous parameters.\n", + "\n", + "## Overview\n", + "- **Objective**: Maximize a noisy, black-box objective function\n", + "- **Parameters**: concentration, print_speed, gap_size, volume\n", + "- **Method**: Batch Bayesian optimization with qNoisyExpectedImprovement\n", + "- **Batch Size**: 8 candidates per iteration\n", + "- **Iterations**: 20 optimization rounds\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All imports successful!\n", + "PyTorch version: 2.7.1+cu126\n" + ] + } + ], + "source": [ + "import torch\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from typing import List, Tuple\n", + "import time\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "import sys\n", + "import os\n", + "sys.path.append(os.path.abspath(os.path.join(os.getcwd(), '..')))\n", + "\n", + "# Import our optimization package\n", + "from bayesian_optimizer import BayesianOptimizer\n", + "from objective_function import MockObjectiveFunction\n", + "\n", + "# Set up matplotlib for better plots\n", + "plt.style.use('default')\n", + "plt.rcParams['figure.figsize'] = (12, 8)\n", + "plt.rcParams['font.size'] = 12\n", + "\n", + "print(\"All imports successful!\")\n", + "print(f\"PyTorch version: {torch.__version__}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parameter Space:\n", + "==================================================\n", + "concentration [ 0.10, 1.00]\n", + "print_speed [ 10.00, 100.00] (mm/s)\n", + "gap_size [ 0.05, 0.50] (mm)\n", + "volume [ 5.00, 25.00] (μL)\n", + "\n", + "Total dimensions: 4\n" + ] + } + ], + "source": [ + "## 2. Parameter Space Definition\n", + "\n", + "# Define the 4-dimensional parameter space for our printing process optimization:\n", + "# Define parameter bounds\n", + "bounds = torch.tensor([\n", + " [0.1, 1.0], # concentration\n", + " [10.0, 100.0], # print_speed\n", + " [0.05, 0.5], # gap_size\n", + " [5.0, 25.0] # volume\n", + "]).T\n", + "\n", + "# Parameter names for plotting\n", + "param_names = ['concentration', 'print_speed', 'gap_size', 'volume']\n", + "param_units = ['', 'mm/s', 'mm', 'μL']\n", + "\n", + "print(\"Parameter Space:\")\n", + "print(\"=\" * 50)\n", + "for i, (name, unit) in enumerate(zip(param_names, param_units)):\n", + " lower, upper = bounds[0, i], bounds[1, i]\n", + " unit_str = f\" ({unit})\" if unit else \"\"\n", + " print(f\"{name:<15} [{lower:>6.2f}, {upper:>6.2f}]{unit_str}\")\n", + " \n", + "print(f\"\\nTotal dimensions: {bounds.shape[1]}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting Bayesian Optimization...\n", + "============================================================\n", + "Batch size: 8\n", + "Iterations: 20\n", + "Initial points: 10\n", + "True optimal score: 0.9993\n", + "============================================================\n", + "BAYESIAN OPTIMIZATION STARTING\n", + "============================================================\n", + "Parameters: 4 dimensions\n", + "Batch size: 8\n", + "Iterations: 20\n", + "Initial points: 10\n", + "============================================================\n", + "Generating 10 initial training points...\n", + "Initial best score: 0.4601\n", + "Initial best parameters: tensor([ 0.1793, 73.1064, 0.3305, 13.7456], dtype=torch.float64)\n", + "\n", + "Iteration 1/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.4601\n", + "Current best parameters: tensor([ 0.1793, 73.1064, 0.3305, 13.7456], dtype=torch.float64)\n", + "\n", + "Iteration 2/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.4601\n", + "Current best parameters: tensor([ 0.1793, 73.1064, 0.3305, 13.7456], dtype=torch.float64)\n", + "\n", + "Iteration 3/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.4728\n", + "Current best parameters: tensor([ 1.0000, 76.9107, 0.3270, 16.2047])\n", + "\n", + "Iteration 4/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.4728\n", + "Current best parameters: tensor([ 1.0000, 76.9107, 0.3270, 16.2047])\n", + "\n", + "Iteration 5/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.4728\n", + "Current best parameters: tensor([ 1.0000, 76.9107, 0.3270, 16.2047])\n", + "\n", + "Iteration 6/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.4916\n", + "Current best parameters: tensor([ 0.4224, 10.0000, 0.3638, 16.2034])\n", + "\n", + "Iteration 7/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.4916\n", + "Current best parameters: tensor([ 0.4224, 10.0000, 0.3638, 16.2034])\n", + "\n", + "Iteration 8/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.4916\n", + "Current best parameters: tensor([ 0.4224, 10.0000, 0.3638, 16.2034])\n", + "\n", + "Iteration 9/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.4916\n", + "Current best parameters: tensor([ 0.4224, 10.0000, 0.3638, 16.2034])\n", + "\n", + "Iteration 10/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.5518\n", + "Current best parameters: tensor([ 0.2830, 45.5636, 0.2645, 7.9626])\n", + "\n", + "Iteration 11/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.9580\n", + "Current best parameters: tensor([ 0.5732, 41.5476, 0.2469, 14.6669])\n", + "\n", + "Iteration 12/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.9580\n", + "Current best parameters: tensor([ 0.5732, 41.5476, 0.2469, 14.6669])\n", + "\n", + "Iteration 13/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 0.9646\n", + "Current best parameters: tensor([ 0.6178, 40.2137, 0.2170, 13.4322])\n", + "\n", + "Iteration 14/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 1.0939\n", + "Current best parameters: tensor([ 0.6122, 42.8861, 0.1982, 12.8696])\n", + "\n", + "Iteration 15/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 1.2429\n", + "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", + "\n", + "Iteration 16/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 1.2429\n", + "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", + "\n", + "Iteration 17/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 1.2429\n", + "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", + "\n", + "Iteration 18/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 1.2429\n", + "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", + "\n", + "Iteration 19/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 1.2429\n", + "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", + "\n", + "Iteration 20/20\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 8 candidates...\n", + "Current best score: 1.2429\n", + "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", + "\n", + "============================================================\n", + "OPTIMIZATION COMPLETED\n", + "============================================================\n", + "Final best score: 1.2429\n", + "Final best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", + "Total evaluations: 170\n", + "\n", + "Optimization completed in 198.91 seconds\n", + "Best score: 1.2429\n", + "Score gap from true optimum: -0.2436\n" + ] + } + ], + "source": [ + "## 3. Run Optimization\n", + "#Execute the complete Bayesian optimization workflow:\n", + "# # Configuration parameters\n", + "BATCH_SIZE = 8\n", + "N_ITERATIONS = 20\n", + "N_INITIAL_POINTS = 10\n", + "NOISE_VARIANCE = 0.01\n", + "SEED = 42\n", + "\n", + "# Create optimizer and objective function\n", + "optimizer = BayesianOptimizer(\n", + " bounds=bounds,\n", + " batch_size=BATCH_SIZE,\n", + " noise_variance=NOISE_VARIANCE,\n", + " seed=SEED\n", + ")\n", + "\n", + "objective = MockObjectiveFunction(noise_std=0.1, seed=SEED)\n", + "\n", + "# Get true optimum for comparison\n", + "true_params, true_max_score = objective.get_optimal_parameters()\n", + "true_score = objective.evaluate_at_optimal()\n", + "\n", + "print(\"Starting Bayesian Optimization...\")\n", + "print(\"=\" * 60)\n", + "print(f\"Batch size: {BATCH_SIZE}\")\n", + "print(f\"Iterations: {N_ITERATIONS}\")\n", + "print(f\"Initial points: {N_INITIAL_POINTS}\")\n", + "print(f\"True optimal score: {true_score:.4f}\")\n", + "\n", + "# Run optimization\n", + "start_time = time.time()\n", + "\n", + "best_params, best_score = optimizer.optimize(\n", + " objective_function=objective,\n", + " n_iterations=N_ITERATIONS,\n", + " n_initial_points=N_INITIAL_POINTS,\n", + " verbose=True\n", + ")\n", + "\n", + "end_time = time.time()\n", + "optimization_time = end_time - start_time\n", + "\n", + "print(f\"\\nOptimization completed in {optimization_time:.2f} seconds\")\n", + "print(f\"Best score: {best_score:.4f}\")\n", + "print(f\"Score gap from true optimum: {(true_score - best_score):.4f}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABcwAAASgCAYAAADFDAuCAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3XlcVNX7B/DPnWGXXUFBNsVdwT39Wgjihpobau4LaplpaZbm8su9NNukrKxcwFTcDTW1zERNyyXLNNMMBXdlkU0EYeb+/pgYGWdAmBm4M8zn/Xr58s69597zzAMMZx7OnCuIoiiCiIiIiIiIiIiIiMjCyaQOgIiIiIiIiIiIiIjIFLBgTkREREREREREREQEFsyJiIiIiIiIiIiIiACwYE5EREREREREREREBIAFcyIiIiIiIiIiIiIiACyYExEREREREREREREBYMGciIiIiIiIiIiIiAgAC+ZERERERERERERERABYMCciIiIiIiIiIiIiAsCCORFRmYWFhUEQhArtY8yYMRAEAUlJSRXaT1nFxMRAEATExMRIHQoRERERkZogCAgLC6vQPhISEiAIAubPn1+h/RARkWlhwZyITN7p06cRFRWFunXrwt7eHs7OzggKCsL06dNx8+ZNo/VjasXqymCObwKKivjF/9na2qJOnTqIiorCxYsXpQ6RiIiISFIXL17Eq6++imbNmsHFxQU2Njbw9vZGr169sHr1auTl5UkdoklISkqCIAgYM2aM1KGUWdH4lyyPJb5fJZKKldQBEBGVRBRFzJw5E8uWLYOVlRW6du2KQYMG4dGjRzh+/Dg++OADfP7554iNjcXAgQMrPJ5169YhNze3QvtYsmQJZs6cidq1a1doP2XVv39/tG/fHl5eXlKHoqV58+bo168fACAzMxMJCQmIiYnBli1bkJCQgLZt20obIBEREZEEFi5ciAULFkCpVKJ9+/YYPXo0nJyccPfuXRw5cgTjx4/HF198gdOnT0sdqsl75pln8Pfff6NGjRpSh0JERJWIBXMiMlkLFy7EsmXLEBAQgD179qBp06Yax7dv344RI0ZgyJAh+OGHHxAeHl6h8fj5+VXo9QHAy8vLpIrTLi4ucHFxkToMnVq0aKExM14URURFRSE2NhYzZ87EwYMHpQuOiIiISALvvPMO5s2bB19fX2zduhXt2rXTarN//34s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wTzANQByGZerJSIqjVEK5qIowsbGRuex+/fvw9bW1hjdEBGRGWPBnIjIeKZNm4bXX38dvr6+qFOnDry8vGBtbS11WEREZIJSclPwoOABAC7HQkRUFka56WdwcDB27typ89j+/fvRunVrY3RDRERmjAVzIiLjWb58OW7fvg0AqFOnDn7//XeJIyIiIlPFG34SEZWPUWaYT5kyBcOGDUO1atUwcuRIAMC1a9fw008/Yc2aNdi2bZsxuiEiIjNWvGAeGChdHEREVYGbmxvu3r0LQPVpzydv8klERFSkeME80I0DcSKipzFKwXzw4MFITEzE/Pnz8cknnwAABgwYACsrKyxYsAC9e/c2RjdERGTGigrmMhng5ydtLERE5q59+/YYN24cnnnmGQDAG2+8AVdXV51tBUFAfHx8JUZHRESmhDPMiYjKxygFcwCYPXs2Ro0ahe+//x53795FjRo10L17d/j7+xurCyIiMmOJiar//fwALrNLRGSYzz//HFOnTsVff/0FQRDw77//lnjfIM4+JyKybIn3E9XbLJgTET2dUQrmCoUCcrkcPj4+GDdunDEuSUREVcj9+0BGhmqb65cTERnO399ffQ8hmUyGb7/9Vj3bnIiIqLjiM8zruNWRMBIiIvNglJt++vj4YMaMGbhw4YIxLkdERFUMb/hJRFRxDh06hCZNmkgdBhERmaiignkNhxpwtnWWOBoiItNnlIJ5x44d8emnnyIoKAjt27fHV199haysLGNcmoiIqgAWzImIKk5oaCgcHR2lDoOIiExQXmEebmbdBMDlWIiIysooBfPNmzfjzp07WLFiBURRxMsvvwwvLy+MHDkSBw8eNEYXRERkxooXzAMDpYuDiIieLicnB1OnToW3tzfs7OzQokULbNq0qdzX+b//+z8IgoBmzZpVQJRERFQWyRnJECECAALdOBAnIioLoxTMAcDFxQUTJ07EiRMncP78ebzyyis4ePAgunXrhjp1uEYWEZEl4wxzIiLzERkZidjYWMybNw/79u1D27ZtMXToUGzcuLHM1/jjjz/wwQcfoGbNmhUYKRERPU3x9cs5w5yIqGyMctPPJzVp0gTLli1DaGgoXnnlFVy7dq0iuiEiIjORmPh4mwVzIiLTtXfvXhw4cAAbN27E0KFDAQCdOnVCcnIypk+fjsGDB0Mul5d6jcLCQkRFRWHChAk4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mBsLABRXDRo0kCSNHDlSI0eOdLrPX/voAIDijYI5ALjOLQVzwzBUunRpp9vOnj0rX19fdzQDAChCrl2TDh3KWI6KkiwWc+MBgOJq7NixsvBDFgBwgxsL5lEVokyMBACKHrcUzBs1aqRFixapa9eumbatXLlSTZs2dUczAIAi5PBhyTagkelYAKDgjB8/3uwQAAAexlYwL+1dWqHlQk2OBgCKFrcUzIcNG6b+/furbNmyeuSRRyRJhw4d0tq1a/XJJ59owYIF7mgGAFCEJCZeX6ZgDgCF49KlS0pKSlKVKlXk4+OWrj4AoIgxDMNeMI8MipS3l7fJEQFA0eLljpP06dNHr732mqZOnapmzZpJkh588EGNGTNGEyZM0L333uuOZgAARQgP/ASAwrNu3TrdcccdKleunCIiIvTzzz9Lkp566iklJCSYHB0AoDCdvHBSF65dkMT85QDgCrcUzCXppZde0r59+/TRRx/p9ddf13//+1/9/vvvGj16tLuaAAAUIRTMAaBwrF27Vvfcc48uX76s559/Xlar1b6tUqVKio2NNS84AEChc3jgZxAdcQDIK7fepxkWFqYhQ4a485QAgCKKgjkAFI6xY8eqW7duWrx4sdLS0jR58mT7tsaNG2vmzJkmRgcAKGwOBXNGmANAnrllhPnPP/+sjRs32l9fuHBB//jHP9SyZUuNHTtWhmG4oxkAQBFiK5h7e0s1apgbCwAUZz/++KOefPJJSZLFYnHYFhISopMnT5oRFgDAJDcWzKMqRpkYCQAUTW4pmD/33HNaunSp/fVLL72k6dOn6+rVq5o4caLef/99dzQDAChCbAXzGjWkUqXMjQUAijMfHx9du3bN6baTJ0+qXLlyhRwRAMBM+84xwhwA8sMtBfNdu3apVatWkjKexjx79mxNmDBB27dv1wsvvKBPPvnEHc0AAIqIs2elc+cylpmOBQAKVrNmzfS///3P6bYFCxbojjvuKOSIAABmunGEec2gmiZGAgBFk1sK5ufOnVOlSpUkSTt27NDZs2f10EMPSZI6duyofTdOZAsAKPYSE68vUzAHgII1evRoLVq0SD179tQXX3whi8WirVu36umnn9aCBQs0atQos0MEABSixKSMzniIf4jK+XKXEQDklVse+hkcHKzDhw9LktatW6cqVaqodu3akqSrV68yhzkAlDA88BMACs/d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FBoaqhYtWuTpfIZhaMOGDQoKClJwcHCejgUAZO/GB37WqkBHHADcxS1DUapUqaJLly45FMw/++wzbd++XXfffXeeHjQEAChaKJgDgLkMw9AzzzyjDz74wOFhRl5eXvrHP/6h9957L9fn6tq1qzp16qShQ4cqOTlZtWvXVnx8vFauXKlZs2bJ29tbkjRkyBDFxcUpMTFRERERkqT7779fjRs3VpMmTRQcHKxjx44pNjZWGzZs0NSpU902Ch4AkIGCOQAUDLf0Wh955BGVLVtWsbGxkqT33ntPw4cPlyS9/fbbWrJkibp16+aOpgAAHsZWMPf2lmrUMDcWACiJ3n33XU2bNk1Dhw7VwIEDFRoaqmPHjikuLk7Tpk1TzZo1NWLEiFyfLyEhQWPGjNHYsWOVlJSkunXrKj4+Xn379rXvk56ervT0dBmGYV935513asGCBXr//feVnJysoKAg3X777Vq6dKm6d+/u1vcMAHAsmEdViDIxEgAoXtxSMP/uu+/01ltv2V+/9957GjBggN5//30NGTJE77zzDgVzACiGDOP6Qz8jIiQGDwJA4fv444/1zDPPKCYmxr6uevXqatasmby9vTV9+vQ8FcwDAgIUExPjcL6/io2NtQ+WsRk1apRGjRqV5/gBAK5hhDkAFAy3zKx+6tQpVa9eXVLGfOb79u3TM888o8DAQA0ZMkS7du1yRzMAAA9z9qyUnJyxzHQsAGCOffv2qUePHk639ejRQ/tunDsLAFBs3Fgwr1mhpomRAEDx4paCub+/v86fPy9J2rRpkwICAnT77bdLkvz8/JSamuqOZgAAHsY2ulyiYA4AZilfvrwOHjzodNvBgwcVGBhYyBEBAApD4tmMznjlspUVUDrA5GgAoPhwS8G8YcOGmjp1qnbu3Klp06apffv2slgskqRDhw6patWq7mgGAOBheOAnAJivU6dOevnll/XDDz84rP/pp580btw4de7c2aTIAAAF5dK1SzqWckwS07EAgLu5ZbbZV155RT169FCTJk1UunRprVmzxr5t2bJluu2229zRDADAw1AwBwDzTZw4UevXr1fz5s1Vv359VatWTcePH9evv/6q0NBQTZw40ewQAQBuduDcAfsyBXMAcC+3FMw7dOig3377TT/88IOaNGmiWjdUTTp06KAmTZq4oxkAgIe5sWAeFWVeHABQkoWHh+un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841PPvnEkGT07t3bmD9/vjF//nzjwoULhmEYxm+//WYEBAQYd911l7F8+XIjISHBaNCggREaGmqcPHnS4Xze3t5Ghw4dHNatW7fO8PHxMXr27GmsXr3amD17thEeHm40aNDAuHz5cqG914KQU/4uXrxoNGvWzLBYLEZMTEymn4N//PGHw/n+mr+NGzcanTt3Nj744APjyy+/NL744gtj6NCh9v3S09ML+y27TU65O3DggNGqVSvjvffeM5YvX26sWLHCGD16tOHn52fccsstRmpqqsP5StK1l5t/tzZdunQxypQpY5w7dy7L85WU6y63/RJ+5qE4oB+eP/TF849+eO7QF3cd/XDX0Q/PH/ririkJfXEK5siVtm3bGpKy/LJ57LHHDEnG/v377euGDx9u1K9f3yhXrpzh4+NjhIaGGgMGDDD27NljwjspfBMnTjSaNGlilC9f3vD29jZCQkKMnj17Gt99953Dfs5yZxiGceLECePRRx81KlasaPj5+RktW7Z06NyXFJ06dTLKli1rJCcnO93OtWcYERERWf4bvTEv27ZtMzp27Gj4+/sbgYGBxgMPPJCpk2kYhiHJaNu2bab1X375pdGyZUvDz8/PqFixovHoo48af/75ZwG+s8KRU/7279+f7c/Bxx57zOF8f83f3r17jW7duhnVq1c3fH19DT8/P6Nhw4bGG2+8UeQ7mjnlLikpyejZs6cRGRlplClTxihdurRx0003GaNGjXLa4SxJ115u/90eOnTI8PLyMh599NFsz1dSrrvc9ksMg595KProh+cPffH8ox+eO/TFXUc/3HX0w/OHvrhrSkJf3PL/QQEAAAAAAAAAUKIxhzkAAAAAAAAAAKJgDgAAAAAAAACAJArmAAAAAAAAAABIomAOAAAAAAAAAIAkCuYAAAAAAAAAAEiiYA4AAAAAAAAAgCQK5gAAAAAAAAAASKJgDgAAAAAAAACAJArmAFDkxcbGymKxaNu2bZKk5cuXa/z48eYGlUMckZGRGjhwYKHGAwAAALgT/XAAKJ4omANAMbN8+XJNmDDB7DCyjWPRokV65ZVXCjkiAAAAoODQDweA4sHH7AAAAEXDxYsX5e/v75Zz3XrrrW45DwAAAFDc0Q8HgMLFCHMAKEYGDhyoqVOnSpIsFov968CBA5IkwzA0bdo0NWnSRGXKlFGFChXUq1cv7du3z+E87dq1U4MGDbRx40a1atVK/v7+Gjx4sCRp3rx5uueee1StWjWVKVNG9erV0+jRo3XhwoVcx+HsVtBDhw5pwIABqly5snx9fVWvXj3961//ktVqte9z4MABWSwWvfPOO/r3v/+tmjVrKiAgQHfccYe+/fZbd6YSAAAAyDX64QBQfDDCHACKkVdeeUUXLlzQggUL9M0339jXV6tWTZL05JNPKjY2Vs8++6zeeustJSUl6dVXX1WrVq20Y8cOValSxX7M8ePHNWDAAI0aNUpvvvmmvLwy/sa6d+9edevWTcOHD1fZsmW1e/duvfXWW/ruu++0du3aXMXxV6dOnVKrVq109epVvfbaa4qMjNTSpUv1/PPPKzExUdOmTXPYf+rUqapbt66mTJlib69bt27av3+/ypcvn/9EAgAAAHlAP5x+OIDig4I5ABQjUVFR9s52y5YtHbZ9++23mj59uv71r3/pueees69v06aNbr75Zv373//WW2+9ZV+flJSk+fPnq0OHDg7nefnll+3LhmHozjvvVL169dS2bVv9/PPPatSoUbZxOPPvf/9bR48e1datW9W8eXNJUufOnZWenq4PPvhAw4cP180332zfv1y5clq6dKm8vb0lSaGhoWrevLlWrFihvn375ipXAAAAgLvQD6cfDqD4YEoWACghli5dKovFogEDBigtLc3+VbVqVTVu3Fjr16932L9ChQqZOumStG/fPvXv319Vq1aVt7e3SpUqpbZt20qSfvvtN5diW7t2rerXr2/vpNsMHDhQhmHYR8zYdO/e3d5Jl6RGjRpJkg4ePOhS+wAAAEBBoR8OAEULI8wBoIT4888/ZRiGw+2eN6pVq5bDa2e3baampqpNmzby8/PT66+/rptvvln+/v46fPiwoqOjdenSJZdiO3PmjCIjIzOtDw0NtW+/UXBwsMNrX19fSXK5fQAAAKCg0A8HgKKFgjkAlBCVKlWSxWLRpk2b7B3bG/11ncViybTP2rVrdezYMa1fv94+mkWSzp07l6/YgoODdfz48Uzrjx07Zo8dAAAAKIrohwNA0cKULABQzGQ1yqNHjx4yDENHjx7V7bffnumrYcOGOZ7b1nn/a6f+ww8/zHUcznTs2FG//vqrtm/f7rD+008/lcViUfv27XM8BwAAAGAm+uEAUDwwwhwAihlbh/utt95S165d5e3trUaN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77+fZd7+avDgwfLx8dGsWbOc9p0lafr06ZKkxx9/XF5eXvr99981evRo3X777QoJCZGvr68iIiL0xBNP6NChQ7lqV8p+Ssob+75/lZfPRK6qVauWli1bprp162r79u368MMPc4z9ypUrevfdd3XrrbeqQoUK8vf3V3h4uO69916tXr1a0vXPQZK0YcMGh2vE9nnhxikfd+/erV69eikkJEReXl72fOQ0Lco333yju+++W+XLl1e5cuXUuXNnbdu2LdN+2f37+OvnGFevb5vLly9r4sSJatiwofz9/RUYGKg2bdo4na71xhwcOHBAffv2VaVKleTn56emTZs6/dwJIHuMMAeKAMMwHF6PHj1aXl5eatGihapXr65z587pq6++0ogRI/Tdd99pzpw5Ts/z7LPPavPmzerevbu6desmb29vSRmd2WHDhqlVq1bq1KmTQkJCdPz4cS1evFg9evTQhx9+qL/97W+Zznft2jV16tRJSUlJuv/++3X16lXFx8frwQcf1JdffqmYmBht375dXbt2la+vrxYsWKCnn35alSpVUp8+fRzO9eqrr2rcuHEKDg5W9+7dVblyZf3888965513tHz5cm3ZskXly5dXZGSkxo0bZ58T3HZLoCQ1adLEvrxy5UpFR0crLS1NPXr0UO3atXXkyBElJCRo2bJlWrdunW677bZc58hdSpcurXnz5unWW2/VoEGD9NNPPyk8PFySNHPmTM2aNUudOnXS6NGjMx2bkJCgFStWqGfPnmrXrp1++uknLVy4UOvWrdOWLVtUp06dHNt/4YUXNHnyZIWEhOjhhx9W2bJltXz5co0ZM0YrV67UmjVrVLp0aYdjrly5og4dOujs2bPq3LmzAgIC7J26jRs3atKkSWrfvr0efPBBlS1bVnv37tWCBQv0xRdf6Ouvv7Z/X9q1a6dz584pJiZGjRs31gMPPGBv48bvnTNbt25Vp06dlJqaqvvvv1/16tXTb7/9ptmzZ2vx4sVavXq10/kkp02bpi+++EL33Xef2rZtq61bt+qzzz7TTz/9pJ9//tlh5AsAAIAnmTlzptLS0tS7d281bNgwy/0ef/xxvfrqq/r999+1YcMGtW/f3mG7O/qQM2fO1BNPPCE/Pz/dd999CgsL0969e/Xxxx9ryZIl+vbbbx1G8e7fv1/t27fXwYMH1bRpU3sxf8+ePXr33Xf197//XUFBQRo3bpxiY2N18OBB+9SUkrJ9VtGjjz6ql19+WXFxcXr22Wczbf/f//4nq9Wqxx57zL7u2rVruvfee7Vq1SrVrVtX/fv3l5+fn9atW6dnnnlG3377rWbNmpVjHqpWraoePXro888/V0JCgvr37++w/dq1a/r000/l7e2tQYMGScrI/wcffKD27durVatWKl26tHbt2qUZM2boiy++0A8//KCwsLAc23aFq5+JXOHv76/nn39ejz/+uGbNmuX0e3OjRx99VJ999pkaNGigRx99VGXKlNGxY8e0efNmrVq1Sp06dVKTJk00btw4TZgwQREREQ4DpP46sOmPP/5Qy5YtVadOHQ0YMECpqakqV65cjnFv3bpVEydO1N13362nnnpKf/zxhxISErRx40Z9+eWXatOmjSvpcPn6ljIGWd1zzz3atGmT6tevr6eeekoXL17U/Pnz1a9fP/3444966623Mh138OBBNW/eXLVq1dIjjzyipKQkzZs3Tw888IBWr16tjh07uvRegBLJAOARJBnO/kmuXbvW8PLyMiwWi7F//37DMAzjjz/+yLRfenq68fDDDxuSjG+++cZh22OPPWZIMkJDQ419+/ZlOvby5cvG4cOHM61PSkoy6tWrZ1SoUMG4ePGiw7aIiAhDktGjRw/j8uXL9vUbN240JBnly5c3br/9duPs2bP2bfv37zdKlSplNGnSJNN7lGTceeedxrlz5xy2zZw505BkDBs2LFP7ERERmWK2xR0UFGRUqlTJ+O233xy27dq1yyhbtmymGHLKUXbatm1rSDIee+wxY9y4cU6/jh8/7nDMvHnzDElG69atjbS0NOPXX381/P39jSpVqhgnTpxwmgNJxpIlSxy2TZkyxZBkdOjQwen7sV0zhmEYmzdvNiQZERERxp9//mlff+3aNaNbt26GJOP11193OI/t+9yxY0cjNTU103v/888/jeTk5Ezrf/jhB8Pf39/o3Lmzw/r9+/fbc+XMunXrDEnGuHHj7OvS09ONOnXqGJKMuXPnOuw/Z84cQ5Jx8803G+np6fb148aNMyQZ5cqVM37++WeHY/r16+f0XAAAAJ6kffv2hiTjo48+ynFfW//mtddes69zVx9yz549RqlSpYybbrrJOHbsmMP+X331leHl5WXcf//9DutbtWplSDLefPPNTLGeOnXKuHTpkv21rS+dFWf9/k6dOhmSjJ07d2bav27dukapUqWM06dP29fZ+obDhg0z0tLS7OvT0tKMwYMHG5KMRYsWZRnDjZYvX25IMtq3b59p22effWZIMu677z77uiNHjjh8XrrxPF5eXsaTTz7psN5Zf9gwsv/8Y3t/69ats69z5TNRVmx9+Kzat/njjz8MSYa3t7dx7dq1LGM/d+6cYbFYjKZNmzp8P2xu/N4ZRsZn5bZt22YbmyTjxRdfdLqPs2vMlmdJxn/+8x+HbZ9//rkhyahdu7bDZwxn/z7+er6/ft9cub7feOMN+2ftG/N44sQJIzw83JBkbNq0yWkOxo8f73CulStXGpKMLl26ZBkDgMwomAMewvYLzlZgfemll4xevXoZPj4+hiRjxIgROZ5j27ZthiRjwoQJDuttv9jffffdPMf1zjvvGJKMDRs2OKy3FVITExMzHVOzZk1DkvHVV19l2ta+fXvD29vboWP0wAMPGJKMX375xWkMTZo0MUJCQjK1n1WHzfYBYOrUqU63Dx8+3JBk7Nq1y74uPzmydYKy+/rxxx8zHffkk08akox//vOfRoMGDQwvLy9j9erVmfazfdj56wcaw8jo5EdFRRmSjAMHDmR6Pzd25oYMGWJIMqZPn57pPLt37za8vLyMmjVrOqy3fZ+dxZ+THj16GL6+vsbVq1ft61wpmG/atMn+BxVnbB/IbrxGbR8aXn755Uz72/5A889//jPP7wkAAKCw1KtXz5BkrFixIsd9R40aZUgyhg4dal/nrj6kre+8bNkyp20/8MADhpeXl3H+/HnDMK5/JmnSpIlDsTErrhQUZ82aZUgynn/+eYf13377rSHJ6Nmzp31denq6ERwcbFSrVs1pcfbs2bOGxWIxevXqlWOstvNFREQYFovF2Lt3r8O2e+65x+kfKLLSoEGDTP1vdxXMXflMlJXcFswvXrxo//xz4wCdv8aenJxsSDJatWplWK3WHNvPTcG8SpUqTv8wYRjZF8z/WhT/6zHr16+3ryusgnlUVJRhsViMPXv2ZNr/o48+MiQZgwYNsq+z5SAyMtLpNV6jRg0jODg4yxgAZMaULICHmTBhgqSMOc6CgoLUunVrDRkyRAMGDLDvc+bMGb399ttavny59u3bpwsXLjic4+jRo7skeUMAAQAASURBVE7P7WzKCptffvlFb7/9tjZu3Kjjx49nmpPP2TmDgoJUq1atTOtDQ0O1f/9+NW3a1Om29PR0nThxQtWrV5eUMWdcqVKl9NlnnzmN7erVqzp16pTOnDmj4ODgLN+DzTfffCNJ+umnn7KcC1vKmM/vr3NBZpejnKxbty5Pc55PmTJFW7Zs0b/+9S9J0ksvvaS77747y/3btm2baZ23t7dat26txMRE/fjjj9k+FPbHH3+UpEy36UpSnTp1FBYWpv379+vcuXMKCgqyb/P19VXjxo2zPO+yZcv0wQcfaNu2bTp9+rTS0tIctp8+fTpfD4/KLm5Juvvuu7VlyxZt375dd911l8O222+/PdP+tilwzp4963JMAAAABc34/2kZs5t72ca2j7N989uHtPWt169fr++++y7T9pMnT8pqtWrv3r1q2rSpvv32W0lS586d5eVVMI9Ni46OVmBgoGbNmqVJkybZp1H89NNPJclh6o7ff/9dZ86c0U033aTXXnvN6fnKlCmj3bt356ptLy8vDR48WOPGjdOMGTPszx46ePCg1qxZo7CwMHXt2tW+v2EYmj17tmJjY7Vjxw6dPXvW4blTf50O0V3y85nIVbm5ViWpXLlyuvfee7VkyRLdeuutevDBB9W6dWu1aNFC/v7+LrXduHFjl6ZbbNOmjdPrtF27dtqwYYN+/PFHp/+GCkpKSooSExMVFhamm2++OdN22+fF7du3Z9rWpEkTp1OKhoeH268HALlDwRzwMLaOcVbOnTunZs2aaf/+/WrevLkeffRRVaxYUT4+Pvb5oa9cueL02KpVqzpd/+2336pDhw5KS0tTx44ddd999ykwMFBeXl766aeftHjxYqfnLF++vNPz+fj4ZLndtu3atWv2dWfOnFFaWpr9jwVZSU1NzVXB/MyZM5KuP3Anu/P9VVY5Kgh+fn7q3r27du7cKR8fHz311FPZ7l+lShWn620xnz9/Ptvjbduzeo/VqlXToUOHdP78eYeCeZUqVbLs/L733nsaNmyYKlSooE6dOqlGjRry9/eXxWLR559/rh07dmR5PeZWbuK+cb8bZXcNOntALgAAgKeoVq2adu/ercOHD+e4r+2B5s4GKeS3D2nrW7/99tvZ7mfrW587d06S7INjCkKZMmXUu3dvzZgxQ19++aW6du2qq1evau7cuQoJCXEoWNvi37t3b7afN5x9NsjKkCFD9OqrryouLk6vvfaa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XBUrVszUyXOmVatWGjVqlCZPnqwGDRqoV69eKlu2rFasWKFdu3apdevWGjlyZK7fr5eXl5599llNmjRJDRs21P3336+rV69q3bp1SkpKUvv27TP9YScgIEAtWrTQxo0bNWDAAN10003y9vbWfffdl+WtohaLRXFxcerUqZP69Omj+++/X3Xr1tWePXv0+eefq1y5cvr0008L7KFSAAAAZhk/frwuXLigf//732rcuLE6d+6sW265RdeuXdOWLVu0detWlSlTRvHx8erQoYPTc+S3D1m3bl198sknGjx4sG655RZ16dJFN998s65du6ZDhw5p06ZNCgkJcXho5qxZs9SuXTuNGjVKn332mdq2bSvDMLR37159+eWX2r17t72Y3LFjR82fP1/R0dHq2rWrypQpo4iICD3yyCM5xvboo49q/PjxGjdunK5du5ZpdLnNK6+8oh07duiDDz7QkiVL1KFDB1WvXl0nT57U3r179fXXX+uNN95Q/fr1c2zTxtvbW4MHD9arr76qb775RlWrVtW9997rsE/VqlXVt29fzZ07V02aNNE999yj8+fPa/Xq1fLz81OTJv/H3n3HZ1Xe/x9/nXvf2XsvIOwpGxVlOFChjmoVa93VWltbbbVOHNVarVa7vq36s8XWURfWSisOBLQKAiJbQBkhCWTvcSe57/v8/ghEYwYBc+fOeD95nAePnHOd63zunNzrc67zuSawcePGLh3vhhtu4G9/+xvXXXcdy5cvJz09nU2bNvHRRx8xf/78NhdFAvGdqKKioqUsqNfrpby8nE2bNrF69Wr8fj/z5s3jmWeeOeLkm/n5+UyfPp2RI0cyceJE0tPTqaqqYunSpRQUFPCjH/2oVfmfuXPn8s9//pOzzz6b4447DpvNxkknncRJJ53U5dg7Mm/ePH72s5/x5ptvMn78eL744guWLFmCy+Xi6aefbvUd4+yzz2b48OG88MIL5OXlMW3aNPbv38/rr7/O2WefzUsvvdSm/2P5+/75z3/Om2++yeuvv8748eM588wzqaur4+WXX6aoqIhbbrlFg4VEAs0UkV4BMLv6lNy2bZu5YMECMz4+3gwJCTEnTpxoPvXUU+bevXtNwLzssstatb/ssstMwNy7d2+Hfb7xxhvmtGnTzLCwMDMyMtI89dRTzVWrVpl/+9vfTMD829/+1qp9ZmammZmZ2W5f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5ET/8IkTJ5KdnY3b7aZfv36Vbh07dqy0D71ez8CBA3njjTcA2LZtG1A2WhuoMGL7TIwZM4Y1a9aUF8hPWLx4MVartbzfeevWrQH466+/Kmz39ddfn/GxR40axZ49e9i5c2eF5Z988skZ71OIk7355pvMmzePG2+8kW+//bbCBMITJ07k22+/9WC6upefn8/333/Pxx9/TG5urqfjCCGEEEII0WjVetLPrVu38vjjj1e5rnv37jz66KNnHUrUv+TEbD56dx2pyTkEBHkTHhUAmkZBvo2fvv2LIwfTuPaW0QQG+3g6qhBCnJK3tzfvv/8+6//YwIH9+wA40XU7H/A/6XOAzp278N577zVYvt27d5f3Jc/OzubLL7/kxx9/ZMqUKeVtR6ZNm8bHH3/MRRddxF133cWAAQMwGo0kJSWxdu1aLr74YqZMmcLbb7/NmjVrmDBhAq1ataK0tJQPPvgAKJs8E8pGo8fExLBixQrGjBlDUFAQISEh5YXt0/X444+zcuVKRo0axdy5cwkKCuLjjz/m22+/5fnnny+f8LN///507NiR2bNn43K5CAwMZPny5fz+++9n/DW7++67+eCDD5gwYQJPPfUU4eHhfPzxx+zfv/+M93nO05SyW33tu4l5/fXXuffee3n++ecr9fBu3749hw4d8lCyuncujKQXQgghhBCirtR6hLmfnx8//fRTlet++ukn/Bpx/xlRxu1ys/yTDaSn5BHTJozAIB+MRj1Gk4HgUF9axgRz9FAGq77c4umoQghx2o4cOVJeLO8LmIALKZsIdBxgBvoc33bfvr0cPXq0wbJdf/31DB48mMGDB3P11Vezbds2XnrpJZYsWVK+jV6v5+uvv+bhhx/myy+/ZMqUKVxyySU8++yzWCwWunfvDpSNqHe5XDz++OOMHz+ea6+9lszMTL7++msuuOCC8v29//77WK1WJk+eTP/+/XniiSdqnbtjx4788ccfdOzYkdtuu41LLrmE3bt3s3DhQu6///4K2b/55hs6derErbfeynXXXYfZbOb1118/469ZREQEv/zyC126dGHmzJlcc801WCyWs9qnECc7cuQIF154YZXrfH19ycvLa9hA9eRcG0kvhBBCiGbEWYjiKgRnoaeTiLpUWFjWw7yw8T6vtR5hfu211/LCCy/gcrmYPn06ERERpKWl8fHHH/PKK69wbxPo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+Hg6hhBCCCGEEI3KGRXMAfbv388vv/xCVlYWN954IxEREaSkpBAYGIiXl1ddZhRCiCarIK+ED99aQ9z+NAKCrLSMDUZRFGzFDg7uTSE5MYfLrhtCz36xno7arJw8unxgW/gz7u/lDVUwHzRoUIXPQ0ND0el0lZb/U0lJCVZr85h49YTiYgeKohASEkJISEjlDZSykzmlJXbcAVb0+nNjdO+5bPfu3XTu3BmDoeKfoj169ChfX13B/Ndff6Vz584sW7aMJ598ksOHDxMZGck111zD/PnzMZlMp5Vhx44d5cdrzhYvXlxpmd1u56+//uKDDz7gmmuu8UAqIYQQQojTkLwSc246OMOh5WRPpxF1ZeVKsNnAywsmTvR0mirV+h2p2+3mxhtvpGvXrsycOZO5c+eSkpICwC233MIzzzxT5yGFaGxUVSX+UDqrvtjMJ++uY/n/rWfX1ngcdqeno4lGZs2qv4jbn0Z062ACg33Q6XQoioLVx0yr2BAcdicrP9tMUaHN01GbFJfLxVVXXUVoaGh5Efbk24svvgiAToH3by77H2DBggVVbh8aGsr06dNxuRp28smRI0fSrVs3fv31V4YMGYLVauWGG24Ayq7meuKJJyrdp3Xr1syYMaPCsrS0NG655Raio6MxmUzExsYyb96803o8qqry/PPP06lTJ8xmM2FhYVx33XXlE6b+M+tvv/3GoEGD8PLyokWLFjz22GO43W4A4uPjCQ0NBWDevHkoioKiKNx510wUncKnSz8hIiqQxGN/j/6fculERowazNZtW5h8yXh8fLxp3bo1CxcuBODbb7+lT58+WK1WunfvzurVqyvkmjFjBq1bt670uJ544olKbXEUReH2229n4cKFdOzYES8vL/r168fGjRvRNI0XXniB2NhYfHx8GD16NIcPHz7l109UVFBQUOFmt9ur3C47O5ugoMpzJpxYlp2dXe0xkpOTOXToEHfeeSd33nknP/30EzNmzGDBggW16jt+LhTLoexn5J+3W265hffff5+rr76aBQsWeDqiEEIIIUSVlC2zCNx9M8qWWZ6OIurSrbfCFVeU/d9I1XqE+X/+8x8++eQTXnjhBcaNG0e3bt3K140fP55FixYxf/78Og0pRGNSXFjKl//3B3t3JmK3OdAb9KhulfVr99OiVTCX/es8WsaGejqmaAQK8krYueUo/oFeGI2Ve/EqikJ4VABJ8dns3pbIoBEdPZCyaTp8+DCffvrpKbcb3gm6Rpf9v25fWaG9ukLckiVLmDt3Lp06darruDVKTU3lmmuuYc6cOTz99NO17p2clpbGgAED0Ol0zJ07l7Zt27Jhwwaeeuop4uPjywvP1Zk5cybvvvsut99+OxMnTiQ+Pp7HHnuMdevWsW3btgojwtPS0pg2bRoPPvgg8+fP59tvv+Wpp54iNzeX119/ncjISFavXs24ceO48cYbuemmm8ru6DahadX37MjMyOCe+25n5q130KVrB9588w1uuOEGjh07xhdffMHDDz+Mv78/8+fP55JLLuHIkSNERUXV6ut0wsqVK9m+fTvPPvssiqLwwAMPMGHCBP71r39x5MgRXn/9dfLz87n33nu59NJL2bFjR4P1vW8OWrZsWeHzxx9/vMoTP1Bzn/+a1qmqSmFhIUuWLClvDzhq1CiKi4t55ZVXmDdvHu3atavyvvPnz+emm24iKirqlH+vKorCY489VuM2TcHRo0crLbNYLISHh1extRBCCCGEEKLWBfNFixbx2GOPce+995aPKDshNja2yj/KhWgunE4Xny38jb+2HCU03J/wqIDyN/VOh4uko5l88u46brjrAkIj/D2cVnhacmI2BfkltGgVXO02er0ORQfxcelSMK+FDh06cPnll/P5559XWB5ghRDfso9DfOHFq8s+XnA13L4IsgrLPs8qhLySivu8/PLL6dChQ/0Gr0JOTg6ff/45o0ePPqP7P/HEE+Tm5rJnzx5atSqbSHPMmDF4eXkxe/Zs7r///mr7pO/fv593332XWbNm8d///rd8ee/evRk4cCAvv/wy//nPf8qXZ2dns2LFCiZPLrsc8oILLsBms/HWW28xZ84cWrVqRd/jM55HR0eXt58pyCshP7e42j7XObk5LP5wKYMGDSQ41JeBAwcQFhbGs88+y+HDh8uL41FRUfTq1Ytly5adcf9qu93ODz/8gLe3N1BWFL3kkktYu3Yt27ZtK39Nz8zM5O6772b37t107979jI7V6DRAD/Njx47h5+dXvthsrnoy4eDg4CpPXuXk5ABUOfr85PumpaVx4YUXVlg+fvx4XnnlFbZt21ZtwfyJJ55g3LhxREVFVVvIP6G5FMxjYmI8HUEIIYQQQogmpdYtWZKTkxk8eHCV6ywWC4WFhWcdSojG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6+vR4vHApNUiNzaWD/okIiIiIiIiIoogTJgTBZnJqD1aNlZ0OYrY5/NDAmA0aEMbHIWURqXCsPj4cIdBRAOVODoFa9tERERERIGUeCbc9nJoo1LDHQkF0syZQG0tEMH5ESbMiYJs+JAUrNm4D44WN6JM+k7XqatvRnycGUMHJYY4OiIiGigk0ToFa9tERERERIEkpi5HfXU1EhMTT3jnPvUhb78d7ghOiLUAiIIsMy0WI4eloLrWAXcnJVeaHC44XV6cMWkQDHqOMCciIiIiIiIiIgoXjjAnCjJJknDZRafB7fFhz4EKSBJgNukgFAF7sxtqjQqzpg3D7DPywh0qERH1ZyzJQkREREREdEJMmBOFQJRZjxuunI4f9pZi07YCVFY3AioJk8en4LRx2Rg+OAWyzBuMiIiIiCJBY2MjHA5Hl8+fCRaPxwOtNrR3HBYVFcHn44PnKTg8HjeKiopC3q7FYkFCQkLI2yUiov6BCXOiENFp1Zg0LhuTxmXD51cgSxKT5EREREQRpra2Fn/7+wvYe/gIhAjd7RMejxslhYXIyh0EtTp0l2nOlmaUV1bB6/WErE0aGOwNNhQcyccDjzwGnU4X0rajDHq89spLTJoT9XHSurMR11wGyZQGnL0u3OFQoMyZA1RVAUlJwLrI7FcmzInCQK3i4wOIiIiIIlFTUxOcHg9mXnML4lIyQtbuoR2bUfTCUky/6makZQ8KabsfvLAUfr8/ZG3SwOBqaYas0WDmtYtC+jddV1GKDW+9iKamJibMifo6+0FonGUQSnO4I6FAOngQKCsDGhvDHUmXmDAnIiIiGgAkAFKQBsvyfinqj+KS05GclROy9mrKS462mxqWdomCJdR/00RERL3FYa5EREREREREREREROAIcyIiIqKBQYjWKVjbJiIiIiIi6gc4wpyIiIiIiIiIiIiICEyYExEREREREREREREBYMKciIiIiIiIiIiIiAgAa5gTERERDQzi6BSsbRMREREREfUDHGFORERERERERERERASOMCciIiIaEKSjU7C2TUREREQUSGLkH2FvqIQ5Opnnm/3J4sWAwwGYzeGOpEtMmBMREREREREREVFkGXwzWqqrYU5MDHckFEg33xzuCE6ICXMiIiKigUA5OgVr20RERERERP0Aa5gTEREREREREREREYEjzImIiIgGEBHuAIiIiIiI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vVPtq7/97W8CgDj//PPFf/7zH/HZZ5+Je++9V6jVanH22WeHY1cGhE8//VS8//774rXXXhMAxGWXXSbef/998f7774vm5mYhROf95XK5xMiRI0VGRoZ4++23xZo1a8T8+fOFWq0WGzZsCNfuEFEAnOpxnMeFwOH1RPjxOiH8eP4ffjyvjww8Xw+/U+2DgfC5zIQ59Zjdbhe33367SE5OFlqtVowZM0asWLGi3TrXXXedACAKCgra5mVlZQkAnU5ZWVmh3YkB4lT7qjM8EQ6u3vRVXV2duOWWW0RSUpLQaDRi6NCh4i9/+Yvw+/0h3IOBozd99cEHH4jp06eL+Ph4YTKZxMiRI8UjjzwiHA5HCPdgYOnus+dY/3TVX5WVleKXv/yliI2NFXq9Xpx++ulizZo1od8JIgqo3hzHeVwIDF5PhB+vE8KP5//hx/P6yMDz9fA71T4YCJ/LkhBHC14REREREREREREREQ1grGFORERERERERERERAQmzImIiIiIiIiIiIiIADBhTkREREREREREREQEgAlzIiIiIiIiIiIiIiIATJgTEREREREREREREQFgwpyIiIiIiIiIiIiICAAT5kREREREREREREREAJgwJyIiIiIiIiIiIiICwIQ5EYXIDz/8gIULFyInJwd6vR5msxkTJkzAU089BZvNFu7wAmrv3r146KGHUFhYGJTtf/rpp3jooYc6XZadnY3rr78+KO0SEREREUWy7777DpdddhlSUlKg1WqRnJyMSy+9FN9++22HdR966CFIkoTa2toTbnfWrFmYNWtWECJu9c477+CZZ57pdJkkSV2e+wfDzp07IUkS7rvvvi7XOXToECRJwu23397j7R77fRMR9QVMmBNR0P3jH//AxIkTsXnzZvz+97/H6tWrsXLlSlx22WV46aWXcOONN4Y7xIDau3cvHn744aAmzB9++OFOl61cuRIPPvhgUNolIiIiIopUzz33HM444wyUlpbiqaeewtq1a7F06VKUlZVh+vTp+Pvf/37K237hhRfwwgsvBDDa9rpLmH/77be46aabgtb2T40dOxYTJ07Em2++Cb/f3+k6r7/+OgD0u+s4IqJj1OEOgIj6t2+//RaLFi3C3Llz8Z///Ac6na5t2dy5c3H33Xdj9erVYYww/FpaWmA0GgOyrfHjxwdkO0REREREfcXXX3+NO++8ExdccAFWrlwJtfrHVMcVV1yB+fPn44477sD48eNxxhlnnPT2R4wYEchwT8rpp58e8jZvvPFG/OY3v8GqVaswb968dsv8fj/efPNNTJw4EWPHjg15bEREocAR5kQUVI899hgkScIrr7zSLll+jFarxcUXXwwAUBQFTz31FPLy8qDT6ZCYmIhf/vKXKC0tbfeaWbNmYdSoUdi8eTNmzJgBo9GI3NxcPPHEE1AUpd26DQ0NuPvuu5Gbm9u2zQsuuAD79+9vW8fj8eDRRx9tazchIQELFy5ETU1Nu21lZ2dj3rx5WL16NSZMmACDwYC8vDy89tprbessW7YMl112GQBg9uzZkCQJkiRh2bJl7WL/4osvMG3aNBiNRtxwww0AgPfeew/nnHMOUlJSYDAYMHz4cNx3331obm5u2/7111+P559/HgDati1JUtto9s5KshQXF+Oaa65BYmIidDodhg8fjqeffrrd76qwsBCSJGHp0qX461//ipycHJjNZkydOhXfffdd551LRERERBQBHn/8cUiShBdffLFdshwA1Go1XnjhBUiShCeeeKLDa0tKSrBgwQJYLBZYrVZcc801Ha4DOivJ0tNrCKB1BPnUqVNhNpthNpsxbtw4vPrqq23b/uSTT1BUVNTu/P6Y40uyHCuXcuy1x1u1ahUkScJ///vftnmHDh3CVVdd1e464Ni1RHeuuuoqGAyGtpHkx/vss89QVlZ2UtcwXemq3Exn1zSVlZW45ZZbkJ6eDq1Wi5ycHDz88MPw+XwnbIeI6GRxhDkRBY3f78e6deswceJEZGRknHD9RYsW4ZVXXsFtt92GefPmobCwEA8++CA2bNiAbdu2IT4+vm3dyspKXH311bj77ruxZMkSrFy5Evfffz9SU1Pxy1/+EgBgt9sxffp0FBYW4t5778WUKVPgcDjwxRdfoKKiAnl5eVAUBZdccgm+/PJL3HPPPZg2bRqKioqwZMkSzJo1C1u2bIHBYGhrd+fOnbj77rtx3333ISkpCf/85z9x4403YvDgwTjzzDNx4YUX4rHHHsMf/vAHPP/885gwYQIAYNCgQW3bqKiowDXXXIN77rkHjz32GGS59bvLQ4cO4YILLsCdd94Jk8mE/fv348knn8T333+PdevWAQAefPBBNDc349///ne7WowpKSmd/k5ramowbdo0eDwePPLII8jOzsbH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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "## 4. Results Analysis and Visualization\n", + "\n", + "# Analyze the optimization results and create comprehensive visualizations:\n", + "# # Get optimization history and training data\n", + "history = optimizer.get_optimization_history()\n", + "train_X, train_Y = optimizer.get_training_data()\n", + "\n", + "# Create comprehensive visualization\n", + "fig, axes = plt.subplots(2, 2, figsize=(15, 12))\n", + "\n", + "# 1. Optimization progress\n", + "iterations = [h['iteration'] for h in history]\n", + "best_values = [h['best_value'] for h in history]\n", + "\n", + "axes[0, 0].plot(iterations, best_values, 'b-o', linewidth=2, markersize=6)\n", + "axes[0, 0].axhline(y=true_score, color='r', linestyle='--', alpha=0.7, \n", + " label=f'True optimum: {true_score:.3f}')\n", + "axes[0, 0].set_xlabel('Iteration')\n", + "axes[0, 0].set_ylabel('Best Observed Value')\n", + "axes[0, 0].set_title('Optimization Progress')\n", + "axes[0, 0].legend()\n", + "axes[0, 0].grid(True, alpha=0.3)\n", + "\n", + "# 2. Improvement over iterations\n", + "improvements = [best_values[i] - best_values[0] for i in range(len(best_values))]\n", + "axes[0, 1].plot(iterations, improvements, 'g-o', linewidth=2, markersize=6)\n", + "axes[0, 1].set_xlabel('Iteration')\n", + "axes[0, 1].set_ylabel('Improvement from Initial')\n", + "axes[0, 1].set_title('Cumulative Improvement')\n", + "axes[0, 1].grid(True, alpha=0.3)\n", + "\n", + "# 3. Parameter exploration (2D projection)\n", + "scatter = axes[1, 0].scatter(train_X[:, 0], train_X[:, 1], c=train_Y.squeeze(), \n", + " cmap='viridis', alpha=0.6, s=50)\n", + "axes[1, 0].scatter(best_params[0], best_params[1], c='red', s=200, marker='*', \n", + " label='Best found', edgecolor='black', linewidth=2)\n", + "axes[1, 0].scatter(true_params[0], true_params[1], c='orange', s=200, marker='*', \n", + " label='True optimum', edgecolor='black', linewidth=2)\n", + "axes[1, 0].set_xlabel('Concentration')\n", + "axes[1, 0].set_ylabel('Print Speed (mm/s)')\n", + "axes[1, 0].set_title('Parameter Exploration')\n", + "axes[1, 0].legend()\n", + "plt.colorbar(scatter, ax=axes[1, 0], label='Objective Value')\n", + "\n", + "# 4. Objective value distribution\n", + "axes[1, 1].hist(train_Y.squeeze().numpy(), bins=20, alpha=0.7, color='skyblue', edgecolor='black')\n", + "axes[1, 1].axvline(best_score, color='red', linestyle='--', linewidth=2, label=f'Best: {best_score:.3f}')\n", + "axes[1, 1].axvline(true_score, color='orange', linestyle='--', linewidth=2, label=f'True: {true_score:.3f}')\n", + "axes[1, 1].set_xlabel('Objective Value')\n", + "axes[1, 1].set_ylabel('Frequency')\n", + "axes[1, 1].set_title('Objective Value Distribution')\n", + "axes[1, 1].legend()\n", + "axes[1, 1].grid(True, alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "polyprintenv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/aamp_app/optimizer/notebooks/interactive_notebook.ipynb b/aamp_app/optimizer/notebooks/interactive_notebook.ipynb new file mode 100644 index 0000000..31e709b --- /dev/null +++ b/aamp_app/optimizer/notebooks/interactive_notebook.ipynb @@ -0,0 +1,299 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "# Interactive Bayesian Optimization\n", + "\n", + "This notebook provides an interactive interface for experimenting with different Bayesian optimization settings.\n", + "\n", + "## Quick Start\n", + "1. Run the setup cells\n", + "2. Adjust parameters in the configuration section\n", + "3. Execute the optimization\n", + "4. Analyze results with interactive plots\n", + "\n", + "## Features\n", + "- **Interactive Parameter Tuning**: Easily modify optimization settings\n", + "- **Real-time Visualization**: See results as they develop\n", + "- **Comparison Tools**: Compare different optimization runs\n", + "- **Export Results**: Save findings for later analysis\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from typing import List, Tuple, Dict\n", + "import time\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "# Import our optimization package\n", + "from bayesian_optimizer import BayesianOptimizer\n", + "from objective_function import MockObjectiveFunction\n", + "\n", + "# Set up plotting\n", + "plt.style.use('default')\n", + "plt.rcParams['figure.figsize'] = (12, 8)\n", + "plt.rcParams['font.size'] = 12\n", + "\n", + "# Global variables for storing results\n", + "optimization_results = []\n", + "current_optimizer = None\n", + "current_objective = None\n", + "\n", + "print(\"Interactive Bayesian Optimization Setup Complete!\")\n", + "print(\"Now you can configure and run optimizations in the cells below.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# EXPERIMENT CONFIGURATION - MODIFY THESE VALUES!\n", + "\n", + "# Optimization Settings\n", + "BATCH_SIZE = 8 # Number of candidates per iteration (try: 4, 8, 12, 16)\n", + "N_ITERATIONS = 15 # Number of optimization iterations (try: 10, 15, 20, 30)\n", + "N_INITIAL_POINTS = 10 # Initial random samples (try: 5, 10, 15, 20)\n", + "NOISE_STD = 0.1 # Objective function noise level (try: 0.05, 0.1, 0.2)\n", + "SEED = 42 # Random seed for reproducibility (try: 42, 123, 456)\n", + "\n", + "# Experiment Name (for tracking different runs)\n", + "EXPERIMENT_NAME = \"default_run\"\n", + "\n", + "# Display configuration\n", + "print(\"CURRENT CONFIGURATION\")\n", + "print(\"=\" * 50)\n", + "print(f\"Experiment Name: {EXPERIMENT_NAME}\")\n", + "print(f\"Batch Size: {BATCH_SIZE}\")\n", + "print(f\"Iterations: {N_ITERATIONS}\")\n", + "print(f\"Initial Points: {N_INITIAL_POINTS}\")\n", + "print(f\"Noise Level: {NOISE_STD}\")\n", + "print(f\"Random Seed: {SEED}\")\n", + "print(f\"Total Evaluations: {N_INITIAL_POINTS + N_ITERATIONS * BATCH_SIZE}\")\n", + "print(\"=\" * 50)\n", + "print(\"=->Change values above and re-run this cell to update configuration!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def run_optimization_experiment():\n", + " \"\"\"Run a complete optimization experiment with current settings.\"\"\"\n", + " global current_optimizer, current_objective\n", + " \n", + " print(f\"Starting Run: {EXPERIMENT_NAME}\")\n", + " print(\"=\" * 60)\n", + " \n", + " # Define parameter bounds\n", + " bounds = torch.tensor([\n", + " [2, 20], # concentration\n", + " [0.01, 20], # print_speed\n", + " [50.0, 100.0], # gap_size\n", + " [6.0, 12.0] # volume\n", + " ]).T\n", + " \n", + " # Create optimizer and objective function\n", + " current_optimizer = BayesianOptimizer(\n", + " bounds=bounds,\n", + " batch_size=BATCH_SIZE,\n", + " noise_variance=0.01,\n", + " seed=SEED\n", + " )\n", + " \n", + " current_objective = MockObjectiveFunction(noise_std=NOISE_STD, seed=SEED)\n", + " \n", + " # Get true optimum for comparison\n", + " true_params, _ = current_objective.get_optimal_parameters()\n", + " true_score = current_objective.evaluate_at_optimal()\n", + " \n", + " print(f\"True optimal score: {true_score:.4f}\")\n", + " print(f\"Starting optimization...\")\n", + " \n", + " # Run optimization\n", + " start_time = time.time()\n", + " \n", + " best_params, best_score = current_optimizer.optimize(\n", + " objective_function=current_objective,\n", + " n_iterations=N_ITERATIONS,\n", + " n_initial_points=N_INITIAL_POINTS,\n", + " verbose=False # Reduce output for cleaner notebook\n", + " )\n", + " \n", + " end_time = time.time()\n", + " optimization_time = end_time - start_time\n", + " \n", + " # Store results\n", + " result = {\n", + " 'experiment_name': EXPERIMENT_NAME,\n", + " 'batch_size': BATCH_SIZE,\n", + " 'n_iterations': N_ITERATIONS,\n", + " 'n_initial_points': N_INITIAL_POINTS,\n", + " 'noise_std': NOISE_STD,\n", + " 'seed': SEED,\n", + " 'best_params': best_params,\n", + " 'best_score': best_score,\n", + " 'true_score': true_score,\n", + " 'optimization_time': optimization_time,\n", + " 'total_evaluations': len(current_optimizer.train_X),\n", + " 'optimizer': current_optimizer,\n", + " 'objective': current_objective\n", + " }\n", + " \n", + " optimization_results.append(result)\n", + " \n", + " print(\"\\nOPTIMIZATION COMPLETE\")\n", + " print(\"=\" * 60)\n", + " print(f\"Time: {optimization_time:.2f} seconds\")\n", + " print(f\"Total evaluations: {result['total_evaluations']}\")\n", + " print(f\"Best score: {best_score:.4f}\")\n", + " print(f\"True optimal: {true_score:.4f}\")\n", + " print(f\"Gap: {(true_score - best_score):.4f}\")\n", + " \n", + " # Parameter comparison\n", + " param_names = ['concentration', 'print_speed', 'gap_size', 'volume']\n", + " print(f\"\\nBest Parameters Found:\")\n", + " for i, (name, value) in enumerate(zip(param_names, best_params)):\n", + " print(f\" {name}: {value:.3f}\")\n", + " \n", + " return result\n", + "\n", + "# Run the experiment\n", + "experiment_result = run_optimization_experiment()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_optimization_results(optimizer, objective):\n", + " \"\"\"Create comprehensive plots of the optimization results.\"\"\"\n", + " \n", + " history = optimizer.get_optimization_history()\n", + " train_X, train_Y = optimizer.get_training_data()\n", + " true_params, _ = objective.get_optimal_parameters()\n", + " true_score = objective.evaluate_at_optimal()\n", + " \n", + " # Create the visualization\n", + " fig, axes = plt.subplots(2, 3, figsize=(18, 12))\n", + " \n", + " # 1. Optimization Progress\n", + " iterations = [h['iteration'] for h in history]\n", + " best_values = [h['best_value'] for h in history]\n", + " \n", + " axes[0, 0].plot(iterations, best_values, 'b-o', linewidth=2, markersize=6)\n", + " axes[0, 0].axhline(y=true_score, color='r', linestyle='--', alpha=0.7, \n", + " label=f'True optimum: {true_score:.3f}')\n", + " axes[0, 0].set_xlabel('Iteration')\n", + " axes[0, 0].set_ylabel('Best Observed Value')\n", + " axes[0, 0].set_title('Optimization Progress')\n", + " axes[0, 0].legend()\n", + " axes[0, 0].grid(True, alpha=0.3)\n", + " \n", + " # 2. Improvement Rate\n", + " improvements = [best_values[i] - best_values[0] for i in range(len(best_values))]\n", + " axes[0, 1].plot(iterations, improvements, 'g-o', linewidth=2, markersize=6)\n", + " axes[0, 1].set_xlabel('Iteration')\n", + " axes[0, 1].set_ylabel('Improvement from Initial')\n", + " axes[0, 1].set_title('Cumulative Improvement')\n", + " axes[0, 1].grid(True, alpha=0.3)\n", + " \n", + " # 3. Parameter Space Exploration (Concentration vs Print Speed)\n", + " scatter = axes[0, 2].scatter(train_X[:, 0], train_X[:, 1], c=train_Y.squeeze(), \n", + " cmap='viridis', alpha=0.6, s=50)\n", + " axes[0, 2].scatter(optimizer.best_parameters[0], optimizer.best_parameters[1], \n", + " c='red', s=200, marker='*', label='Best found', \n", + " edgecolor='black', linewidth=2)\n", + " axes[0, 2].scatter(true_params[0], true_params[1], c='orange', s=200, marker='*', \n", + " label='True optimum', edgecolor='black', linewidth=2)\n", + " axes[0, 2].set_xlabel('Concentration')\n", + " axes[0, 2].set_ylabel('Print Speed (mm/s)')\n", + " axes[0, 2].set_title('Parameter Space Exploration')\n", + " axes[0, 2].legend()\n", + " plt.colorbar(scatter, ax=axes[0, 2], label='Objective Value')\n", + " \n", + " # 4. Gap Size vs Volume\n", + " scatter2 = axes[1, 0].scatter(train_X[:, 2], train_X[:, 3], c=train_Y.squeeze(), \n", + " cmap='viridis', alpha=0.6, s=50)\n", + " axes[1, 0].scatter(optimizer.best_parameters[2], optimizer.best_parameters[3], \n", + " c='red', s=200, marker='*', label='Best found', \n", + " edgecolor='black', linewidth=2)\n", + " axes[1, 0].scatter(true_params[2], true_params[3], c='orange', s=200, marker='*', \n", + " label='True optimum', edgecolor='black', linewidth=2)\n", + " axes[1, 0].set_xlabel('Gap Size (mm)')\n", + " axes[1, 0].set_ylabel('Volume (μL)')\n", + " axes[1, 0].set_title('🔍 Gap Size vs Volume')\n", + " axes[1, 0].legend()\n", + " plt.colorbar(scatter2, ax=axes[1, 0], label='Objective Value')\n", + " \n", + " # 5. Objective Value Distribution\n", + " axes[1, 1].hist(train_Y.squeeze().numpy(), bins=20, alpha=0.7, color='skyblue', \n", + " edgecolor='black')\n", + " axes[1, 1].axvline(optimizer.best_observed_value, color='red', linestyle='--', \n", + " linewidth=2, label=f'Best: {optimizer.best_observed_value:.3f}')\n", + " axes[1, 1].axvline(true_score, color='orange', linestyle='--', linewidth=2, \n", + " label=f'True: {true_score:.3f}')\n", + " axes[1, 1].set_xlabel('Objective Value')\n", + " axes[1, 1].set_ylabel('Frequency')\n", + " axes[1, 1].set_title('Objective Value Distribution')\n", + " axes[1, 1].legend()\n", + " axes[1, 1].grid(True, alpha=0.3)\n", + " \n", + " # 6. Parameter Convergence\n", + " param_names = ['concentration', 'print_speed', 'gap_size', 'volume']\n", + " eval_order = np.arange(len(train_X))\n", + " \n", + " for i, name in enumerate(param_names):\n", + " color = plt.cm.Set1(i)\n", + " axes[1, 2].scatter(eval_order, train_X[:, i], alpha=0.6, s=30, \n", + " c=color, label=name)\n", + " axes[1, 2].axhline(y=true_params[i], color=color, linestyle='--', alpha=0.7)\n", + " \n", + " axes[1, 2].set_xlabel('Evaluation Order')\n", + " axes[1, 2].set_ylabel('Parameter Value')\n", + " axes[1, 2].set_title('Parameter Convergence')\n", + " axes[1, 2].legend()\n", + " axes[1, 2].grid(True, alpha=0.3)\n", + " \n", + " plt.tight_layout()\n", + " plt.show()\n", + " \n", + " return fig\n", + "\n", + "# Create the plots\n", + "if current_optimizer is not None:\n", + " fig = plot_optimization_results(current_optimizer, current_objective)\n", + " print(\"Plots created successfully!\")\n", + "else:\n", + " print(\"No optimization results to plot. Run the optimization first!\")\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/aamp_app/optimizer/notebooks/model_integration_demo.ipynb b/aamp_app/optimizer/notebooks/model_integration_demo.ipynb new file mode 100644 index 0000000..329e1da --- /dev/null +++ b/aamp_app/optimizer/notebooks/model_integration_demo.ipynb @@ -0,0 +1,775 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "# Model Integration Demo: Scikit-Learn to BoTorch Optimization\n", + "\n", + "This notebook demonstrates how to integrate a pretrained scikit-learn logistic regression model with BoTorch for Bayesian optimization of polymer printing parameters.\n", + "\n", + "## Overview\n", + "\n", + "We'll demonstrate:\n", + "1. Loading a pretrained logistic regression model\n", + "2. Using the model objective factory to create a BoTorch-compatible objective function\n", + "3. Running Bayesian optimization to find optimal printing parameters\n", + "4. Analyzing and visualizing the optimization results\n", + "\n", + "## Background\n", + "\n", + "The logistic regression model was trained on polymer printing data with features:\n", + "- **Base parameters**: concentration, print_speed, gap_size, volume\n", + "- **Engineered features**: polynomial terms, interactions, and solvent indicators\n", + "- **Target**: Binary success/failure outcome\n", + "\n", + "The model expects exactly 18 features in a specific order and uses a StandardScaler for normalization.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Libraries imported successfully\n", + "PyTorch version: 2.7.1+cu126\n", + "NumPy version: 2.2.3\n", + "Pandas version: 2.2.3\n" + ] + } + ], + "source": [ + "# Import required libraries\n", + "import sys\n", + "import os\n", + "sys.path.append('..') # Add parent directory to path to import our modules\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import torch\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.preprocessing import StandardScaler\n", + "import joblib\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "# Set up plotting style\n", + "plt.style.use('seaborn-v0_8')\n", + "sns.set_palette(\"husl\")\n", + "\n", + "# Set random seeds for reproducibility\n", + "np.random.seed(42)\n", + "torch.manual_seed(42)\n", + "\n", + "print(\"Libraries imported successfully\")\n", + "print(f\"PyTorch version: {torch.__version__}\")\n", + "print(f\"NumPy version: {np.__version__}\")\n", + "print(f\"Pandas version: {pd.__version__}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model path: /home/vpalacio/Documents/data/polyprint/PProDOT_Samples/supervised_analysis/RandomForest_model.joblib\n", + "Scaler path: /home/vpalacio/Documents/data/polyprint/PProDOT_Samples/supervised_analysis/feature_scaler.joblib\n" + ] + } + ], + "source": [ + "# Load your pretrained model and scaler\n", + "# Update these paths to point to your actual trained model files\n", + "MODEL_PATH = \"/home/vpalacio/Documents/data/polyprint/PProDOT_Samples/supervised_analysis/RandomForest_model.joblib\"\n", + "SCALER_PATH = \"/home/vpalacio/Documents/data/polyprint/PProDOT_Samples/supervised_analysis/feature_scaler.joblib\"\n", + "\n", + "print(f\"Model path: {MODEL_PATH}\")\n", + "print(f\"Scaler path: {SCALER_PATH}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading pretrained model and scaler...\n", + "Model loaded successfully: RandomForestClassifier\n", + "Scaler loaded successfully: StandardScaler\n", + "\n", + "Model Information:\n", + " Model type: RandomForestClassifier\n", + "Error loading model: 'RandomForestClassifier' object has no attribute 'coef_'\n", + "Please ensure the files are valid joblib/pickle files.\n" + ] + } + ], + "source": [ + "# Load the pretrained model and scaler\n", + "print(\"Loading pretrained model and scaler...\")\n", + "\n", + "try:\n", + " # Load the trained logistic regression model\n", + " model = joblib.load(MODEL_PATH)\n", + " print(f\"Model loaded successfully: {type(model).__name__}\")\n", + " \n", + " # Load the trained scaler\n", + " scaler = joblib.load(SCALER_PATH)\n", + " print(f\"Scaler loaded successfully: {type(scaler).__name__}\")\n", + " \n", + " # Display model information\n", + " print(f\"\\nModel Information:\")\n", + " print(f\" Model type: {type(model).__name__}\")\n", + " print(f\" Number of features: {model.coef_.shape[1]}\")\n", + " print(f\" Model intercept: {model.intercept_[0]:.4f}\")\n", + " print(f\" Scaler mean: {scaler.mean_[:5]}...\") # Show first 5 values\n", + " print(f\" Scaler scale: {scaler.scale_[:5]}...\") # Show first 5 values\n", + " \n", + "except FileNotFoundError as e:\n", + " print(f\"Error: Could not find file - {e}\")\n", + " print(\"Please check the file paths and try again.\")\n", + "except Exception as e:\n", + " print(f\"Error loading model: {e}\")\n", + " print(\"Please ensure the files are valid joblib/pickle files.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Creating objective function from pretrained model...\n", + "Model expects 18 features\n", + "Objective function created successfully\n", + "\n", + "Testing objective function with sample parameters...\n", + "Test input shape: torch.Size([2, 4])\n", + "Test parameters:\n", + " Sample 1: concentration=0.6, print_speed=45.0, gap_size=0.2, volume=15.0\n", + " Sample 2: concentration=0.3, print_speed=80.0, gap_size=0.1, volume=20.0\n", + "\n", + "Test results shape: torch.Size([2, 1])\n", + "Success probabilities:\n", + " Sample 1: 0.8181\n", + " Sample 2: 0.8181\n", + "\n", + "Objective function is working correctly\n" + ] + } + ], + "source": [ + "# Import the model objective factory\n", + "from model_objective_factory import create_objective_from_model\n", + "\n", + "# Create the objective function from your pretrained model\n", + "print(\"Creating objective function from pretrained model...\")\n", + "\n", + "objective_function = create_objective_from_model(model, scaler)\n", + "print(\"Objective function created successfully\")\n", + "\n", + "# Test the objective function with a sample input\n", + "print(\"\\nTesting objective function with sample parameters...\")\n", + "test_params = torch.tensor([\n", + " [0.6, 45.0, 0.2, 15.0], # Moderate values\n", + " [0.3, 80.0, 0.1, 20.0], # Different combination\n", + "], dtype=torch.float32)\n", + "\n", + "print(f\"Test input shape: {test_params.shape}\")\n", + "print(f\"Test parameters:\")\n", + "print(f\" Sample 1: concentration={test_params[0,0]:.1f}, print_speed={test_params[0,1]:.1f}, gap_size={test_params[0,2]:.1f}, volume={test_params[0,3]:.1f}\")\n", + "print(f\" Sample 2: concentration={test_params[1,0]:.1f}, print_speed={test_params[1,1]:.1f}, gap_size={test_params[1,2]:.1f}, volume={test_params[1,3]:.1f}\")\n", + "\n", + "# Call the objective function\n", + "test_results = objective_function(test_params)\n", + "print(f\"\\nTest results shape: {test_results.shape}\")\n", + "print(f\"Success probabilities:\")\n", + "print(f\" Sample 1: {test_results[0,0]:.4f}\")\n", + "print(f\" Sample 2: {test_results[1,0]:.4f}\")\n", + "\n", + "print(\"\\nObjective function is working correctly\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parameter bounds:\n", + " Concentration: [20.0, 100.0] mg/mL\n", + " Print Speed: [0.0, 20.0] mm/s\n", + " Gap Size: [50.000, 200.000] mm\n", + " Volume: [4.0, 15.0] μL\n", + "\n", + "Bayesian optimizer initialized with:\n", + " Batch size: 6\n", + " Parameter dimensions: 4\n", + " Seed: 42\n" + ] + } + ], + "source": [ + "# Import the Bayesian optimizer\n", + "from bayesian_optimizer import BayesianOptimizer\n", + "\n", + "# Define parameter bounds based on typical polymer printing ranges\n", + "bounds = torch.tensor([\n", + " [20.0, 0.0, 50, 4.0], # Lower bounds: [concentration, print_speed, gap_size, volume]\n", + " [100.0, 20.0, 200, 15.0] # Upper bounds: [concentration, print_speed, gap_size, volume]\n", + "], dtype=torch.float64)\n", + "\n", + "print(\"Parameter bounds:\")\n", + "print(f\" Concentration: [{bounds[0,0]:.1f}, {bounds[1,0]:.1f}] mg/mL\")\n", + "print(f\" Print Speed: [{bounds[0,1]:.1f}, {bounds[1,1]:.1f}] mm/s\")\n", + "print(f\" Gap Size: [{bounds[0,2]:.3f}, {bounds[1,2]:.3f}] mm\")\n", + "print(f\" Volume: [{bounds[0,3]:.1f}, {bounds[1,3]:.1f}] μL\")\n", + "\n", + "# Initialize the Bayesian optimizer\n", + "optimizer = BayesianOptimizer(\n", + " bounds=bounds,\n", + " batch_size=6, # Evaluate 6 candidates per iteration\n", + " seed=42\n", + ")\n", + "\n", + "print(f\"\\nBayesian optimizer initialized with:\")\n", + "print(f\" Batch size: {optimizer.batch_size}\")\n", + "print(f\" Parameter dimensions: {optimizer.n_dims}\")\n", + "print(f\" Seed: {optimizer.seed}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting Bayesian optimization...\n", + "This will find the optimal printing parameters to maximize success probability.\n", + "============================================================\n", + "============================================================\n", + "BAYESIAN OPTIMIZATION STARTING\n", + "============================================================\n", + "Parameters: 4 dimensions\n", + "Batch size: 6\n", + "Iterations: 12\n", + "Initial points: 8\n", + "============================================================\n", + "Generating 8 initial training points...\n", + "Initial best score: 0.9881\n", + "Initial best parameters: tensor([ 27.0488, 14.0236, 143.5105, 8.8101], dtype=torch.float64)\n", + "\n", + "Iteration 1/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9881\n", + "Current best parameters: tensor([ 27.0488, 14.0236, 143.5105, 8.8101], dtype=torch.float64)\n", + "\n", + "Iteration 2/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9886\n", + "Current best parameters: tensor([ 20.0000, 16.0668, 123.0113, 9.5847])\n", + "\n", + "Iteration 3/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9886\n", + "Current best parameters: tensor([ 20.0000, 16.0668, 123.0113, 9.5847])\n", + "\n", + "Iteration 4/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9886\n", + "Current best parameters: tensor([ 20.0000, 16.0668, 123.0113, 9.5847])\n", + "\n", + "Iteration 5/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9886\n", + "Current best parameters: tensor([ 20.0000, 16.0668, 123.0113, 9.5847])\n", + "\n", + "Iteration 6/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9897\n", + "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", + "\n", + "Iteration 7/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9897\n", + "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", + "\n", + "Iteration 8/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9897\n", + "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", + "\n", + "Iteration 9/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9897\n", + "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", + "\n", + "Iteration 10/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9897\n", + "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", + "\n", + "Iteration 11/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9897\n", + "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", + "\n", + "Iteration 12/12\n", + "----------------------------------------\n", + "Fitting GP model...\n", + "Optimizing acquisition function...\n", + "Evaluating 6 candidates...\n", + "Current best score: 0.9897\n", + "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", + "\n", + "============================================================\n", + "OPTIMIZATION COMPLETED\n", + "============================================================\n", + "Final best score: 0.9897\n", + "Final best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", + "Total evaluations: 80\n", + "============================================================\n", + "Optimization completed\n", + "Best success probability: 0.9897\n", + "Best parameters:\n", + " Concentration: 20.000 mg/mL\n", + " Print Speed: 8.2 mm/s\n", + " Gap Size: 126.225 mm\n", + " Volume: 10.2 μL\n" + ] + } + ], + "source": [ + "# Run Bayesian optimization\n", + "print(\"Starting Bayesian optimization...\")\n", + "print(\"This will find the optimal printing parameters to maximize success probability.\")\n", + "print(\"=\" * 60)\n", + "\n", + "# Run the optimization\n", + "best_params, best_score = optimizer.optimize(\n", + " objective_function=objective_function,\n", + " n_iterations=12, # Number of optimization iterations\n", + " n_initial_points=8, # Number of initial random points\n", + " verbose=True\n", + ")\n", + "\n", + "print(\"=\" * 60)\n", + "print(\"Optimization completed\")\n", + "print(f\"Best success probability: {best_score:.4f}\")\n", + "print(f\"Best parameters:\")\n", + "print(f\" Concentration: {best_params[0]:.3f} mg/mL\")\n", + "print(f\" Print Speed: {best_params[1]:.1f} mm/s\")\n", + "print(f\" Gap Size: {best_params[2]:.3f} mm\")\n", + "print(f\" Volume: {best_params[3]:.1f} μL\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimization Summary:\n", + " Total evaluations: 80\n", + " Best score: 0.9897\n", + " Worst score: 0.3873\n", + " Mean score: 0.9386\n", + " Score improvement: 0.6024\n", + "\\nTop 5 parameter combinations:\n", + " 1. Score: 0.9897 | Conc: 20.000, Speed: 8.2, Gap: 126.225, Vol: 10.2\n", + " 2. Score: 0.9888 | Conc: 20.000, Speed: 5.9, Gap: 145.855, Vol: 9.2\n", + " 3. Score: 0.9886 | Conc: 20.000, Speed: 19.1, Gap: 121.952, Vol: 8.5\n", + " 4. Score: 0.9886 | Conc: 20.000, Speed: 16.1, Gap: 123.011, Vol: 9.6\n", + " 5. Score: 0.9883 | Conc: 24.166, Speed: 20.0, Gap: 130.074, Vol: 6.4\n", + "\\nCreating visualizations...\n" + ] + } + ], + "source": [ + "# Get all evaluated points during optimization\n", + "train_X, train_Y = optimizer.get_training_data()\n", + "\n", + "print(f\"Optimization Summary:\")\n", + "print(f\" Total evaluations: {len(train_Y)}\")\n", + "print(f\" Best score: {train_Y.max().item():.4f}\")\n", + "print(f\" Worst score: {train_Y.min().item():.4f}\")\n", + "print(f\" Mean score: {train_Y.mean().item():.4f}\")\n", + "print(f\" Score improvement: {(train_Y.max() - train_Y.min()).item():.4f}\")\n", + "\n", + "# Find top 5 parameter combinations\n", + "print(f\"\\\\nTop 5 parameter combinations:\")\n", + "sorted_indices = torch.argsort(train_Y.flatten(), descending=True)\n", + "for i, idx in enumerate(sorted_indices[:5]):\n", + " params = train_X[idx]\n", + " score = train_Y[idx].item()\n", + " print(f\" {i+1}. Score: {score:.4f} | \"\n", + " f\"Conc: {params[0]:.3f}, Speed: {params[1]:.1f}, \"\n", + " f\"Gap: {params[2]:.3f}, Vol: {params[3]:.1f}\")\n", + "\n", + "# Convert to numpy for plotting\n", + "eval_params = train_X.cpu().numpy()\n", + "eval_scores = train_Y.cpu().numpy().flatten()\n", + "\n", + "print(f\"\\\\nCreating visualizations...\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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/DDDMiaTCaPReNfKpKoqFosFg0F+8oUQ4r9Kp9M53Z8CAwOpVKkS0dHRzJ07l7fffpsSJUrkad13+z6WU1arlQMHDtCmTZs7Wk9m+9O9e3e+/fZb1q9fT48ePTJMX7NmDdWqVaNWrVp3tP1/i4SEBFavXs1LL73E888/r31funRpHnroIV544QUOHDhAmTJlCrCU917hwoWdrsUyZcrg5+dHnz592LhxI08//XSGZe6Xa8wVX1/fTKel/9vh5s2bnDt37h6USggh7k/pn8dKlSpF48aNqV+/Pm+99RZVqlShe/fugC0ImBvp22/SM5vN6PX6u9Ye4+o5LKt7REG5efMmTZs2pWzZspnOs3z5curVq8e7776rfRcUFETt2rW5desWBw8e5PHHH78Xxb1v3I9tXXciu/bC9Nff/v37adGihfb5bl1HQuSG9NgT4l9gwYIFPPLII05BPUf9+/enXLlyLFq0CIDU1FSqVavGF198wdSpU2nSpAm1atWiT58+nD9/HrB1Q588eTLXr1+nWrVqzJkzJ0M6nRUrVlCtWjVOnjzJc889R61atXj00Uf58ccfuXLlCv3796d27dq0atWK9evXa+VJ32U9fbd++3/ff/+9tsy2bdt4/vnnadCgAaGhoXTu3JmNGzdq01u1asWWLVtYv3691n3fVSrOLVu20KNHD2rVqkWdOnV47rnn2Llzpzb9r7/+olq1auzatYs33niDevXq0ahRI0aMGJHpG/rZ8fHxoXTp0ly+fBm4nWJgw4YNdOzYkWbNmuW4fAB79uyhS5cuhISE0KpVK1asWMHEiRN55JFHnOpjwoQJjBo1itq1a/P7778DcOPGDd555x2aNGlCcHAwTzzxBGFhYU7r37VrF7169aJBgwbUqVOHzp07s27dOm36rVu3GDVqFI888gjBwcG0aNGCCRMmkJycnKf6EUIIUXDsKfsuXboEgMViYfbs2bRr145atWrRtGlThgwZwsWLF7Vl7PfXP//8k8cee0xrgMrJsnl9dgA4cuQI/fv3JzQ0lNq1a9OrVy8thdLFixepUaMG8fHxvPfee06pCLdv307Pnj2pXbs2devW5aWXXnJ6Nshsf9ILDg6mRo0aTs8nduHh4ezfv19b9tChQ/Tv359GjRpRu3ZtOnTowLfffpvpccgsZeGwYcMypBJavnw57du319JCjR07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tum5557TKaecohtuuEHdunXTE088oQsuuEC33367nnvuObtDBAAAiIqwW3HecsstuuaaayRJl112ma666iq99lpp7/grr7wystEBkVTDij3W2QISkDWxb2m/abDGHmKE8RMAINm4nQ6N6J6tKQs3aq/XJ5fTkNdnqp7LqbN7tJLb6ZDHX/k6eLXRrl17Pffcy3rxxWd1ySXna9euncrKaq5Bg07Q+edfLLfbrTvvnKB//es+DRlyoho2bKSjjx6oMWOurtHzH3XUAJ133oW67bYbtXv3Lh12WDfdddf/SZJ69uyjK6+8Wg8/PFEFBVvVsmUrXX/9zerS5dAaPfewYWfoueee0i+/LNKVV15bYf95512k22+/Raeddrxyczvpttvu1Ntvv65//et+ZWQ01AknnKwvvvhU5513jp57LrhqcejQM7R58yZdffXl8nhK1LlzFz355DPKyMioUWx1xddff60pU6bo888/V+PGjXXmmWdqxIgRatNmf+XhIYccor///e+65JJLbIwUAAAgOgyzlv0g1q9fryVLlqh169Y67LDDIhVXVOTnh/7EWyQYhuR2O+Xx+CrkjBJRPJ5vypzv5dxS2mqlaPiJkuWTmEH7hp0ouSvmvOPxnGuLc7Y7mthIhnN2rlijlAVLJEkleV3lz82R2+2U8fG3cuRvkyQVnXGS5Az9qfJEkAyvs1UkzzcrK/KTXYyfSiXbv8u6hutvL66/vbj+seP1+fXD2u3asrNEzTNS1DOnsdwuB9ffZrG4B6IxhpKkLl26qHfv3ho5cqSOP/74SqsshwwZolmzZkUlhupEc/xUU7zP1Q7Xr3a4frXD9asdrt+Bs/PanffBOdpatFXN0ppVut7eihW/65ttXrVs3zG2we3T56Jhar57u1Jat9GOVyrGGInrV9PxU9gVe5JUUlKiBQsWaNOmTapXr55atWqlQw+t2afPANtYK/b85e8saytO2vEBCccRumIv6Gsq9hBljJ8AAMnI5XSob/tMu8NAApk9e7batWunkpKSQFJv165datCgQdDj7ErqAQCA8C3K/1kbd29Qq/Rsu0OpVPPflyujYIt8BQV2hxJ+Ym/RokW69NJLtX379qDtrVu31n/+8x8dfPDBkYoNiCxHDdfYY50tIPFYE/bWxL5JUh+xwfgJAAAgMpxOp0477TSNHTtWJ598siTp7bff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z1qxR7UuooTxEwAAKO+II/ICrT8RvltuuUU33nijTj31VElSv379ZJqmBg4cqH/84x82RwcAABB9NU7sPf7446pfv75effVVeb1ePfroo3riiSc0fvz4aMYHRE65Nfb2f83kPpAUqqjYoxUnooXxEwAAQGQ1bNhQzzzzjP744w+tXLlSDodDHTp0UIcOHewODQAAHKCXB7+hEn+JUhwpdodSqRm3P6gj6vuV3T7X7lBqntj79ttv9cILL6h9+/aSpHvvvVdjxoxhYgrxw1FZxZ7lMST2gMS17z0gsLYea+whBhg/AQAAREfHjh3VsWPHoG1+v18OxvYAAMSdw5v3sDuEam05qIt2Zbrkze1kdyg1T+wVFBQEJqWk0gFUfn5+NGICoqOSVpyGScUekBTK7u+y+z8osce9j+hg/AQAABBZnTt3llHF/91//fXXGEYDAAAQezVO7JVnGIZMa9UTUNexxh6Q1EyHQ4a0vwWnSStOxB7jJwAAgNq54447ghJ7fr9f69ev16effqpLLrnExsgAAABi44ATe0DcsQz8jUoSeyYVe0DicpSr2PPtr9jj3gcAAADiw1//+teQ28866yw98sgjGjFiRIwjAgAAtfXxqg9V7C1WqitVJ7YfbHc4IeX+92tlpniV8vtvKjnR3hhrnNjzer165JFHgj5l7vP59PDDDwc9bty4cZGLDoggsyYVe0zuA4nL2FeVV9aCk4o9xADjJwAAgNjo0KGDfvvtN7vDAAAAB+DGOddp4+4NapWeXWcTe8c/OVEZBVvka5WtbfGS2GvRooVmzZoVtK158+Z67733At8bhsHEFOoua9LOb03sVfIYAImlfMVe0Bp7JPYQHYyfAABAmYkT71VJSYn++c+77Q4lIf3000/auXOn3WEAAABEXY0Te59//nk04wCir7KKPevkPok9IHHtS94Zfr9kmqV/B/Zx7yM6GD8BABB7a9as1ksvPaf583/Qzp071aBBAx12WDddeOFo/eUvnaN23I8++kBvv/2G1q9fJ5/Pp1atWmnYsLN0xhmlrSFvvnl81I6dTPr3719hm8fj0Y4dOzRq1CgbIgIAAIgt1thD8ghK7Fkm9IPa8TG5DySs8sl9P/c+AABAovntt+UaO/ZSDRt2ll544VU1bdpMmzdv0uuvv6zLLx+tp556XgcfHPnk3jffzNHDD0/UXXfd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v19TpkyRY984PiMjQ126dNGECRNsjg4AAByIeWumac7KVzVvzTS7Q6lU5y8/VvNZM1Rv+lS7Qwk/sTdt2jSdccYZkqQ9e/Zo5MiReu2113TjjTfqrbfeiniAQKSYqmSNPT+JPSAplF9nk1aciCHGTwAAAJExd+5c3X///UFr6qWmpuqWW27R999/b3N0AAAA0Rf2TObkyZP16KOPSpJmz54tt9ut999/Xy+++KJeffXVSMcHRI6jksSe9WsHiT0gYVVoxcm9j9hh/AQAABAZBQUFysrKqrA9LS1NxcXFNkQEAAAQW2En9tatW6djjz1WkvTtt9/q5JNPlsvl0iGHHKL169dHPEAgYspX6wS+Zp0tICkYll95pp+KPcQU4ycAAIDI6NChg7788ssK299880116NAh9gEBAADEmCvcH6hXr56Ki4tVr149/fe//9VZZ50lSSoqKpLT6Yx4gEDEWJN21gl9WnECyaFCxR5JfcQO4ycAAIDIuOKKK3Tttddq0KBB8vl8uvvuu/XLL79o8eLFgQ4JAAAAiSzsxN6RRx6pu+66S263W4ZhqFevXpKkN954Q507d454gEDEBFXsWXeQ2AOSgrUqz9zfitM0DO59RB3jJwAAgMg48cQT9eqrr2ry5Mnq1KmTFi5cqA4dOui2225Tt27d7A4PAAAg6sJO7N1666268847tXXrVj3++ONyOp3atm2bnnrqKU2aNCkaMQKREbTG3v5KHYOKPSApmJb72/D7Zfj9pWl91tdDDDB+AgAAqD2/368lS5aoW7duevjhh+0OBwAAwBZhJ/Zat26tZ599NmhbZmamvvrqK6WlpUUsMCDiKqvYs6y3Z5LYAxJXhVac++591tdDDDB+AgCg7jvrrCH6298u0LBhZ9kdCirhcDh0/vnna8GCBbQzBwAASeuAZjM3bNigJ598Urfccktg22+//RaxoICosFbrWCr2rIk9KvaABBbUitO/v3KX+x4xwvgJAIDouuqqyzRx4r0h93366Uc64YQB2rNnd4yjQqQNGTJEkydPlmma1T8YAAAgAYWd2FuwYIFOPvlkffTRR3rvvfckSWvXrtW5556rTz/9NOIBAhFTg4o9WvIBCcwoX7G3L7FHxR5igPETAADRN2TIMH3++Sfau7e4wr4PPnhPgwadoPr1022IDJGUn5+vF154QX369NEZZ5yh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BYiIiIiIiIiIiJSAj483Tr32RsGiJX67rEhNDEGatfv/5u8LJyBXPF9IETJS/9khr9c/UXo/g9nx/QcA758/gp6xCYqXUcxn8L2nL0pa5lfIvrPbyru98CPuGwy0C+ba5+05zZuIQlFh0CpeAhF7Dys0FjbsERERERERERERKYmCRUugSJlyv11OTQ2QZsOF7bwuvfVPRNkjPZ/B7Pr+C/bzgX4BU4V9BwT7+ShkvznBO/QFQmP8kV+3qKJDSVMhTw8YfA+C5Pt3RYfCZ+wRERERERERERERERERKQM27BEREREREREREREREREpATbsERERERERERERERERESkBNuwRERERERERERERERERKQE27BEREREREREREf2fr68vBg0ahCpVqsDBwQHLly+HVCpVdFhEREREAAANRQdARERERERERESUGwiCgNGjR6N8+fK4desWvn//jsGDB6NAgQIYOHCgosMjIiIiYo89IiIiIiIiIiIiAHj58iU8PDwwa9YsGBkZwczMDEOHDsXhw4cVHRoRERERAPbYIyIiIiIiIiIiAgC8efMGxYsXh7GxsWxapUqV4O3tjcjISOjr66drOyJRNgUI4Ju/7+/3DyTezi8FhCzet0+k6vcTEIkADQ11iMUSCMkq0MfHB99+KG5YVmWp/7Tqj9KH9Ze29HwGs+v7DwBCgwMQnxAPXR3dLN5y7tj/7+ouO/cvFUtk/wZ4f0x1GUXXv1Qqlv0/tfN80rTszAFk+xIEfj0QERERERERERFt2rQJ165dw/Hjx2XTPn/+jGbNmuHatWsoWbKkAqMjIiIi4lCcREREREREREREREREREqBDXtEREREREREREQAChQogLCwMLlpoaGhAAATExMFREREREQkjw17REREREREREREAKytrfH161dZYx4AuLu7o3z58tDT01NgZERERESJ2LBHREREREREREQEoGLFirCxscGCBQsQEREBDw8PbN26Fb169VJ0aEREREQAAJEgCIKigyAiIiIiIiIiIsoNAgICMHv2bDx8+BB6enro2bMnRo8ereiwiIiIiACwYY+IiIiIiIiIiIiIiIhIKXAoTiIiIiIiIiIiIiIiIiIlwIY9IiIiIiIiIiIiIiIiIiXAhj0iIiIiIiIiIiIiIiIiJcCGPSIiIiIiIiIiIiIiIiIlwIa9TPL19cWgQYNQ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cmPXW1hYYMiQIbJn1mVFXU+bNg137tzBvXv3ZOv4+vqicePGmDt3Lnr06AFBEDB+/Hj8+++/MDExwbVr19C/f3/ExcXhyJEjcvWwdetW7N27Fz9+/ICtrS1mz56N0NBQjB07Furq6jh79iyCg4MxcuRIfPv2Dd27d0evXr3k9pf0Hli2bBnc3NwQFxcHMzMzDBw4UG4Yi5/r41d1R0RElNWYY8nL6hwrtRwlLX369EmRl/ycz5w4cQLTp0/HhQsXUK5cOdlyjo6OsLW1xerVqwEAO3bswPr166Gjo4ONGzeiSpUqqe4zvcff0dERNWvWRNmyZbF//36EhISgcuXKmDdvnuy4AMD9+/fh4uKCN2/eQE1NDeXLl8fgwYPRvHlz2TI+Pj5YsWIFHjx4gNjYWBQrVgxOTk4YOnSobH9v377FvHnz8OrVKxgaGqJDhw4wNTXF4sWL8erVK2hqav62PomISLkwJ5GXXdd9Xrx4gX379uHJkycIDg6Gjo4OypYti/r166Nnz55yPfRu376NxYsXw9fXF2XKlMH48eNRtWpV9O3bFwEBAVi1ahWeP3+O9evX4/Dhw5g/fz7ev3+P/PnzY/Dgwejbt+8vY3ny5An27duH58+f49u3b1BTU0PJkiXh6OiIwYMHy56HlzyfSsqFUjN69GiMGTMGQOLzGVevXo2nT59CKpWidOnS6NGjh+x6DRGpLjbsERERERERUZ6X1LC3ZMkSRYdCREREuUjSjcm/GlqUiCgncShOIiIiIiIiIiIiIiIiIiXAhj0iIiIiIiIiIiIiIiIiJcChOImIiIiIiIiIiIiIiIiUAHvsERERERERERERERERESkBNuwRERERERERERERERERKQE27BEREREREREREREREREpATbsERERERERERERERERESkBNuwRERERERERERERERERKQENRQeQk4KDf2Tr9rW01BEfL8nWfeQmLK9qY3lVG8ur2ljerGVqapBt21YGzJ+yFsur2lhe1cbyqr68VmbmUNknu/On7KBK739VKgugWuVhWXInVSoLoFrlycqy5OR5OTZgULZuX6fI9mzdvjJgj70sIhLJ/6vqWF7VxvKqNpZXtbG8pEzy2vFjeVUby6vaWF7Vl9fKnNfKS7+mSu8HVSoLoFrlYVlyJ1UqC6Ba5VGlslDWy1M99oiIiIiIiIiIiIiIiCh7SCEoOgSVxx57REREREREREREREREREqAPfaIiIiIiIiIiIiIiIgo06SQKjoElccee0RERERERERERERERERKgD32iIiIiIiIiIiIiIiIKNMEPmMv27HHHhEREREREREREREREZESYI89IiIiIiIiIiIiIiIiyjT22Mt+7LFHREREREREREREREREpAQU3rB3584d1K5dG87Ozr9ddvfu3WjUqBFsbGzQpUsXvH79OgciJCIiypiwsFCcP38WYWGhig6FVJSq5U/8zBAREVF2U7X8iYiIKLeSCEK2/pGCG/b++ecfLFiwAKVLl/7tslevXsWaNWuwePFiPHz4EA0aNMCwYcMQHR2dA5ESERGl36BBfTFgQC8MGtRP0aGQClLF/GngQH5miIiIKPuoYv5ERESUWwnZ/EcKbtjT1tbGsWPH0pVYHT16FJ07d0atWrWgq6uLUaNGQSQS4fr16zkQ6e+FhYXi7NkzvNOciCiP8/b+hDt3bgEA7tz5F58/eysyHFJBqpQ/AfzMEP2MPViJiLKequVPRHkRcyQiov8otGGvb9++MDAwSNeyb968QeXKlWWvRSIRLC0tc81wCAMH9kWfPj14pzkRUR53/vzZX77Oap07t0WDBvZwdKwNR8c6aNu2OWbMmIqgoMAs2f65c6cRFhaWrmUFQcCOHVvRsWNrNG1aH0OG9MOLF89k88PDwzBnznQ0a9YALVo0wpIl8xEXF5vm9q5du4wePTrC0bE2+vTpCjc3V7l9bdmyAW3bNke9eg4YN24kvn71++N9KRNVyp8A4OzZ03Kvc/oz065dc8yePV0hn5m4uFisXLkU7ds3//9npi/c3B6mufyHD+8xbtwING/eAO3bt8CaNSuQkJAgm3/y5DF0794RTZrURZcu7bF//+5k+4rDmjUr0L59CzRtWg8DB/bC/ft3/7iclH3Y65uIKOupWv5ElBcxRyJSHlII2fpHueAZe+kVGhoKY2NjuWlGRkYICQnJ0HZEoqz/+/z5vzvNb9/+Fz4+3tmyn9z2l131mVv/WF7V/mN5VfsvJ8t7/vxpufPO+fOns71szs6TcfPmfdy8eQ87d+5DWFgYli1bmOltS6USuLisQkREWLqWP3x4Py5cOItVq1xw6dIN1K1bD9OmTUR0dCREImDx4r8RGhqKw4dPYN++w/D0/ICNG9elui0Pj7dY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nfXy/AFrqvT4q47PrtLjywN27FUSef1C/1CMV9wYr7gxXnFjvJXvv3xTCmD7qTIxXnFjvOLGeMXvvxYz21BVix17NUdMsQDiioex1LyhQwfh7NnfAAC9AJzB++eqFAD0/P/XAODk5IygoB9rJI8VJa9lU5TKjqVaO/b6r67S4686OadKjy8PuMYeEREREREREREREZFIpaam4NKli9LXv+F/kyUK+F+nHgD8/vsFpKamVGPuiKpWXFwcxowZAwsLC9jZ2WH16tWQSCQlvufFixewtLSEn59fNeWyfNixR0REREREREREREQkUmfO/IqsrCyZbcrKynBz+wLKysoy27OysqRP9hF9GKGKf8qRE0HAlClToKOjg0uXLmH//v345ZdfsHv37hLf5+PjU2iK1NqEHXtERERERERERERERCJ16tRJmdctWrTEqVNnsH379/j113No0aJliemJ5NXdu3cRGRmJRYsWQVtbG61bt8a4ceNw8ODBYt9z6dIlPH78GI6OjtWY0/Jhxx4RERERERERERERkUj980+M9Hc3t0G4cCEUHTpYAwCsrTvi4sVQuLl9IU0TE/OkurNIYiIRqvanHCIiImBgYID69etLtxkbGyMmJgZpaWmF0mdkZMDb2xtLly4t9DRrbcKOPSIiIiIiIiIiIiIikVq82BvOzn3h778T/v47oampJbNfU1ML/v674O+/E87OfbF4sXcN5ZSociUlJUFbW1tmW97rpKSkQum3bNmCjh07wsbGplry96Fqb5cjERERERERERERERFVSPfujujeveRpBRUUFPD554Px+eeDqylXJF7le6qutoiKisKxY8dw8mTtn4qWT+wRERERERERERERERGRqDRo0ADJycky2/Ke1NPV1ZXZvnTpUsyYMaPQ9tqIT+wRERERERERERERERFRhQnlXAevKpmamuLZs2dISkqCjo4OAODOnTto06YN1NXVpeni4+MRHh6OR48eYfXq1QCA9PR0KCoq4sKFCzh27FiN5L847NgjIiIiIiIiIiIiIiIiUWnfvj3MzMzg4+ODJUuW4Pnz5wgICMCkSZMAAM7OzvDx8YGlpSUuXbok896VK1eicePG+PLLL2si6yVixx4RERERERERERERERFVnFB7ntgDgI0bN2Lx4sXo2rUr1NXV4enpCU9PTwDAkydPkJ6eDiUlJTRu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cfbs2Qo1Igs2QufMmYOEhATs27evMrJZJq9fv0aPHj2wbNky9O/fv9rOS0RERJUjLi4OPXr0wN69e2Fra1vT2ZGRnp4OJycnjB8/vlBHZG2SlZWFXr16YfDgwZg4cWJNZ4eIiKjK5a3JV533HypLTbYvhgwZgrZt28LHx6daz0tE4sepOImIqtjQoUOhrq6OwMDADz7G7t270bVrV9y/fx8SiQTXrl3D2bNn4eTkVIk5Ld2WLVtgaGiIfv36Vet5iYiISPzq1auHWbNmYfv27UhKSqrp7BQrKCgISkpKGDlyZE1nhYiIiEpRU+2L3377DTExMZg6dWq1nZOI/js4FScRURVTVVXFli1b4OHhAWtra+k89uUxbNgwvHjxAhMnTsSbN2+gr6+PCRMmwNPTswpyXLQzZ87gzJkzOHz4cJFz6BMRERFV1MCBA3Hr1i3MmTMHO3bs+OB1jqvK7du3sX37duzbtw/16tWr6ewQERFRGVR3+yI2NhZLlizBhg0buIwJEVUJTsVJREREREREREREREREJAf4yAURERERERERERERERGRHGDHHhEREREREREREREREZEcYMceERERERERERERERERkRxgxx4RERERERERERERERGRHGDHHhEREREREREREREREZEcYMceERERERERERERERERkRxgxx4RERERERERERERERGRHGDHHhEREREREREREREREZEcYMceERERERERERERERERkRz4PyndLDwJE5RHAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Visualizations created successfully\n" + ] + } + ], + "source": [ + "# Create comprehensive visualizations\n", + "fig, axes = plt.subplots(2, 3, figsize=(18, 12))\n", + "fig.suptitle('Bayesian Optimization Results with Pretrained Model', fontsize=16, fontweight='bold')\n", + "\n", + "# 1. Optimization progress\n", + "ax1 = axes[0, 0]\n", + "ax1.plot(range(len(eval_scores)), eval_scores, 'o-', alpha=0.7, linewidth=2, markersize=6)\n", + "ax1.set_xlabel('Evaluation Number')\n", + "ax1.set_ylabel('Success Probability')\n", + "ax1.set_title('Optimization Progress')\n", + "ax1.grid(True, alpha=0.3)\n", + "ax1.axhline(y=best_score, color='red', linestyle='--', alpha=0.7, label=f'Best: {best_score:.4f}')\n", + "ax1.legend()\n", + "\n", + "# 2. Parameter distributions\n", + "param_names = ['Concentration', 'Print Speed', 'Gap Size', 'Volume']\n", + "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", + "\n", + "ax2 = axes[0, 1]\n", + "for i, (param_name, color) in enumerate(zip(param_names, colors)):\n", + " # Normalize parameters to [0,1] for comparison\n", + " param_values = eval_params[:, i]\n", + " param_min = bounds[0, i].item()\n", + " param_max = bounds[1, i].item()\n", + " normalized_values = (param_values - param_min) / (param_max - param_min)\n", + " \n", + " ax2.scatter(normalized_values, eval_scores, alpha=0.6, label=param_name, \n", + " color=color, s=30)\n", + "\n", + "ax2.set_xlabel('Normalized Parameter Value')\n", + "ax2.set_ylabel('Success Probability')\n", + "ax2.set_title('Parameter Values vs Success Probability')\n", + "ax2.legend()\n", + "ax2.grid(True, alpha=0.3)\n", + "\n", + "# 3. Score distribution\n", + "ax3 = axes[0, 2]\n", + "ax3.hist(eval_scores, bins=15, alpha=0.7, color='skyblue', edgecolor='black')\n", + "ax3.axvline(x=best_score, color='red', linestyle='--', linewidth=2, label=f'Best: {best_score:.4f}')\n", + "ax3.axvline(x=np.mean(eval_scores), color='green', linestyle='--', linewidth=2, label=f'Mean: {np.mean(eval_scores):.4f}')\n", + "ax3.set_xlabel('Success Probability')\n", + "ax3.set_ylabel('Frequency')\n", + "ax3.set_title('Distribution of Success Probabilities')\n", + "ax3.legend()\n", + "ax3.grid(True, alpha=0.3)\n", + "\n", + "# 4-6. Individual parameter analysis\n", + "for i, param_name in enumerate(param_names[:3]):\n", + " ax = axes[1, i]\n", + " \n", + " # Create scatter plot\n", + " param_values = eval_params[:, i]\n", + " scatter = ax.scatter(param_values, eval_scores, c=eval_scores, cmap='viridis', \n", + " alpha=0.7, s=50, edgecolors='black', linewidth=0.5)\n", + " \n", + " # Highlight best point\n", + " best_idx = np.argmax(eval_scores)\n", + " ax.scatter(param_values[best_idx], eval_scores[best_idx], \n", + " color='red', s=150, marker='*', edgecolors='black', linewidth=2,\n", + " label=f'Best: {param_values[best_idx]:.3f}')\n", + " \n", + " # Get units for each parameter\n", + " units = ['mg/mL', 'mm/s', 'mm', 'μL']\n", + " ax.set_xlabel(f'{param_name} ({units[i]})')\n", + " ax.set_ylabel('Success Probability')\n", + " ax.set_title(f'Success vs {param_name}')\n", + " ax.legend()\n", + " ax.grid(True, alpha=0.3)\n", + " \n", + " # Add colorbar to the last subplot\n", + " if i == 2:\n", + " plt.colorbar(scatter, ax=ax, label='Success Probability')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(\"Visualizations created successfully\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Performing parameter sensitivity analysis...\n", + "Analyzing sensitivity for Concentration...\n", + " Range tested: [20.000, 36.000]\n", + " Score range: [0.9789, 0.9897]\n", + " Sensitivity: 0.000677 (score change per unit parameter change)\n", + "Analyzing sensitivity for Print Speed...\n", + " Range tested: [4.233, 12.233]\n", + " Score range: [0.9645, 0.9897]\n", + " Sensitivity: 0.003155 (score change per unit parameter change)\n", + "Analyzing sensitivity for Gap Size...\n", + " Range tested: [96.225, 156.225]\n", + " Score range: [0.9688, 0.9906]\n", + " Sensitivity: 0.000363 (score change per unit parameter change)\n", + "Analyzing sensitivity for Volume...\n", + " Range tested: [7.965, 12.365]\n", + " Score range: [0.9754, 0.9897]\n", + " Sensitivity: 0.003243 (score change per unit parameter change)\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAPZCAYAAAD+1mNdAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Xd8U2X7x/FP2qSbDlaBMgVBVtlLK8iQoaKCoog/1EcQhAfXAyooUh7FgYDjQREBARcKKA4ERFQQUYaIFkSGyt6re6Ztfn+EnCQdkEIhHd/36+XLcnKSc+fKOSf3uXKf6zbZbDYbIiIiIiIiIiIiIiKSj4+3GyAiIiIiIiIiIiIiUlIpiS4iIiIiIiIiIiIiUggl0UVERERERERERERECqEkuoiIiIiIiIiIiIhIIZREFxEREREREREREREphJLoIiIiIiIiIiIiIiKFUBJdRERERERERERERKQQSqKLiIiIiIiIiIiIiBRCSXQRERERERERERERkUIoiS4iIiIi+UyfPp1GjRrRqFEjDh06dN71x44da6x/IZYsWWI8f+PGjRf0GqXZoUOHjPc/ffr0S749x7bGjh17ybdVHrjuv0uWLPF2czxSEvaBknbcd+vWjUaNGjF48GBvN0VERERKGLO3GyAiIiLFZ/DgwWzatCnfcrPZTOXKlWnbti0PPPAAV111lRda511Hjhzh008/pX379nTo0OGybvu3337jgw8+4LfffuPUqVPk5uYSFhZGw4YNueWWW7j11lsva3s8ERAQQHh4OAC+vr7G8pSUFObNm0fjxo3p0aOHsTw4ONhY/0L4+/sbzzebnV3U3NxcZs6cSbVq1ejfv/8Fv/65/Pnnn/Tr18/49+jRoxk2bNgl2VZJ4Yh1cHDwJXn9VatWMWrUKOPfr776KjfccMMl2VZpZbVa+fzzz/n666/ZsWMHSUlJBAUFUbduXbp27crdd99NaGjoRW/nl19+YcOGDfTr14+aNWsayy/1PlDcLsf3W1hYGKmpqYSEhFxMUwuNuYiIiJReSqKLiIiUUa4JzZSUFI4dO8ZXX33FypUrmTFjBp07d/Ze47zgs88+44033mDUqFGXNYm+ePFinnnmGWw2GwB+fn6YTCZOnTrFqVOn+Pnnn4mLiyM2NvaytckTDzzwAA888EC+5d9++y1vvPEG/fr1c0uiP/PMMzzzzDMXvL0bb7yRG2+8Md/yLVu28Prrr9O+fftLlkT/6quv3P69bNmyMp9Ev9SjfvPG9KuvvlIS3cWxY8cYMWIEf/75p7EsODiY5ORk4uLiiIuL4/333+fNN9+kVatWF7WtN954gw0bNtC+fXu3hG5JGPl9oS7V99tnn31WLO0rLOYiIiJSeqmci4iISBn1/fffs3HjRjZu3EhcXBwvv/wyPj4+WK1WYmNjjaTuxcrOzi6W17nUvv7660vyuud6/2lpabzwwgvYbDaqVKnC4sWL2bZtG1u3buXDDz+kUqVKACxYsIC///77krSvuF2qOHprezabjRUrVgAYSfydO3fyzz//XNLtlmWpqamsWbMGcMZ07dq1JCUlefwapeW8ciEyMzMZPny4kUC///77+emnn9iyZQtbtmzhueeeIzAwkNOnTzNs2DAOHjx4wds6c+YMv/zyS3E1vcS4XN9vF6KsxlxERKS8UxJdRESkHDCbzdxyyy106dIFsJc22bNnj/H4Tz/9xJAhQ4iJiaFFixbccMMN/O9//yM9Pd3tdRz1YocMGcKGDRvo2bMnzZo1M5JjSUlJvPrqq/Tp04eWLVtyzTXXMGTIEH7//Xe313Gtg/vbb7/x0Ucf0a1bN1q2bMmgQYPYtWsXALNmzaJLly40b96ce+65p8Bk0tq1a7nvvvto27Yt0dHR3HLLLSxcuDDftnbv3g3YRwjmrTv9zz//MHr0aK655hqaNWtGt27dmDp1KmlpaW7bcrR5woQJLFu2jC5dutCpU6dC4/7XX38Zr9G9e3eio6MBMJlMtG3blqeeeopbbrmFhx56yK1kCsDSpUsZOHAgrVq1omXLltx55518880354zj119/zc0330zz5s3p2rUr77zzTr427d+/n2eeeYZevXrRokULOnXqxODBg/Mlq/PWRN+4cSONGjVi9erVgH3Epms95bw10T/55BPj3x999FG+dvTu3ZtGjRrRsWNHsrOz89VGdtQIf//99wHYtGmTUat48uTJhdZRPnLkiPHY6NGjC/1sHH799VeOHDkCwPDhw6levTqQfyS1g+s+cPjwYUaOHEnbtm1p3bo1Dz/8MKdPn3ZbPzc3l48//pjbb7+d9u3b07ZtW26//Xa++OKL8yb6+vfvT6NGjWjZsmW+Y9ER/0aNGrFo0SJjW0uWLGHgwIFcffXVtGjRgp49e/L8889z4sSJAt9H3nrYGzdu5MEHH+S6666jefPmdOnShYcffpg//vjjPJF0+vbbb8nIyMBsNvPEE0/g7++P1WrNt/+Cey34t99+m/nz53P11Vdz2223GetkZ2fz3nvvcdttt9GqVSuio6Pp06cP06ZNIzEx0e31Bg8eTKNGjejWrVuh23E99h3r9+rVi8zMTCZNmkRMTAzNmzdn4MCBbN26NV+blyxZwg033EDz5s3p1auX2/nGEwsXLmTnzp0A3HvvvTz55JNUrlwZgMDAQO644w5efvllwH5OnTZtmvHc9957z3gfO3fuZP78+fTq1YvmzZvTs2dPt2Nt7NixdOrUiZycHADuuecet2OmoH3AcRw3a9aMrKwsnnvuOTp06EDbtm159NFHSU5OJi0tjXHjxtG+fXtat27NuHHjyMzMdHuPnn4XFIfzfb+dOXOGyZMnG3Fq3bo1d9xxBx9++KERG4eCaqK7ngtPnDjBggUL6NWrF82aNaNXr158/vnnbvE7V8xFRESk9FI5FxERkXLEkSAESE5OBmDlypU8+uij5Obm4uPjg5+fH//88w9vvvkmW7ZsYf78+fleJyUlhTFjxhAfH28kf7OyshgyZIiRdHKMpFy3bh3r169n/vz5tG/fPt9rLVu2jPfffx+LxYLVauXXX39l1KhR3HfffUybNs1YvnHjRh5//HE+/vhj47mLFi1iwoQJ2Gw2TCYTFouFnTt3MmHCBI4fP87DDz+Mv78/YWFhRrItICDA+A9g+/btDB48mNTUVMBem/vw4cPMnj2buLg43n33XXx83McdHD58mHHjxpGbm3vOesKudXXXr1/P8ePHiYyMNJbddNNN3HTTTfme9+qrrzJz5kzAXo/cx8eH33//nYceeojnn3+e22+/Pd9zVq1axdy5czGbzVitVo4cOcLLL79MtWrVjNHA+/fvp3///qSkpAD28hFJSUls2rSJTZs2MXbsWP71r38V+F7MZjPh4eEkJCQA9rI0QUFBhb7/66+/nokTJ2K1Wvnhhx+46667jMf27dvH3r17AejVq5dbDXQHX19fwsPDSUpKIjc3F7PZTEhICCEhIdx2223MnTvXeN+u5XkcSX6Am2++ucC2uVq2bBkAdevWpVGjRnTv3p0PPviAZcuW8cgjjxT6vMTERO655x6OHTtGTk4ONpuNlStXkpqa6vbj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IlI2KYkuIiJSRoWHhxt/O2oMx8TEcN999xkjbTMzMxkwYECRR9G5vrbDp59+ylNPPVXkdromxlxHQbvW/nVd7kiKuJY0cdS/zcuR/CqM62ukp6fnm7wRKDA5UtD7P5fw8HCGDx/O8OHDyczMZOvWrXz//fd8/PHHpKWlcfLkSd566y2eeuoptzYV1v6C2uQot+DgKAPi6rrrruO5557j1Vdf5cyZM/z111/89ddfLFy4kNDQUF566SW3uxAuVlhYGJ06dWLt2rWsWbOGCRMmsGbNGmMkrqNW+4UICQnh+uuvZ+nSpaxatYoxY8YYI/Tr1q1rJMULk5mZybfffgvY61oXlIA9duwYmzdvpl27dgW+N1eu8Xbso7t37+aOO+4ocL/yVKdOnahRowZHjhxh1apVdO7cmR9//BGw363hemzMmDGD8ePH8+OPPxp3LKxdu5ZXXnmFrl278tprrxlzARTksccew2q1smjRIjIzM9myZQtbtmxhzpw51K1bl1mzZlGnTp1Cn3/8+HE2b94M2EfLFxTT33//ncOHDxvlNlzlPa5c75wo6Ecr13r8ruteKNdtFPR5uk407Chj4pD3+CtMeHi4UU/bMaFtYY4dO2b8XdCPIHnj5dqGi9nnHC703Hyh3wVF4cn3m+v3QkHnbMf+U5R9x5PzrIiIiJRNSqKLiIiUUd9///05J70Ee11nRwI9JiaGKVOmEBERwYYNG85Z+qGgEcuzZ882HpsxYwYxMTFYLBYGDRrkcb3gonB9bwsXLjTqqV/oa4wePZphw4Z59Lyijth2THwK9qRLu3btaNeuHd27dzfKgDhKkri2ad26dUbZneJyxx13cNtttxEXF8eWLVvYvHkz69atIykpiUceeYSvv/6amjVrFtv2evfuzdq1azl8+DD//POPMVK2du3a5010n89tt93G0qVL2bdvH3v27DFKxjhqvJ/LmjVrPEqeffXVVwUm0T3xwQcfGMnMESNG8MADDxAcHMxrr73mUWkUsO9rt956KzNmzGD16tUcO3bMKIeR931GRkYye/ZsTp48ycaNG/n999/54YcfOHDgAKtXr+b55593uwslLz8/P8aPH89jjz3G5s2b+e2331i/fj2///47+/btY/jw4axYsaLQuweWLVvmNnq6IDabjWXLlhV4rOU9rlyTuI6JgV05SvFA/uRm3vIuBT2/qIKCgoy/8/5wd74f7BzMZjPNmjXjt99+4++//8432bArR2miChUqFDiqPO97cv1x7Xzn/kvpcnwXePL95vpDR0Gfj6NETt4fREREREQKoproIiIi5dihQ4eMv/v370/FihUxmUz8+eefxvLzJcXyvladOnXo2rUrFouFrKwsYyJQT1/HU456yOBe2gXsIzhdJ7J05TqRo+tI2bw1hE+dOnXRo1tfffVVbr31Vlq3bl3gqFPXkjiO0Z2Fva+cnByOHz9+UaUQsrOzOXDgAKdPn6Z169YMHTqUmTNnGgldq9VqTAh4PueaENNVjx49jFIUjrrrYJ80tSgK2l7Hjh2NEc2LFi0yym0UpZRLQEAACxYsYOHChW7/OcqXrFy5ssAJYD3hmNAQ4O677zaSfq4lgjw5Lvr374/JZOLkyZPMnz8fsJfRyJvcT0xMZMeOHVSpUoWbbrqJ8ePHs3LlSqMEhWOUeGEyMjLYtWuXUef70UcfZeHChUbCe+/evQXW5nb46quvjLbljefChQupV68ecO5a865CQkKoW7cuYC9hlLem/2+//QbYE8aO13aU2ImPj3e7qyMuLs6jbZ6La4mX7du3uz3mqMftCUc9/5ycHP73v/8VuM63335rlBm54YYbCpyrwPH+HVwngXbEzZVjHoRL7XJ/FxSmadOmxg8+eZP3Bw4cMOYjcMwfcSlcrpiLiIjIpackuoiISDnmOrGdI3m6efNmZsyYYSQ+PZ0EzvFax48f5/jx42RkZDBu3DhjJO7p06dJSkoqtrZ369bNuJV+9uzZxoSjX375JV26dCE6OtqYFNS1hMWWLVuwWq1YrVZq1aplJFBWrlzJunXrAHtCvU+fPrRp04aJEydecBtDQkLYsWMHaWlpjBo1yq1G7/Hjx3n++eeNdR2TArrWCX/ttdc4deoUNpuNOXPm0LlzZ5o1a3bBCcF+/fpx/fXX89hjjxmjeHNyctizZ4+xTqVKlc75Go6Y//HHH6Smpp43qe8o6QLw7rvvGvuAp6VcHNv7559/OH36tNv2TCaTkZBcsGABAC1btnRLdhYkJSXFGBF/zTXX0KZNG1q2bOn2n2OUd3x8vJH4L6q8x5fNZuO9995j/fr1xnJPjq9atWoZCXPH+7zpppvcRoSvXLmS9u3bc+utt7pNTnvy5EljhLKjRnlBTpw4Qdu2bbn55pt59dVXjR+h0tPTjR8D/P39Cx21u2/fPiOx3KNHj3zxbNmypTH55s6dO42E6vnceeedgL2UyvPPP09qairp6elMmzbNaNftt99uTCLrSB5nZ2fzzjvvYLPZ2LNnD3PmzPFoe+fSsGFD4zNdt24dq1evJjc3l2+++cbYnzzRr18/WrRoAcAnn3zCxIkTOXHiBGD/IWPx4sU8/vjjgH0feuyxxwp8nU8//ZRNmzaRm5vL119/bexXtWrVMn6gcz33OUa2F0dN8nO53N8FhYmMjDTmDPjll1/46KOPyM7O5tSpU0yYMME4RgYOHFis2/VGzEVEROTSUxJdRESkHOvcubORFJs/fz4tWrTg7rvvZuDAgTRv3hywJ6g9KY/hSIqmpaXRrVs32rZty7p16/jvf/8L2GtQx8TE8OWXXxZL2ytWrMjYsWMB+4R/119/PS1btjSST126dKF3796APXnoKIuyceNGWrZsyYQJEwCYOHEiQUFBZGVlMWTIEFq0aEHfvn1JSkqiZs2ajBw58oLbeN9999G1a1fAPnK1f//+NG/enJYtW9K5c2cjFu3bt+f//u//AHuibujQoYB99GxMTAwtW7bklVdeAexJRUcCrqgeeughfH192bx5M1dffTVt2rShRYsWvPDCCwB06NDhvKVLHJMN7tmzh3bt2jFkyJDzbtcxCaoj6XnllVe6jbj3ZHsJCQnExMTkm1C1X79+mEwmI+nrySj0b7/91li/R48eBa7TrVs3o7yIpyOn83L9oWDkyJG0bNmSF198kcmTJxs/Uj344IPExsae97Vuu+02gELfZ7du3YzyOI899hjNmzenTZs2dO7cmb1792KxWBg+fHihr1+1alVjItt3333XmJS0devWxvt/4IEHCq0B7RiFDoXH1LXevuv653LPPfcYPzAtWbKEdu3a0aZNGyMp3rx5cx599FFj/TvuuMOI7Ztvvknr1q3p06cPN910k7GOp3dR5OXj48OoUaMAe1L0wQcfJDo6moceesj4gcATFouFGTNmGJ/XRx99xLXXXkvr1q1p1aoV48ePJy0tjTp16jBnzhy3CWxdtWzZksGDBxMdHc0jjzxivK/Ro0cb6zRq1Mj4seWtt96iadOmxT4Bal6X+7vgXCZOnGhMfjpx4kRatmzJNddcY/zgcNdddxW6v14ob8RcRERELj0l0UVERMqxSpUqMWvWLFq2bElQUBCVKlVi9OjRjBkzhv/85z9ERUXh5+dHrVq1zvtao0aN4l//+heRkZH4+fnRoUMHPvzwQ2699VZuuukmAgICCA0NPedo2KIaNGgQb7/9Nu3btzdKZTRs2JAxY8bwxhtvuNVYnjJlCldccQUWi4WIiAjq168P2JNwixYtok+fPlSsWJGcnByioqL4v//7Pz766CO30cRF5UiWTZs2ja5duxqvlZ2dTZUqVYiJieGFF15g/vz5buUaxowZw0svvUR0dDQBAQH4+vrSrFkznn32WSMRdSF69uzJ3Llz6dGjB5GRkWRlZVGhQgWio6OZMGECc+bMOW+994kTJ9K0aVMsFguhoaEF1mrOy7WkCxStlMt//vMf2rdvj7+/PyEhITRp0sTt8aioKONHBbPZnC/JXhBHAtfX19f4kSOvypUrG3X2XZPuRRETE8OkSZOoV68eAQEB1KtXjzfeeIM+ffrwxBNPEBoaSmBgYIGTbObVs2dPY7R148aNufLKK90et1gszJs3j1GjRtGwYUMCAwOxWq3Url2bW265hU8++cRIRhfmqaeeYtKkSbRt25bw8HAyMjKMCRtnzpzJQw89VOhzHYn28PBw2rZtW+A6zZs3p1q1am7rn4/ZbGbmzJmMGzeOJk2aYLFYsFgsNGzYkMcee4wPPvjArVZ5zZo1eeONN2jYsKFxrI8ePZrHHnvMGCGckZHh0bYLcuedd/LUU08Z58Z69eoxZcoU444IT1WuXJmPP/6Yl156iZiYGCpVqkRWVhaVKlWiTZs2TJgwgSVLlhQ4OavDsGHDGD16NFWrVsVisXDllVcybdo0t2Ogfv36PPnkk1SpUgU/Pz9q1KhB9erVL/j9e8Ib3wWFqVatGp9++in33XefcYdKSEgI7dq1Y9q0aRd1p1FhvBFzERERufRMtstVlE5EREREpJglJyfTuXNn0tLS6NGjB2+++aa3m3RJrF+/3pjs96mnnuLee+/1boPEK5YsWcK4ceMAeO+99+jQoYOXWyQiIiJSPpi93QARERERkaKKj48nJSWFF198kbS0NADuv/9+L7eqeOXk5JCUlMTBgweNEbPh4eFGaRcREREREbk8lEQXERERkVLntttuM2qsg71GeJs2bbzYouJ39OhRtzriYC/1ExIS4qUWiYiIiIiUT0qii4iIiEipExERwYkTJ6hYsSI33XQTjzzyiLebVOzMZjPh4eGkpqZSs2ZNHnjgAY1CFxERERHxAtVEFxEREREREREREREphI+3GyAiIiIiIiIiIiIiUlIpiS4iIiIiIiIiIiIiUggl0UVERERERERERERECqEkuoiIiIiIiIiIiIhIIZREFxEREREREREREREphJLoIiIiIiIiIiIiIiKFUBJdRERERERERERERKQQSqKLiIiIiIiIiIiIiBRCSXQRERERERERERERkUIoiS4iZd7WrVt5+OGH6datG82aNaNjx44MHDiQTz/91GttatSoEVOnTi308bFjx3LNNdd4/HrTp0+nUaNGZGZmFkfzzuuff/5h3LhxdO3alWbNmnH11VczePBgPvvss8uy/dKuW7duPPbYY95uhoiIiEiJo7578VmyZAmNGjVy+69Zs2b07NmTZ599ltOnT5/3NS5lv/XYsWNMmjSJXr160bJlS9q0acNNN93Ea6+9dtmuazy1ceNGGjVqxMaNG73dFBHxEpPNZrN5uxEiIpfKxo0buf/+++nTpw933303kZGRJCQk8MUXXzB//nzGjh3Lv/71r8verpMnTxIUFERwcDAA999/P9dffz133XUXAMnJyVitVipWrOjR66WmppKWlkaVKlUA2LJlCw899BA//fRTsbd9zZo1PPLII7Rr147777+fOnXqEB8fz1dffcW7775Lnz59eOWVV4p9u5fTM888Q1hYGGPGjLno18rOzqZt27Z8+umn1K9fH4AzZ85gsVioUKHCRb++iIiISFmhvnvx9t2XLFnCuHHj+PDDD6lTpw4AGRkZ7Ny5k5dffhmbzcbSpUsJDAws9DUupN+6dOlS3n//fRYtWnTO17355pupXr06o0aN4oorriArK4sNGzYwdepUOnTowMyZMz1/s5fYxo0bueeee3jvvffo0KGDt5sjIl5g9nYDREQupY8++ogqVaowZcoUTCYTADVq1KBJkyakpKSwfft2r7TL0WEGyM3NJS4ujuuvv95YVtTkanBwsNGpB/j9998vuo0FOXnyJKNHj+a6667jtddeM2IaFRVFs2bNqF27Nv/973/p378/MTExl6QNl8Pvv/9Oly5dCn3carVisVg8eq2dO3eSnp7utszTCywRERGR8kR990sjIiLC7T3UqlWL0NBQ7rnnHr755htuueWWfM9x9HcvpN/qyfv5+uuvOXnyJAsWLKB27drG8vr162M2m/n88885c+aM+s0iUmKonIuIlGlWq5WcnBysVmu+x55//vl8t2V+8MEH9OnTxyhRMmHCBJKTk43HR48ezS233MLmzZvp168f0dHRdOvWzW2Uhc1mY+bMmfTq1Yvo6Gg6duzIqFGjOHjwoLGO45bQQ4cO0bhxY1JSUpg4cSKNGjUC3G8JHTRoEAMGDMjX/nfeeYfGjRtz/Phxt1tCx44dy+TJkzl16hSNGjXi2WefpXnz5kyfPj3fawwZMoT+/fsDzts9//nnn0LjuWjRItLT0xk3bpxxYeNq4MCBfPfdd24J9NWrV3PHHXcQHR1Ny5YtGTRoEBs2bDAe/+mnn2jUqBGbNm1izJgxtGnThg4dOjB69GhSUlKM9bKysnj55Zfp3Lkz0dHR9O3bl6+++spt+z///DMDBw6kRYsWtG7dmmHDhrm9n0WLFtGoUSP++usvhg8fTsuWLbnmmmv473//S3Z2tvHZ7N69m9mzZ9OoUSMOHTrE9OnTadu2rfHeHn30UQDS09OZNGkS3bp1o3nz5nTu3JmnnnqK+Ph4I6a33XYbADfccAODBw8G8t8Wm5ycTGxsLDExMTRr1owuXbrwwgsvuCXfBw4cyMiRI1m1ahV9+vShefPm9OnTh++++67Qz0tERESkNFHfvXj77ufiaPvRo0cBCu3vuvZb9+3bR6NGjVi+fDmTJk2iQ4cOtGnThmHDhnH8+HEABg8ezAcffEBcXByNGjViyZIlBW7f8RlnZGTke+zOO+/ko48+MhLoo0ePpm/fvqxbt46+ffvSrFkzunfvnq+U5IEDB3jooYdo164dzZs3p3///qxevdptnbS0NCZNmkTnzp1p1qwZ119/PbNmzcK1SENKSgpPPPEEbdq0oXXr1jzyyCNG/15Eyi8l0UWkTLvuuus4ceIEgwYNYtWqVW6d6rzefvttnn/+eW655RaWLl3Kiy++yNq1a/n3v/9trGOxWDhz5gyvv/4648eP56uvvqJVq1ZMnDjR6Gh/8sknvP322zzxxBN8/fXXzJo1i5SUFIYNG5Zvm9WrV2fx4sUAjBkzhnXr1uVbp2/fvmzdupXDhw+7LV++fDkdO3YkMjLSbfnTTz9Nnz59qFixIuvWrWP06NH07NmTzz77zK1zGB8fz4YNG7j99tsBe5J33bp11K1bt9AYbdy4kauuuopq1aoV+LiPjw81a9Y0/r1+/XpGjBhBw4YNWbRoEQsWLKBq1aoMHTqUnTt3AmA222+Keumll+jQoQOff/45zzzzDMuWLWP+/PnGa02YMIEvvviC2NhYvvrqK26++WbGjBljJJE3b97M0KFDqVGjBosWLWL+/Pmkp6fzf//3f5w5c8ZtW7GxsfTr14+vvvqKIUOGsGDBApYuXQpgfAb/93//x7p166hevToAOTk5fPDBB8ycOZOJEycCMGnSJL744gsmTpzIypUrmTZtGhs2bGDChAlGTJ966ikAPvzwwwIvhgBGjBjBd999R2xsLMuWLeOJJ57gs88+Y+zYscY6FouFv//+m0WLFjF16lQ+//xzqlSpwhNPPOH2Y4OIiIhIaaW+e/H23c/lwIEDAG79+oL6u64cfekZM2YQGRnJ4sWLef3119m0aROvvfYaYE/Gt2rViqZNm7Ju3TpuuOGGArd/zTXXYLFYuP/++/n44485duxYoW01m80cO3aMd955h+eff54vvviCNm3aMG7cOLZu3QpAQkICgwYN4sCBA7z11lt8/vnntG7dmpEjR7oN4HnooYdYunQpY8eOZdmyZQwZMoTp06fz5ptvGus899xzrFq1iokTJ7JkyRKuvvpqpkyZ4llgRaTMUhJdRMq0AQMG8OCDD7J7925GjRpF+/btufXWW5kyZQp79+411rNarcyePZsbbriBBx98kHr16tGlSxeefvppNm7cSFxcnLHuiRMneOaZZ2jTpg21a9fmX//6Fzk5Ofzxxx8AbN++nerVq9O9e3dq1KhBdHQ0r776KlOmTCE3N9etfb6+vsYIi5CQELfbLB169+6NxWJh5cqVxrKDBw/yxx9/FHjrZYUKFQgICMDHx4cqVaoQHBzMXXfdxeHDh906kN988w1ms5m+ffsCEBAQQJUqVfD19S00nidOnDCSyp6YM2cONWvW5LnnnuOqq66iSZMmvPTSSwQGBvL++++7rduhQwcGDBhArVq1uOmmm7jyyiuNTvGJEyf44osvGDVqFN27d6d27do88MAD3HvvvZw8eRKAWbNmERkZyeTJk2nUqBHR0dG88sorJCcn88knn7ht64YbbqB3797UrFmTf/3rXwQHBxvbcnwGgYGBbvFIS0vjnnvuoVmzZsY6o0ePZunSpXTu3JkaNWrQrl07+vTpw7p167DZbAQEBBi390ZERBAeHp4vRr///ju//PILY8aM4frrr6dOnTrceOONDB06lJUrVxqjg8A++dLkyZNp2rQp9evX5+677yYlJYU9e/Z4/JmIiIiIlFTquxdv370gNpuNv//+m+eee46qVavSo0cP47GC+rsFueKKK3jggQeoXbs2MTExdOzY0ehLh4eHY7FYMJvNVKlShYCAgAJfo0GDBkyZMgWbzUZsbCxdunShe/fuxmfoymQykZSUxJNPPkl0dDT169fnueeeIzAwkC+//BKw/xhy6tQpXnnlFdq2bUv9+vV5+umnadiwIXPmzAHsk9auW7eO//znP9xwww3UqVOHgQMHcueddzJ//nyysrJIT09n+fLl3HXXXfTt25e6dety55130rVr1yLFWUTKHiXRRaTMe+yxx1i3bh2vvvoqAwYMID09nTlz5nDjjTeyYMECAPbs2UNycnK+SWI6deoE2Cf7cQgKCqJhw4bGv8PCwgBITEwEoGvXruzfv597772XTz75hCNHjhAREUGzZs3w8Sn6aTciIoKYmBi3jvjy5csJCgqiZ8+eHr1G27ZtufLKK/n000/dXqN3795FquFosVgoynzU27Zto23btm6lXwICArjqqqvYsWOH27otW7Z0+3dYWJgR0z/++IPc3FyaNm3qts64ceMYOHAgAHFxcbRu3dqtVnmVKlW48sor3T6/vNsymUxu2zqXZs2auf3barXy2muv0b17d9q0aUOrVq149913SUtLIysr67yvBxgXHG3btnVb3qpVK2w2m1uc6tSp41YXMu++JyIiIlLaqe9efH13h9tuu41WrVrRqlUrmjdvzi233EJERAQffPABISEhbuvm7e8W5Fz99qLo06cPa9as4Z133uGBBx6gYsWKLFmyhHvuuYcxY8a4XXcEBwdz1VVXGf/29/enXr16xo8rcXFxVKlShfr16xvrmEwmOnbsaOwPjh9XCtpvkpOT+euvv9i/fz9ZWVn54tC6desivz8RKVs0saiIlAuhoaHccMMNxu2Ef/zxB0888QQvvPACvXr1Mm4VnTRpEi+++GK+5ztGO4O9I+7KkSB2dPK6dOnCe++9x/vvv89LL71EcnIyTZs25ZlnnqFVq1YX1P6+ffsyevRojh49SvXq1Vm+fDk9e/bM15ZzufPOO5k6dSrJyclkZmbyyy+/8N577xWpHVWr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ESNG8MYbbzBkyBBmzJhR4DpBQUFMnTqVF198kZtvvpm6devy6KOP0rp1a3777TfGjBnDK6+8QkxMDO3bt+e1117jm2++4eOPP873Wm+88QZTp04lNjaW+Ph4IiMjGTx4MKNGjSpSu0VERERKM/Xdi7/v7g333HMPcXFxDBkyhIceeoh777033zq9e/emSpUqfPDBB4wfP55Tp07h4+NDrVq16N27N0OHDnUr6xMeHs4DDzzAhAkT2L9/P9WrV2fatGnGZKPh4eEsWLCAqVOnMnz4cDIyMqhRowb33HMPw4YNM15n+vTpvPrqqzz77LOcOXOGiIgIunTpwujRo416+c8++yyxsbE89dRTAFx77bVMnDiR++67j+zs7EsZOhEpwUw2zUYmIlJubN++nf79+7NgwQLatGnj7eaIiIiIiEgh1He3Gzt2LJs2beL777/3dlNEpBzTSHQRkXLg9OnT7N27l6eeeoqePXuW6064iIiIiEhJpr67iEjJoyS6iEg58NRTT7Fhwwa6devGc8895+3miIiIiIhIIdR3FxEpeVTORURERERERERERESkEIVPDy0iIiIiIiIiIiIiUs4piS4iIiIiIiIiIiIiUggl0UVERERERERERERECqEkuoiIiIiIiIiIiIhIIczebkBpcfJksle26+fnS1ZWjle2XRopXp5TrDynWBWN4uU5xapoFC/PKVae82asqlSp4JXtlgVF7ZvrmCgaxctzipXnFKuiUbw8p1gVjeLlOcXKc6U9Vp70yzUSvQQzmdz/L+emeHlOsfKcYlU0ipfnFKuiUbw8p1h5TrEqH/Q5F43i5TnFynOKVdEoXp5TrIpG8fKcYuW58hIrJdFFRERERERERERERAqhJLqIiIiIiIiIiIiISCG8mkQ/dOgQQ4YMoWXLlnTq1IkpU6aQm5ubb72srCymTZtG165dadmyJffccw8HDx40Hj9x4gSPPfYYnTp1ok2bNowbN46MjIwib6ekCZzyIgFPPUnglBe93RQRERERERERKSWCprxI8DPjCFI+QUSkWHgtiW6z2Rg1ahQRERH88MMPfPjhh6xYsYL58+fnW3fWrFksW7aMOXPmsGHDBho2bMjIkSONRPiYMWNISUlh+fLlrFy5kn379jF58uQib6ekCXj/XfxnvEHA++96uykiIiIiIiIiUkoEfPAuQW+/ScAH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i2cjckRERDylkegi4qp58+YcOXKE+Ph4IiIiANi6dSsNGjQgODjYbd3t27eTlJREp06dAPukpAkJCbRu3dpYJzfXWTvbZrORk5ODj48PO3fu5PPPP2fs2LHG43v27MHPz4+qVasWqc1FvUiz2cr26KjiZrPln1RU8SuY9i3PKVZFczHxSpnyKmRkQEBAuYi59q2iUbw8p1h5rqzHShOLlmDWnn3IvrU/1p59vN0UEREpYzQSXURcNW7cmOjoaCZNmkRSUhK7du1i1qxZ3H333QD07t2bzZs3A7Br1y7GjBnDgQMHSExMZOrUqfTq1YuaNWsSHBxMu3btmDlzJmfOnCErK4vZs2djNptp06YNERERLFy4kFmzZpGVlcW+fft4/fXXueuuu/D1LbvlBkqrrVs1qahIaZXVsw9ZN/cjS/kEEZFioSS6iIhIOeSaRNdIdBEBeP3110lOTubaa6/lX//6FwMHDmTQoEEA7N27l7S0NAD69etH3759GTBgAN26daNatWq88MILxutMmzaNsLAwbrrpJjp16sSaNWt4++23qVSpEpGRkcyaNYvvvvuODh06cP/993PdddcxZswYr7xnObe4OOcPGy1alN1yECIiIiLnY7LZyvJA++Jz8mTyZd+mYwIQq1W3TnpC8fKcYuU5xapoFC/PeTtW8fHQqFEFALp1y+bjj9MvfyOKwNvxKk0UK895O1ZVqlS4/BstI4rSN/f251zauMYrJiaIXbt8sVhs7NmTgr+/t1tXsmjf8pxiVTSKl+cUq6JRvDynWHmuLMTKk365aqKXYL5xv+Gbm0Oujy/Z0a283RwRESlDQkPBZLJhs5lUzkVERPJJTYW//rLfuNy4ca4S6CKljDnuN8jKAj8/slsonyAicrGURC/BQgffhe/RI+RUr8GZuJ3ebo6IiJQhvr4QFgYJCSrnIiIi+R086ENurv374aqrVA9dpLQJvUf5BBGR4qSa6CIiIuWUoy66RqKLiEheqanOv8PCSum92SIiIiLFREl0ERGRcioiwpFEh1wNMhQRERdpac4fWIOClEQXERGR8k1JdBERkXLKMRLdZjORfPnnzxYRkRIsLc35d1CQ99ohIiIiUhIoiS4iIlJOOUaig+qii4iIO41EFxEREXFSEl1ERKSccoxEB9VFFxERd6mprkl0LzZEREREpARQEl1ERKScck2iayS6iIi4ci/nopHoIiIiUr4piS4iIlJOuZZz0Uh0ERFx5V7OxYsNERERESkBlEQXEREppzQSXURECqOR6CIiIiJOSqKLiIiUUxqJLiIihdHEoiIiIiJOZm83QAqX8PMvmH19yM7J9XZTRESkDAoLc/6tkegiIuLKfSS699ohIhcm/qdfwGYDk/p4IiLFQUn0EswWUgEsvtisOaDBHyIiUsw0El1ERAqjkegipZstpIK3myAiUqaonIuIiEg55VoTXUl0ERFxpZHoIiIiIk5KoouIiJRT7hOLerEhIiJS4riORA8O1kh0ERERKd9UzqUEC3jrDcypKfgGh5D+4ChvN0dERMoYPz97YiQ11aSR6CIi4sY1iR4Y6MWGiMgFCXzrDUzJSdgqhJI+QvkEEZGLpSR6CRb41hv4Hj1CTvUaSqKLiMglERFhT6JrYlEREXHlKOcSEGDD19e7bRGRoguc6ZJPUBJdROSiqZyLiIhIOeYo6ZKQYMKmu/VFROSs1FT7j6uaVFRERERESXQREZFyLSLCnhyxWk2kpnq5MSIiUmI4RqJrUlERERERJdFFRETKNdfJRVUXXUREHBw10TUSXURERERJdBERkXLNNYmuuugiIgJgs2kkuoiIiIgrJdFFRETKMUc5F9BIdBERscvMhNxcjUQXERERcVASXUREpBxTORcREcnLMQodNBJdREREBJREFxERKdciIpx/q5yLiIgApKY6vw80El1ERERESXQREZFyTSPRRUQkr9RU598aiS4iIiICZm83QAqXHd0CW82a5FSs5O2miIhIGeVaE10j0UVEBCAtTSPRRUq77OgW5NaIIrdyZW83RUSkTFASvQRL/mAhFosvVmsOqO8qIiKXgPtIdO+1Q0RESg73kei6EBEpjZLeX+jtJoiIlCkq5yIiIlKOaSS6iIjkpYlFRURERNwpiS4iIlKOuY5ET0xUEl1ERFTORURERCQvJdFFRETKscBACAiwJ0g0El1EREATi4qIiIjkpZroJViF/7sT3zOnyalYSfXMRETkkgkPt3HsmImEBCXRRUREI9FFyoLQwXfic+oUuZUrK58gIlIMlEQvwcxb4/A9egRT9RreboqIiJRhERE2jh1DSXQREQFUE12kLHDkE3KUTxARKRYq5yIiIlLOOeqip6WZyMjwcmNERMTrUlM1El1ERETElZLoIiIi5ZwmFxUREVcaiS4iIiLiTkl0ERGRci4iwplE1+SiIiLimkQPDtZIdBEREREl0UVERMq5sDDn36qLLiIiKuciIiIi4k5JdBERkXJOI9FFRMSVyrmIiIiIuFMSXUREpJxzrYmekOC9doiISMmQlqaR6CIiIiKulEQXEREp5zQSXUREXGkkuoiIiIg7JdFFRETKOfeR6Eqii4iUd46a6BaLDYvFy40RERERKQHMRX2CzWbDZNIF9uWQPmIU5tQUsoNDvN0UEREpwzQSXaR0Ur9cLhXHSHSNQhcpvdIfHIUpOQlbhVBvN0VEpEwochL9uuuu47bbbuO2224jKirqUrRJzsoYMQqLxRerNQdUilBERC4RjUQXKZ3UL5dLxZlE10WISGmVPmKUt5sgIlKmFLmcy4ABA/j666+5/vrrGTJkCN988w3Z2dmXom0iIiJyGWgkukjppH65XCqOci4aiS4iIiJiZ7LZbBc0vGD79u0sXbqUFStWkJ2dTb9+/bj99tupW7duMTexZDh5Mvmyb9NkwhiJfmGfUvmieHlOsfKcYlU0ipfnSlKsbDaoUSOEnBwT0dE5fPtt2vmfdJmVpHiVdIqV57wdqypVKhTL65S3fjkUrW/u7c+5tDGZoHr1ELKzTTRvnsN335W874SSQvuW5xSrolG8PKdYFY3i5TnFynNlIVae9MsvOInuYLPZWL58Oc8++yxJSUlcffXVPPLII0RHR1/My5Y43kii+6QmY/b1ITsnl9zg4rnIKsvKwkF7uShWnlOsikbx8lxJi1WTJsGcOuVD7dq5bN6c6u3m5FPS4lWSKVae83asiiuJ7lBe+uWgJPqlZLVCVJR93+zYMZsvv0z3cotKLu1bnlOsiqY44mVKSbaPlDCZsIWU3XyC9q2iUbw8p1h5rizEypN+eZHLuThkZ2ezYsUKhg0bxpNPPkmlSpUYO3YsTZo04d577+Xzzz8/72scOnSIIUOG0LJlSzp16sSUKVPIzc3Nt15WVhbTpk2ja9eutGzZknvuuYeDBw8aj585c4YxY8bQqVMn2rZty+DBg9m+fbvba8yYMYOYmBhatWrFfffd5/b8kir86naE1a5O+NXtvN0UEREp4xx10VXORaT0KY5+uYhDmsvAc5VzESm9Iq5pR+X6NYm4RvkEEZHiUOSJRf/55x8++eQTvvjiC5KTk+nZsyfz5s2jXTvniblt27aMHz+eW2+9tdDXsdlsjBo1igYNGvDDDz9w+vRphg4dSqVKlbj//vvd1p01axbLli1jzpw5REVFMXXqVEaOHMkXX3yBj48PEydOJDU1lRUrVhAUFMSbb77JsGHDWLt2Lb6+vnz44YesWbOGRYsWERwczEsvvcT8+fN55plnivr2RUREyqTwcPv/k5NNWK1gsXi1OSLigeLql4u4Sktz/piqiUVFRERE7IqcRL/xxhupU6cOQ4YMoX///kRERORbp0uXLqSkpJzzdbZt28auXbuYP38+YWFhhIWFMWzYMObNm5cvib569WoGDBhA/fr1AXj88cdp06YNcXFxtGrVih07dnDfffcRfjYD0LdvX2bOnMnJkyepVq0ac+fO5dVXX6VGjRoAvPjii0V92yIiImWa6+SiiYkmKldW4kSkpCuufrmIK41EFxEREcmvyOVc5s6dy8qVKxkyZIhbRz09PZ3ly5cb//7tt9/O+Tp//vknUVFRRuIboEmTJuzbty9fR99ms+Faut1iseDv78+OHTsAuO6661ixYgUnT54kIyODzz77jCZNmhAZGcmxY8c4duwY+/fvp2fPnnTo0IFHH32U+Pj4or51ERGRMstRzgUgIcF77RARzxVXv1zEVWqqRqKLiIiI5FXkkegjRowgLi4u3/KMjAxiY2O54YYbPHqd+Ph4wsLC3JY5/h0fH09ISIixvEuXLixevJgePXpQo0YN5s2bh9VqJTExEbCPTB8xYgQxMTEA1KhRgzlz5mAymTh+/Dgmk4lvv/2WhQsXkpGRwcMPP8yECROYPn16kd67yYtlYr257dLCESPF6vwUK88pVkWjeHmupMXKdSR6QoIJk6lkJU5KWrxKMsXKc6U9VsXVLxdx5V7OxYsNERERESlBPE6iL168mE8++YSsrCwGDhyY7/ETJ04QdIl6WcOHDychIYH77rsPs9nM4MGDqV27NmazvfkTJ04EYO3atYSGhjJv3jyGDBnCsmXLsFqtWK1WxowZY4zQefjhh3nggQfIzMzE39/fozb4+flekvd2Lq4XdhbL5d9+aWMyga+vLyYTpXY24MtFsfKcYlU0ipfnSlqsKlVy/p2c7IvFUrKyiiUtXiWZYuW50horb/bLpexzL+dSig4MERERkUvI4yR6586dycjIYNu2bdSrVy/f440bNy7ShEWVKlUi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/DIhISHMnz8fs9lMmzZtqFSp0jm3U565JtFTUryVRHduNyhISXSR0swWHExuiPIJIiLFxaMker9+/dw664WxWq0X3SBxSlj/KxaLr71+mfqwIiJyiWliUZGS71L2y19//XUmTJjAtddeS3BwMIMGDWLQoEEA7N27l7Szw5T79evH7t27GTBgANnZ2XTv3p0JEyYYrzNt2jQmT57MTTfdRGZmJo0aNeLtt9+mUqVK591OeVahgvPvlBTvtCE11fl3UJB32iAixSP+51+93QQRkTLFoyS6p7eDLly48KIaIyIiIt7jPrGoFxsiIoW6lP3yatWqMWvWrAIf27Vrl/G3yWRi7NixjB07tsB1q1atyrRp0y5oO+WZa01075Vz0Uh0ERERkYIUeWJRsNda3L17N5mZmcayo0ePMnnyZO68885ia5yIiIhcPn5+9hq4qakmjUQXKSXULy87SlpNdI1EFxEREXEqchL9p59+YtSoUaSnp2MymbCdnXbVZDJx0003FXsDRURE5PKJiLAn0TWxqEjJp3552eJezkUj0UVERERKkiIn0V955RXuuecebr31Vm6++WaWL1/Otm3b+OKLL3j66acvRRvLraCJ4zEnJWIJDSM1dpK3myMiIuVAeLiNQ4fsNdFtNvCg9LKIeIn65WWL+0h077QhNdU1ie6dNohI8QieOB5TYgK2sHBSJyqfICJysYqcRN+3bx+LFi3C19cXk8lErVq1qFWrFhEREcTGxvL6669finaWS/5LPsH36BF8q9dQEl1ERC6LiAh7EsdqNZGaCiEhXm6QiBRK/fKyxbUmuvdGojv/1kh0kdLN/zN7PiGneg0l0UVEioFPUZ9gNpvJyMgAIDg4mJSzU8d37NiRn3/+uXhbJyIiIpeV6+SiiYkahi5SkqlfXra4/mipiUVFRERESpYiJ9FbtmzJmDFjyMjIoF69erz22mucPn2aFStWYLFYLkUbRURE5DJxTaKrLrpIyaZ+edniPhLdO23QxKIiIiIiBStyEn3cuHEcOXIEgOHDh7No0SJiYmIYPXo0gwYNKvYGioiIyOXjKOcC9rroIlJyqV9etvj7g5+f/RxcEkaiBwdrJLqIiIiIQ5FrotetW5cvvvgCgC5durBixQr++OMPoqKiaNasWbE3UERERC4fjUQXKT3ULy97KlSwcfq0yYtJdOffGokuIiIi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SpUqBAQEWO178OBB4uLiaNKkCZB2UdKYmBjq1q1r2cdsNrNz504mT55s9dgjR46wfv16xo4da9l2/PhxfHx8KF26tF1jtneRZjYXrOqoTB10iIszULJk3gw+MTHjtVKkiNkq8VaQ4uVMipXtFCv7OCpehSHmem3ZR/GynWJlO3ePld1JdIBz585RtWpVWrRoAcDRo0c5ffo0FStWdOjgRERERFyZtzesXn2NTz7xtvQUdjaDATw8PDCZTC41iV22zIfoaAOXL6vowpEcNS+vXr06tWrVYtKkSYSHh3Pu3DkiIiIYMmQIAB06dGDSpEnUr1+fyMhIZs6cyapVqwgODmbGjBm0b9+eChUqWI538eJFLl++TJUqVazOExISwurVqylevDh9+/bl7NmzzJ49m549e960Yr2wCQrKeAMnJEDJknlznvSLikJaT3QRERERycruJPru3bsZPHgwkydP5pFHHgHgp59+4u2332bBggXqiy4iIiKFSpkyZoYNS3b2MCzSe5+mt2RwFd9/70V0tCfR0QaMRlC+9PY5el4+e/Zsxo8fT/PmzQkICKBXr1706tULgBMnTnD1f38p6tq1K3/99Rfdu3cnNTWVtm3bMn78eKtjXbx4EcBS1Z4uNDSUiIgIZsyYwfz58wkJCaFjx44MGzbslmLgzgIDM97A8fEGIG/e0Jn/AFikiAt9aIiIiIi4ELuT6G+//TYjRoygY8eOlm19+vTB29ubGTNmsHbtWocOUEREREQKvvRWFGazgStXDJQqpWTd7XL0vLxMmTJERERke19kZKTltsFgYOzYsVYtWW4UFhZm9ZjMGjRowOrVq+0aW2GUfmFRSLu4aF5RJbqIiIjIzdmdRD969Ch9+vTJ0mfxiSeeYPr06Q4bmMD13k/jlRBPamDQzXcWERERcWElSmQkzaOilER3BM3L3Zt1JXrenUeV6CLu6fp/nsYQF4e5aFFnD0VExC3YnUQPCgrixIkTVK5c2Wr7kSNHslx0SG7PtdGvWL6OnUff3hQRERHJFyVKmCy31RfdMTQvd2/WPdHzqxJdiw4Rd3F19CvOHoKIiFuxO4neuXNnBg8ezFNPPUWFChUwmUz8/fffrFq1ii5duuTBEEVERESkoMtcia4kumNoXu7eMifR03qi543ExIzbauciIiIikj27k+gjRozA39+fiIgIrly5AkDx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Y8YM2rdvT4UKFSzHu3jxIpcvX6ZKlSp2nUdyFhSU9l5JSLj998rVqxm3dWFRERERkZtTEl1EREREnMJgyGjpoiS6882ePZv4+HiaN29Ov3796NGjB7169QLgxIkTXP1f5rVr16506tSJ7t2706ZNG8qUKcOUKVOsjnXx4kUAS7W5reeRnAUFpf03IQFMpts71tWr1pXoIiIiIpI7tXMREREREacpWdLMmTNw5YoBkwk8VOLhNGXKlCEiIiLb+yIjIy23DQYDY8eOZezYsTkeKywszOoxtp5HchYYmJbsNpsNXL0KgYG3fixVoouIiIjYR8sUEREREXGa9Ep0o9HADdebFJFM0pPoAPHxt/fNDVWii4iIiNhHSXQRERERcZr0JDrA5cuamorkJL2dC9x+X3RVoouIiIjYRysVEREREXEa6yS6+qKL5CT9wqIA8fG3dyxVoouIiIjYRz3RXVjcui/wwkSq/tYhIiIibqpkyYwEni4uKpKzvGvncluHEhEXFfvpF5CaCl5K+4iIOII+TV2Ysco9eHh7YkwxggpERERExA2VLGmy3FYSXSRnmS8kevtJ9IzbqkQXcU/GKvc4ewgiIm5FJc4iIiIi4jRq5yJim8ztXBISbu9YiYlq5yIiIiJiDyXRRURERMRpMrdzURJdJGfWSXRdWFREREQkP6mdiwvz/fQTPJOT8PDx5Xq3J5w9HBERERGHy1yJrnYuIjnLu57oqkQXcUe+n36C4do1zP7+JD2mfIKIyO1SEt2FFXl9PJ7nzmIsW05JdBEREXFLqkQXsU1QUMbt+PjbO1bmSvSAgNs7loi4poA3MvIJSqKLiNw+tXMREREREacJCABf37REuirRRXLmyHYu6okuIiIiYh8l0UVERETEaQyGjGp0VaKL5Czv2rnc1qFERERECgUl0UVERETEqdL7ol++bMBkcvJgRFxUYGDGbUddWNTX14yn520dSkRERKRQUBJdRERERJwqPYluNBqIjXXyYERclHU7l9s7VnolekCAWrmIiIiI2EJJdBERERFxKl1cVOTm/PzAyyvtvXL77VzS/qtWLiIiIiK2URJdRERERJwqvRIdICpK01OR7BgMEBSUdttRPdF1UVERERER22iVIiIiIiJOlbkSPSpKlegiOUm/uGh8/K0fw2xWJbqIiIiIvZREFxERERGnylyJrnYuIjlL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eRGcOHGcvXt/4fff9zJt2iQWL17AvHkRlCpVOtfHnzlzmoCAAIoWDbZsq1s37Stp27dvy/U+o9HIww+3tjqe0WgkJiaGrl27s2PH93Tp8jBNmzbjgQda0KpVWwwGA//5T19GjRpGt26P0LRpM1q1akvjxk1zHOPhwwdp0yb9fgOhoaE88URPunfvYdknMDCIgICMcrxz585QsWJFy8+enp6UKVOOc+fOWrZVrHiH5XZoaBkALl26SJkyZfn66018+uknXLx4AZPJREpKCg880MKyf5kyZawmU6GhZfj33xM5Poebad26LbNmTef3338jLKw233+/lX79nr3l44m4gxIlTJbbUVFKootkx7on+u1XogcEqBJdREQcyxlr6vTzutO6ukmTB2jWrCWtWxeMdXXFihX46ivnrKv37fud2rXr5vm6Wkl0EZFMhg5N5s0387cS/fnnk+1+3N13V+Luuyvx+OM9uHw5imeffZpPP/2EwYOHZtnXfMPgc3suOd3n4WHA39+fb7/dkeNjP/hgNb/99is//7yLt96awtat3zJp0nTuuacqn3yygd27d7F7905ee200Xbt2Z+jQEdkeJ/MFUHKScZXxDDf+Ff7G520wZO5glnafj48vv/22h9mzZzJx4ps88EALvLy8ePbZPrk8Nu3xtlQo5KRIkQCaN2/F119vJjg4hEuXLtCsWYubP1DEjWXuia5KdJHsOb4nupLoIiLiWNmtqfOau62r9+79lZ9+2slbb01h27aCsa7es+dXp62rv/lmM8WLl8jzdbVTk+inT58mPDycvXv34u/vT7du3XjppZeyXA02OTmZuXPnsnHjRqKjo6lVqxaTJ0+2/HXkypUrTJkyhZ07d5KSkkL16tUZO3Ys9913n13nERHp1CnV6q/XBgN4e3uSkmLMs0S6rS5evMAHHyxl4MDnKVq0qGV7iRIlueeee4mOvgKAr68vKSmpVo9LV65cea5eTeTy5ShKlCgJpP01PDAwMNf7ypevwLVr1zh79gzlypUH0r5OlpKSQnBwMa5du4aHh4FGjZrQrFkzWrZsy5AhzxAXF4fBkDZRaNmyNS1btqZ27bq8/fa0HH/Z34ry5Stw8uS/lp+NRiPnz5+lfPkKlm1nzpy23L5w4TwApUqVYu/eX7jjjjtp2bINAElJ1zl16hSNG2O1f2pqquVrbhcunL9p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iIh9CsuFRUVE8ouS6C7Ma/8+PM+dxVC2nLOHIiIiIiIiIiIFRHo+wah8goiIQ6idi4iIiIiIiIiIiIhIDpREFxERERERERERERHJgZLoIiIiIiIiIiIiIiI5UBJdRERERERERERERCQHSqKLiIiIiIiIiIiIiORASXQRERERERERERERkRwoiS4iIiIiIiIiIiIikgMl0UVEREREREREREREcmAwm81mZw9CRERERERERERERMQVqRJdRERERERERERERCQHSqKLiIiIiIiIiIiIiORASXQRERERERERERERkRwoiS4iIiIiIiIiIiIikgMl0UVEREREREREREREcqAkuoiIiIiIiIiIiIhIDpREFxERERERERERERHJgZLoIiIiIiIiIiIiIiI5UBJdRERERERERERERCQHSqKLiIiIiIiIiIiIiORASXQXcfr0aZ577jkaNmxIkyZNGDNmDLGxsQD89NNP/N///R9hYWE8+OCDbNiwwcmjdb7c4vXzzz/TvXt36tSpQ+vWrXn33XedPFrnyi1W6UwmE926daN3795OGqVryC1WCQkJjBkzhrp169KwYUPGjRvH9evXnTxi58otXl9++SWdOnWidu3atG7dmtmzZ2MymZw8Yuc5fPgwffv2pX79+jRu3Jjhw4dz8eJFQJ/xN8otVvp8zyq3eKXTZ3ya3GKlz/jCYcqUKVStWtXZw3BZrVu3pmbNmoSFhVn+TZw40dnDcmnvvfcezZo1o06dOvTt25dTp045e0gu59dff7V6TaX/q1q1KmfOnHH28FzOwYMH6dOnD/Xq1aNJkyaMGjWKK1euOHtYLmv//v089dRT1KtXj+bNm7NkyRJnD8ml7Nixg6ZNmzJy5Mgs92kNklVO8TKbzSxZsoSaNWuyatUqJ43OteT22nLnNZuS6C7iueeeo1ixYmzbto0NGzZw4sQJpk+fzoULF3juued4/PHH+eWXX3jttdf473//y/79+509ZKfKKV7nzp1j8ODBPP744/z666+8++67LF++nM8//9zZQ3aanGKV2cqVK/nnn3+cM0AXklusXnvtNfz9/dm+fTvr1q3j77//5quvvnLyiJ0rp3gdOXKE0aNH8+KLL/Lbb7+xdOlS1q5dy0cffeTsITtFUlISAwYMoEGDBuzatYuNGzcSFRXFhAkT9Bl/g9xipc/3rHKLV2b6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REREpLDr0KEDBw4c4MCBA/z555+sXr0ao9FIv379SEhIyLK/rfPhG3377bc3TaIPGDCAP//8k/fee489e/awe/duhg8fzqJFi5g5c+YtnTc/Xbt2jRkzZvDss88SEhKSr+c+deoUp0+fpmnTpjfd96mnniI1NZUPPvggH0YmIoWFkugiIrmYMGECgYGBLFiwgBo1auDh4UGRIkV45JFHmDdvHqVLl7YkpGNiYhg3bhytW7embt26dO3alS+//NJyrFmzZtG1a1e+/fZbS1V7ly5d2L9/v2WfqKgoRowYQf369alfvz6DBw/m1KlTlvt//fVXevTowf3330+zZs0YNmwYZ8+etdzfvHlzlixZwhtvvEGjRo2oX78+L774IklJSRw5coSmTZtiNBrp3LkzY8aM4fTp05aK6TZt2jBkyBAA/vnnHwYNGkSjRo2oU6cO3bp148cffwRg69atdOvWDYD69evzzjvv8PPPP1O1alX+/vtvAIxGIxERETz88MPUqVOH9u3bM2fOHEtrlJ07d1K1alX++OMPevToQe3atWndujXr1q3L8f+F2Wxm6dKlPPnkk/j5+Vkd56effqJz586EhYXx2GOPcerUKWbNmkXjxo1p2LAh06ZNsxwnOTmZiRMn0qxZMxo2bMikSZOYN28ezZs3t+yTkJDAH3/8YUmiN2/enIULFzJmzBjq1KlDs2bN+PTTT/nll1949NFHuf/+++nVqxfnz5+3+bVVvHhxOnXqxNKlS21+jIiIiIjcnnLlyvHqq69y7tw59u7dC0CbNm2YNWsWPXr0oGHDhkBaFfvIkSOBm89dp02bxpQpU9i/fz9hYWHs3Lkzy3mjoqI4cuQIPXr0oEqVKnh6euLj40O7du2YN2+eZd75zz//ULVqVTZu3EivXr2oXbs27du3Z/369ZZjmUwm3nvvPdq0acP9999Phw4dWLhwoeVbopBW9NK/f3/q1KlD48aNGTBgAH/99Zfl/oSEBF566SUaNWpEkyZNeOutt276DckNGzYQHx9Pjx49LNt69OjBpEmTePPNN6lfvz4NGzZkwYIF/PXXXzz++OPcf//9dO7cmcjISKvn9+2339KzZ09q1arFww8/zJ9//smHH35I8+bNqVevHqNHj7Yq2Pnhhx8ICwsjODgYs9nM3LlzLc//gQce4NVXX7X8UcTHx4eePXuybNmy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//7Hq6++yogRI447I8Rdf/FE6xr269ePlStX8ttvv1FWVsZLL73E7t27PcvNFy2qbu5tOfXUU2tkfSIiIiJSPSIjI9m3bx+ZmZmeYLZZUlISy5cv56677mLbtm04HA5sNhs///wzzzzzDL169SI5Odlz/2XLlrF582ZsNhsvvfQSWVlZnH/++QBce+21vP7666xZs4aysjIyMjK47rrrePjhhwFXvfG4uDjuvfdeMjMzsdlsvPzyy1x44YXs3r0bq9XKgAEDeO2119ixYwdFRUUsWLCgXND9cKeddlqN1xg/ePAgmzZt8rpY63333cf48eM9PwJkZ2fz+++/eyUvpaen43A4aNeuXY32V0TqLgXRRUSOolGjRrzzzjv06tWLadOm0aNHD/r378+SJUuYOnWqVx3COXPm0KxZM4YOHUpKSgqvvvoqzz33HH379q30+ubMmUO9evU4//zz6du3L1lZWSxYsICQkBCaNGlCWloaP//8M2eddRZ9+/Zl8+bNLF682HNRnWNp3749KSkpTJo0iWnTph3xPp06dWLixIncf//99OzZk9dff52HHnqI1NRUHnroIV577TXOOussOnTowFVXXcUTTzxR7jmuu+46hg8fzi233EK3bt246667+Pe//80tt9xS6dfiSH1v1KgRa9euPe7nAJg0aRJnnXUW1157LX379iUzM5PU1FRCQkKw2Wx89913Pguiz5o1i06dOnn9u/766z3Lv/zySzp06OD1hUlEREREap+hQ4dit9s577zz+Oyzz8otP+WUU3j99dex2+2MHj2abt260adPH2bMmMEFF1zA888/73X/UaNGce+993rm4/PmzfOcuTp69GiuvfZaZsyYQZcuXbj66qs588wzufPOOwGIiYlh4cKFZGdnc84553DmmWeyevVqXnjhBc81fe677z7atWvHpZdeyoABA8jOzub888+ntLS0wm3s168f33//faWuk+Qra9eupVWrVl7XIpo6dSonnXQSV1xxBZ07d+aiiy4iNjaWGTNmeO6zZs0aEhMTOeOMM2qsryJSt1mchxfaEhERCVCLFi1i0aJFfPzxx0e8wGhllZSUeD1+ypQp7N27l1deecUX3ayUzMxMzjnnHO69914uvvjiGluviIiIiASunTt3cs455/Dyyy9z5pln+rs7XgoLCxk0aBDjxo1j1KhR/u5OhWw2G4MHD+byyy9n/Pjx/u6OiNQRykQXEZFa46qrriImJoZFixYd93MsXryYs88+m40bN+JwOPjuu+/4+OOPGTRokA97emzPPPMM7dq146KLLqrR9YqIiIiIHI/o6GgmTZrE/PnzycrK8nd3KvTf//6X0NBQrrnmGn93RUTqEKu/OyAiIlJZ4eHhPPPMM1x55ZX06NGDHj16VPk5/vWvf/H3338zfvx4cnJySE5O5vrrr2fkyJHV0OMj++ijj/joo4948803CQnR79kiIiIiUjsMGzaMn3/+mSlTprBgwYLjvt5RdVm/fj3z58/nlVdeITo62t/dEZE6ROVcREREREREREREREQqoPQ3EREREREREREREZEKKIguIiIiIiIiIiIiIlIBBdFFRERERERERERERCqgILqIiIiIiIiIiIiISAUURBcRERERERERERERqYCC6CIiIiIiIiIiIiIiFVAQXURERERERERERESkAgqii4iIiIiIiIiIiIhUQEF0EREREREREREREZEKKIguIiIiIiIiIiIiIlIBBdFFRERERERERERERCqgILqIiIiIiIiIiIiISAUURBcRERERERERERERqYCC6CIiIiIiIiIiIiIiFVAQXUSkEjZs2MBNN93EwIED6dixI71792bEiBG89dZbfutTu3btmDt3boXLp06dyllnnVXp53vqqado164dJSUlvujeMWVkZDB9+nTOOeccOnXqRI8ePUhNTeWxxx4jNze3RvrgVlRUxAsvvMDQoUPp1asXZ5xxBgMHDuSuu+5i165dnvstX76cdu3asWXLlhrtn4iIiIgcm+bsvvHcc8/Rrl07/vrrrwrvM3fuXNq3b8/evXsr9ZzHeh1ERAKdxel0Ov3dCRGRQPbtt9/y73//m/PPP5+rrrqK5ORksrOzWbFiBYsXL2bq1Klce+21Nd6v/fv3Ex0dTUxMDAD//ve/Oe+887jyyisByMvLo7S0lHr16lXq+QoKCigsLKRhw4YA/O9//+PGG2/kq6++8nnfP//8c26++WZ69OjB6NGjad26NUVFRXz//fc8/fTTREVF8frrr9OgQQOfr/tIrr32WtLT07n99tvp3LkzYWFhbN68mblz51JUVMSqVauIjY2luLiYvLw86tWrR2hoaI30TURERESOTXN2383Z//77bwYMGMB//vMfbr311nLLHQ4H/fv357TTTiMtLa1Sz9muXTv+85//MHnyZJ/1U0SkJln93QERkUD32muv0bBhQ+bMmYPFYgGgadOmdOjQgfz8fDZu3OiXfrknzuCayK5fv57zzjvP0xYXF1el54uJifFM7gF+/vnnE+7jkezfv59JkyYxcOBAHn30Uc9rCtC2bVvOPvtsLrvsMt5//31Gjx5dLX0w27JlC19//TWzZs1i6NChnvYWLVrQokUL7rjjDn799Vd69+5NZGQkkZGR1d4nEREREakazdl9Jzk5mb59+7JixQpuvvlmQkK8ixh89dVX/P3339x9990+X7eISKBSORcRkWMoLS3FbrdTWlpabtkDDzxQ7rTE//73v5x//vl07NiRf/zjH9xzzz3k5eV5lk+aNIlLLrmEH374gWHDhtG5c2cGDhzIG2+84bmP0+nk+eefZ/DgwXTu3JnevXszceJEduzY4bmP+5TInTt30r59e/Lz85k5cybt2rUDvE8NHTlyJMOHDy/X/xdeeIH27dvz999/e50aOnXqVB5++GEOHDhAu3btuO++++jUqRNPPfVUuecYM2YMl156KVC5cidLly6luLiYadOmeQXQ3Zo3b85XX31VLoD+8ssvc9FFF9GlSxfOPPNMxowZw+bNmz3L165dS7t27Vi7di0TJ06ka9eudO/enalTp1JUVFRhf9z79UinxJ522mmsWLGC3r17H3H7Bg4cSLt27cr9M79OBw8eZNq0aaSkpNCxY0cuvPBC3nzzzQr7IyIiIiJVpzm7b+fs//znP9mzZw/ffPNNuWVvv/02DRo0YMCAAYArKeX666+nR48edOzYkQsuuIAlS5ZU+Nzffvst7dq148svv/Rqv/zyyxk1ahTg2p/t2rVjyZIlzJ49m169etG1a1fuuOMOSkpKePLJJ0lJSaFnz55MmzYNm83meZ7CwkJmzZpF37596dixI+eddx5paWmoEIOInAgF0UVEjqF///7s27ePkSNH8vHHH3tNrg83f/58HnjgAS655BLee+89HnzwQb788ktuuOEGz33CwsLIzMzkiSeeYPr06bz//vt07dqVmTNneibcb775JvPnz+f222/ngw8+IC0tjfz8fMaOHVtunU2aNGHZsmUATJ48mbVr15a7T2pqKhs2bPCq7w2watUqevfuTXJyslf7XXfdxfnnn0+9evVYu3YtkyZNYtCgQbz99ttek8+srCzWrVvHP//5TwAuuOAC1q5dy8knn1zha/T999/Trl07GjVqVOF9rFbvE6VWrFjBAw88wBVXXMH777/Pyy+/DMC4ceMoLi72esysWbMYPHgw7777LtOnT2flypU88sgjFa6rTZs2NG/enIceeojHH3+c9PT0Sk+w33zzTdauXev5d/nll2O1WunXrx8ANpuN0aNHs27dOh566CHee+89UlNTueuuu3jnnXcqtQ4REREROTbN2X07Z+/fvz8NGzZk+fLlXu15eXl88sknDB06FKvVysGDB7nqqqvIyspi/vz5rFixgtTUVO6///6jBtKPJSwsDHD92NGgQQOWLVvGTTfdxDvvvMO1115LaWkpr732GjfeeCPLly9n1apVnsfeeOONvPfee0ydOpWVK1cyZswYnnrqKZ555pnj7o+IiILoIiLHMHz4cK6//np+//13Jk6cSK9evRg6dChz5swhIyPDc7/S0lIWLFjABRdcwPXXX0+rVq3o168fd911F99++y3r16/33Hffvn3cfffddO/enZYtW3Lttddit9v59ddfAdi4cSNNmjThnHPOoWnTpnTu3JnHHnuMOXPm4HA4vPoXGhrqqaEYGxvrdcqo25AhQwgLC+PDDz/0tO3YsYNff/2VSy65pNz94+LiiIyMJCQkhIYNGxITE8OVV17Jrl27WLduned+H330EVarldTUVAAiIyNp2LDhUeuF79u3j6ZNmx71NT/cueeey8cff8yoUaNo3rw57dq14+qrr2bv3r38/vvvAJ6s9n79+pGamkqLFi0YNmwYqampvPfeexUGxsPCwnj22Wdp27Ytzz33HBdffLEni+jdd9+lrKyswn7Vq1ePhg0b0rBhQ7Zu3cqbb77JTTfdROfOnQH49NNP+f3337n//vvp168frVq14vrrr2fgwIHMnz+/Sq+BiIiIiFRMc3bfztmtVitDhw7lk08+8fpBYtWqVZSUlHgC8m+99RY5OTnMnTuX7t2706ZNG8aPH89ZZ53F4sWLK3z+ymrYsCFjx47lpJNOYvTo0URHR5OZmcmkSZM4+eSTGTVqFNHR0fz222+A6+Kya9eu5bbbbuOCCy7gpJNOYsSIEVxxxRUsXrzYK2NdRKQqFEQXEamEW2+9lbVr1/LYY48xfPhwioqKWLhwIRdeeCGvvvoqAFu3biUvL48zzzzT67EpKSmA66I/btHR0Zx66qme/yckJACQk5MDwIABA/jrr7+45pprePPNN9m9ezdJSUl07NixXE3CykhKSqJPnz5eE/JVq1YRHR3NoEGDKvUcPXr04JRTTuGtt97yeo4hQ4ZUqZaj1WrFbreXa7/00kvp2rWr1z+3sLAwli9fzoUXXkjPnj3p2rUrN954IwDZ2dlez9OtWzev/7dv3568vDz2799fYZ/atWvHO++8w1tvvcWkSZPo2LEja9euZcqUKVx22WVkZmYedZtycnK4/fbb6dmzJ//5z3887evXr8disdCrVy+v+6ekpLB169ZyfRcRERGR46c5u+/m7OAq6VJcXMzKlSs9bW+//TY9e/akVatWgCto3aRJE1q0aOH12K5du7J9+3by8/OrtM7DdezY0XPbYrGQmJjIaaedVq7NHeh3/whypP2bl5fHH3/8cUL9EZHgpQuLiohUUnx8PBdccAEXXHABAL/++iu33347s2fPZvDgwZ6J26xZs3jwwQfLPd4cxI2OjvZa5s6idmdL9+vXj5dffplXXnmFhx56iLy8PE4//XTuvvtur+ByVaSmpjJp0iT27NlDkyZNWLVqFYMGDSrXl6O54oormDt3Lnl5eZSUlPD99997SqtUVuPGjdm5c2e59qefftpTw/Kjjz7yqlv56KOP8tJLLzF+/HjOO+88oqOjWb9+PVOmTCn3PId/OYiKigI4al10t44dO9KxY0fGjh1LQUEBixcv5sknn+S5557jrrvuqvBxd999N8XFxcyZM8frC1NeXh5Op7PcJN6d3X7gwAESExOP2S8RERERqRzN2X0zZwc4+eST6dmzJ2+//TYjRoxg27Zt/PTTTzz88MOe++Tn53t+XDCLj48HoKCggNjY2Cqv2809l3ezWCxERkaWa3PvE/f+HTZsmNd93GcGHDhw4Lj7IiLBTUF0EZFjKCwsBMpPojt27Mhtt93GDTfcwNatWz3B0MmTJ3tqYptVNfOje/fudO/enbKyMn766SeefPJJ/vOf//DZZ59V+bnAdRHMqKgoPvzwQ/r27cvmzZu54447qvQcQ4cOZd68eXz66afk5+fTsmVLevToUaXn6Nu3Lw888AAZGRmeDBbAq8RL/fr1vR6zatUqzj//fG666SZPm/uUzcO5M4Pc3PsvJiamwj4dPHiw3DpjYmK44YYb+Pjjj0lPT6/wscuWLePDDz/kmWeeKVenMj4+noiIiArrnzdp0qTC5xURERGRytOc3eCLObvbP//5T+644w4yMjJ49913iYuLY8iQIZ7lcXFxbN++vdzj3GdcHimA7v4x4nDFxcVEREQcVz/d3AH9xYsXHzFZ5UhldEREKkPlXEREjmLfvn306tXriFe4BzwX/UlOTqZVq1bEx8eza9cuTjrpJM+/5s2bU1ZW5qmBWBlr1qzxnGpotVrp2bMnd911F3l5eUecpLod7YKYUVFRnHvuufzf//0fH374IY0bN6Z3795H7cfhzxcXF8f555/P+++/z/vvv8/w4cMrvU1uF198MQ0bNuTee++tsCbhn3/+6fX//Pz8cq+fOzB9eB9/+uknr///9ttvJCUl0aBBgyOu6/777+ecc845YlaKzWZj//795YLjbhkZGcyePZsRI0Zw7rnnllvepUsXSkpKKCoq8hoTkZGRxMfHEx4efsTnFREREZHK05zd93N2N3cZmI8++ogPPviA1NRUr0zwM844g927d5c70/T777+nTZs2R0xkcWepm0u95Ofney7YeiLOOOMMwJVxbt6/8fHxREVFVSmjX0TETJnoIiJH0ahRI6688koWLVqE3W7nwgsvpGHDhuTm5vLNN9/w5JNPcskll9CyZUsArrvuOp5++mmaN29O3759KSoq4sUXX+TTTz9l9erVNGrUqFLrXb58Ob/99ht33303rVu3Jj8/n0WLFlG/fn3atGlT7v7uiegPP/xAly5daN269RGfNzU1lQkTJpCVlUVqaupRazXGx8eTnZ3NunXraNKkCSeddBIAV155JVdeeSUWi6XcFe6Li4vJy8ujXr16FV6oKCEhgccee4wbbriBq666irFjx3LaaafhcDhIT09n2bJlrFmzxqu2eJcuXfjoo49ITU0lOjqaxYsX06xZM8BVt9I9WQb45JNPOPXUU+nZsyc//PAD77//PqNGjapwO//1r3+xevVqrr76am644Qbat29PWFgYf/31Fy+88AKFhYWMGTOm3ONKS0uZNGkSSUlJXHfddV6n/oaFhZGYmMiAAQM49dRTmTJlCtOmTeOkk07izz//ZNasWbRv354nn3yywn6JiIiISOVozu77ObtbZGQkF110Ea+88gr79+9n3rx5XssvvfRSFi1axOTJk7njjjuIj4/n3Xff5bvvvvMq+2LmDmq/99579O/fn9LSUh588MHjytw/XMeOHenTpw/3338/TqeT0047jd27d/Pwww8TEhLCG2+8UWEmvIjI0SiILiJyDHfddRcdO3Zk+fLlrFq1iqysLCIjIznllFO4/fbbufzyyz33HTduHDExMSxZsoSHH36Y6OhounbtypIlSyo9GQdXjca5c+dy9913s3//fuLi4jjjjDN48cUXy9UABNfk+ZprruGNN97g+++/54033jji8/7jH/8gPj6e33//nccee+yofRg+fDifffYZ48ePZ8SIEZ7TSDt37kxycjIdO3Y8YtmVadOmsWrVqiN+cXDr0aMH77zzDi+//DKPPvooe/bswWKx0KRJE3r37s17773HKaec4rn/jBkzmD59Otdccw0JCQmMHDmSsWPHcvDgQV5++WXCwsLo2bMnAJMmTWLlypU88MADhIaGMmzYMG699dYK+9KqVSveeOMNT/3zffv2UVpaSqNGjTjzzDN56623jvgF5++//2bjxo0A5bLQe/XqxSuvvEJ4eDiLFy9m7ty5TJo0iby8PBo1asTgwYO5+eabj/r6i4iIiEjlac7u+zm7eR2vvfYap59+Oh06dPBaVq9ePV555RUeeeQRrrvuOkpKSmjdujUPP/wwQ4cOPeLzRUVF8cgjjzBnzhx69+5N48aNmTBhArm5uWRlZR2zP8fy1FNP8dhjj3HfffeRmZlJUlIS/fr1Y9KkSQqgi8hxsziPdh6RiIjIYTZu3Mill17Kq6++Svfu3f3dHY9vv/2Wq6++mpdffrnchTxFRERERIJJoM7ZRURqK2Wii4hIpRw8eJCMjAzuvPNOBg0apMm4iIiIiEiA0ZxdRKR6KIguIiKVcuedd7Ju3ToGDhzI/fff7+/uiIiIiIjIYTRnFxGpHirnIiIiIiIiIiIiIiJSgYov8SwiIiIiIiIiIiIiEuQURBcRERERERERERERqYCC6CIiIiIiIiIiIiIiFVAQXURERERERERERESkAlZ/d6C22L8/z99dCCjh4aHYbHZ/d0NMtE8Ci/ZH4NE+CSzaH4FH+6TmNWwY5+8u1FqBMjfXcSOVoXEix6IxIseiMSLHojFyYiozL1cmulSZxeL9V/xP+ySwaH8EHu2TwKL9EXi0T0SqTseNVIbGiRyLxogci8aIHIvGSM1QEF1ERERERERERERE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iAQNzctFRORwmpdLXeRdE91//ZDgVOULi27ZsoU333yTFStWkJeXx6BBg1i0aBE9expvzD169GD69OkMHTrUl30Vk5wc4xe3xEQFPesK/9VEN26rJrqIiEjtoHm5iIiIBBNloos/VTmIfuGFF3LSSScxZswYLr30UpKSksrdp1+/fuTn5/ukg3Jk5iC6yrnUHeZfVQsLa2693uVcNJ5ERERqA83LRUREJJioJrr4U5WD6C+++CL/+Mc/yrUXFRXx2WefccEFFwDw008/nXjvpELui4oCxMf7sSPiU9HR/qqJbtxWTXQREZHaQfNyERERCSbmeIUy0aWmVbkm+vjx44/YXlxczIwZM064Q1I52dnKRK+L/FcTXZnoIiIitY3m5SIiIhJMzCVwlYkuNa3SmejLli3jzTffxGazMWLEiHLL9+3bR7Q5AijVKifHuK0get0RCDXRlYkuIiIS2DQvFxERkWCkTHTxp0oH0fv27UtxcTG//PILrVq1Kre8ffv2umBRDTKXc1EQve4w10T3XyZ6za1XREREqk7zchEREQlGERHKRBf/qXQQPTk5mVGjRrFnzx5uv/326uyTVILKudRNgVETXeNJREQkkGleLiIiIsHIOxPdf/2Q4FSpIPpff/3FSSedBMDw4cPJyMio8L5HyoYR31Mmet2kTHQRERE5Gs3LRUREJFh510RXORepWZUKol988cWsX78egPPPPx+LpfxAdTqdWCwWNm3a5NseyhHl5JiD6H7siPhUYNRE148yIiIigUrzchEREQlWykQXf6pUEP2FF17w3H7ppZeOOFmXmmW+sGh8vIKedYU5E72wsObWqwuLiq/kvvwa2GwQHu7vroiI1Emal4uISGVoXi51kXdNdM2BpGZVKojeo0cPz+0zzzyz2jojleedia4gel0RE+Ovmujmci4aT3L8ys7o6u8uiIjUaZqXi4hIZWheLnWRMtHFnyoVRB8xYkSln/D1118/7s5I5bmD6JGRTmUO1yHmfem/ci41tloRERGpIs3LRUREJFipJrr4U6WC6CeffLJOFQ0w7iC6SrnULVYrhIc7sdksfrywqMaUiIhIoNK8XERERIKVMtHFnyoVRH/ooYequx9SRe4gemKiAp51TXS0q3RdzZZzMW4rE11ORPhHq10DKjIS26Dz/d0dEZE6R/NyERGpDM3LpS6KiDBu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XE9t4r6wqD8vKio1IzrauF1YWH0723Xak4uC6CIiIrVDdczLRURERGoLc+JhSYkfOyJBpdLlXA4ePMjJJ5/s+X+bNm3Yv3//Ca28cePGpKWlHXFZenq657bFYmHq1KlMnTr1iPdt1KgR8+bNO6711Cbuci666nDd559MdI0rERGR2qA65uUiIiIitYV3TXT/9UOCS6Uz0Q9nsVhwqmhmjbHbzZnofu6MVDvzlaarNxPduK1MdBERkdpJ83IREREJJt410VWqQWrGcQfRpWbl5Rm3lYle94WEGB8Kh6oaVYvDLywqIiIiIiIiIhLIvGui+68fElwqXc6lrKyMxx57zCvLxW638+ijj3rd77bbbvNd78TDXcoFFEQPFtHRToqLLdV6kQxz7TBloouIiNQOmpeLiIhIMPOuia5MdKkZlQ6iJycn895773m1NWrUiPfff9/zf4vFosl6NTEH0ePjFUQPBu6SLtVbE12Z6CIiIrWN5uUiIiISzFQTXfyh0kH0//u//6vOfsgxmIPoiYkKdgYD9y+r1ZmJrproIiIitY/m5SIiIhLMzOVcVBNdaopqotcS3pnofuyI1JjoaNff6qyJrkx0EREREREREalNvC8s6seOSFBREL2WyM01bqsmenBwZ6KXlVkoLa2edSgTXURERERERERqE3f5W1AmutScSpdzEf/KzlY5l2Bj/lAoKoKwMN+vwzsT3ffPL8En+6PPwW6H0FB/d0VEREREJGhpXi51mTLRxR8URK8lcnN1YdFgY77adGGhpVr2u3cmusaVnDhHcmN/d0FEREREJOhpXi51mXdNdP/1Q4KLT8q55Ofn++Jp5CjMNdFVziU4uGuiQ/XVRVc5FxERkbrlROblO3fuZMyYMXTp0oWUlBTmzJmDw+Eodz+bzca8efMYMGAAXbp04eqrr2bHjh2e5ZmZmUyePJmUlBR69OjBqFGj2Lhxo9d6xo8fT69evUhJSeH2228nJyfnuPstIiIiwcU7E13lXKRmVDmInpGRwciRIz3/nz59Oj169ODss89m8+bNPu2cGMzlXBREDw7R0cZ+Liqqng8FXVhURESk9vLlvNzpdDJx4kSSkpL44osvWLJkCatXr2bx4sXl7puWlsbKlStZuHAh69at49RTT2XChAmegPvMmTPJyspi9erVfP3113Tr1o2xY8dit9sBGD9+PImJiXz22We8++67ZGRk8Mgjjxz/CyEiIiJBxbsmuv/6IcGlykH0WbNm0aZNGwC+++47Vq1axUsvvcS//vUv5s2b5/MOiou5nEtCgh87IjXm8Jro1UGZ6OJrkS8vIuq5p4l8eZG/uyIiUuf5cl7+yy+/kJ6ezvTp00lISKB169aMHTuWpUuXlrvvZ599xvDhw2nTpg2RkZFMmTKFjIwM1q9fD8CmTZsYOHAgiYmJhIeHk5qayoEDB9i/fz95eXl07NiRyZMnExMTQ8OGDRk6dCg//PDDib8gIiLioXm51GXKRBd/qHJN9F9//ZUnnngCcE2gBw8ezJlnnkmXLl1YtEhvztXFfIaraqIHB3MmemFh9Waih4Q4q+XCpRJ8ouc9TOie3dibNKX46mv93R0RkTrNl/Py3377jWbNmpGYmOhp69ChA9u2bSM/P5/Y2FhPu9PpxOk05ilhYWFERESwadMmunbtSv/+/Vm9ejWDBg0iLi6Ot99+mw4dOpCcnIzFYuHBBx/0WveuXbto0qTJcbwCIiJSEc3LpS5TTXTxhyoH0UtLSz2T6HXr1nHNNdcAEB4eTklJiW97Jx7umujR0Qp2BouayER3H7KRkWDRj7ciIiK1ii/n5VlZWSQcdrqj+/9ZWVleQfR+/fqxbNkyzj33XJo2bcqiRYsoLS311DWfMmUK48ePp0+fPgA0bdqUhQsXYjnCZOOXX35hyZIlPPnkk1XqL/h/7uJev7/7IYFN40SOpSbGiMZf7ab3kfLM8ZKSEkvQvzYaIzWjykH0li1b8uWXXxIdHc0ff/xB3759AdiwYQONGjXyeQfFxR1ET0xUFnqwiIqq/pro7udVPXQREZHax1/z8nHjxpGdnc3o0aOxWq2MGjWKli1bYrW6vlrMnDkTgC+//JL4+HgWLVrEmDFjWLlyJTExMZ7n+fHHHxk/fjy33347/fr1q1IfwsNDfbY9x8tigdDQUCwWcGoqJRXQOJFjqa4xYg6qhYX5/z1Tjp/eR44sIsJJSYmF4mJL0I9xjZGaUeUg+rhx4xg/fjwOh4OxY8dSr149cnJymDhxIldffXV19FEwaqLroqLBw/zLamFh9azDfdqT6qGLiIjUPr6cl9evX5/s7GyvtqysLADq1avn1R4ZGcmMGTOYMWMG4CrvsnDhQpKTkykoKGD58uW8+uqrJCcnAzBhwgRefPFF1qxZw5AhQwBX+ZkpU6Zw7733cuGFF1Z52202u9+zrdxfVMvK7PrCKhXSOJFjqa4x4n4upxNKS+2+e2KpcXofObLISNfZ9UVFGuMaIzWjykH0888/n+7du5OZmclpp50GQHx8PLfffjupqak+76CAzWbUxFY99OBhroleXZno7proCqKLiIjUPr6cl3fq1Indu3eTlZVFUlIS4Mpob9u2rVf2OMDGjRvJzc0lJSUFcJVkyc7Oplu3bp77OBwOz22n04ndbickJASA//3vf9xxxx08+eST/OMf/6j6hnue97gf6lNOZ+D0RQKXxokcS3WOEY29ukHvI94iIpyAheJivS5uGiPVK+R4HpSXl+eZqO/bt49XXnmlXA1F8R13KRcA07WepI7zzkSvniC6u1yq68NHREREahtfzcvbt29P586dmTVrFrm5uaSnp5OWlsZVV10FwJAhQ/jhhx8ASE9PZ/LkyWzfvp2cnBzmzp3L4MGDad68OTExMfTs2ZPnn3+ezMxMbDYbCxYswGq10r17d8rKypg+fTo333zzCQXQRUREJLi5kwF1eUapKVUOor/11ltceumlABQWFjJixAiWLFnClClTWLp0qc87KJCba9xWJnrw8M5E9/3zO51Ghrs5YC8iIiK1g6/n5U888QR5eXmcffbZXHvttYwYMYKRI0cCkJGRQeGh+nLDhg0jNTWV4cOHM3DgQBo3bszs2bM9zzNv3jwSEhK46KKLSElJ4fPPP2f+/PnUr1+fn3/+mS1btvDQQw/RqVMnr3+7du3ywasiIiIiwcB9HbnqOnNf5HBVLueyePFiHn/8cQA++OADwsLCWLlyJb///jt33HEHV1xxha/7GPTMmeiqiR48oqON29WRiW7+tVYXFhUREal9fD0vb9y4MWlpaUdclp6e7rltsViYOnUqU6dOPeJ9GzVqxLx58464rEePHl7PJSIiInI83Jno7mu9iVS3Kmei79y5kwEDBgDw1VdfMWTIEKxWKx06dFD2SDXJzlYQPRi5f1WF6slEN3/QqCa6iIhI7aN5uYiIiAQrd1lau91CaamfOyNBocpB9IiICIqLi3E6nXz77bf07t0bgKKiIkJDQ33eQYHcXAXRg5G5xEp1nJ5UUmI8p2qii6/Y27SlrN1p2Nu09XdXRETqPM3LRUSkIpqXS11nTgZUXXSpCVUu59K9e3fu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iIiIZEJBdBERERERERERERGRTCiILiIiIiIiIiIiIiKSCQXRRUREREREREREREQyoSC6iIiIiIiIiIiIiEgmFEQXEREREREREREREcmEgugiIiIiIiIiIiIiIplQEF1EREREREREREREJBMGk8lkcnQjRERERERERERERETyImWii4iIiIiIiIiIiIhkQkF0EREREREREREREZFMKIguIiIiIiIiIiIiIpIJBdFFRERERERERERERDKhILqIiIiIiIiIiIiISCYURBcRERERERERERERyYSC6CIiIiIiIiIiIiIimVAQXUREREREREREREQkEwqii4iIiIiIiIiIiIhkQkF0SWfHjh3UqVOHYcOGpZu3e/dunn32WUJCQmjevDlr1qyxmr9w4UIaN25M5cqV6dixI4cPH86tZt/TMusTk8nE3LlzefTRR1m6dGm69dQf9nG7v5Gff/6Zjh07UqVKFRo3bsz06dOt5qtPbO92/bFx40aeffZZHn/8cRo2bMjkyZNJTk42z1d/2Mft+iRNSkoK7du3p3v37lbT1Se2l1l/nDp1ivLlyxMSEmL1b8OGDeZl1B8iGTt8+DA9evSgWrVq1K5dmxEjRnD16lVHN0vykIMHD9KtWzeqVatG/fr1mTt3rqObJHlATr5rS/5wt9/9Jf/ISTxCckZBdLEye/Zsxo8fz/33359u3oULF3j55Zfp0KED//vf/3jzzTcZO3YsBw8eBGDTpk18/PHHTJw4kZ9//pmGDRvSv39/YmNjc/tl3FNu1yf9+/dnz549FCpUKN089Yd93K4/zp07x4ABA+jQoQO//PIL06dPZ8GCBaxevRpQn9jD7frj0KFDvP766wwfPpz9+/fz2Wef8c0337Bo0SJA/WEvt+sTS1988QUnTpywmqY+sb3b9ce1a9dwd3fn0KFDVv9atWoFqD9EMpOcnEy/fv2oUqUKu3fvZsOGDVy9epW33nrL0U2TPCI6Opq+ffvyxBNPsGvXLmbNmsXcuXOtfqSU/Ccn37Ulf7jb7/6Sf+QkHiE5pyC6WPH09OTLL7/M8A9y7dq13H///fTo0QNvb28aNWpE06ZNWblyJQArV66kQ4cO1KpVC29vbwYNGoTBYGDLli25/TLuKbfrk8cff5zw8HC8vLzSzVN/2Mft+uPy5ct07NiRTp064ebmRqVKlahduzZ79+4F1Cf2cLv+8PHx4cMPP6Rhw4a4uLhQoUIFqlevzt9//w2oP+zldn2S5uLFi8ycOZMePXpYTVef2N7t+iM6Oho/P79M11V/iGTs0qVLXL58mdatW+Ph4YG/vz9Nmzbl6NGjjm6a5BG//vorcXFxDBo0CE9PTx555BE6d+7Ml19+6eimiQPl5Lu25A93+91f8o+cxCMk5xREFys9evSgYMGCGc47cuQIjzzyiNW0ihUrmi/tvnW+wWCgQoUKuvQ7h27XJwMHDsRgMGQ4T/1hH7frj5CQEEaPHm017ezZsxQvXhxQn9jD7frjwQcfpFmzZgAkJSWxZ88efvnlF5588klA/WEvt+uTNO+99x5du3aldOnSVtPVJ7Z3u/6IiYkhJSWFfv36UaNGDZ588knmzJmDyWQC1B8imQkKCqJSpUqsWLGCuLg4rl69yqZNm2jUqJGjmyZ5REpKCiaTyTyeAhQoUEA/tORzOfmuLfnD3X73l/wjJ/EIyTkF0SXLIiMj02Ws+fv7m+s/RkZG4u/vbzXfz89P9SEdRP3heIsWLeLUqVM899xzgPrEUVatWkVISAgvv/wyw4cPp0GDBoD6w1F++uknjh49Sr9+/dLNU5/kLnd3d0qXLk3nzp3ZsWMHoaGhzJw505wpqf4QyZjBYOCTTz5h69atPP7449SuXZvk5GRee+01RzdN8oiqVavi5eVFWFgYcXFxHD16lK+//pro6GhHN03yqDt91xYRya5b4xGScwqiS46l/Rqa2a+i+rXUMdQfjrV48WI++eQTZs2aRVBQEKA+cZS2bdty8OBBZs+ezaeffsqyZcsA9YcjJCQk8M477/DWW2/h4eGRbr76JHc1adKE5cuX06RJE7y8vKhbty6dOnXiq6++AtQfIplJTEykf//+tGzZkv3797Nz504KFizIiBEjHN00ySP8/f359NNP+fHHH6lbty7vv/8+rVu3xs3NzdFNEyejz1wRuRsZxSMk5xRElywLDAwkKirKalpkZCSBgYEABAQE3Ha+5C71h+NMnTqV8PBwFi9eTJUqVczT1SeO4+7uTvXq1enRoweLFy8G1B+OMHPmTHPWZkbUJ45XsmRJLl++DKg/RDKza9cuTp8+zdChQ/H19eW+++7jlVdeYfPmzcoaFbMnnniCb775hv3797Nw4UK8vb0VyJBM3em7tohIVmUWj5CcUxBdsiwkJCRdTbaDBw9SuXJl8/zff//dPC85OZkjR46Y50vuUn84xvz58/n2229ZtmwZ5cuXt5qnPslds2bNYujQoVbTkpOTcXFJ/ehTf+S+NWvWsGXLFp544gmeeOIJ3n33Xfbv388TTzzBuXPn1Ce57LvvvuOLL76wmnbs2DFKlSoF6G9EJDMmk4mUlBSraUajEVDWqKRKSEhg1apVXL9+3Tztp59+omrVqg5sleRld/quLSKSFbeLR0jOKYguWda6dWvOnDnDggULiI+PZ+PGjfz444906tQJgM6dO/PVV1+xZ88ebty4wUcffYSXlxdNmjRxcMvzJ/VH7jt16hSffPIJYWFhBAcHp5uvPsldNWvWZPPmzWzYsIGkpCT++usvli1bRtOmTQH1hyMsX76c9evXs3r1alavXs2QIUN49NFHWb16NUWLFlWf5DJ3d3cmT57Mzp07SUpKYteuXXz11Vd069YN0N+ISGYef/xxfH19CQsLIz4+nujoaGbPnk2VKlUICAhwdPMkD3B3dycsLIxZs2aRlJTEpk2b2L17Nz179nR00ySPutN3bRGRO7lTPEJyzmCyvGW45HshISEAJCUlAZjr9h06dAiAvXv38u6773Ls2DGCg4MZMWIEzZs3N6+/dOlSwsPDuXLlCo8++ihvv/02Dz30UC6/intLZn0yb948+vTpA6TW5nRzc8PFxYUaNWowb948QP1hD7f7G5k+fTphYWG4u7tbrRMcHMx3330HqE9s7U5j1saNG5k5cyYnT57E39+fp556iiFDhpjrcas/bO9OfWLp66+/5ptvvmHRokXmaeoT27pTfyxfvpx58+Zx4cIFSpYsSZ8+fWjfvr15ffWHSMYOHjzIBx98wNGjR3F3d6dmzZq88cYbFCtWzNFNkzzi0KFDhIaG8s8//1C8eHFGjBhBs2bNHN0scaCcfteWe19OvvtL/pDTeITkjILoIiIiIiIiIiIiIiKZUDkXEREREREREREREZFMKIguIiIiIiIiIiIiIpIJBdFFRERERERERERERDKhILqIiIiIiIiIiIiISCYURBcRERERERERERERyYSC6CIiIiIiIiIiIiIimVAQXUREREREREREREQkEwqii4iIiIiIiIiIiIhkQkF0ERHJlhYtWvDxxx/bfT/jxo3jxRdfxGQy2X1fp06dolatWuzevdvu+xIRERERSXP69GnKly/Pjz/+6OimZCg8PJw2bdoQHx+f421FRUXRpEkTvvnmGxu0TEQkd7k5ugEiIpLq6tWrzJ49m23btnH+/HkAihYtSoMGDXj55ZcJDAy06/6NRiOLFy9m9erVnDlzBqPRSGBgIA0aNGDYsGH4+fkB8N1339m1HQCrVq1iy5YtrFu3DoPBYPf9lSpVinfeeYehQ4eyZs0agoKC7L5PEREREXFeffr0IS4ujqVLl2Y4/+2332bLli388MMPuLk5Z+hlz549zJw5k1WrVuHl5ZWldUaNGsWxY8dYsWJFunn+/v58/PHH9OjRg4oVK1KhQgVbN1lExG6UiS4ikgecPn2aNm3acOzYMT788EP27dvHzz//zPjx49m3bx9t27blypUrdm3DpEmTmDt3Lq+//jo7d+5k3759hIWFsX//fvr27WvXfVuKi4tjypQp9O3bl4CAgFzb75NPPkmZMmWYPn16ru1TRERERJxT9+7d2b9/P3/88Ue6eTdu3GDNmjV07drVaQPoJpOJiRMn0qFDB+6//36bbbdy5co0btyYDz74wGbbFBHJDQqii4jkAaGhofj5+TF9+nQqVaqEi4sLHh4e1KhRgzlz5tCpUydiY2PNyx88eJDu3btTo0YNqlWrRrdu3Th06JB5/vDhw+nbty/h4eHUq1eP6tWrM2jQIK5evZppG3bu3Enjxo2pXbs2Hh4euLq68sgjj/Dxxx/Tu3dvEhMTAWjSpAlTpkwBYMaMGYSEhJj/Pfroo5QvX958iWZKSgozZsygSZMmPPbYY7Rs2ZLPPvuMpKSkTNuxZs0arl27RufOnc3TOnfuzPjx43n//fepXr06NWvWZNasWfz555906NCBxx57jDZt2hAREQHAiRMnKF++PJs2baJLly5UrlyZVq1a8fvvv7No0SLq169PtWrVGDlyJMnJyeb99OjRg6+++oro6OjsdJ+IiIiI5DONGjXi/vvvZ8mSJenmrV27FqPRyPPPPw/AypUrefbZZ6lSpQrNmjXj3Xff5fr16xlut3v37gwbNsxq2rBhw+jevTuQes5evnx5du/eTZs2bQgJCeG5557j1KlTTJ06lVq1alGzZk0mTZpktY1ly5bRokULKleuTLNmzZg0aRJxcXGZvr6dO3fyxx9/0LNnT/O0UaNGmV9TmqVLl1K+fPnbvFPp9ezZk59++sl87i4i4gwURBcRcbDIyEh27txJ7969M8xUCQwMZNCgQZQqVQqAxMRE+vXrR5kyZdixYwc//fQTpUqVYuDAgaSkpADg7u7O/v37iY2N5YcffmDVqlUcO3aMN954I9N2VKhQgS1btrBt2zarwHLZsmVp1aoVHh4e6dYZOHAghw4dMv979tlnKVasGI0aNQJg/vz5fP3110yfPp39+/fzwQcf8MUXXzB37txM27Ft2zaqV69udcmou7s769atIyQkhN27d9O7d2+mTZvGlClTCAsLY8eOHRgMBqZNmwZgfh/nzp3L5MmT2blzJ56enrzyyitcvHiRzZs3M3PmTNasWcPWrVvN+6lbty4pKSn89NNPmbZPRERERMRgMNC1a1fWrl2bLiC+bNkynn76aQIDA1m1ahUTJkxg2LBh/Pzzz4SHh/O///2PESNG3NV+085zP//8c+bPn8+WLVu4ePEiPXv2pGjRouzYsYPRo0czb948jh49CsDGjRv56KOPGD9+PL/++iufffYZP/30ExMnTsx0P9u2beOBBx6gZMmSd9XO26lcuTL+/v5s377d5tsWEbEXBdFFRBzs1KlTmEwmHnzwwSwt7+HhwebNmxk7dixeXl54e3vzzDPPcPHiRc6ePWteztXVlcGDB+Ph4UHJkiV54YUX2LFjBwkJCRlud+zYsVSuXJkBAwZQvXp1evXqRVhYGL///nuW2rV69WpWr17NRx99ZC7DMm/ePHr37k3FihVxdXUlJCSEnj17smzZsky3ExERQcWKFdNNL1myJE8//TTu7u40b96clJQUWrZsSfHixSlUqBB16tTh+PHjVuu0bt2aUqVKUbBgQerUqcOlS5d45ZVX8PT0pGbNmgQGBnLs2DHz8v7+/hQvXjzDy3JFRERERCx16NABwOpGmb/++itHjx6lR48eACxatIhWrVrRuHFjPDw8KFu2LP369WPbtm23vUr0Tjp37kxgYCBFixalatWqAHTr1g13d3datGgBYD43njdvHs899xw1atTA1dWVBx98kEGDBvHVV19hNBoz3H5m5+S24OLiwsMPP6xzbhFxKs5ZnEtE5B7i6uoKkC7Tu2fPnuzfvx9IrUlYrVo1Fi5cCMCWLVtYuHAhp06dIj4+HpPJBGAVIC9TpoxVZnvJkiVJTk7m4sWL5qx2S4GBgcyaNYsLFy7wv//9j99++42NGzfy6aef8swzzzBlypRMb/J57Ngx3nrrLYYMGUK1atUAuHbtGpcvX2bixIm8//775mXT2pqYmJhhdvvVq1cpXLhwuuklSpQwP07LUi9evLh5mre3d7ofCCwzZ7y9vbnvvvus9pnROgEBAXavPy8iIiIizq9AgQK0a9eOpUuXmsutLF26lOrVq5sD0CdPnuSpp56yWq9MmTKYTCZOnjzJfffdd1f7tjw39vb2TndeDBAfHw+knqsfPnyYxYsXW23DZDJx7tw5SpcunW77V69ezXaZluzQObeIOBsF0UVEHKx06dK4u7tz5MgRKlWqZJ6eFjCH1PqDZ86cAWD//v28/vrrDBs2jBdeeAFfX192795Nr169rLabVtolTVrw2tPT87btCQoKonXr1rRu3RpIreE4ZswY2rVrR7169dItn5CQwNChQ6levbrVDUjTAu6TJ09O98XhTjIK1ru4pL94KqNpt9vOnZbPbN8iIiIiIhl54YUXWLJkCXv27KF8+fJs3LiRyZMnWy1z6/ll2nl5Vs87bz2vz2jd253nuri4MHToUKtz9buV1vbbtS0rdM4tIs5G5VxERBysYMGCNGvWjIULF5pv3nkry5PTX3/9FS8vL1566SV8fX0BrG4qmubUqVNWN/A8deoU7u7uGWa7nDlzhrfffptTp06lm9esWTOATC83nTBhAlFRUUyaNMnqZLhAgQIUKVKEw4cPWy1/+fJlq5uk3iowMDBHl7bmVGaZ8CIiIiIitypbtix169blyy+/ZN26dQQGBtK8eXPz/DJlyvDXX39ZrfPPP//g4uLC/fffn257np6e6UqsWJZsvBsPPPBAunPy6OhooqOjM10nMDCQyMjIdNMvX75s9fzWcopZpXNuEXE2CqKLiOQB48aNw2g00rt3b/bv309SUhLJycn89ddfTJo0ifXr11O9enUg9dLN+Pgkli84AAAENUlEQVR4Dhw4QHJyMuvWrTPfCPPcuXPmbSYkJBAWFkZsbCynTp1iyZIltGrVKsMslSJFirBr1y6GDRvGb7/9RmJiIikpKZw4cYJ33nmHwMDADLPQ169fz5dffslHH31EYGBguvm9e/dm2bJl7Nixg6SkJI4fP85LL73EpEmTMn0vKlSo4LD6iFFRUZw7d86ul66KiIiIyL2lR48ebNmyha+++oquXbuayzVCaonGDRs2sH37doxGI3/++SezZs2iVatW+Pv7p9vWQw89xG+//UZUVBQA33///V0HqtP07t2bTZs2sXbtWhITE7lw4QLDhg1j+PDhma6T2Tn52bNnWblyJUajkb179/Ljjz9muz0pKSn8+eefOucWEaeici4iInlAYGAgX3/9NQsWLODtt9/mzJkzGI1GihQpQrVq1Vi8eDFVqlQB4Mknn+T555+nf//+uLi40Lx5c2bMmMHLL7/Mq6++ypQpUwAICQnB09OTJ598kvj4eGrXrs2bb76Z4f49PDxYvnw5n332GaNGjeLChQtAanC9Xr16rFy5MsMg+bJly0hJSaF3795W019++WUGDhxIr169iIuLIzQ0lIsXLxIQEMBTTz3Fa6+9lul70bBhQ95//30SEhLuWHrG1nbu3InBYMjwBwMRERERkYw0aNCAokWLcuzYMTp27Gg175lnnuHKlSuMHz+e8+fPU6xYMVq0aMHgwYMz3NaLL77IX3/9xVNPPUVAQAANGzakTZs2HDly5K7b17JlS65cucKnn37K6NGjKVCgAI0aNWLUqFGZrtOwYUM+//xzzp07Z1Vv/cEHH+S7777jvffe45FHHqF///6MHj2apKQk8/2YDh48SEhISLptbt26lfvuu4+DBw8SFRVFw4YN7/o1iYjkNoPp1oJWIiLi9NJqqC9atMjRTcm22NhYnnzySfr372++QVNu6dSpEw899BDjx4/P1f2KiIiIiOQlJpOJtm3bUrt2bXOwfdSoURw7dowVK1bkaNtDhw4lOjqa+fPn26KpIiK5QuVcREQkT/Hx8WH48OF89tlnGdZhtJfvvvuOEydO8Morr+TaPkVERERE8iKDwcAbb7zB8uXL+ffff2223YMHD7J161b+7//+z2bbFBHJDQqii4hIntOuXTuaNm3KyJEjyY0Lpk6dOkVoaCgff/wxQUFBdt+fiIiIiEheV6tWLXPJyPj4+BxvLzo6mqFDhxIaGkrFihVt0EIRkdyjci4iIiIiIiIiIiIiIplQJrqIiIiIiIiIiIiISCYURBcRERERERERERERyYSC6CIiIiIiIiIiIiIimVAQXUREREREREREREQkEwqii4iIiIiIiIiIiIhkQkF0EREREREREREREZFMKIguIiIiIiIiIiIiIpIJBdFFRERERERERERERDKhILqIiIiIiIiIiIiISCb+HxEshEPhofBWAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sensitivity analysis completed\n" + ] + } + ], + "source": [ + "# Parameter sensitivity analysis around the optimal point\n", + "print(\"Performing parameter sensitivity analysis...\")\n", + "\n", + "# Create parameter variations around the optimal point\n", + "n_points = 50\n", + "sensitivity_results = {}\n", + "\n", + "fig, axes = plt.subplots(2, 2, figsize=(15, 10))\n", + "fig.suptitle('Parameter Sensitivity Analysis Around Optimal Point', fontsize=14, fontweight='bold')\n", + "\n", + "for param_idx, param_name in enumerate(param_names):\n", + " print(f\"Analyzing sensitivity for {param_name}...\")\n", + " \n", + " # Create range around optimal value\n", + " optimal_value = best_params[param_idx].item()\n", + " param_min = bounds[0, param_idx].item()\n", + " param_max = bounds[1, param_idx].item()\n", + " \n", + " # Create range ±20% around optimal value, bounded by parameter limits\n", + " range_width = (param_max - param_min) * 0.2\n", + " test_min = max(param_min, optimal_value - range_width)\n", + " test_max = min(param_max, optimal_value + range_width)\n", + " test_values = np.linspace(test_min, test_max, n_points)\n", + " \n", + " # Create test parameters (fix other parameters at optimal values)\n", + " test_params_list = []\n", + " for test_val in test_values:\n", + " test_param = best_params.clone()\n", + " test_param[param_idx] = test_val\n", + " test_params_list.append(test_param)\n", + " \n", + " # Stack into batch tensor\n", + " test_batch = torch.stack(test_params_list).float()\n", + " \n", + " # Evaluate objective function\n", + " with torch.no_grad():\n", + " test_scores = objective_function(test_batch).cpu().numpy().flatten()\n", + " \n", + " # Store results\n", + " sensitivity_results[param_name] = {\n", + " 'values': test_values,\n", + " 'scores': test_scores,\n", + " 'optimal_value': optimal_value,\n", + " 'optimal_score': best_score\n", + " }\n", + " \n", + " # Plot results\n", + " ax = axes[param_idx // 2, param_idx % 2]\n", + " ax.plot(test_values, test_scores, 'b-', linewidth=2, label='Success Probability')\n", + " ax.axvline(x=optimal_value, color='red', linestyle='--', linewidth=2, \n", + " label=f'Optimal: {optimal_value:.3f}')\n", + " ax.axhline(y=best_score, color='green', linestyle=':', alpha=0.7, \n", + " label=f'Best Score: {best_score:.4f}')\n", + " \n", + " units = ['mg/mL', 'mm/s', 'mm', 'μL']\n", + " ax.set_xlabel(f'{param_name} ({units[param_idx]})')\n", + " ax.set_ylabel('Success Probability')\n", + " ax.set_title(f'Sensitivity: {param_name}')\n", + " ax.legend()\n", + " ax.grid(True, alpha=0.3)\n", + " \n", + " # Calculate sensitivity metrics\n", + " score_range = test_scores.max() - test_scores.min()\n", + " param_range = test_max - test_min\n", + " sensitivity = score_range / param_range if param_range > 0 else 0\n", + " \n", + " print(f\" Range tested: [{test_min:.3f}, {test_max:.3f}]\")\n", + " print(f\" Score range: [{test_scores.min():.4f}, {test_scores.max():.4f}]\")\n", + " print(f\" Sensitivity: {sensitivity:.6f} (score change per unit parameter change)\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(\"Sensitivity analysis completed\")\n" + ] + }, + { + "cell_type": "raw", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "## How to Use This Notebook\n", + "\n", + "### Prerequisites\n", + "1. **Trained Model**: You need a trained logistic regression model (.pkl or .joblib file)\n", + "2. **Trained Scaler**: You need a fitted StandardScaler (.pkl or .joblib file)\n", + "3. **Model Requirements**: Your model should be compatible with the feature engineering pipeline\n", + "\n", + "### Quick Start\n", + "1. Run all cells in order\n", + "2. When prompted, provide the paths to your model and scaler files\n", + "3. The notebook will automatically:\n", + " - Load your model\n", + " - Create the objective function with proper feature engineering\n", + " - Run Bayesian optimization\n", + " - Analyze results and provide recommendations\n", + "\n", + "### Customization Options\n", + "- **Parameter Bounds**: Modify the `bounds` tensor to match your experimental ranges\n", + "- **Optimization Settings**: Adjust `n_iterations`, `n_initial_points`, and `batch_size` as needed\n", + "- **Visualization**: Customize plots by modifying the plotting sections\n", + "\n", + "### Expected Output\n", + "- Optimal printing parameters\n", + "- Success probability predictions\n", + "- Sensitivity analysis\n", + "- Detailed visualizations\n", + "- Actionable recommendations\n", + "\n", + "### Troubleshooting\n", + "- **Model Loading Issues**: Check file paths and ensure files are valid joblib/pickle files\n", + "- **Feature Count Mismatch**: The factory function automatically adapts to your model's feature count\n", + "- **Memory Issues**: Reduce `batch_size` or `n_iterations` for faster execution\n", + "\n", + "For more information, see the main package documentation and example scripts.\n" + ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "polyprintenv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/aamp_app/optimizer/notebooks/tutorial_notebook.ipynb b/aamp_app/optimizer/notebooks/tutorial_notebook.ipynb new file mode 100644 index 0000000..3af6f30 --- /dev/null +++ b/aamp_app/optimizer/notebooks/tutorial_notebook.ipynb @@ -0,0 +1,70 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setup and imports\n", + "import torch\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from bayesian_optimizer import BayesianOptimizer\n", + "from objective_function import MockObjectiveFunction\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "# Configure plots\n", + "plt.style.use('default')\n", + "plt.rcParams['figure.figsize'] = (10, 6)\n", + "plt.rcParams['font.size'] = 12\n", + "\n", + "print(\"Tutorial setup complete!\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "## Hands-On Example\n", + "\n", + "# Let's implement a simple optimization step-by-step:\n", + "\n", + "# Step 1: Define the problem\n", + "print(\"Step 1: Problem Definition\")\n", + "print(\"=\" * 40)\n", + "\n", + "# Parameter bounds\n", + "bounds = torch.tensor([\n", + " [0.1, 1.0], # concentration\n", + " [10.0, 100.0], # print_speed\n", + " [0.05, 0.5], # gap_size\n", + " [5.0, 25.0] # volume\n", + "]).T\n", + "\n", + "param_names = ['concentration', 'print_speed', 'gap_size', 'volume']\n", + "param_units = ['', 'mm/s', 'mm', 'μL']\n", + "\n", + "print(\"Parameter space:\")\n", + "for i, (name, unit) in enumerate(zip(param_names, param_units)):\n", + " lower, upper = bounds[0, i], bounds[1, i]\n", + " unit_str = f\" ({unit})\" if unit else \"\"\n", + " print(f\" {name}: [{lower:.2f}, {upper:.2f}]{unit_str}\")\n", + "\n", + "print(f\"\\nTotal search space size: {np.prod(bounds[1] - bounds[0]):.2e}\")\n", + "print(\"This is a 4-dimensional continuous optimization problem!\")\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/aamp_app/optimizer/objective_function.py b/aamp_app/optimizer/objective_function.py new file mode 100644 index 0000000..6ab40c0 --- /dev/null +++ b/aamp_app/optimizer/objective_function.py @@ -0,0 +1,199 @@ +""" +Mock Objective Function for Bayesian Optimization Demo + +This module contains a realistic simulation of a black-box objective function +for a printing process optimization problem. The function has a single maximum +and includes realistic noise levels. +""" + +import torch +import numpy as np +from typing import Tuple + + +class MockObjectiveFunction: + """ + Mock objective function simulating a printing process optimization. + + This function represents a realistic black-box optimization scenario where: + - The objective is to maximize print quality (score) + - The function has a single global maximum + - Realistic noise is added to simulate measurement uncertainty + - The function depends on 4 continuous parameters + + Parameters: + concentration: [0.1, 1.0] - Material concentration + print_speed: [10.0, 100.0] - Printing speed (mm/s) + gap_size: [0.05, 0.5] - Gap size between layers (mm) + volume: [5.0, 25.0] - Volume per drop (μL) + """ + + def __init__(self, noise_std: float = 0.1, seed: int = 42): + """ + Initialize the mock objective function. + + Args: + noise_std: Standard deviation of Gaussian noise added to observations + seed: Random seed for reproducible results + """ + self.noise_std = noise_std + self.seed = seed + + # Set random seed for reproducible results + torch.manual_seed(seed) + np.random.seed(seed) + + # Define the optimal parameters (global maximum) + # These represent the "true" best settings for our simulated process + self.optimal_params = torch.tensor([ + 0.6, # concentration: medium-high concentration + 45.0, # print_speed: moderate speed + 0.2, # gap_size: medium gap + 15.0 # volume: medium volume + ]) + + # Parameter bounds for normalization + self.bounds = torch.tensor([ + [0.1, 1.0], # concentration + [10.0, 100.0], # print_speed + [0.05, 0.5], # gap_size + [5.0, 25.0] # volume + ]) + + # Maximum possible score (achieved at optimal parameters) + self.max_score = 1.0 + + def __call__(self, X: torch.Tensor) -> torch.Tensor: + """ + Evaluate the objective function on a batch of parameter sets. + + Args: + X: Input tensor of shape (batch_size, 4) containing parameter values + + Returns: + Tensor of shape (batch_size, 1) containing noisy objective values + """ + # Ensure input is the correct shape + if X.dim() == 1: + X = X.unsqueeze(0) + + batch_size = X.shape[0] + + # Normalize parameters to [0, 1] range for easier computation + X_normalized = self._normalize_parameters(X) + optimal_normalized = self._normalize_parameters(self.optimal_params.unsqueeze(0)) + + # Calculate base score using a combination of Gaussian and polynomial terms + scores = self._calculate_base_score(X_normalized, optimal_normalized.squeeze(0)) + + # Add realistic noise to simulate measurement uncertainty + noise = torch.randn(batch_size, 1) * self.noise_std + noisy_scores = scores + noise + + # Ensure scores are non-negative (realistic for quality metrics) + noisy_scores = torch.clamp(noisy_scores, min=0.0) + + return noisy_scores + + def _normalize_parameters(self, X: torch.Tensor) -> torch.Tensor: + """ + Normalize parameters to [0, 1] range based on their bounds. + + Args: + X: Parameter tensor of shape (batch_size, 4) + + Returns: + Normalized parameter tensor + """ + lower_bounds = self.bounds[:, 0] + upper_bounds = self.bounds[:, 1] + + # Normalize to [0, 1] + X_normalized = (X - lower_bounds) / (upper_bounds - lower_bounds) + + return X_normalized + + def _calculate_base_score(self, X_norm: torch.Tensor, optimal_norm: torch.Tensor) -> torch.Tensor: + """ + Calculate the base (noise-free) objective score. + + This function creates a realistic objective landscape with: + - A single global maximum at the optimal parameters + - Smooth transitions between parameter regions + - Realistic parameter interactions + + Args: + X_norm: Normalized parameter tensor (batch_size, 4) + optimal_norm: Normalized optimal parameters (4,) + + Returns: + Base scores tensor (batch_size, 1) + """ + batch_size = X_norm.shape[0] + + # Calculate distance from optimal parameters + distances = torch.norm(X_norm - optimal_norm, dim=1) + + # Primary Gaussian component centered at optimal parameters + gaussian_scores = torch.exp(-8 * distances**2) + + # Add parameter-specific effects to create more realistic landscape + + # Concentration effect: too low or too high concentration reduces quality + conc_effect = 1.0 - 2.0 * (X_norm[:, 0] - 0.5)**2 + + # Print speed effect: very slow or very fast reduces quality + speed_effect = 1.0 - 1.5 * (X_norm[:, 1] - 0.4)**2 + + # Gap size effect: optimal gap size is critical + gap_effect = 1.0 - 3.0 * (X_norm[:, 2] - 0.3)**2 + + # Volume effect: moderate volume is best + volume_effect = 1.0 - 1.2 * (X_norm[:, 3] - 0.5)**2 + + # Combine all effects + combined_effects = (conc_effect + speed_effect + gap_effect + volume_effect) / 4.0 + + # Final score combines Gaussian and parameter effects + base_scores = 0.7 * gaussian_scores + 0.3 * combined_effects + + # Scale to reasonable range and ensure maximum is achievable + base_scores = self.max_score * torch.clamp(base_scores, min=0.0, max=1.0) + + return base_scores.unsqueeze(1) + + def get_optimal_parameters(self) -> Tuple[torch.Tensor, float]: + """ + Get the true optimal parameters and maximum score. + + Returns: + Tuple of (optimal_parameters, max_score) + """ + return self.optimal_params, self.max_score + + def evaluate_at_optimal(self) -> float: + """ + Evaluate the function at the optimal parameters (useful for comparison). + + Returns: + Score at optimal parameters (without noise) + """ + optimal_normalized = self._normalize_parameters(self.optimal_params.unsqueeze(0)) + base_score = self._calculate_base_score(optimal_normalized, optimal_normalized.squeeze(0)) + return base_score.item() + + +# Convenience function for backward compatibility +def mock_objective(X: torch.Tensor, noise_std: float = 0.1) -> torch.Tensor: + """ + Convenience function to evaluate the mock objective function. + + Args: + X: Input tensor of shape (batch_size, 4) + noise_std: Standard deviation of noise to add + + Returns: + Noisy objective values of shape (batch_size, 1) + """ + objective = MockObjectiveFunction(noise_std=noise_std) + return objective(X) \ No newline at end of file diff --git a/aamp_app/optimizer/optimization_progress.png b/aamp_app/optimizer/optimization_progress.png new file mode 100644 index 0000000000000000000000000000000000000000..52371bab271e84da569a467493e731e06c3ab4ea GIT binary patch literal 228134 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�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.13.0 +botorch>=0.8.0 +gpytorch>=1.9.0 + +# Scientific computing and utilities +numpy>=1.21.0 +scipy>=1.7.0 +matplotlib>=3.5.0 +seaborn>=0.11.0 + +# Optional: for better numerical stability +scikit-learn>=1.0.0 + +# For interactive notebooks +jupyter>=1.0.0 +pandas>=1.3.0 \ No newline at end of file diff --git a/aamp_app/optimizer/setup.py b/aamp_app/optimizer/setup.py new file mode 100644 index 0000000..8943a77 --- /dev/null +++ b/aamp_app/optimizer/setup.py @@ -0,0 +1,53 @@ +""" +Setup script for the Bayesian Optimization package. +""" + +from setuptools import setup, find_packages + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +with open("requirements.txt", "r", encoding="utf-8") as fh: + requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")] + +setup( + name="polyprint-bayesian-optimizer", + version="1.0.0", + author="Polyprint Project", + description="A complete Bayesian optimization package for manufacturing process optimization", + long_description=long_description, + long_description_content_type="text/markdown", + packages=find_packages(), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering :: Mathematics", + ], + python_requires=">=3.8", + install_requires=requirements, + extras_require={ + "dev": [ + "pytest>=6.0", + "black>=22.0", + "flake8>=4.0", + "mypy>=0.900", + ] + }, + entry_points={ + "console_scripts": [ + "run-bo-demo=optimizer.run_demo:main", + ], + }, + include_package_data=True, + package_data={ + "optimizer": ["*.md", "*.txt"], + }, +) \ No newline at end of file diff --git a/aamp_app/pages/bayesian-optimization.py b/aamp_app/pages/bayesian-optimization.py index 5dcb781..d8088e5 100644 --- a/aamp_app/pages/bayesian-optimization.py +++ b/aamp_app/pages/bayesian-optimization.py @@ -3,8 +3,10 @@ import dash import numpy as np import pandas as pd -from datetime import datetime, timedelta -import random +import io +import contextlib +import matplotlib.pyplot as plt +import base64 dash.register_page( __name__, @@ -13,6 +15,177 @@ title="Optimization" ) +# def plot_optimization_results(optimizer, objective): +# """Create selective plots of the optimization results (1, 3, and 4 only).""" + +# import matplotlib.pyplot as plt +# import numpy as np +# import base64 +# # import io + +# history = optimizer.get_optimization_history() +# train_X, train_Y = optimizer.get_training_data() +# true_params, _ = objective.get_optimal_parameters() +# true_score = objective.evaluate_at_optimal() + +# # Create a 1x3 subplot layout for the three selected plots +# fig, axes = plt.subplots(1, 3, figsize=(21, 6)) # Wider layout + +# ### 1. Optimization Progress (axes[0]) +# iterations = [h['iteration'] for h in history] +# best_values = [h['best_value'] for h in history] + +# axes[0].plot(iterations, best_values, 'b-o', linewidth=2, markersize=6) +# axes[0].axhline(y=true_score, color='r', linestyle='--', alpha=0.7, +# label=f'True optimum: {true_score:.3f}') +# axes[0].set_xlabel('Iteration') +# axes[0].set_ylabel('Best Observed Value') +# axes[0].set_title('Optimization Progress') +# axes[0].legend() +# axes[0].grid(True, alpha=0.3) + +# ### 3. Parameter Space Exploration (axes[1]) +# scatter = axes[1].scatter(train_X[:, 0], train_X[:, 1], c=train_Y.squeeze(), +# cmap='viridis', alpha=0.6, s=50) +# axes[1].scatter(optimizer.best_parameters[0], optimizer.best_parameters[1], +# c='red', s=200, marker='*', label='Best found', +# edgecolor='black', linewidth=2) +# axes[1].scatter(true_params[0], true_params[1], c='orange', s=200, marker='*', +# label='True optimum', edgecolor='black', linewidth=2) +# axes[1].set_xlabel('Concentration') +# axes[1].set_ylabel('Print Speed (mm/s)') +# axes[1].set_title('Parameter Space Exploration') +# axes[1].legend() +# plt.colorbar(scatter, ax=axes[1], label='Objective Value') + +# ### 4. Gap Size vs Volume (axes[2]) +# scatter2 = axes[2].scatter(train_X[:, 2], train_X[:, 3], c=train_Y.squeeze(), +# cmap='viridis', alpha=0.6, s=50) +# axes[2].scatter(optimizer.best_parameters[2], optimizer.best_parameters[3], +# c='red', s=200, marker='*', label='Best found', +# edgecolor='black', linewidth=2) +# axes[2].scatter(true_params[2], true_params[3], c='orange', s=200, marker='*', +# label='True optimum', edgecolor='black', linewidth=2) +# axes[2].set_xlabel('Gap Size (mm)') +# axes[2].set_ylabel('Volume (μL)') +# axes[2].set_title('🔍 Gap Size vs Volume') +# axes[2].legend() +# plt.colorbar(scatter2, ax=axes[2], label='Objective Value') + +# plt.tight_layout() +# plt.show() + +# buf = io.BytesIO() +# fig.savefig(buf, format="png", bbox_inches='tight') +# buf.seek(0) +# encoded_image = base64.b64encode(buf.read()).decode("utf-8") +# buf.close() +# plt.close(fig) + +# return encoded_image + + +# def plot_optimization_results(optimizer, objective): +# """Create comprehensive plots of the optimization results.""" + +# history = optimizer.get_optimization_history() +# train_X, train_Y = optimizer.get_training_data() +# true_params, _ = objective.get_optimal_parameters() +# true_score = objective.evaluate_at_optimal() + +# # Create the visualization +# fig, axes = plt.subplots(2, 3, figsize=(18, 12)) + +# # 1. Optimization Progress +# iterations = [h['iteration'] for h in history] +# best_values = [h['best_value'] for h in history] + +# axes[0, 0].plot(iterations, best_values, 'b-o', linewidth=2, markersize=6) +# axes[0, 0].axhline(y=true_score, color='r', linestyle='--', alpha=0.7, +# label=f'True optimum: {true_score:.3f}') +# axes[0, 0].set_xlabel('Iteration') +# axes[0, 0].set_ylabel('Best Observed Value') +# axes[0, 0].set_title('Optimization Progress') +# axes[0, 0].legend() +# axes[0, 0].grid(True, alpha=0.3) + +# # 2. Improvement Rate +# # improvements = [best_values[i] - best_values[0] for i in range(len(best_values))] +# # axes[0, 1].plot(iterations, improvements, 'g-o', linewidth=2, markersize=6) +# # axes[0, 1].set_xlabel('Iteration') +# # axes[0, 1].set_ylabel('Improvement from Initial') +# # axes[0, 1].set_title('Cumulative Improvement') +# # axes[0, 1].grid(True, alpha=0.3) + +# # 3. Parameter Space Exploration (Concentration vs Print Speed) +# scatter = axes[0, 2].scatter(train_X[:, 0], train_X[:, 1], c=train_Y.squeeze(), +# cmap='viridis', alpha=0.6, s=50) +# axes[0, 2].scatter(optimizer.best_parameters[0], optimizer.best_parameters[1], +# c='red', s=200, marker='*', label='Best found', +# edgecolor='black', linewidth=2) +# axes[0, 2].scatter(true_params[0], true_params[1], c='orange', s=200, marker='*', +# label='True optimum', edgecolor='black', linewidth=2) +# axes[0, 2].set_xlabel('Concentration') +# axes[0, 2].set_ylabel('Print Speed (mm/s)') +# axes[0, 2].set_title('Parameter Space Exploration') +# axes[0, 2].legend() +# plt.colorbar(scatter, ax=axes[0, 2], label='Objective Value') + +# # 4. Gap Size vs Volume +# scatter2 = axes[1, 0].scatter(train_X[:, 2], train_X[:, 3], c=train_Y.squeeze(), +# cmap='viridis', alpha=0.6, s=50) +# axes[1, 0].scatter(optimizer.best_parameters[2], optimizer.best_parameters[3], +# c='red', s=200, marker='*', label='Best found', +# edgecolor='black', linewidth=2) +# axes[1, 0].scatter(true_params[2], true_params[3], c='orange', s=200, marker='*', +# label='True optimum', edgecolor='black', linewidth=2) +# axes[1, 0].set_xlabel('Gap Size (mm)') +# axes[1, 0].set_ylabel('Volume (μL)') +# axes[1, 0].set_title('🔍 Gap Size vs Volume') +# axes[1, 0].legend() +# plt.colorbar(scatter2, ax=axes[1, 0], label='Objective Value') + +# # 5. Objective Value Distribution +# # axes[1, 1].hist(train_Y.squeeze().numpy(), bins=20, alpha=0.7, color='skyblue', +# # edgecolor='black') +# # axes[1, 1].axvline(optimizer.best_observed_value, color='red', linestyle='--', +# # linewidth=2, label=f'Best: {optimizer.best_observed_value:.3f}') +# # axes[1, 1].axvline(true_score, color='orange', linestyle='--', linewidth=2, +# # label=f'True: {true_score:.3f}') +# # axes[1, 1].set_xlabel('Objective Value') +# # axes[1, 1].set_ylabel('Frequency') +# # axes[1, 1].set_title('Objective Value Distribution') +# # axes[1, 1].legend() +# # axes[1, 1].grid(True, alpha=0.3) + +# # 6. Parameter Convergence +# # param_names = ['gap_size', 'volume'] +# # eval_order = np.arange(len(train_X)) + +# # for i, name in enumerate(param_names): +# # color = plt.cm.Set1(i) +# # axes[1, 2].scatter(eval_order, train_X[:, i], alpha=0.6, s=30, +# # c=color, label=name) +# # axes[1, 2].axhline(y=true_params[i], color=color, linestyle='--', alpha=0.7) + +# # axes[1, 2].set_xlabel('Evaluation Order') +# # axes[1, 2].set_ylabel('Parameter Value') +# # axes[1, 2].set_title('Parameter Convergence') +# # axes[1, 2].legend() +# # axes[1, 2].grid(True, alpha=0.3) + +# plt.tight_layout() +# plt.show() + +# buf = io.BytesIO() +# fig.savefig(buf, format="png", bbox_inches='tight') +# buf.seek(0) +# encoded_image = base64.b64encode(buf.read()).decode("utf-8") +# buf.close() +# plt.close(fig) # Close the figure to prevent it from showing + +# return encoded_image + layout = html.Div( [ html.Div([ @@ -87,104 +260,117 @@ html.Div(id="bo-hyperparam-fields"), + dbc.Row( + [ + dbc.Col([html.H5("Select Parameters to Include")], width=5), + ], + className="mb-2", + ), + + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + dcc.Dropdown( + id="bo-params-dropdown", + options=["Concentration", "Print Speed", "Gap Size", "Volume"], + multi=True, + value=["Concentration", "Print Speed", "Gap Size", "Volume"], + ), + ], + width=6, + ), + ], + className="mb-3", + ), + ], + id="bo-hyperparam-fields", + ), + dbc.Row([ dbc.Col([ - html.Label("Number of Batches"), - dbc.Input(id="bo-num-batches", type="number", value=3, min=1) + html.Label("Batch Size"), + dbc.Input(id="bo-batch-size", type="number", value=8, min=1) ], width=4), - dcc.Interval(id="bo-generator-timer", interval=1000, n_intervals=0, disabled=True), - dcc.Store(id="bo-generated-data", data=[]), dbc.Col([ - html.Label("Stopping Criterion"), - dcc.Dropdown( - id="bo-stopping-criterion", - options=[ - {"label": "Max Iterations", "value": "max_iter"}, - # {"label": "Convergence (No Improvement)", "value": "no_improve"}, - {"label": "Time Limit (minutes)", "value": "time_limit"}, - # {"label": "Target Objective Reached", "value": "target_value"}, - ], - placeholder="Select stopping rule" + html.Label("Target Objective (Required)"), + dbc.Input( + id="bo-target-objective", + type="number", + min=0, + placeholder="e.g., 0.95" ) ], width=4), - dbc.Col([ - html.Label("Stopping Threshold"), - dbc.Input(id="bo-stopping-value", type="number", placeholder="Enter threshold") - ], width=4), + # dbc.Col([ + # html.Label("Number of Iterations (Required)"), + # dbc.Input(id="bo-maxiter", type="number", placeholder="e.g., 15", min=1) + # ], width=4), ], className="mb-3"), - dbc.Button("Generate Optimizer Parameters", id="bo-generate-btn", color="primary", className="mb-3"), - html.Div(id="optimizer-output"), - - html.Div([ - dbc.Row([ - dbc.Col([ - dbc.Button("Pause", id="bo-pause-btn", color="warning", className="me-2"), - html.Span("Status: ", style={"fontWeight": "bold"}), - html.Span(id="bo-status-label", children="Idle") - ]) - ], className="mb-3") - ]), - - dcc.Store(id="bo-status-store", data="idle"), - - html.Div(id="bo-generated-table"), - - dbc.Button("Save Optimizer Parameters", id="bo-save-btn", color="success", className="mt-3", disabled=True), - - dbc.Alert(id="bo-save-alert", is_open=False, color="success", className="mt-3") + dbc.Button("Generate and Save Optimizer Parameters", id="bo-generate-btn", color="primary", className="mb-3"), + html.Pre(id="optimizer-output1", style={"whiteSpace": "pre-wrap", "border": "1px solid #ccc", "padding": "10px"}), ], className="container", ) @callback( - Output("optimizer-output", "children"), + Output("optimizer-output1", "children"), Input("bo-generate-btn", "n_clicks"), - State("bo-num-batches", "value"), - State("bo-stopping-criterion", "value"), - State("bo-stopping-value", "value"), + State("bo-batch-size", "value"), + State("bo-target-objective", "value"), + # State("bo-maxiter", "value"), prevent_initial_call=True ) -def generate_optimizer_parameters(n_clicks, num_batches, stopping_criterion, stopping_value): +def generate_optimizer_parameters(n_clicks, bs, target_objective): import torch from optimizer import BayesianOptimizer, MockObjectiveFunction - # Define search space bounds - if n_clicks > 0: - # bounds = torch.tensor([ - # [0.1, 1.0], # concentration - # [10.0, 100.0], # print_speed - # [0.05, 0.5], # gap_size - # [5.0, 25.0] # volume - # ]).T - bounds = torch.tensor([ - [2, 20], # concentration - [50.0, 100.0], # print_speed - [0.01, 20], # gap_size - [6.0, 12.0] # volume - ]).T - - # Create optimizer and objective function - optimizer = BayesianOptimizer(bounds=bounds, batch_size=num_batches) - objective = MockObjectiveFunction() - - # Run optimization - best_params, best_score = optimizer.optimize( - objective_function=objective, - n_iterations=stopping_value, - n_initial_points=10 - ) - return html.Div([ - html.H3("Optimization Results:"), - html.P(f"Best concentration: {best_params[0]:.4f}"), - html.P(f"Best print speed: {best_params[1]:.4f}"), - html.P(f"Best gap size: {best_params[2]:.4f}"), - html.P(f"Best volume: {best_params[3]:.4f}"), - html.P(f"Best score: {best_score:.4f}") - ]) + f = io.StringIO() + # plot_images = [] + with contextlib.redirect_stdout(f): + # Define search space bounds + if n_clicks > 0: + bounds = torch.tensor([ + [0.1, 1.0], # concentration + [10.0, 100.0], # print_speed + [0.05, 0.5], # gap_size + [5.0, 25.0] # volume + ]).T + # bounds = torch.tensor([ + # [2, 20], # concentration + # [0.01, 20], # print_speed + # [50.0, 100.0], # gap_size + # [6.0, 12.0] # volume + # ]).T + + # Create optimizer and objective function + optimizer = BayesianOptimizer(bounds=bounds, batch_size=bs) + objective = MockObjectiveFunction() + + # Run optimization + best_params, best_score, plot_images = optimizer.optimize( + objective_function=objective, + n_iterations=200, + n_initial_points=10, + target=target_objective + ) + print("\n\n") + # plot = plot_optimization_results(optimizer, objective) + # plot_images.append(plot) + # plot_images.append("plot") + + log_text = f.getvalue() + + # Create image components from the base64-encoded plot images + image_components = [html.Img(src=f"data:image/png;base64,{img}", style={"width": "100%"}) for img in plot_images] + + # Return the log text and plot images as components + return image_components + [log_text] # @callback( # Output("bo-generator-timer", "disabled"), @@ -195,7 +381,7 @@ def generate_optimizer_parameters(n_clicks, num_batches, stopping_criterion, sto # Input("bo-generator-timer", "n_intervals"), # State("bo-status-store", "data"), # 🆕 check if paused # State("bo-generated-data", "data"), -# State("bo-num-batches", "value"), +# State("bo-batch-size", "value"), # prevent_initial_call=True # ) # def manage_bo_generation(n_clicks, n_intervals, status, current_data, num_batches): @@ -245,27 +431,6 @@ def generate_optimizer_parameters(n_clicks, num_batches, stopping_criterion, sto # return dash.no_update, dash.no_update, dash.no_update, dash.no_update -@callback( - Output("bo-save-alert", "children"), - Output("bo-save-alert", "is_open"), - Input("bo-save-btn", "n_clicks"), - prevent_initial_call=True -) -def save_bo_params(n): - return "BO parameter sets saved successfully!", True -@callback( - Output("bo-stopping-value", "placeholder"), - Input("bo-stopping-criterion", "value") -) -def update_bo_stopping_placeholder(mode): - if mode == "max_iter": - return "e.g., 10 iterations" - elif mode == "no_improve": - return "e.g., 3 stagnant batches" - elif mode == "time_limit": - return "e.g., 30 minutes" - return "Enter threshold" - # @callback( # Output("bo-status-store", "data"), # Output("bo-pause-btn", "children"), From 8cd1f8033e2e72538c1fab1f3a156c052a4870d9 Mon Sep 17 00:00:00 2001 From: weiqiz3 Date: Sun, 21 Sep 2025 15:09:01 -0500 Subject: [PATCH 104/125] test for retrieving plots with gridfs --- aamp_app/pages/bayesian-optimization.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aamp_app/pages/bayesian-optimization.py b/aamp_app/pages/bayesian-optimization.py index d8088e5..dd12e97 100644 --- a/aamp_app/pages/bayesian-optimization.py +++ b/aamp_app/pages/bayesian-optimization.py @@ -7,6 +7,8 @@ import contextlib import matplotlib.pyplot as plt import base64 +from pymongo import MongoClient +import gridfs dash.register_page( __name__, @@ -15,6 +17,9 @@ title="Optimization" ) +client = MongoClient('mongodb://localhost:27017/') +db = client['diaogroup'] + # def plot_optimization_results(optimizer, objective): # """Create selective plots of the optimization results (1, 3, and 4 only).""" @@ -360,6 +365,9 @@ def generate_optimizer_parameters(n_clicks, bs, target_objective): target=target_objective ) print("\n\n") + buf = io.BytesIO() + fig.savefig(buf, format='png') + buf.seek(0) # plot = plot_optimization_results(optimizer, objective) # plot_images.append(plot) # plot_images.append("plot") From 0028b7d10ff16c7389183deb374cc44aa95799a5 Mon Sep 17 00:00:00 2001 From: weiqiz3 Date: Sun, 21 Sep 2025 15:30:29 -0500 Subject: [PATCH 105/125] test for retrieving plots with gridfs --- aamp_app/pages/bayesian-optimization.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/aamp_app/pages/bayesian-optimization.py b/aamp_app/pages/bayesian-optimization.py index dd12e97..9e09048 100644 --- a/aamp_app/pages/bayesian-optimization.py +++ b/aamp_app/pages/bayesian-optimization.py @@ -368,6 +368,17 @@ def generate_optimizer_parameters(n_clicks, bs, target_objective): buf = io.BytesIO() fig.savefig(buf, format='png') buf.seek(0) + experiment_id = ObjectId("64f5d2a1b1234567890abcdef") + file_id = fs.put(buf.getvalue(), filename="sample_plot.png", metadata={"experiment_id": experiment_id}) + print(f"Stored file in GridFS with file_id: {file_id}") + plots_collection = db['plots'] + plot_doc = { + "name": "sample_matplotlib_plot", + "experiment_id": experiment_id, + "file_id": file_id + } + plots_collection.insert_one(plot_doc) + print("Inserted plot document referencing GridFS file.") # plot = plot_optimization_results(optimizer, objective) # plot_images.append(plot) # plot_images.append("plot") From 0c5460119de29978ff00a982b93e5d44598eb3b7 Mon Sep 17 00:00:00 2001 From: weiqiz3 Date: Sat, 27 Sep 2025 17:49:19 -0500 Subject: [PATCH 106/125] Store plots for sampler --- aamp_app/app.py | 15 +++++++++------ aamp_app/pages/bayesian-optimization.py | 2 +- aamp_app/pages/sampler.py | 1 + 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/aamp_app/app.py b/aamp_app/app.py index b678217..6f966f0 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -2564,6 +2564,7 @@ def update_temperature_options(selected_solvents): Output("sampler-alert", "is_open"), Output("sampler-alert", "color"), Output("sampler-save-button", "disabled"), + Output("sampler-results-plots", "children"), Input("sampler-generate-button", "n_clicks"), [ State("sampler-campaign-name", "value"), @@ -2852,28 +2853,29 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string ) res = html.Div([ - html.Div([ + table + ]) + plots = html.Div([ html.Div([ dcc.Graph(figure=pca_fig) ], className="col-md-6"), html.Div([ dcc.Graph(figure=umap_fig) ], className="col-md-6"), - ], className="row"), - table - ]) + ], className="row") else: res = html.Div([ html.P("Not enough data points for visualization. Generate more samples."), html.H3("Generated Parameter Sets"), table ]) + plots = None - return res, f"Generated {len(parameter_sets)} parameter sets using simple random sampling.", True, "success", False + return res, f"Generated {len(parameter_sets)} parameter sets using simple random sampling.", True, "success", False, plots except Exception as e: print(f"Error generating parameter sets: {e}") - return None, f"Failed to generate parameter sets: {str(e)}", True, "danger", True + return None, f"Failed to generate parameter sets: {str(e)}", True, "danger", True, None @app.callback( @@ -2895,6 +2897,7 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string State({"type": "sampler-dropdown", "id": "printing-gap"}, "value"), State({"type": "sampler-dropdown", "id": "precursor-volume"}, "value"), State({"type": "sampler-dropdown", "id": "motor-speed"}, "value"), + State("sampler-results-plots", "children") prevent_initial_call=True ) def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_name, polymer_name, diff --git a/aamp_app/pages/bayesian-optimization.py b/aamp_app/pages/bayesian-optimization.py index 9e09048..c3b2a97 100644 --- a/aamp_app/pages/bayesian-optimization.py +++ b/aamp_app/pages/bayesian-optimization.py @@ -371,7 +371,7 @@ def generate_optimizer_parameters(n_clicks, bs, target_objective): experiment_id = ObjectId("64f5d2a1b1234567890abcdef") file_id = fs.put(buf.getvalue(), filename="sample_plot.png", metadata={"experiment_id": experiment_id}) print(f"Stored file in GridFS with file_id: {file_id}") - plots_collection = db['plots'] + plots_collection = db['optimizer_plots'] plot_doc = { "name": "sample_matplotlib_plot", "experiment_id": experiment_id, diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py index 80cab80..184b7ef 100644 --- a/aamp_app/pages/sampler.py +++ b/aamp_app/pages/sampler.py @@ -540,6 +540,7 @@ ), html.Div(id="sampler-results-table", className="mb-3"), + html.Div(id="sampler-results-plots", className="mb-3"), dbc.Button( "Save Parameter Sets", From aeed52d4b0bc2a52b63e55bc4a242038a6a2b435 Mon Sep 17 00:00:00 2001 From: Weiqi Zhang Date: Wed, 1 Oct 2025 15:29:49 -0500 Subject: [PATCH 107/125] done testing saving optimizer plots --- aamp_app/optimizer/bayesian_optimizer.py | 9 +++-- aamp_app/pages/bayesian-optimization.py | 51 ++++++++++++------------ 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/aamp_app/optimizer/bayesian_optimizer.py b/aamp_app/optimizer/bayesian_optimizer.py index 064b797..5ea37cb 100644 --- a/aamp_app/optimizer/bayesian_optimizer.py +++ b/aamp_app/optimizer/bayesian_optimizer.py @@ -81,7 +81,7 @@ def plot_optimization_results(optimizer, objective): buf.close() plt.close(fig) - return encoded_image + return encoded_image, fig class BayesianOptimizer: @@ -216,6 +216,7 @@ def optimize( ) -> Tuple[torch.Tensor, float]: img = [] + l = [] if verbose: print("=" * 60) @@ -262,7 +263,9 @@ def optimize( if verbose: print(f"Current best score: {self.best_observed_value:.4f}") print(f"Current best parameters: {self.best_parameters}") - img.append(plot_optimization_results(self, objective_function)) + encoded_img, fig = plot_optimization_results(self, objective_function) + img.append(encoded_img) + l.append(fig) if self.best_observed_value >= target: if verbose: @@ -277,7 +280,7 @@ def optimize( print(f"Final best parameters: {self.best_parameters}") print(f"Total evaluations: {len(self.train_X)}") - return self.best_parameters, self.best_observed_value, img + return self.best_parameters, self.best_observed_value, img, l def get_optimization_history(self) -> List[dict]: return self.iteration_history diff --git a/aamp_app/pages/bayesian-optimization.py b/aamp_app/pages/bayesian-optimization.py index c3b2a97..137048c 100644 --- a/aamp_app/pages/bayesian-optimization.py +++ b/aamp_app/pages/bayesian-optimization.py @@ -9,6 +9,7 @@ import base64 from pymongo import MongoClient import gridfs +from bson import ObjectId dash.register_page( __name__, @@ -19,6 +20,7 @@ client = MongoClient('mongodb://localhost:27017/') db = client['diaogroup'] +fs = gridfs.GridFS(db) # def plot_optimization_results(optimizer, objective): # """Create selective plots of the optimization results (1, 3, and 4 only).""" @@ -326,12 +328,13 @@ @callback( Output("optimizer-output1", "children"), Input("bo-generate-btn", "n_clicks"), + State("recipe-builder-campaign-dropdown", "value"), State("bo-batch-size", "value"), State("bo-target-objective", "value"), # State("bo-maxiter", "value"), prevent_initial_call=True ) -def generate_optimizer_parameters(n_clicks, bs, target_objective): +def generate_optimizer_parameters(n_clicks, camp, bs, target_objective): import torch from optimizer import BayesianOptimizer, MockObjectiveFunction @@ -340,45 +343,41 @@ def generate_optimizer_parameters(n_clicks, bs, target_objective): with contextlib.redirect_stdout(f): # Define search space bounds if n_clicks > 0: + collection = db['campaigns'] + entry = collection.find_one({"campaign_name": camp}) bounds = torch.tensor([ - [0.1, 1.0], # concentration - [10.0, 100.0], # print_speed - [0.05, 0.5], # gap_size - [5.0, 25.0] # volume + [min(entry['concentration_range']), max(entry['concentration_range'])], # concentration + [min(entry['motor_speed']), max(entry['motor_speed'])], # print_speed + [min(entry['printing_gap']), max(entry['printing_gap'])], # gap_size + [min(entry['precursor_volume']), max(entry['precursor_volume'])] # volume ]).T - # bounds = torch.tensor([ - # [2, 20], # concentration - # [0.01, 20], # print_speed - # [50.0, 100.0], # gap_size - # [6.0, 12.0] # volume - # ]).T + print(bounds) # Create optimizer and objective function optimizer = BayesianOptimizer(bounds=bounds, batch_size=bs) objective = MockObjectiveFunction() # Run optimization - best_params, best_score, plot_images = optimizer.optimize( + best_params, best_score, plot_images, ff = optimizer.optimize( objective_function=objective, n_iterations=200, n_initial_points=10, target=target_objective ) print("\n\n") - buf = io.BytesIO() - fig.savefig(buf, format='png') - buf.seek(0) - experiment_id = ObjectId("64f5d2a1b1234567890abcdef") - file_id = fs.put(buf.getvalue(), filename="sample_plot.png", metadata={"experiment_id": experiment_id}) - print(f"Stored file in GridFS with file_id: {file_id}") - plots_collection = db['optimizer_plots'] - plot_doc = { - "name": "sample_matplotlib_plot", - "experiment_id": experiment_id, - "file_id": file_id - } - plots_collection.insert_one(plot_doc) - print("Inserted plot document referencing GridFS file.") + for i, fig in enumerate(ff): + experiment_id = entry['_id'] + buf = io.BytesIO() + fig.savefig(buf, format="png") + buf.seek(0) + file_id = fs.put(buf.getvalue(), filename=f"sample_plot_{i}.png", metadata={"experiment_id": experiment_id}) + plots_collection = db['optimizer_plots'] + plot_doc = { + "name": f"sample_matplotlib_plot_{i}", + "experiment_id": experiment_id, + "file_id": file_id + } + plots_collection.insert_one(plot_doc) # plot = plot_optimization_results(optimizer, objective) # plot_images.append(plot) # plot_images.append("plot") From f1ac9ab0f047be597bfb6bc344e169d1b48481d3 Mon Sep 17 00:00:00 2001 From: Weiqi Zhang Date: Wed, 1 Oct 2025 16:16:52 -0500 Subject: [PATCH 108/125] done debugging sampler --- README.md | 8 +++++++- aamp_app/app.py | 28 +++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a3533e..3b17e50 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,13 @@ cd aamp_app ``` They can be commented again once the collections are created in the database. -10. Run the app +10. Run the following: +``` +pip install --upgrade dotenv +plotly_get_chrome +``` + +11. Run the app ``` python -m app ``` diff --git a/aamp_app/app.py b/aamp_app/app.py index 6f966f0..6cd60ca 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -31,6 +31,7 @@ import plotly.graph_objects as go from scipy.stats import gaussian_kde from string import Template +from pymongo import MongoClient from db.validation import solutions, films, devices, recipes try: @@ -59,6 +60,8 @@ def load_env(file_path=".env"): mongo_db_name, ) +p1, p2 = None, None + try: db_list = mongo.client.list_database_names() except Exception: @@ -2863,6 +2866,9 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string dcc.Graph(figure=umap_fig) ], className="col-md-6"), ], className="row") + global p1, p2 + p1 = pca_fig + p2 = umap_fig else: res = html.Div([ html.P("Not enough data points for visualization. Generate more samples."), @@ -2897,7 +2903,6 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string State({"type": "sampler-dropdown", "id": "printing-gap"}, "value"), State({"type": "sampler-dropdown", "id": "precursor-volume"}, "value"), State({"type": "sampler-dropdown", "id": "motor-speed"}, "value"), - State("sampler-results-plots", "children") prevent_initial_call=True ) def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_name, polymer_name, @@ -2982,6 +2987,27 @@ def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_na campaign_result = mongo.db.campaigns.insert_one(campaign_doc) campaign_id = campaign_result.inserted_id + cc = MongoClient('mongodb://localhost:27017/') + dd = cc['diaogroup'] + ff = GridFS(dd) + pca_bytes = p1.to_image(format="png") + umap_bytes = p2.to_image(format="png") + pca_id = ff.put(pca_bytes, filename=f"{campaign_name}_pca.png") + umap_id = ff.put(umap_bytes, filename=f"{campaign_name}_umap.png") + pca_doc = { + "name": f"{campaign_name}_pca", + "image_id": pca_id, + "campaign_id": campaign_id + } + umap_doc = { + "name": f"{campaign_name}_umap", + "image_id": umap_id, + "campaign_id": campaign_id + } + coll = dd['sampler_plots'] + coll.insert_one(pca_doc) + coll.insert_one(umap_doc) + sets_to_insert = [{ "campaign_id": campaign_id, "polymer_name": polymer_name, From 47c145269578f99400d84b79d2b6fd045f1ef163 Mon Sep 17 00:00:00 2001 From: Weiqi Zhang Date: Sun, 15 Feb 2026 16:18:19 -0600 Subject: [PATCH 109/125] Update AAMP app --- .../Image_processing/__init__.py | 18 + .../Image_processing/config.py | 48 + .../Image_processing/coverage.py | 149 ++ .../Image_processing/pipeline.py | 123 ++ .../run_active_learning_v4.py | 1252 +++++++++++++++++ .../Image_processing/scoring.py | 174 +++ .../Image_processing/uvvis.py | 125 ++ aamp_app/Image_Processing/README.md | 308 ++++ .../Image_Processing/constrained_bo_ver2.py | 767 ++++++++++ .../image_processing_changhyun.py | 522 +++++++ aamp_app/Image_Processing/requirements.txt | 11 + aamp_app/app.py | 27 +- aamp_app/pages/bayesian-optimization.py | 342 ++--- aamp_app/pages/home.py | 3 +- aamp_app/pages/manual-run.py | 201 +++ 15 files changed, 3793 insertions(+), 277 deletions(-) create mode 100644 aamp_app/Image_Processing/Image_processing/__init__.py create mode 100644 aamp_app/Image_Processing/Image_processing/config.py create mode 100644 aamp_app/Image_Processing/Image_processing/coverage.py create mode 100644 aamp_app/Image_Processing/Image_processing/pipeline.py create mode 100644 aamp_app/Image_Processing/Image_processing/run_active_learning_v4.py create mode 100644 aamp_app/Image_Processing/Image_processing/scoring.py create mode 100644 aamp_app/Image_Processing/Image_processing/uvvis.py create mode 100644 aamp_app/Image_Processing/README.md create mode 100644 aamp_app/Image_Processing/constrained_bo_ver2.py create mode 100644 aamp_app/Image_Processing/image_processing_changhyun.py create mode 100644 aamp_app/Image_Processing/requirements.txt create mode 100644 aamp_app/pages/manual-run.py diff --git a/aamp_app/Image_Processing/Image_processing/__init__.py b/aamp_app/Image_Processing/Image_processing/__init__.py new file mode 100644 index 0000000..f4c9e10 --- /dev/null +++ b/aamp_app/Image_Processing/Image_processing/__init__.py @@ -0,0 +1,18 @@ +"""Custom image processing pipeline for coverage analysis.""" + +from .config import UniformityConfig +from .coverage import analyze_film_coverage +from .pipeline import analyze_image +from .scoring import compute_uniformity_score, _match_reference_for +from .uvvis import analyze_uvvis_data, find_uvvis_file + +__all__ = [ + "UniformityConfig", + "analyze_film_coverage", + "analyze_image", + "compute_uniformity_score", + "_match_reference_for", + "analyze_uvvis_data", + "find_uvvis_file", +] + diff --git a/aamp_app/Image_Processing/Image_processing/config.py b/aamp_app/Image_Processing/Image_processing/config.py new file mode 100644 index 0000000..ba36c9b --- /dev/null +++ b/aamp_app/Image_Processing/Image_processing/config.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional +from bson import ObjectId + + +@dataclass +class UniformityConfig: + """Lightweight configuration object. + + Maintains compatibility with the existing imageproc.uniformity.config interface + by keeping the same field names. + """ + + campaign_id: Optional[ObjectId] = None + use_reference: bool = True + reference_name: str = "background_normal_0deg" + + roi_x: int = 0 + roi_y: int = 0 + roi_width: int = 100 + roi_height: int = 100 + + uvvis_abs_threshold: float = 0.1 + uvvis_wavelength_min: float = 300.0 + uvvis_wavelength_max: float = 800.0 + uvvis_required: bool = False + + coverage_threshold: float = 0.1 + coverage_smoothing: float = 1.5 + # Sigma multiplier (K value) used in HSV S channel threshold calculation. Adjust default value here if needed. + coverage_k: float = 3.0 + coverage_min_std: float = 10.0 + + # Uniformity calculation sensitivity parameters (for Reference-based Absolute Scoring) + uniformity_std_sensitivity: float = 15.0 # K1: Sensitivity to Std difference + uniformity_ent_sensitivity: float = 2.0 # K2: Sensitivity to Entropy difference + + compute_learning_features: bool = False + + extra: dict = field(default_factory=dict) + + @property + def roi_bbox(self) -> tuple[int, int, int, int]: + return (self.roi_x, self.roi_y, self.roi_width, self.roi_height) + diff --git a/aamp_app/Image_Processing/Image_processing/coverage.py b/aamp_app/Image_Processing/Image_processing/coverage.py new file mode 100644 index 0000000..c4c7850 --- /dev/null +++ b/aamp_app/Image_Processing/Image_processing/coverage.py @@ -0,0 +1,149 @@ +"""HSV-based film coverage analysis module. + +Instead of the traditional reference subtraction approach, +this module calculates coverage by combining dynamic thresholds +based on reference image S-channel statistics with sample image HSV information. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Tuple, Dict + +import cv2 +import numpy as np +from skimage.measure import shannon_entropy + + +@dataclass +class ROIParams: + x: int + y: int + width: int + height: int + + +def _clamp_roi(image: np.ndarray, roi: ROIParams) -> ROIParams: + """Clamp ROI to ensure it stays within image boundaries.""" + h, w = image.shape[:2] + x = max(0, min(roi.x, w - 1)) + y = max(0, min(roi.y, h - 1)) + width = max(1, min(roi.width, w - x)) + height = max(1, min(roi.height, h - y)) + return ROIParams(x, y, width, height) + + +def _extract_roi(image: np.ndarray, roi: ROIParams) -> np.ndarray: + """Extract the image region corresponding to the clamped ROI.""" + roi = _clamp_roi(image, roi) + return image[roi.y:roi.y + roi.height, roi.x:roi.x + roi.width] + + +def _ensure_same_shape(sample_roi: np.ndarray, reference_roi: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Resize reference ROI if sample and reference ROI sizes differ.""" + if sample_roi.shape[:2] == reference_roi.shape[:2]: + return sample_roi, reference_roi + resized_ref = cv2.resize(reference_roi, (sample_roi.shape[1], sample_roi.shape[0])) + return sample_roi, resized_ref + + +def _apply_mask_smoothing(mask: np.ndarray, sigma: float) -> np.ndarray: + """Apply Gaussian blur to reduce noise.""" + if sigma <= 0: + return mask + blurred = cv2.GaussianBlur(mask, (0, 0), sigma) + return (blurred > 127).astype(np.uint8) * 255 + + +def analyze_film_coverage( + image: np.ndarray, + reference: np.ndarray, + roi_x: int, + roi_y: int, + roi_width: int, + roi_height: int, + smoothing: float = 1.5, + k: float = 3.0, + min_std: float = 10.0, +) -> Tuple[float, np.ndarray, np.ndarray, np.ndarray, Dict[str, float]]: + """HSV-based film coverage analysis. + + Args: + image: Sample BGR image + reference: Reference BGR image + roi_x, roi_y, roi_width, roi_height: ROI coordinates + smoothing: Gaussian sigma for mask post-processing + k: Standard deviation multiplier for dynamic threshold + min_std: Minimum value to use when standard deviation is 0 + + Returns: + (coverage_pct, sample_roi_bgr, reference_roi_bgr, final_mask, debug_info) + """ + roi = ROIParams(roi_x, roi_y, roi_width, roi_height) + sample_roi = _extract_roi(image, roi) + reference_roi = _extract_roi(reference, roi) + sample_roi, reference_roi = _ensure_same_shape(sample_roi, reference_roi) + + # Convert to HSV + sample_hsv = cv2.cvtColor(sample_roi, cv2.COLOR_BGR2HSV) + reference_hsv = cv2.cvtColor(reference_roi, cv2.COLOR_BGR2HSV) + + # Reference S channel statistics + ref_s = reference_hsv[:, :, 1].astype(np.float32) + ref_s_mean = float(np.mean(ref_s)) + ref_s_std = float(np.std(ref_s)) + effective_std = ref_s_std if ref_s_std > 0 else float(min_std) + saturation_threshold = float(np.clip(ref_s_mean + k * effective_std, 0, 255)) + + # Condition A: S channel dynamic threshold + sample_s = sample_hsv[:, :, 1].astype(np.float32) + condition_a = sample_s > saturation_threshold + + # Condition B: V channel Otsu (dark regions) + sample_v = sample_hsv[:, :, 2].astype(np.uint8) + otsu_threshold, otsu_mask = cv2.threshold( + sample_v, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU + ) + condition_b = otsu_mask > 0 + + # Final mask + final_mask = np.logical_or(condition_a, condition_b).astype(np.uint8) * 255 + final_mask = _apply_mask_smoothing(final_mask, smoothing) + + total_pixels = final_mask.size + film_pixels = int(np.sum(final_mask > 0)) + coverage_percentage = float(film_pixels) / float(total_pixels) if total_pixels else 0.0 + + # Reference statistics (for Uniformity calculation) + ref_s_uint8 = reference_hsv[:, :, 1].astype(np.uint8) + ref_entropy = float(shannon_entropy(ref_s_uint8)) + + # Sample statistics (for Uniformity calculation) + sample_s_uint8 = sample_hsv[:, :, 1].astype(np.uint8) + sample_std = float(np.std(sample_s_uint8.astype(np.float32))) + sample_entropy = float(shannon_entropy(sample_s_uint8)) + + debug_info: Dict[str, float] = { + "coverage_percentage": coverage_percentage, + "roi_width": float(sample_roi.shape[1]), + "roi_height": float(sample_roi.shape[0]), + "ref_s_mean": ref_s_mean, + "ref_s_std": ref_s_std, + "effective_std": effective_std, + "saturation_threshold": saturation_threshold, + "k_value": k, + "min_std_floor": min_std, + "otsu_threshold": float(otsu_threshold), + "condition_a_pixels": int(np.sum(condition_a)), + "condition_b_pixels": int(np.sum(condition_b)), + "film_pixels": film_pixels, + "total_pixels": int(total_pixels), + # Reference statistics (for Uniformity calculation) + "ref_std": ref_s_std, + "ref_entropy": ref_entropy, + "sample_std": sample_std, + "sample_entropy": sample_entropy, + } + + return coverage_percentage, sample_roi, reference_roi, final_mask, debug_info + diff --git a/aamp_app/Image_Processing/Image_processing/pipeline.py b/aamp_app/Image_Processing/Image_processing/pipeline.py new file mode 100644 index 0000000..e3f5744 --- /dev/null +++ b/aamp_app/Image_Processing/Image_processing/pipeline.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, Dict, Tuple + +import numpy as np + +from .config import UniformityConfig +from .coverage import analyze_film_coverage +from .scoring import analyze_uniformity +from .uvvis import analyze_uvvis_data + + +def analyze_image( + image_name, + fs, + image_bgr: np.ndarray, + cfg: UniformityConfig, + ref_bgr: np.ndarray | None = None, +) -> Tuple[ + Dict[str, Any], + np.ndarray | None, + np.ndarray | None, + np.ndarray | None, + Dict[str, Any] | None, +]: + """Simplified coverage analysis pipeline.""" + + sample_roi = None + reference_roi = None + coverage_mask = None + coverage_debug: Dict[str, Any] | None = None + + metrics: Dict[str, Any] = { + "coverage_percentage": None, + "coverage_threshold_met": False, + "sample_status": "reference_missing" if cfg.use_reference else "reference_not_required", + } + + if image_bgr is None: + metrics["sample_status"] = "image_not_loaded" + return metrics, sample_roi, reference_roi, coverage_mask, coverage_debug + + if cfg.use_reference and ref_bgr is None: + metrics["coverage_reason"] = "reference_not_found" + return metrics, sample_roi, reference_roi, coverage_mask, coverage_debug + + uvvis_stats: Dict[str, Any] | None = None + if image_name is not None: + uvvis_passed, uvvis_stats = analyze_uvvis_data( + image_name=image_name, + fs=fs, + campaign_id=cfg.campaign_id, + abs_threshold=cfg.uvvis_abs_threshold, + wavelength_min=cfg.uvvis_wavelength_min, + wavelength_max=cfg.uvvis_wavelength_max, + ) + metrics["uvvis_passed"] = uvvis_passed + metrics["uvvis_reason"] = uvvis_stats.get("uvvis_reason") if uvvis_stats else None + + reference = ref_bgr if ref_bgr is not None else image_bgr + + coverage_pct, sample_roi, reference_roi, mask, debug_info = analyze_film_coverage( + image=image_bgr, + reference=reference, + roi_x=cfg.roi_x, + roi_y=cfg.roi_y, + roi_width=cfg.roi_width, + roi_height=cfg.roi_height, + smoothing=cfg.coverage_smoothing, + k=cfg.coverage_k, + min_std=cfg.coverage_min_std, + ) + + coverage_mask = mask + coverage_debug = debug_info + + threshold_met = coverage_pct >= cfg.coverage_threshold + status = "coverage_pass" if threshold_met else "coverage_fail" + comparator = ">=" if threshold_met else "<" + + metrics = { + "coverage_percentage": coverage_pct, + "coverage_threshold_met": threshold_met, + "sample_status": status, + "coverage_reason": f"coverage {coverage_pct:.3f} {comparator} threshold {cfg.coverage_threshold:.3f}", + } + + for key, value in debug_info.items(): + metrics[f"coverage_{key}"] = value + + # Uniformity Score calculation (Reference-based Absolute Scoring) + # ref_stats is already included in debug_info (calculated in coverage.py) + # May return None if reference is not available + ref_stats = { + "ref_std": debug_info.get("ref_std"), # May be None + "ref_entropy": debug_info.get("ref_entropy"), # May be None + "sample_std": debug_info.get("sample_std", 0.0), + "sample_entropy": debug_info.get("sample_entropy", 0.0), + } + + # Use ideal reference if reference is not available + use_model_ref = (ref_stats.get("ref_std") is None) or (not cfg.use_reference or ref_bgr is None) + + if sample_roi is not None: + uniformity_score, uniformity_debug = analyze_uniformity(ref_stats, cfg, use_model_reference=use_model_ref) + metrics["uniformity_score"] = uniformity_score + for key, value in uniformity_debug.items(): + metrics[f"uniformity_{key}"] = value + # Also add to coverage_debug + if coverage_debug is not None: + coverage_debug.update(uniformity_debug) + else: + # Set default value if sample ROI is not available + metrics["uniformity_score"] = None + if coverage_debug is not None: + coverage_debug["uniformity_score"] = None + + if uvvis_stats: + metrics.update(uvvis_stats) + + return metrics, sample_roi, reference_roi, coverage_mask, coverage_debug + diff --git a/aamp_app/Image_Processing/Image_processing/run_active_learning_v4.py b/aamp_app/Image_Processing/Image_processing/run_active_learning_v4.py new file mode 100644 index 0000000..8076878 --- /dev/null +++ b/aamp_app/Image_Processing/Image_processing/run_active_learning_v4.py @@ -0,0 +1,1252 @@ +#!/usr/bin/env python3 +""" +Active Learning Script for PProDOT Film Optimization (v4). + +Phase 3: Active Learning & Mapping +- Step 1: Repeated Stratified 5-Fold CV for model validation +- Step 2: Uncertainty-based active learning suggestion + +v3 Changes: +- CV predictions are now clipped to match training data clipping + (pred_abs clipped to [0, 2.0], pred_cov clipped to [0, 1.0]) + This ensures consistency between training data and predictions for R² calculation. + +v4 Changes: +- Added predicted values (pred_abs, pred_cov, pred_uni) to suggestions CSV +- Added uncertainty values (sigma_abs, sigma_cov, sigma_uni) to suggestions CSV +- Column order: score, p_valid, score_raw, predictions, uncertainties, weights, contributions, etc. +""" + +import torch +import numpy as np +import pandas as pd +from pathlib import Path +from typing import Tuple, Dict, List, Optional +from scipy.stats import norm +import random +import warnings +warnings.filterwarnings('ignore') + +# BoTorch imports +from botorch.models import SingleTaskGP +from botorch.fit import fit_gpytorch_mll +from botorch.optim import optimize_acqf +from botorch.utils.transforms import normalize, unnormalize +from botorch.utils.sampling import draw_sobol_samples + +# GPyTorch imports +from gpytorch.mlls import ExactMarginalLogLikelihood +from gpytorch.kernels import ScaleKernel, RBFKernel +from gpytorch.priors import GammaPrior + +# Scikit-learn imports +from sklearn.model_selection import RepeatedStratifiedKFold +from sklearn.metrics import r2_score, mean_squared_error, accuracy_score + + +# ============================================================================ +# Configuration +# ============================================================================ + +# Parameter bounds (original scale) - Same as constrained_bo_ver2.py +BOUNDS_ORIGINAL = torch.tensor([ + [0.01, 20.0], # Speed + [25.0, 107.0], # Temperature + [50.0, 200.0], # Gap + [5.0, 15.0] # Volume +]).T + +# Log scale parameters +LOG_SCALE_PARAMS = [True, False, False, False] # Only Speed is log scale + +# UV-Vis clipping threshold +UVVIS_CLIP_THRESHOLD = 2.0 + +# Valid thresholds (Phase 3: Mapping focus - lower threshold for coverage) +VALID_ABS_THRESHOLD = 0.1 +VALID_COV_THRESHOLD = 0.1 # Phase 3: Any film formation is considered valid for mapping +UVVIS_THRESHOLD = VALID_ABS_THRESHOLD # Alias for backward compatibility +COVERAGE_THRESHOLD = VALID_COV_THRESHOLD # Alias for backward compatibility + +# Hard cutoff for P(valid) in active learning +HARD_CUTOFF_P_VALID = 0.05 + +# CV settings +N_SPLITS = 5 +N_REPEATS = 5 + +# Active Learning settings +SOBOL_GRID_SIZE = 3000 +BATCH_SIZE = 8 + +UNI_CLIP_PERCENTILE = 95 # Clip σ_Uni at 95th percentile + +# Output Settings +SOLVENT = "CB" +CONCENTRATION = 40 + +# Random Seed +SEED = 42 + + +# ============================================================================ +# Data Loading & Preprocessing +# ============================================================================ + +def load_and_preprocess_data(csv_path: Path, round_num: Optional[int] = None) -> Tuple[pd.DataFrame, int, torch.Tensor]: + """ + Load CSV and preprocess data. + Also loads previous round suggestions for round-level diversity. + + Args: + csv_path: Path to CSV file + round_num: Round number to use (uses data from rounds <= round_num). + If None, uses maximum round in CSV. + + Returns: + df: Preprocessed DataFrame with Valid column (filtered by round_num if specified) + current_round: Round number used (round_num if specified, else max round in data) + X_prev_suggestions: Previous round suggestions as tensor (n_prev, 4) or None + """ + print("=" * 60) + print("[Data] Loading and preprocessing...") + print("=" * 60) + + if not csv_path.exists(): + raise FileNotFoundError(f"CSV file not found: {csv_path}") + + df = pd.read_csv(csv_path) + print(f" Loaded {len(df)} rows from {csv_path}") + + # Filter by round number if specified (similar to constrained_bo_ver2.py) + if round_num is not None and 'round#' in df.columns: + df = df[df['round#'] <= round_num].copy() + if len(df) == 0: + raise ValueError(f"No data found for round {round_num} in {csv_path}") + rounds_used = sorted(df['round#'].unique().tolist()) + print(f" Using data from rounds: {rounds_used} (round_num={round_num})") + if -1 in rounds_used: + print(f" (Includes historical data from round -1)") + current_round = round_num + elif 'round#' in df.columns: + current_round = int(df['round#'].max()) + print(f" Current round (auto-detected): {current_round}") + else: + current_round = 0 + print(f" Warning: No 'round#' column found, assuming round 0") + + # Clip uvvis_max_abs to 2.0 + if 'uvvis_max_abs' in df.columns: + # Drop NaNs in uvvis_max_abs + n_nans = df['uvvis_max_abs'].isna().sum() + if n_nans > 0: + print(f" Dropping {n_nans} rows with NaN uvvis_max_abs") + df = df.dropna(subset=['uvvis_max_abs']) + + n_clipped = (df['uvvis_max_abs'] > UVVIS_CLIP_THRESHOLD).sum() + if n_clipped > 0: + print(f" Clipping {n_clipped} uvvis_max_abs values > {UVVIS_CLIP_THRESHOLD} to {UVVIS_CLIP_THRESHOLD}") + df['uvvis_max_abs'] = df['uvvis_max_abs'].clip(upper=UVVIS_CLIP_THRESHOLD) + + if 'coverage_percentage' in df.columns: + n_nans = df['coverage_percentage'].isna().sum() + if n_nans > 0: + print(f" Dropping {n_nans} rows with NaN coverage_percentage") + df = df.dropna(subset=['coverage_percentage']) + + # Calculate Valid column + if 'Valid' not in df.columns: + df['Valid'] = (df['uvvis_max_abs'] >= UVVIS_THRESHOLD) & (df['coverage_percentage'] >= COVERAGE_THRESHOLD) + print(f" Calculated Valid column: {df['Valid'].sum()}/{len(df)} valid samples ({100*df['Valid'].mean():.1f}%)") + + # Load previous round suggestions for round-level diversity + # Get data directory from CSV path + csv_dir = csv_path.parent + # Check both data_dir and results directory for previous suggestions + results_dir_check = csv_dir / "results" + X_prev_suggestions = None + if current_round > 0: + # Try results directory first, then fall back to data_dir + prev_suggestions_path = results_dir_check / f"round{current_round}_suggestions.csv" + if not prev_suggestions_path.exists(): + prev_suggestions_path = csv_dir / f"round{current_round}_suggestions.csv" + if prev_suggestions_path.exists(): + print(f" Loading previous round suggestions from {prev_suggestions_path}") + prev_df = pd.read_csv(prev_suggestions_path) + + # Extract inputs from previous suggestions (same format as main data) + if all(col in prev_df.columns for col in ['Speed', 'Temperature', 'gap', 'Precursor Volume']): + speed_prev = prev_df['Speed'].values + temp_prev = prev_df['Temperature'].values + gap_prev = prev_df['gap'].values + vol_prev = prev_df['Precursor Volume'].values + + # Convert Speed to log scale + speed_prev_log = np.log10(speed_prev) + + # Stack inputs: [Speed_log, Temperature, gap, Volume] + X_prev_suggestions = torch.tensor( + np.column_stack([speed_prev_log, temp_prev, gap_prev, vol_prev]), + dtype=torch.float64 + ) + print(f" Loaded {len(X_prev_suggestions)} previous round suggestions") + else: + print(f" Warning: Previous suggestions file missing required columns, skipping") + else: + print(f" No previous round suggestions found (expected: {prev_suggestions_path})") + + # Check required columns + required_cols = ['Speed', 'Temperature', 'gap', 'Precursor Volume', + 'uvvis_max_abs', 'coverage_percentage', 'uniformity_score'] + missing_cols = [col for col in required_cols if col not in df.columns] + if missing_cols: + raise ValueError(f"Missing required columns: {missing_cols}") + + return df, current_round, X_prev_suggestions + + +def prepare_inputs_outputs(df: pd.DataFrame) -> Tuple[torch.Tensor, Dict[str, torch.Tensor], torch.Tensor]: + """ + Prepare inputs (X) and outputs (Y) for model training. + + Returns: + X: Input tensor (n_samples, 4) [Speed_log, Temperature, gap, Volume] + Y_dict: Dictionary with Y_abs, Y_cov, Y_uni, Y_valid + valid_mask: Boolean mask for valid samples (for uniformity model) + """ + # Extract inputs + speed = df['Speed'].values + temperature = df['Temperature'].values + gap = df['gap'].values + volume = df['Precursor Volume'].values + + # Convert Speed to log scale + speed_log = np.log10(speed) + + # Stack inputs: [Speed_log, Temperature, gap, Volume] + X = torch.tensor(np.column_stack([speed_log, temperature, gap, volume]), dtype=torch.float64) + + # Extract outputs + Y_abs = torch.tensor(df['uvvis_max_abs'].values, dtype=torch.float64).reshape(-1, 1) + Y_cov = torch.tensor(df['coverage_percentage'].values, dtype=torch.float64).reshape(-1, 1) + + # Uniformity: only valid samples with non-NaN uniformity_score + uniformity_values = df['uniformity_score'].values + valid_mask = (df['Valid'].values) & (~pd.isna(uniformity_values)) + # Ensure valid_mask is 1D boolean array for proper indexing + valid_mask = valid_mask.astype(bool) + + Y_uni = torch.tensor(uniformity_values, dtype=torch.float64).reshape(-1, 1) + Y_valid = torch.tensor(df['Valid'].values, dtype=torch.float64).reshape(-1, 1) + + Y_dict = { + 'abs': Y_abs, + 'cov': Y_cov, + 'uni': Y_uni, + 'valid': Y_valid + } + + return X, Y_dict, valid_mask + + +def create_strata(df: pd.DataFrame) -> np.ndarray: + """ + Create stratification labels (3 groups). + + Groups: + 0: Valid (Abs >= 0.1 & Cov >= 0.1) - successful film + 1: Invalid & Abs >= 0.1 (thick but low coverage/fragmented) + 2: Invalid & Abs < 0.1 (thin film failure) + """ + valid = df['Valid'].values + abs_high = (df['uvvis_max_abs'] >= UVVIS_THRESHOLD).values + + strata = np.zeros(len(df), dtype=int) + strata[valid] = 0 # Valid samples + strata[(~valid) & (abs_high)] = 1 # Invalid but thick + strata[(~valid) & (~abs_high)] = 2 # Invalid and thin + + return strata + + +# ============================================================================ +# GP Model Building +# ============================================================================ + +def build_gp_model(X_normalized: torch.Tensor, Y: torch.Tensor) -> SingleTaskGP: + """ + Build and fit a GP model. + + Args: + X_normalized: Normalized input tensor (n_samples, n_dims) + Y: Output tensor (n_samples, 1) + + Returns: + Fitted GP model + """ + model = SingleTaskGP( + X_normalized, + Y, + covar_module=ScaleKernel( + RBFKernel( + lengthscale_prior=GammaPrior(3.0, 1.0), + ard_num_dims=4 + ), + outputscale_prior=GammaPrior(2.0, 0.5) + ) + ) + + model.train() + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + model.eval() + + return model + + +# ============================================================================ +# Step 1: Cross-Validation +# ============================================================================ + +def run_cross_validation( + X: torch.Tensor, + Y_dict: Dict[str, torch.Tensor], + valid_mask: np.ndarray, + strata: np.ndarray, + bounds: torch.Tensor +) -> Tuple[pd.DataFrame, Dict[str, float]]: + """ + Run Repeated Stratified K-Fold Cross-Validation. + + Returns: + results_df: DataFrame with CV results for all folds + mean_r2_dict: Dictionary with mean R² scores for weighting calculation + """ + print("\n" + "=" * 60) + print("[CV] Running Repeated Stratified 5-Fold CV...") + print("=" * 60) + + # Normalize bounds for log-scale Speed + bounds_normalized = bounds.clone() + for i, is_log in enumerate(LOG_SCALE_PARAMS): + if is_log: + bounds_normalized[0, i] = np.log10(bounds[0, i].item()) + bounds_normalized[1, i] = np.log10(bounds[1, i].item()) + + # Check if we have enough samples per stratum + unique_strata, counts = np.unique(strata, return_counts=True) + min_samples_per_stratum = counts.min() + + if min_samples_per_stratum < N_SPLITS: + print(f" Warning: Minimum samples per stratum ({min_samples_per_stratum}) < n_splits ({N_SPLITS})") + print(f" Falling back to 2-group stratification (Valid/Invalid)") + strata = (strata < 2).astype(int) # Valid=True -> 0, Valid=False -> 1 + + # Initialize CV + rskf = RepeatedStratifiedKFold(n_splits=N_SPLITS, n_repeats=N_REPEATS, random_state=42) + + # Store results + results = [] + + fold_idx = 0 + for train_idx, test_idx in rskf.split(X, strata): + fold_idx += 1 + + # Split data + X_train = X[train_idx] + X_test = X[test_idx] + + Y_abs_train = Y_dict['abs'][train_idx] + Y_abs_test = Y_dict['abs'][test_idx] + + Y_cov_train = Y_dict['cov'][train_idx] + Y_cov_test = Y_dict['cov'][test_idx] + + # Uniformity: only valid samples + valid_train_mask = valid_mask[train_idx] + Y_uni_test = Y_dict['uni'][test_idx] + valid_test_mask = valid_mask[test_idx] + + # Normalize inputs + X_train_norm = normalize(X_train.double(), bounds_normalized.double()) + X_test_norm = normalize(X_test.double(), bounds_normalized.double()) + + # Build and train models + model_abs = build_gp_model(X_train_norm, Y_abs_train) + model_cov = build_gp_model(X_train_norm, Y_cov_train) + + # Uniformity model: only if we have enough valid samples (>= 2) + if valid_train_mask.sum() >= 2: + # Fix: First slice Y_uni by train_idx, then apply valid_train_mask + Y_uni_train_full = Y_dict['uni'][train_idx] # First slice by train_idx + valid_train_mask_tensor = torch.tensor(valid_train_mask, dtype=torch.bool) + X_uni_train = X_train[valid_train_mask_tensor] + Y_uni_train = Y_uni_train_full[valid_train_mask_tensor] # Then apply mask + X_uni_train_norm = normalize(X_uni_train.double(), bounds_normalized.double()) + model_uni = build_gp_model(X_uni_train_norm, Y_uni_train) + else: + model_uni = None + + # Predictions + with torch.no_grad(): + # Absorbance + posterior_abs = model_abs.posterior(X_test_norm) + pred_abs = posterior_abs.mean.squeeze(-1).cpu().numpy() + # Clip predictions to match data clipping (consistency with training data) + pred_abs = np.clip(pred_abs, 0.0, UVVIS_CLIP_THRESHOLD) + + # Coverage + posterior_cov = model_cov.posterior(X_test_norm) + pred_cov = posterior_cov.mean.squeeze(-1).cpu().numpy() + # Clip coverage to [0, 1] for physical consistency + pred_cov = np.clip(pred_cov, 0.0, 1.0) + + # Uniformity (only for valid test samples) + if model_uni is not None and valid_test_mask.sum() > 0: + valid_test_mask_tensor = torch.tensor(valid_test_mask, dtype=torch.bool) + X_uni_test_norm = normalize(X_test[valid_test_mask_tensor].double(), bounds_normalized.double()) + posterior_uni = model_uni.posterior(X_uni_test_norm) + pred_uni = posterior_uni.mean.squeeze(-1).cpu().numpy() + else: + pred_uni = np.full(valid_test_mask.sum(), np.nan) if valid_test_mask.sum() > 0 else np.array([]) + + # Ground truth + true_abs = Y_abs_test.squeeze().cpu().numpy() + true_cov = Y_cov_test.squeeze().cpu().numpy() + if valid_test_mask.sum() > 0: + valid_test_mask_tensor = torch.tensor(valid_test_mask, dtype=torch.bool) + true_uni = Y_uni_test[valid_test_mask_tensor].squeeze().cpu().numpy() + else: + true_uni = np.array([]) + + # Metrics + r2_abs = r2_score(true_abs, pred_abs) + rmse_abs = np.sqrt(mean_squared_error(true_abs, pred_abs)) + + r2_cov = r2_score(true_cov, pred_cov) + rmse_cov = np.sqrt(mean_squared_error(true_cov, pred_cov)) + + if len(true_uni) > 0 and not np.isnan(pred_uni).all(): + r2_uni = r2_score(true_uni, pred_uni) + rmse_uni = np.sqrt(mean_squared_error(true_uni, pred_uni)) + else: + r2_uni = np.nan + rmse_uni = np.nan + + # Classification accuracy (Physics-informed) + pred_valid = (pred_abs >= UVVIS_THRESHOLD) & (pred_cov >= COVERAGE_THRESHOLD) + true_valid = valid_test_mask + accuracy = accuracy_score(true_valid, pred_valid) + + results.append({ + 'fold': fold_idx, + 'repeat': (fold_idx - 1) // N_SPLITS + 1, + 'split': ((fold_idx - 1) % N_SPLITS) + 1, + 'r2_abs': r2_abs, + 'rmse_abs': rmse_abs, + 'r2_cov': r2_cov, + 'rmse_cov': rmse_cov, + 'r2_uni': r2_uni, + 'rmse_uni': rmse_uni, + 'accuracy': accuracy + }) + + if fold_idx % 10 == 0: + print(f" Completed {fold_idx}/{N_SPLITS * N_REPEATS} folds...") + + results_df = pd.DataFrame(results) + + # Summary statistics + print("\n[CV] Summary Statistics:") + print("-" * 60) + for metric in ['r2_abs', 'rmse_abs', 'r2_cov', 'rmse_cov', 'r2_uni', 'rmse_uni', 'accuracy']: + mean_val = results_df[metric].mean() + std_val = results_df[metric].std() + print(f" {metric:12s}: {mean_val:8.4f} ± {std_val:8.4f}") + + # Compute mean R² scores for adaptive weighting + # Handle NaN values in r2_uni (when valid samples are insufficient) + mean_r2_abs = float(results_df['r2_abs'].mean()) + mean_r2_cov = float(results_df['r2_cov'].mean()) + mean_r2_uni = float(results_df['r2_uni'].dropna().mean()) if results_df['r2_uni'].notna().any() else 0.0 + + mean_r2_dict = { + 'abs_r2': mean_r2_abs, + 'cov_r2': mean_r2_cov, + 'uni_r2': mean_r2_uni + } + + print(f"\n[CV] Mean R² Scores (for weighting):") + print(f" Abs: {mean_r2_abs:.4f}, Cov: {mean_r2_cov:.4f}, Uni: {mean_r2_uni:.4f}") + + return results_df, mean_r2_dict + + +# ============================================================================ +# Step 2: Active Learning Suggestion +# ============================================================================ + +def compute_active_learning_scores( + models: Dict[str, SingleTaskGP], + X_train_normalized: torch.Tensor, + bounds_normalized: torch.Tensor, + X_train_all: torch.Tensor, + mean_r2_dict: Dict[str, float], + X_prev_suggestions: torch.Tensor = None +) -> Tuple[torch.Tensor, torch.Tensor, Dict]: + """ + Compute active learning scores using Adaptive Weighting based on CV R² scores. + + Args: + models: Dictionary with 'abs', 'cov', 'uni' GP models + X_train_normalized: Normalized training data [0, 1] + bounds_normalized: Normalized bounds + X_train_all: Original scale training data (for diversity calculation) + mean_r2_dict: Dictionary with mean R² scores from CV + X_prev_suggestions: Previous round suggestions (optional) + + Returns: + candidates_normalized: Suggested candidates (in [0, 1] normalized space) + scores: Final scores for each candidate + metadata: Dictionary with intermediate values including weights and contributions + """ + print("\n" + "=" * 60) + print("[Optimization] Active Learning Suggestion...") + print("=" * 60) + + # Step 0: Compute adaptive weights from CV R² scores + print("Step 0: Computing adaptive weights from CV R² scores...") + r2_abs = mean_r2_dict['abs_r2'] + r2_cov = mean_r2_dict['cov_r2'] + r2_uni = mean_r2_dict['uni_r2'] + + # Raw weights: max(0.1, 1.0 - R²) + raw_w_abs = max(0.1, 1.0 - r2_abs) + raw_w_cov = max(0.1, 1.0 - r2_cov) + raw_w_uni = max(0.1, 1.0 - r2_uni) + + # Normalize to sum to 1.0 + total_raw = raw_w_abs + raw_w_cov + raw_w_uni + w_abs = raw_w_abs / total_raw + w_cov = raw_w_cov / total_raw + w_uni = raw_w_uni / total_raw + + print(f" R² scores: Abs={r2_abs:.4f}, Cov={r2_cov:.4f}, Uni={r2_uni:.4f}") + print(f" Adaptive weights: W_Abs={w_abs:.4f}, W_Cov={w_cov:.4f}, W_Uni={w_uni:.4f}") + + # Step 1: Generate Sobol grid for normalization + print(f"Step 1: Generating {SOBOL_GRID_SIZE} Sobol samples for normalization...") + unit_bounds = torch.stack([ + torch.zeros(4, dtype=torch.float64), + torch.ones(4, dtype=torch.float64) + ]) + + sobol_grid_unit = draw_sobol_samples( + bounds=unit_bounds, + n=1, + q=SOBOL_GRID_SIZE + ).squeeze(0).double() # (SOBOL_GRID_SIZE, 4) in [0, 1] + + # Model expects inputs in [0, 1] normalized space (no transformation needed) + # Use unit hypercube samples directly as model input + sobol_grid_norm = sobol_grid_unit + + # Predict uncertainties on Sobol grid (all in [0, 1] normalized space) + with torch.no_grad(): + posterior_abs = models['abs'].posterior(sobol_grid_norm) + sigma_abs_pool = posterior_abs.stddev.squeeze(-1).cpu().numpy() + + posterior_cov = models['cov'].posterior(sobol_grid_norm) + sigma_cov_pool = posterior_cov.stddev.squeeze(-1).cpu().numpy() + + posterior_uni = models['uni'].posterior(sobol_grid_norm) + sigma_uni_pool = posterior_uni.stddev.squeeze(-1).cpu().numpy() + + # Compute normalization statistics for Abs and Cov (no clipping) + mean_abs = sigma_abs_pool.mean() + std_abs = sigma_abs_pool.std() + mean_cov = sigma_cov_pool.mean() + std_cov = sigma_cov_pool.std() + + # For σ_Uni: Clip first, then recompute statistics on clipped values + # This prevents invalid region's explosive σ from making valid region's σ look too small + uni_clip_threshold = np.percentile(sigma_uni_pool, UNI_CLIP_PERCENTILE) + sigma_uni_pool_clipped = np.clip(sigma_uni_pool, None, uni_clip_threshold) + mean_uni = sigma_uni_pool_clipped.mean() # Recompute mean on clipped values + std_uni = sigma_uni_pool_clipped.std() # Recompute std on clipped values + + print(f" Sigma statistics (Abs): mean={mean_abs:.4f}, std={std_abs:.4f}") + print(f" Sigma statistics (Cov): mean={mean_cov:.4f}, std={std_cov:.4f}") + print(f" Sigma statistics (Uni): mean={mean_uni:.4f}, std={std_uni:.4f} (after clipping at {uni_clip_threshold:.4f})") + + # Step 2: Generate candidate pool using Sobol sampling + print(f"\nStep 2: Generating candidate pool (batch_size={BATCH_SIZE})...") + + # Generate large candidate pool using Sobol sampling + pool_size = BATCH_SIZE * 20 # 160 candidates + candidates_pool_unit = draw_sobol_samples( + bounds=unit_bounds, + n=1, + q=pool_size * 2 # Larger pool for better diversity + ).squeeze(0).double() # (pool_size*2, 4) in [0, 1] + + # Model expects inputs in [0, 1] normalized space (no transformation needed) + # Use unit hypercube samples directly as model input + candidates_pool_norm = candidates_pool_unit + + # Compute scores for all candidates + print(f" Computing scores for {len(candidates_pool_norm)} candidates...") + + with torch.no_grad(): + # Predict uncertainties + posterior_abs = models['abs'].posterior(candidates_pool_norm) + mu_abs = posterior_abs.mean.squeeze(-1) + sigma_abs = posterior_abs.stddev.squeeze(-1) + + posterior_cov = models['cov'].posterior(candidates_pool_norm) + mu_cov = posterior_cov.mean.squeeze(-1) + sigma_cov = posterior_cov.stddev.squeeze(-1) + + posterior_uni = models['uni'].posterior(candidates_pool_norm) + mu_uni = posterior_uni.mean.squeeze(-1) + sigma_uni = posterior_uni.stddev.squeeze(-1) + + # Apply physical constraints to predictions + # Absorbance: Data is clipped at 2.0 during loading, so clamp predictions to [0, 2.0] for safety + mu_abs = torch.clamp(mu_abs, 0.0, 2.0) + + # Coverage: Must be in [0, 1] (0% to 100%) + # This prevents probability calculation distortion from unrealistic predictions (e.g., 1.2 or -0.1) + mu_cov = torch.clamp(mu_cov, 0.0, 1.0) + + # Clip σ_Uni BEFORE normalization (clip raw sigma_uni values) + sigma_uni_np = sigma_uni.cpu().numpy() + sigma_uni_clipped = np.clip(sigma_uni_np, None, uni_clip_threshold) + + # Normalize uncertainties (after clipping for σ_Uni) + # Convert to torch tensors for consistent type handling + norm_sigma_abs = torch.tensor((sigma_abs.cpu().numpy() - mean_abs) / (std_abs + 1e-8), dtype=torch.float64) + norm_sigma_cov = torch.tensor((sigma_cov.cpu().numpy() - mean_cov) / (std_cov + 1e-8), dtype=torch.float64) + norm_sigma_uni = torch.tensor((sigma_uni_clipped - mean_uni) / (std_uni + 1e-8), dtype=torch.float64) + + # Compute P(valid) + # P(valid) = P(Abs >= 0.1) * P(Cov >= 0.1) + # Using normal approximation + mu_abs_np = mu_abs.cpu().numpy() + sigma_abs_np = sigma_abs.cpu().numpy() + mu_cov_np = mu_cov.cpu().numpy() # mu_cov is already clamped to [0, 1] + sigma_cov_np = sigma_cov.cpu().numpy() + + # Calculate probabilities using clamped mu_cov (physical constraint applied) + p_abs_high = 1 - norm.cdf((VALID_ABS_THRESHOLD - mu_abs_np) / (sigma_abs_np + 1e-8)) + p_cov_high = 1 - norm.cdf((VALID_COV_THRESHOLD - mu_cov_np) / (sigma_cov_np + 1e-8)) + + # Combine probabilities + p_valid = p_abs_high * p_cov_high + + # Clip final probability to [0, 1] range (after calculation) + p_valid = np.clip(p_valid, 0.0, 1.0) + + # Convert p_valid to torch tensor to avoid type conflicts + p_valid_tensor = torch.tensor(p_valid, dtype=torch.float64) + + # Compute individual terms (contributions) for each component + term_abs = torch.tensor(w_abs, dtype=torch.float64) * norm_sigma_abs + term_cov = torch.tensor(w_cov, dtype=torch.float64) * norm_sigma_cov + term_uni = torch.tensor(w_uni, dtype=torch.float64) * norm_sigma_uni * (p_valid_tensor ** 2) + + # Compute raw score using adaptive weighting + # Score = W_Abs * Norm(σ_Abs) + W_Cov * Norm(σ_Cov) + W_Uni * Norm(σ_Uni) * P_valid^2 + score_raw = term_abs + term_cov + term_uni + + # Hard cutoff: P(valid) < HARD_CUTOFF_P_VALID 이면 Score를 0으로 + score_raw[p_valid_tensor < HARD_CUTOFF_P_VALID] = 0.0 + + # Diversity bonus (멀리 있을수록 좋음) + print(f" Applying diversity bonus...") + + # Combine current training data with previous round suggestions for round-level diversity + if X_prev_suggestions is not None: + X_train_all_with_prev = torch.cat([X_train_all, X_prev_suggestions], dim=0) + print(f" Including {len(X_prev_suggestions)} previous round suggestions in diversity calculation") + else: + X_train_all_with_prev = X_train_all + + X_train_norm = normalize(X_train_all_with_prev.double(), bounds_normalized.double()) + + # Compute all distances at once (more efficient) + distances = torch.cdist(candidates_pool_norm, X_train_norm) # (n_candidates, n_train) + min_dists = distances.min(dim=1).values # (n_candidates,) + + # Diversity weight: Tanh 방식 (부드러운 변환) + # 거리가 가까우면(0에 가까움) weight가 0에 가까워지고, + # 거리가 멀면 weight가 1에 가까워져서 score를 보존함 + # Scale factor 0.2는 정규화된 공간(0~1)에서의 거리 감도 조절용 + diversity_weight = torch.tanh(min_dists / 0.2) + + # Apply diversity weight to raw scores + scores_final = torch.tensor(score_raw, dtype=torch.float64) * diversity_weight + + # Compute density median (nearest neighbor distance median) + print(f" Computing density median...") + # Combine all existing data points + if X_prev_suggestions is not None: + X_all_existing = torch.cat([X_train_normalized, normalize(X_prev_suggestions.double(), bounds_normalized.double())], dim=0) + else: + X_all_existing = X_train_normalized + + # Compute distance matrix (excluding self-distances) + distances_all = torch.cdist(X_all_existing, X_all_existing) + # Fill diagonal with inf to exclude self-distances + distances_all.fill_diagonal_(float('inf')) + # Get minimum distance for each point (nearest neighbor) + min_dists_all = distances_all.min(dim=1).values + # Compute median + density_median = float(torch.median(min_dists_all).item()) + print(f" Density median (nearest neighbor distance): {density_median:.6f}") + + # Select top candidates + top_indices = torch.argsort(scores_final, descending=True)[:BATCH_SIZE] + candidates_selected = candidates_pool_norm[top_indices] + scores_selected = scores_final[top_indices] + + print(f"\n Selected {BATCH_SIZE} candidates:") + for i, idx in enumerate(top_indices): + p_valid_val = p_valid_tensor[idx].item() + print(f" [{i+1}] Score: {scores_final[idx]:.4f}, P(valid): {p_valid_val:.4f}") + + # Compute contributions and primary reasons for selected candidates + contrib_abs_selected = term_abs[top_indices].cpu().numpy() + contrib_cov_selected = term_cov[top_indices].cpu().numpy() + contrib_uni_selected = term_uni[top_indices].cpu().numpy() + + # Primary reason: which term has the maximum contribution + primary_reasons = [] + for i in range(len(top_indices)): + contribs = { + 'Uncertainty (Abs)': contrib_abs_selected[i], + 'Uncertainty (Cov)': contrib_cov_selected[i], + 'Uncertainty (Uni)': contrib_uni_selected[i] + } + primary_reason = max(contribs, key=contribs.get) + primary_reasons.append(primary_reason) + + # Extract predictions and uncertainties for selected candidates + pred_abs_selected = mu_abs[top_indices].cpu().numpy() + pred_cov_selected = mu_cov[top_indices].cpu().numpy() + pred_uni_selected = mu_uni[top_indices].cpu().numpy() + sigma_abs_selected = sigma_abs[top_indices].cpu().numpy() + sigma_cov_selected = sigma_cov[top_indices].cpu().numpy() + sigma_uni_selected = sigma_uni[top_indices].cpu().numpy() + + metadata = { + 'sigma_abs_pool_mean': mean_abs, + 'sigma_abs_pool_std': std_abs, + 'sigma_cov_pool_mean': mean_cov, + 'sigma_cov_pool_std': std_cov, + 'sigma_uni_pool_mean': mean_uni, + 'sigma_uni_pool_std': std_uni, + 'uni_clip_threshold': uni_clip_threshold, + 'p_valid': p_valid_tensor[top_indices].cpu().numpy(), + 'scores_raw': score_raw[top_indices].cpu().numpy() if isinstance(score_raw, torch.Tensor) else score_raw[top_indices], + 'scores_final': scores_selected.cpu().numpy(), + # Predictions + 'pred_abs': pred_abs_selected, + 'pred_cov': pred_cov_selected, + 'pred_uni': pred_uni_selected, + # Uncertainties + 'sigma_abs': sigma_abs_selected, + 'sigma_cov': sigma_cov_selected, + 'sigma_uni': sigma_uni_selected, + # Adaptive weighting + 'weight_abs': w_abs, + 'weight_cov': w_cov, + 'weight_uni': w_uni, + 'contrib_abs': contrib_abs_selected, + 'contrib_cov': contrib_cov_selected, + 'contrib_uni': contrib_uni_selected, + 'primary_reason': primary_reasons, + 'density_median': density_median + } + + return candidates_selected, scores_selected, metadata + + +def train_all_models(df: pd.DataFrame) -> Tuple[Dict[str, SingleTaskGP], Dict[str, float], torch.Tensor, torch.Tensor]: + """ + Train all GP models and compute normalization statistics. + + Args: + df: DataFrame with accumulated data + + Returns: + models: Dictionary of fitted models + normalization_stats: Dictionary of normalization statistics + X: Original input tensor + bounds_normalized: Normalized bounds tensor + """ + print("\n" + "=" * 60) + print("[Model] Training all models...") + print("=" * 60) + + # Prepare inputs and outputs + X, Y_dict, valid_mask = prepare_inputs_outputs(df) + + # Prepare bounds (with log scale conversion) + bounds_normalized = BOUNDS_ORIGINAL.clone() + for i, is_log in enumerate(LOG_SCALE_PARAMS): + if is_log: + bounds_normalized[0, i] = np.log10(BOUNDS_ORIGINAL[0, i].item()) + bounds_normalized[1, i] = np.log10(BOUNDS_ORIGINAL[1, i].item()) + + X_normalized = normalize(X.double(), bounds_normalized.double()) + + # Build models + model_abs = build_gp_model(X_normalized, Y_dict['abs']) + model_cov = build_gp_model(X_normalized, Y_dict['cov']) + + # Uniformity model: only valid samples + valid_mask_tensor = torch.tensor(valid_mask, dtype=torch.bool) + if valid_mask_tensor.sum() >= 2: + X_uni = X[valid_mask_tensor] + Y_uni = Y_dict['uni'][valid_mask_tensor] + X_uni_normalized = normalize(X_uni.double(), bounds_normalized.double()) + model_uni = build_gp_model(X_uni_normalized, Y_uni) + else: + model_uni = None + print(" Warning: Not enough valid samples for Uniformity model") + + models = { + 'abs': model_abs, + 'cov': model_cov, + 'uni': model_uni + } + + # Normalization statistics will be computed in compute_active_learning_scores() + # to ensure consistency with run_active_learning.py (ver1) + # Return empty dict here; it will be populated from metadata in main() + # This avoids generating Sobol samples here, which would change the random state + # and cause different suggestions than ver1 + normalization_stats = {} + + return models, normalization_stats, X, bounds_normalized + + +def calculate_model_predictions_on_grid( + models: Dict[str, SingleTaskGP], + stats: Dict[str, float], + grid_tensor: torch.Tensor, + weights: Dict[str, float] +) -> Dict[str, np.ndarray]: + """ + Calculate model predictions and acquisition scores on a grid. + + Args: + models: Dictionary of trained models + stats: Normalization statistics + grid_tensor: Normalized grid points (n_points, 4) + weights: Adaptive weights {'abs': w_abs, 'cov': w_cov, 'uni': w_uni} + + Returns: + Dictionary with keys: + 'mean_abs', 'mean_cov', 'mean_uni', 'p_valid', + 'std_abs', 'std_cov', 'std_uni', + 'score' + """ + # Unpack stats + mean_abs = stats['sigma_abs_mean'] + std_abs = stats['sigma_abs_std'] + mean_cov = stats['sigma_cov_mean'] + std_cov = stats['sigma_cov_std'] + mean_uni = stats['sigma_uni_mean'] + std_uni = stats['sigma_uni_std'] + uni_clip_threshold = stats['uni_clip_threshold'] + + # Unpack weights + w_abs = weights['abs'] + w_cov = weights['cov'] + w_uni = weights['uni'] + + with torch.no_grad(): + # Predict + posterior_abs = models['abs'].posterior(grid_tensor) + mu_abs = posterior_abs.mean.squeeze(-1) + sigma_abs = posterior_abs.stddev.squeeze(-1) + + posterior_cov = models['cov'].posterior(grid_tensor) + mu_cov = posterior_cov.mean.squeeze(-1) + sigma_cov = posterior_cov.stddev.squeeze(-1) + + if models['uni'] is not None: + posterior_uni = models['uni'].posterior(grid_tensor) + mu_uni = posterior_uni.mean.squeeze(-1) + sigma_uni = posterior_uni.stddev.squeeze(-1) + else: + mu_uni = torch.zeros_like(mu_abs) + sigma_uni = torch.zeros_like(sigma_abs) + + # Apply constraints + mu_abs = torch.clamp(mu_abs, 0.0, 2.0) + mu_cov = torch.clamp(mu_cov, 0.0, 1.0) + + # Clip sigma_uni + sigma_uni_np = sigma_uni.cpu().numpy() + sigma_uni_clipped = np.clip(sigma_uni_np, None, uni_clip_threshold) + + # Normalize uncertainties + norm_sigma_abs = (sigma_abs.cpu().numpy() - mean_abs) / (std_abs + 1e-8) + norm_sigma_cov = (sigma_cov.cpu().numpy() - mean_cov) / (std_cov + 1e-8) + norm_sigma_uni = (sigma_uni_clipped - mean_uni) / (std_uni + 1e-8) + + # Compute P(valid) + mu_abs_np = mu_abs.cpu().numpy() + sigma_abs_np = sigma_abs.cpu().numpy() + mu_cov_np = mu_cov.cpu().numpy() + sigma_cov_np = sigma_cov.cpu().numpy() + + p_abs_high = 1 - norm.cdf((VALID_ABS_THRESHOLD - mu_abs_np) / (sigma_abs_np + 1e-8)) + p_cov_high = 1 - norm.cdf((VALID_COV_THRESHOLD - mu_cov_np) / (sigma_cov_np + 1e-8)) + p_valid = p_abs_high * p_cov_high + p_valid = np.clip(p_valid, 0.0, 1.0) + + # Compute Score + term_abs = w_abs * norm_sigma_abs + term_cov = w_cov * norm_sigma_cov + term_uni = w_uni * norm_sigma_uni * (p_valid ** 2) + + score = term_abs + term_cov + term_uni + + # Hard cutoff + score[p_valid < HARD_CUTOFF_P_VALID] = 0.0 + + return { + 'mean_abs': mu_abs_np, + 'mean_cov': mu_cov_np, + 'mean_uni': mu_uni.cpu().numpy(), + 'p_valid': p_valid, + 'std_abs': sigma_abs_np, + 'std_cov': sigma_cov_np, + 'std_uni': sigma_uni_np, + 'score': score + } + + +# ============================================================================ +# Task 1: Evaluation & Logging (New) +# ============================================================================ + +def get_data_density_metrics(X_norm: torch.Tensor) -> Dict[str, float]: + """ + Compute data density metrics based on nearest neighbor distances in normalized space. + + Args: + X_norm: Normalized input tensor (n_samples, n_dims) + + Returns: + Dictionary with Density_Median, Density_P25, Density_P75 + """ + if len(X_norm) < 2: + return { + 'Density_Median': 0.0, + 'Density_P25': 0.0, + 'Density_P75': 0.0 + } + + # Compute distance matrix (excluding self-distances) + distances = torch.cdist(X_norm, X_norm) + # Fill diagonal with inf to exclude self-distances + distances.fill_diagonal_(float('inf')) + + # Get minimum distance for each point (nearest neighbor) + min_dists = distances.min(dim=1).values + min_dists_np = min_dists.cpu().numpy() + + # Compute stats + median = float(np.median(min_dists_np)) + p25 = float(np.percentile(min_dists_np, 25)) + p75 = float(np.percentile(min_dists_np, 75)) + + return { + 'Density_Median': median, + 'Density_P25': p25, + 'Density_P75': p75 + } + + +def evaluate_current_state(df: pd.DataFrame, save_cv_path: Optional[Path] = None) -> Dict[str, float]: + """ + Evaluate current state of the campaign. + + Args: + df: DataFrame with accumulated data + save_cv_path: Optional path to save CV results CSV + + Returns: + Dictionary containing all metrics (CV performance, Density, Weights) + """ + print("\n" + "=" * 60) + print("[Evaluation] Evaluating current state...") + print("=" * 60) + + # Prepare inputs and outputs + X, Y_dict, valid_mask = prepare_inputs_outputs(df) + + # Create strata + strata = create_strata(df) + + # Prepare bounds (with log scale conversion) + bounds_normalized = BOUNDS_ORIGINAL.clone() + for i, is_log in enumerate(LOG_SCALE_PARAMS): + if is_log: + bounds_normalized[0, i] = np.log10(BOUNDS_ORIGINAL[0, i].item()) + bounds_normalized[1, i] = np.log10(BOUNDS_ORIGINAL[1, i].item()) + + # 1. Density Metrics + X_normalized = normalize(X.double(), bounds_normalized.double()) + density_metrics = get_data_density_metrics(X_normalized) + print(f" Density Median: {density_metrics['Density_Median']:.4f}") + + # 2. Cross-Validation + cv_results, mean_r2_dict = run_cross_validation(X, Y_dict, valid_mask, strata, BOUNDS_ORIGINAL) + + if save_cv_path: + cv_results.to_csv(save_cv_path, index=False) + print(f" CV results saved to {save_cv_path}") + + # 3. Adaptive Weights + r2_abs = mean_r2_dict['abs_r2'] + r2_cov = mean_r2_dict['cov_r2'] + r2_uni = mean_r2_dict['uni_r2'] + + raw_w_abs = max(0.1, 1.0 - r2_abs) + raw_w_cov = max(0.1, 1.0 - r2_cov) + raw_w_uni = max(0.1, 1.0 - r2_uni) + + total_raw = raw_w_abs + raw_w_cov + raw_w_uni + w_abs = raw_w_abs / total_raw + w_cov = raw_w_cov / total_raw + w_uni = raw_w_uni / total_raw + + # Assemble Metrics + metrics = { + # Basic + 'Total_Samples': len(df), + 'Valid_Samples': int(df['Valid'].sum()), + 'Valid_Rate': float(df['Valid'].mean()), + + # CV Performance (Abs) + 'CV_Abs_R2_Mean': cv_results['r2_abs'].mean(), + 'CV_Abs_R2_Std': cv_results['r2_abs'].std(), + 'CV_Abs_RMSE_Mean': cv_results['rmse_abs'].mean(), + + # CV Performance (Cov) + 'CV_Cov_R2_Mean': cv_results['r2_cov'].mean(), + 'CV_Cov_R2_Std': cv_results['r2_cov'].std(), + 'CV_Cov_RMSE_Mean': cv_results['rmse_cov'].mean(), + + # CV Performance (Uni) + 'CV_Uni_R2_Mean': cv_results['r2_uni'].mean(), + 'CV_Uni_R2_Std': cv_results['r2_uni'].std(), + 'CV_Uni_RMSE_Mean': cv_results['rmse_uni'].mean(), + + # CV Performance (Accuracy) + 'CV_Valid_Acc_Mean': cv_results['accuracy'].mean(), + 'CV_Valid_Acc_Std': cv_results['accuracy'].std(), + + # Strategy Weights + 'W_Abs': w_abs, + 'W_Cov': w_cov, + 'W_Uni': w_uni + } + + # Add Density Metrics + metrics.update(density_metrics) + + return metrics + + +def append_progress_log(round_num: int, metrics_dict: Dict[str, float], log_path: Path): + """ + Append metrics to the campaign progress log CSV. + """ + # Create row dictionary + row = {'Round': round_num} + row.update(metrics_dict) + + # Create DataFrame + df_row = pd.DataFrame([row]) + + # Define column order (optional, but good for readability) + cols_order = [ + 'Round', 'Total_Samples', 'Valid_Samples', 'Valid_Rate', + 'CV_Abs_R2_Mean', 'CV_Abs_R2_Std', 'CV_Abs_RMSE_Mean', + 'CV_Cov_R2_Mean', 'CV_Cov_R2_Std', 'CV_Cov_RMSE_Mean', + 'CV_Uni_R2_Mean', 'CV_Uni_R2_Std', 'CV_Uni_RMSE_Mean', + 'CV_Valid_Acc_Mean', 'CV_Valid_Acc_Std', + 'Density_Median', 'Density_P25', 'Density_P75', + 'W_Abs', 'W_Cov', 'W_Uni' + ] + + # Reorder columns if they exist in the row + existing_cols = [c for c in cols_order if c in df_row.columns] + remaining_cols = [c for c in df_row.columns if c not in cols_order] + df_row = df_row[existing_cols + remaining_cols] + + # Append to file + if not log_path.exists(): + df_row.to_csv(log_path, index=False) + print(f"[Log] Created new progress log at {log_path}") + else: + df_row.to_csv(log_path, mode='a', header=False, index=False) + print(f"[Log] Appended Round {round_num} stats to {log_path}") + + +# ============================================================================ +# Main Function +# ============================================================================ + +def main(): + """Main execution function.""" + # Set random seeds for reproducibility + torch.manual_seed(SEED) + np.random.seed(SEED) + random.seed(SEED) + if torch.cuda.is_available(): + torch.cuda.manual_seed(SEED) + + # Data directory - specify directory path here + # All files (input CSV and output files) will be in this directory + data_dir = Path("/home/weiqizhang/Lab_Automation/aamp_app/Image_Processing/Round5") + + # Round number - specify which round to use (uses data from rounds <= round_num) + # Suggestions will be generated for round_num + 1 + # round_num = 8 # Removed hardcoded value to use auto-detection or CLI if needed + + # Construct CSV path from directory + csv_path = data_dir / "PProDOT_CB_Campaign_parameters.csv" + + # Load and preprocess data (including previous round suggestions) + # If round_num is None, it will use the max round in the CSV + df, current_round, X_prev_suggestions = load_and_preprocess_data(csv_path, round_num=None) + + # ======================================================================== + # Step 1: Evaluate Current State & Log Progress + # ======================================================================== + + # Create results directory if it doesn't exist + results_dir = data_dir / "results" + results_dir.mkdir(parents=True, exist_ok=True) + + # Define paths (all output files go to results directory) + cv_output_path = results_dir / f"cv_results_round{current_round}_v4.csv" + progress_log_path = results_dir / "campaign_progress_v4.csv" + + # Evaluate + metrics = evaluate_current_state(df, save_cv_path=cv_output_path) + + # Log progress + append_progress_log(current_round, metrics, progress_log_path) + + # Extract mean R2 scores for adaptive weighting + mean_r2_dict = { + 'abs_r2': metrics['CV_Abs_R2_Mean'], + 'cov_r2': metrics['CV_Cov_R2_Mean'], + 'uni_r2': metrics['CV_Uni_R2_Mean'] + } + + # ======================================================================== + # Step 2: Active Learning Suggestion + # ======================================================================== + + # Train all models + models, normalization_stats, X, bounds_normalized = train_all_models(df) + + X_normalized = normalize(X.double(), bounds_normalized.double()) + + # Compute active learning scores (with previous round suggestions for diversity) + candidates_normalized, scores, metadata = compute_active_learning_scores( + models, X_normalized, bounds_normalized, X, mean_r2_dict, X_prev_suggestions + ) + + # Update normalization_stats from metadata (for visualization) + normalization_stats = { + 'sigma_abs_mean': metadata['sigma_abs_pool_mean'], + 'sigma_abs_std': metadata['sigma_abs_pool_std'], + 'sigma_cov_mean': metadata['sigma_cov_pool_mean'], + 'sigma_cov_std': metadata['sigma_cov_pool_std'], + 'sigma_uni_mean': metadata['sigma_uni_pool_mean'], + 'sigma_uni_std': metadata['sigma_uni_pool_std'], + 'uni_clip_threshold': metadata['uni_clip_threshold'] + } + + # Decode candidates to original scale + candidates_unnorm = unnormalize(candidates_normalized, bounds_normalized.double()) + candidates_original = candidates_unnorm.clone() + for i, is_log in enumerate(LOG_SCALE_PARAMS): + if is_log: + candidates_original[:, i] = 10 ** candidates_unnorm[:, i] + + # Round discrete parameters + candidates_original[:, 1] = torch.round(candidates_original[:, 1]) # Temperature + candidates_original[:, 2] = torch.round(candidates_original[:, 2]) # Gap + candidates_original[:, 3] = torch.round(candidates_original[:, 3]) # Volume + candidates_original[:, 0] = torch.round(candidates_original[:, 0] * 100.0) / 100.0 # Speed (2 decimals) + + # Save suggestions to results directory + next_round = current_round + 1 + suggestions_path = results_dir / f"round{next_round}_suggestions_v4.csv" + + # Determine starting sample number + # If we have previous data for this round (unlikely for new suggestions, but good for consistency), continue numbering + # Otherwise start from 1 + start_sample = 1 + + # Construct DataFrame with requested column order + # Header: round#, Sample #, Temperature, Speed, gap, Solvent, Concentration, Precursor Volume + + suggestions_df = pd.DataFrame({ + 'round#': [next_round] * BATCH_SIZE, + 'Sample #': range(start_sample, start_sample + BATCH_SIZE), + 'Temperature': candidates_original[:, 1].cpu().numpy().astype(int), + 'Speed': candidates_original[:, 0].cpu().numpy(), + 'gap': candidates_original[:, 2].cpu().numpy().astype(int), + 'Solvent': [SOLVENT] * BATCH_SIZE, + 'Concentration': [CONCENTRATION] * BATCH_SIZE, + 'Precursor Volume': candidates_original[:, 3].cpu().numpy().astype(int), + + # Metadata follows + 'score': scores.cpu().numpy(), + 'p_valid': metadata['p_valid'], + 'score_raw': metadata['scores_raw'], + # Predictions + 'pred_abs': metadata['pred_abs'], + 'pred_cov': metadata['pred_cov'], + 'pred_uni': metadata['pred_uni'], + # Uncertainties + 'sigma_abs': metadata['sigma_abs'], + 'sigma_cov': metadata['sigma_cov'], + 'sigma_uni': metadata['sigma_uni'], + # Adaptive weighting information + 'Weight_Abs': [metadata['weight_abs']] * len(candidates_original), + 'Weight_Cov': [metadata['weight_cov']] * len(candidates_original), + 'Weight_Uni': [metadata['weight_uni']] * len(candidates_original), + # Contributions + 'Contrib_Abs': metadata['contrib_abs'], + 'Contrib_Cov': metadata['contrib_cov'], + 'Contrib_Uni': metadata['contrib_uni'], + # Primary reason and density + 'Primary_Reason': metadata['primary_reason'], + 'Density_Median': [metadata['density_median']] * len(candidates_original) + }) + + suggestions_df.to_csv(suggestions_path, index=False) + print(f"\n[Optimization] Suggestions saved to {suggestions_path}") + + print("\n" + "=" * 60) + print("Active Learning Complete!") + print("=" * 60) + + +if __name__ == "__main__": + main() + diff --git a/aamp_app/Image_Processing/Image_processing/scoring.py b/aamp_app/Image_Processing/Image_processing/scoring.py new file mode 100644 index 0000000..d68de44 --- /dev/null +++ b/aamp_app/Image_Processing/Image_processing/scoring.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any, Dict, Tuple + +import cv2 +import numpy as np + +from .config import UniformityConfig +# analyze_image is only used inside compute_uniformity_score function, so use lazy import + + +# Ideal (Perfect) Reference statistics (used when reference is not available) +MODEL_REF_STD = 2.3 +MODEL_REF_ENTROPY = 2.9 + + +def analyze_uniformity( + ref_stats: Dict[str, float], + cfg: UniformityConfig, + use_model_reference: bool = False, +) -> Tuple[float, Dict[str, float]]: + """Calculate Uniformity Score using Reference-based Absolute Scoring (RAS) method. + + Args: + ref_stats: Dictionary containing Reference and Sample statistics + - ref_std: Reference S channel standard deviation (None if not available) + - ref_entropy: Reference S channel entropy (None if not available) + - sample_std: Sample S channel standard deviation + - sample_entropy: Sample S channel entropy + cfg: UniformityConfig object (includes sensitivity parameters) + use_model_reference: If True, use ideal reference (default: False) + + Returns: + (uniformity_score, debug_info) + - uniformity_score: float value between 0 and 1 + - debug_info: Contains all intermediate calculation values (including use_model_reference flag) + """ + sample_std = ref_stats.get("sample_std", 0.0) + sample_entropy = ref_stats.get("sample_entropy", 0.0) + + # Reference statistics: use actual reference if available, otherwise use ideal reference + if use_model_reference or ref_stats.get("ref_std") is None: + ref_std = MODEL_REF_STD + ref_entropy = MODEL_REF_ENTROPY + use_model_reference = True + else: + ref_std = ref_stats.get("ref_std", 0.0) + ref_entropy = ref_stats.get("ref_entropy", 0.0) + + # Step 1: Roughness Score (Std) + delta_std = max(0.0, sample_std - ref_std) + score_std = np.exp(-delta_std / cfg.uniformity_std_sensitivity) + + # Step 2: Texture Score (Entropy) + delta_ent = max(0.0, sample_entropy - ref_entropy) + score_ent = np.exp(-delta_ent / cfg.uniformity_ent_sensitivity) + + # Step 3: Final Score + final_score = (score_std * 0.5) + (score_ent * 0.5) + + debug_info: Dict[str, float] = { + "ref_std": ref_std, + "ref_entropy": ref_entropy, + "sample_std": sample_std, + "sample_entropy": sample_entropy, + "delta_std": delta_std, + "delta_entropy": delta_ent, + "score_std": float(score_std), + "score_entropy": float(score_ent), + "uniformity_std_sensitivity": cfg.uniformity_std_sensitivity, + "uniformity_ent_sensitivity": cfg.uniformity_ent_sensitivity, + "use_model_reference": 1.0 if use_model_reference else 0.0, # Convert bool to float + "uniformity_score": float(final_score), + } + + return float(final_score), debug_info + + +def compute_uniformity_score( + image_path: str, + metadata: Dict[str, Any] | None = None, + verbose: bool = False, +) -> float: + """Calculate temporary Uniformity score based on coverage.""" + # Lazy import to avoid circular dependency + from .pipeline import analyze_image + + image_path_obj = Path(image_path) + if not image_path_obj.exists(): + raise FileNotFoundError(f"Image not found: {image_path}") + + image_bgr = cv2.imread(str(image_path_obj)) + if image_bgr is None: + raise ValueError(f"Could not load image: {image_path}") + + metadata = metadata or {} + cfg = _create_config_from_metadata(metadata, image_path_obj) + + reference_bgr = None + if cfg.use_reference: + ref_path = _match_reference_for(image_path_obj, cfg.reference_dir, cfg.reference_name) + if ref_path is not None and ref_path.exists(): + reference_bgr = cv2.imread(str(ref_path)) + elif verbose: + print(f"[WARN] Reference not found for {image_path_obj.name}") + + metrics, *_ = analyze_image( + image_bgr=image_bgr, + cfg=cfg, + ref_bgr=reference_bgr, + image_path=image_path_obj, + ) + + # Use new CV-based uniformity score (full ROI based) + uniformity_score = metrics.get("uniformity_score") + if uniformity_score is not None: + return float(uniformity_score) + + # Fallback: coverage percentage (previous method) + coverage_pct = metrics.get("coverage_percentage") + if coverage_pct is None: + return 0.0 + return float(coverage_pct) + + +def _create_config_from_metadata(metadata: Dict[str, Any], image_path: Path) -> UniformityConfig: + return UniformityConfig( + input_dir=str(image_path.parent), + use_reference=metadata.get("reference_dir") is not None, + reference_dir=metadata.get("reference_dir"), + reference_name=metadata.get("reference_name", "background_normal_0deg"), + roi_x=metadata.get("roi_x", 0), + roi_y=metadata.get("roi_y", 0), + roi_width=metadata.get("roi_width", 100), + roi_height=metadata.get("roi_height", 100), + uvvis_dir=metadata.get("uvvis_dir"), + uvvis_abs_threshold=metadata.get("uvvis_abs_threshold", 0.1), + uvvis_wavelength_min=metadata.get("uvvis_wavelength_min", 300.0), + uvvis_wavelength_max=metadata.get("uvvis_wavelength_max", 800.0), + coverage_threshold=metadata.get("coverage_threshold", 0.1), + coverage_smoothing=metadata.get("coverage_smoothing", 1.5), + coverage_k=metadata.get("coverage_k", 3.0), + coverage_min_std=metadata.get("coverage_min_std", 10.0), + uniformity_std_sensitivity=metadata.get("uniformity_std_sensitivity", 20.0), + uniformity_ent_sensitivity=metadata.get("uniformity_ent_sensitivity", 3.0), + compute_learning_features=metadata.get("compute_learning_features", False), + ) + + +def _match_reference_for( + image_path: Path, + reference_dir: str | None, + reference_name: str | None, +) -> Path | None: + """Find image matching reference_name pattern within reference_dir.""" + + search_dir = Path(reference_dir) if reference_dir else image_path.parent + if reference_name: + for candidate in sorted(search_dir.iterdir()): + if candidate.is_file() and candidate.stem.startswith(reference_name): + return candidate + + if reference_dir: + same_name = Path(reference_dir) / image_path.name + try: + if same_name.exists() and same_name.resolve() != image_path.resolve(): + return same_name + except Exception: + if same_name.exists() and str(same_name) != str(image_path): + return same_name + + return None + diff --git a/aamp_app/Image_Processing/Image_processing/uvvis.py b/aamp_app/Image_Processing/Image_processing/uvvis.py new file mode 100644 index 0000000..26db2b9 --- /dev/null +++ b/aamp_app/Image_Processing/Image_processing/uvvis.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from io import BytesIO +from typing import Dict, Tuple +import re + +import pandas as pd + + +def _extract_round_sample_id(image_stem: str) -> str | None: + match = re.search(r"(R\d+S\d+)", image_stem, re.IGNORECASE) + if not match: + return None + return match.group(1).upper() + + +def find_uvvis_file(image_name, fs, campaign_id): + round_sample_id = _extract_round_sample_id(image_name) + if round_sample_id is None: + return None + + candidates = fs.find_one({"filename": {"$regex": f"^{round_sample_id}.*\\.csv$"}, "campaign_id": campaign_id}) + if candidates is None: + return None + return candidates + + +def _find_header_row(uvvis_data: bytes) -> int | None: + text = uvvis_data.decode("utf-8", errors="ignore") + for idx, line in enumerate(text.splitlines()): + if "wavelength" in line.lower(): + return idx + return None + + +def analyze_uvvis_data( + image_name, + fs, + campaign_id, + abs_threshold: float = 0.1, + wavelength_min: float = 300.0, + wavelength_max: float = 800.0, +) -> Tuple[bool, Dict[str, float | str]]: + """Load UV-Vis CSV, compute simple statistics, and threshold check.""" + + uvvis_file = find_uvvis_file(image_name, fs, campaign_id) + if uvvis_file is None: + return False, { + "uvvis_reason": "file_not_found", + "uvvis_abs_threshold": abs_threshold, + } + + data = uvvis_file.read() + header_row = _find_header_row(data) + read_kwargs = {"encoding": "utf-8", "engine": "python"} + try: + if header_row is not None: + df = pd.read_csv(BytesIO(data), header=header_row, **read_kwargs) + else: + df = pd.read_csv(BytesIO(data), **read_kwargs) + except Exception: + return False, { + "uvvis_reason": "read_error", + "uvvis_file": str(uvvis_file.filename), + "uvvis_abs_threshold": abs_threshold, + } + + df = df.dropna(axis=1, how="all") + df = df.rename(columns=lambda c: str(c).strip()) + + abs_col = None + wavelength_col = None + for col in df.columns: + cname = str(col).lower() + if abs_col is None and ("abs" in cname or "absorbance" in cname): + abs_col = col + if wavelength_col is None and ("wavelength" in cname or cname.startswith("wl")): + wavelength_col = col + + if abs_col is None or wavelength_col is None: + return False, { + "uvvis_reason": "missing_columns", + "uvvis_file": str(uvvis_file.filename), + "uvvis_abs_threshold": abs_threshold, + } + + df = df[[wavelength_col, abs_col]].copy() + df[wavelength_col] = pd.to_numeric(df[wavelength_col], errors="coerce") + df[abs_col] = pd.to_numeric(df[abs_col], errors="coerce") + df = df.dropna() + df = df[(df[wavelength_col] >= wavelength_min) & (df[wavelength_col] <= wavelength_max)] + + if df.empty: + return False, { + "uvvis_reason": "empty_data", + "uvvis_file": str(uvvis_file.filename), + "uvvis_abs_threshold": abs_threshold, + } + + max_abs = float(df[abs_col].max()) + mean_abs = float(df[abs_col].mean()) + median_abs = float(df[abs_col].median()) + min_abs = float(df[abs_col].min()) + data_points = int(len(df)) + peak_idx = int(df[abs_col].idxmax()) + peak_wavelength = float(df.loc[peak_idx, wavelength_col]) + + passed = max_abs >= abs_threshold + + stats: Dict[str, float | str] = { + "uvvis_file": str(uvvis_file.filename), + "uvvis_abs_threshold": abs_threshold, + "uvvis_wavelength_min": wavelength_min, + "uvvis_wavelength_max": wavelength_max, + "uvvis_passed": passed, + "uvvis_reason": "ok" if passed else "below_threshold", + "uvvis_max_abs": max_abs, + "uvvis_mean_abs": mean_abs, + "uvvis_median_abs": median_abs, + "uvvis_min_abs": min_abs, + "uvvis_data_points": data_points, + "uvvis_peak_wavelength": peak_wavelength, + } + + return passed, stats diff --git a/aamp_app/Image_Processing/README.md b/aamp_app/Image_Processing/README.md new file mode 100644 index 0000000..38a60df --- /dev/null +++ b/aamp_app/Image_Processing/README.md @@ -0,0 +1,308 @@ +# PProDOT Demo Campaign + +A project for optimizing PProDOT film manufacturing through image analysis and Bayesian optimization. + +## Overview + +This project provides tools for: +- **Image Analysis**: Analyzing film coverage and uniformity from sample images +- **UV-Vis Analysis**: Processing UV-Vis spectroscopy data +- **Bayesian Optimization**: Optimizing manufacturing parameters (Speed, Temperature, Gap, Volume) using constrained Bayesian optimization + +## Project Structure + +``` +PProDOT_Demo_campaign/ +├── Image_processing/ # Image analysis module +│ ├── __init__.py # Module exports +│ ├── config.py # Configuration dataclass (UniformityConfig) +│ ├── coverage.py # HSV-based film coverage analysis +│ ├── pipeline.py # Main image analysis pipeline +│ ├── scoring.py # Uniformity score calculation +│ └── uvvis.py # UV-Vis data analysis +├── constrained_bo_ver2.py # Constrained Bayesian Optimizer +├── image_processing_changhyun.py # Batch image processing script +├── requirements.txt # Python dependencies +└── README.md # This file +``` + +## Module Descriptions + +### Image_processing Module + +A comprehensive image analysis module for evaluating film quality. + +#### `config.py` - UniformityConfig +Configuration dataclass that holds all analysis parameters: +- ROI (Region of Interest) coordinates +- Reference image settings +- UV-Vis analysis thresholds +- Coverage and uniformity calculation parameters + +**Usage:** +```python +from Image_processing import UniformityConfig + +cfg = UniformityConfig( + roi_x=580, + roi_y=400, + roi_width=913, + roi_height=415, + reference_dir="path/to/reference", + uvvis_dir="path/to/uvvis", + coverage_threshold=0.1, + uniformity_std_sensitivity=15.0, + uniformity_ent_sensitivity=2.0 +) +``` + +#### `coverage.py` - Film Coverage Analysis +HSV-based film coverage analysis module. Instead of traditional reference subtraction, it uses: +- Dynamic thresholds based on reference image S-channel statistics +- Sample image HSV information +- Otsu thresholding on V channel for dark regions + +**Key Functions:** +- `analyze_film_coverage()`: Main function that calculates coverage percentage and generates masks + +**Returns:** +- Coverage percentage (0-1) +- Sample and reference ROI images +- Binary mask showing film regions +- Debug information dictionary + +#### `pipeline.py` - Analysis Pipeline +Main pipeline that orchestrates the entire analysis process: +- Loads and processes images +- Analyzes coverage +- Calculates uniformity scores +- Processes UV-Vis data if available + +**Key Functions:** +- `analyze_image()`: Complete analysis pipeline for a single image + +#### `scoring.py` - Uniformity Scoring +Calculates uniformity scores using Reference-based Absolute Scoring (RAS): +- Compares sample statistics (std, entropy) with reference +- Uses ideal reference model when actual reference is unavailable +- Returns score between 0 and 1 + +**Key Functions:** +- `analyze_uniformity()`: Calculate uniformity score from statistics +- `compute_uniformity_score()`: High-level function to compute score from image path + +#### `uvvis.py` - UV-Vis Analysis +Processes UV-Vis spectroscopy CSV files: +- Finds matching UV-Vis files based on sample ID +- Extracts absorbance data +- Checks against thresholds + +**Key Functions:** +- `analyze_uvvis_data()`: Analyze UV-Vis file and return statistics +- `find_uvvis_file()`: Locate UV-Vis file for a given image + +### constrained_bo_ver2.py + +Constrained Bayesian Optimization system for parameter optimization. + +**Purpose:** Optimizes manufacturing parameters (Speed, Temperature, Gap, Precursor Volume) to maximize uniformity score while satisfying constraints (UV-Vis ≥ 0.1 AND Coverage ≥ 0.9). + +**Key Features:** +- **Objective Model**: Learns uniformity_score from valid samples only +- **Constraint Model**: Learns P(valid|x) from all samples +- **Acquisition Function**: EI(x) × P(valid|x) +- Handles discrete parameters (Temperature, Gap, Volume) +- Log-scale handling for Speed parameter +- Avoids duplicate parameter combinations + +**Usage:** +```python +from constrained_bo_ver2 import ConstrainedBayesianOptimizer +import torch + +# Define parameter bounds: [Speed, Temperature, gap, volume] +bounds = torch.tensor([ + [0.01, 20.0], # Speed + [25.0, 107.0], # Temperature + [50, 200], # Gap + [5, 15] # Volume +]).T + +# Initialize optimizer +optimizer = ConstrainedBayesianOptimizer( + bounds=bounds, + csv_path='data/Round0_3/PProDOT_CB_Campaign_parameters.csv', + round_num=0, + batch_size=8, + discrete_or_not=[False, True, True, True], + discrete_points=[...] # Discrete values for each parameter +) + +# Suggest candidates +candidates, metadata = optimizer.suggest() + +# Save to CSV +optimizer.save_candidates_to_csv(candidates, metadata=metadata) +``` + +**Input CSV Format:** +The CSV should contain columns: +- `round#`, `Sample #` +- `Speed`, `Temperature`, `gap`, `Precursor Volume` +- `uvvis_max_abs`, `coverage_percentage`, `uniformity_score` + +### image_processing_changhyun.py + +Batch image processing script for analyzing multiple directories of images. + +**Purpose:** Processes images from multiple directories, calculates metrics, and saves results to CSV files. + +**Features:** +- Processes images from specified directories +- Matches images with reference and UV-Vis data +- Generates debug mask images with analysis results +- Saves comprehensive results to CSV + +**Usage:** +```python +# Edit main() function to specify directories +main_dirs = [ + Path("data/Round0"), + Path("data/Round0_1"), + # Add more directories... +] + +# Run the script +python image_processing_changhyun.py +``` + +**Directory Structure Expected:** +``` +data/ +└── Round0/ + ├── Raw/ # Sample images (e.g., R0S01.png) + ├── blank/ # Reference images + ├── UV-Vis/ # UV-Vis CSV files + └── PProDOT_CB_Campaign_parameters.csv # Parameter data +``` + +**Output:** +- `processing_results_simple.csv`: Analysis results with all metrics +- `Debug_Masks/`: Visual debug images showing analysis results + +## Setup + +### Virtual Environment Activation + +**Windows (PowerShell):** +```powershell +.\venv\Scripts\Activate.ps1 +``` + +**Windows (Command Prompt):** +```cmd +.\venv\Scripts\activate.bat +``` + +### Package Installation + +```powershell +pip install -r requirements.txt +``` + +## Dependencies + +- **numpy**: Numerical computations +- **opencv-python**: Image processing +- **pandas**: Data manipulation +- **scikit-image**: Image analysis utilities +- **torch**: PyTorch for Bayesian optimization +- **botorch**: Bayesian optimization library +- **gpytorch**: Gaussian process models + +## Workflow + +### 1. Image Analysis Workflow + +1. **Prepare Data Structure:** + - Organize images in `Raw/` directory + - Place reference images in `blank/` directory + - Add UV-Vis CSV files to `UV-Vis/` directory + +2. **Run Batch Processing:** + ```python + python image_processing_changhyun.py + ``` + +3. **Review Results:** + - Check `processing_results_simple.csv` for metrics + - Review `Debug_Masks/` for visual analysis + +### 2. Bayesian Optimization Workflow + +1. **Prepare CSV with Initial Data:** + - Include parameter values and corresponding metrics + - Ensure columns: `round#`, `Sample #`, `Speed`, `Temperature`, `gap`, `Precursor Volume`, `uvvis_max_abs`, `coverage_percentage`, `uniformity_score` + +2. **Run Optimization:** + ```python + python constrained_bo_ver2.py + ``` + +3. **Review Suggestions:** + - Check generated candidates + - Check metadata (EI, P(valid), scores)) + - New candidates are saved to CSV + +4. **Iterate:** + - Run experiments with suggested parameters + - Add results to CSV + - Update `round_num` and run again + +## Key Concepts + +### Coverage Analysis +- Uses HSV color space for better film detection +- Dynamic threshold based on reference S-channel statistics +- Combines saturation and value channel analysis + +### Uniformity Score +- Reference-based Absolute Scoring (RAS) method +- Compares sample roughness (std) and texture (entropy) with reference +- Score ranges from 0 (poor) to 1 (excellent) + +### Validity Constraints +- **UV-Vis constraint**: `uvvis_max_abs >= 0.1` +- **Coverage constraint**: `coverage_percentage >= 0.9` +- Only valid samples are used for objective model training + +### Bayesian Optimization +- Uses Gaussian Process (GP) models for both objective and constraints +- Acquisition function balances exploration and exploitation +- Handles discrete and continuous parameters appropriately + +## Notes + +- Speed parameter is handled in log scale for better optimization +- Reference images should be named with prefix matching `reference_name` (default: "background_normal_0deg") +- Image filenames should follow pattern: `R{round#}S{sample#}.{ext}` (e.g., R0S01.png) +- UV-Vis files should match pattern: `{round_sample_id}_*.csv` (e.g., R0S01_*.csv) + +## Troubleshooting + +**Issue: Reference images not found** +- Check `reference_dir` path in configuration +- Verify reference image naming matches `reference_name` pattern + +**Issue: UV-Vis files not found** +- Ensure UV-Vis directory path is correct +- Check filename pattern matches sample ID extraction + +**Issue: Low uniformity scores** +- Adjust `uniformity_std_sensitivity` and `uniformity_ent_sensitivity` parameters +- Review ROI coordinates to ensure correct region is analyzed + +**Issue: BO not suggesting good candidates** +- Check if enough valid samples exist in CSV +- Verify constraint thresholds are appropriate +- Review objective model predictions in metadata diff --git a/aamp_app/Image_Processing/constrained_bo_ver2.py b/aamp_app/Image_Processing/constrained_bo_ver2.py new file mode 100644 index 0000000..b08f476 --- /dev/null +++ b/aamp_app/Image_Processing/constrained_bo_ver2.py @@ -0,0 +1,767 @@ +#!/usr/bin/env python3 +""" +Constrained Bayesian Optimization for PProDOT Film Optimization. + +This implements a constrained BO system with: +- Objective Model: Learns uniformity_score from valid samples only +- Constraint Model: Learns P(valid|x) from all samples +- Acquisition: EI(x) × P(valid|x) +""" +from io import BytesIO +import torch +import numpy as np +import pandas as pd +from pathlib import Path +from typing import Optional, List, Tuple + +# BoTorch imports +from botorch.models import SingleTaskGP +from botorch.fit import fit_gpytorch_mll +from botorch.acquisition import qLogNoisyExpectedImprovement +from botorch.optim import optimize_acqf +from botorch.utils.transforms import normalize, unnormalize +from botorch.utils.sampling import draw_sobol_samples + +# GPyTorch imports +from gpytorch.mlls import ExactMarginalLogLikelihood +from gpytorch.kernels import ScaleKernel, RBFKernel +from gpytorch.priors import GammaPrior + + +class ConstrainedBayesianOptimizer: + """ + Constrained Bayesian Optimizer with separate objective and constraint models. + + Objective Model: Learns uniformity_score from valid samples (uvvis>=0.1 AND coverage>=0.9) + Constraint Model: Learns P(valid|x) from all samples + Acquisition: EI(x) × P(valid|x) + """ + + def __init__( + self, + bounds: torch.Tensor, + csv_data: bytes, + round_num: int = 0, + batch_size: int = 8, + seed: int = 42, + discrete_or_not: List[bool] = [False, True, True, True], + discrete_points: Optional[List[torch.Tensor]] = None, + verbose: bool = True, + log_scale_params: List[bool] = [True, False, False, False], # Speed is log scale + uvvis_threshold: float = 0.1, + coverage_threshold: float = 0.9 + ): + """ + Initialize Constrained Bayesian Optimizer. + + Args: + bounds: Parameter bounds tensor of shape (2, n_dims) [original scale] + csv_data: Path to CSV file containing training data + round_num: Current round number (uses data from rounds <= round_num) + batch_size: Number of candidates to suggest per iteration + seed: Random seed + discrete_or_not: List indicating which parameters are discrete + discrete_points: List of discrete values for each parameter + verbose: Whether to print progress messages + log_scale_params: List indicating which parameters are in log scale + uvvis_threshold: UV-Vis absorbance threshold for validity + coverage_threshold: Coverage percentage threshold for validity + """ + # Set device (default CPU) + self.device = torch.device("cpu") + + # Convert bounds to log scale for log-scale parameters + self.log_scale_params = log_scale_params + bounds_log = bounds.clone() + for i, is_log in enumerate(log_scale_params): + if is_log: + bounds_log[0, i] = np.log10(bounds[0, i].item()) + bounds_log[1, i] = np.log10(bounds[1, i].item()) + self.bounds = bounds_log.double() + self.bounds_original = bounds.double() + + self.batch_size = batch_size + self.seed = seed + self.discrete_or_not = discrete_or_not + self.discrete_points = discrete_points if discrete_points is not None else [] + self.csv_data = csv_data + self.round_num = round_num + self.verbose = verbose + self.uvvis_threshold = uvvis_threshold + self.coverage_threshold = coverage_threshold + + torch.manual_seed(seed) + np.random.seed(seed) + + assert bounds.shape[0] == 2, "Bounds must have shape (2, n_dims)" + assert (bounds[1] > bounds[0]).all(), "Upper bounds must be greater than lower bounds" + + self.n_dims = bounds.shape[1] + self.objective_model = None + self.constraint_model = None + self.train_X_all = None # All samples (normalized) + self.train_Y_obj = None # Objective values (valid samples only) + self.train_X_obj = None # Valid samples only (normalized) + self.train_Y_valid = None # Validity flags (all samples) + self.train_Y_mean = None # For denormalization + self.train_Y_std = None + + # Set to track already-seen parameter combinations + self.seen_param_keys = set() + + # Load initial data from CSV + self._load_data() + + def _load_data(self) -> None: + """Load and preprocess training data from CSV file.""" + df = pd.read_csv(BytesIO(self.csv_data)) + + # Filter by round number: use all rounds up to and including round_num + if 'round#' in df.columns: + df_all_rounds = df[df['round#'] <= self.round_num].copy() + if self.verbose: + rounds_used = sorted(df_all_rounds['round#'].unique().tolist()) + print(f" Using data from rounds: {rounds_used}") + else: + df_all_rounds = df.copy() + + # Build seen_param_keys from all rows (including NaN uniformity_score) + self.seen_param_keys.clear() + for _, row in df_all_rounds.iterrows(): + try: + sp = round(float(row['Speed']), 2) + T = int(round(float(row['Temperature']))) + g = int(round(float(row['gap']))) + v = int(round(float(row['Precursor Volume']))) + self.seen_param_keys.add((sp, T, g, v)) + except (KeyError, ValueError, TypeError): + # Skip rows with missing or invalid parameter values + continue + + if self.verbose: + print(f" Seen parameter combinations: {len(self.seen_param_keys)}") + + # Filter rows with valid uniformity_score for training + df = df_all_rounds.dropna(subset=['uniformity_score']).copy() + + if len(df) == 0: + raise ValueError(f"No valid data found for round {self.round_num} in {self.csv_data}") + + # Extract parameters: [Speed, Temperature, gap, Precursor Volume] + speed = df['Speed'].values + temperature = df['Temperature'].values + gap = df['gap'].values + volume = df['Precursor Volume'].values + + # Convert Speed to log scale + speed_log = np.log10(speed) + + # Stack in order: [Speed (log), Temperature, gap, volume] + train_X_all = np.column_stack([speed_log, temperature, gap, volume]) + train_Y_all = df['uniformity_score'].values.reshape(-1, 1) + + # Compute validity flags + uvvis_values = df['uvvis_max_abs'].values if 'uvvis_max_abs' in df.columns else np.zeros(len(df)) + coverage_values = df['coverage_percentage'].values if 'coverage_percentage' in df.columns else np.zeros(len(df)) + + valid_mask = (uvvis_values >= self.uvvis_threshold) & (coverage_values >= self.coverage_threshold) + valid_flags = valid_mask.astype(float).reshape(-1, 1) + + if self.verbose: + n_valid = valid_mask.sum() + print(f" Total samples: {len(df)}") + print(f" Valid samples (uvvis>={self.uvvis_threshold} AND coverage>={self.coverage_threshold}): {n_valid}/{len(df)} ({100*n_valid/len(df):.1f}%)") + + # Store data + self.train_X_all = torch.tensor(train_X_all, dtype=torch.float64) + self.train_Y_valid = torch.tensor(valid_flags, dtype=torch.float64) + + # Objective model uses only valid samples + if valid_mask.sum() > 0: + self.train_X_obj = torch.tensor(train_X_all[valid_mask], dtype=torch.float64) + self.train_Y_obj = torch.tensor(train_Y_all[valid_mask], dtype=torch.float64) + + # Store normalization parameters for objective + self.train_Y_mean = self.train_Y_obj.mean().item() + self.train_Y_std = max(self.train_Y_obj.std().item(), 1e-6) # Numerical stability + + if self.verbose: + print(f" Objective model: {len(self.train_X_obj)} valid samples") + print(f" Best observed score: {self.train_Y_obj.max().item():.6f}") + else: + raise ValueError("No valid samples found for objective model!") + + if self.verbose: + print(f" Constraint model: {len(self.train_X_all)} total samples") + print(f" Validity rate: {valid_flags.mean().item():.2%}") + + def _build_models(self) -> None: + """Build objective and constraint GP models.""" + if self.train_X_obj is None or self.train_Y_obj is None: + raise ValueError("No objective training data available.") + if self.train_X_all is None or self.train_Y_valid is None: + raise ValueError("No constraint training data available.") + + # Normalize all data + train_X_all_normalized = normalize(self.train_X_all.double(), self.bounds.double()) + train_X_obj_normalized = normalize(self.train_X_obj.double(), self.bounds.double()) + + # Normalize objective values + train_Y_obj_normalized = (self.train_Y_obj.double() - self.train_Y_mean) / self.train_Y_std + + # Constraint values are already 0/1, no normalization needed + + # Build Objective Model (valid samples only) + self.objective_model = SingleTaskGP( + train_X_obj_normalized, + train_Y_obj_normalized, + covar_module=ScaleKernel( + RBFKernel( + lengthscale_prior=GammaPrior(3.0, 1.0), + ard_num_dims=self.n_dims + ), + outputscale_prior=GammaPrior(2.0, 0.5) + ) + ) + + self.objective_model.train() + mll_obj = ExactMarginalLogLikelihood(self.objective_model.likelihood, self.objective_model) + fit_gpytorch_mll(mll_obj) + self.objective_model.eval() + + # Build Constraint Model (all samples) + self.constraint_model = SingleTaskGP( + train_X_all_normalized, + self.train_Y_valid.double(), + covar_module=ScaleKernel( + RBFKernel( + lengthscale_prior=GammaPrior(3.0, 1.0), + ard_num_dims=self.n_dims + ), + outputscale_prior=GammaPrior(2.0, 0.5) + ) + ) + + self.constraint_model.train() + mll_const = ExactMarginalLogLikelihood(self.constraint_model.likelihood, self.constraint_model) + fit_gpytorch_mll(mll_const) + self.constraint_model.eval() + + if self.verbose: + print(" Models fitted successfully") + + def _decode_and_snap(self, candidates_normalized: torch.Tensor) -> torch.Tensor: + """ + Decode normalized candidates to original scale with discrete snapping. + + Args: + candidates_normalized: Tensor of shape (n, n_dims) in normalized [0,1] space + + Returns: + Tensor of shape (n, n_dims) in original scale with discrete snapping applied + """ + # Inverse transform + candidates_unnormalized = unnormalize(candidates_normalized, self.bounds.double()) + + # Convert log-scale parameters back to original scale + candidates_original = candidates_unnormalized.clone() + for i, is_log in enumerate(self.log_scale_params): + if is_log: + candidates_original[:, i] = 10 ** candidates_unnormalized[:, i] + + # Apply discrete constraints + for i in range(candidates_original.shape[0]): + for j in range(candidates_original.shape[1]): + if self.discrete_or_not[j] and j < len(self.discrete_points): + column_values = self.discrete_points[j] + closest_value = torch.abs(column_values - candidates_original[i, j]) + candidates_original[i, j] = column_values[torch.argmin(closest_value)] + + # Round Speed (continuous variable) to 2 decimal places + candidates_original[:, 0] = torch.round(candidates_original[:, 0] * 100.0) / 100.0 + + return candidates_original + + def _compute_metadata(self, candidates_normalized: torch.Tensor, acquisition_function) -> dict: + """ + Compute all metadata for candidates. + + Args: + candidates_normalized: Tensor of shape (n, n_dims) in normalized space + acquisition_function: qLogNoisyExpectedImprovement instance + + Returns: + Dictionary with all metadata arrays + """ + with torch.no_grad(): + # EI log and raw + ei_log = acquisition_function(candidates_normalized.unsqueeze(1)).squeeze(-1) # (n,) + ei_raw = torch.clamp(torch.exp(ei_log), min=0.0) + + # Constraint GP posterior + constraint_posterior = self.constraint_model.posterior(candidates_normalized) + mu_valid_raw = constraint_posterior.mean.squeeze(-1) # (n,) + p_valid = torch.sigmoid(mu_valid_raw) + + # Objective GP posterior + objective_posterior = self.objective_model.posterior(candidates_normalized) + mu_obj = objective_posterior.mean.squeeze(-1) # (n,) + sigma_obj = objective_posterior.stddev.squeeze(-1) # (n,) + + # Denormalize objective predictions + mu_obj_denorm = mu_obj * self.train_Y_std + self.train_Y_mean + + # Constrained score + score_constrained = ei_raw * p_valid + + return { + 'ei_log': ei_log.cpu().numpy(), + 'ei_raw': ei_raw.cpu().numpy(), + 'p_valid': p_valid.cpu().numpy(), + 'score_constrained': score_constrained.cpu().numpy(), + 'mu_obj': mu_obj_denorm.cpu().numpy(), + 'sigma_obj': sigma_obj.cpu().numpy(), + 'mu_valid_raw': mu_valid_raw.cpu().numpy() + } + + def suggest(self, verbose: bool = None) -> Tuple[torch.Tensor, dict]: + """ + Suggest next batch of candidates using constrained acquisition. + + Acquisition: EI_raw(x) × P(valid|x) + + Returns: + candidates: Tensor of shape (batch_size, n_dims) containing suggested candidates + metadata: Dictionary with all metadata for each candidate + """ + if verbose is None: + verbose = self.verbose + + if verbose: + print("=" * 60) + print("GENERATING CONSTRAINED BO CANDIDATES") + print("=" * 60) + print(f"Objective samples: {len(self.train_X_obj)}") + print(f"Constraint samples: {len(self.train_X_all)}") + print(f"Batch size: {self.batch_size}") + print(f"Seen parameter combinations: {len(self.seen_param_keys)}") + print("=" * 60) + + # Build models + if verbose: + print("Building GP models...") + self._build_models() + + # Normalize training data + train_X_all_normalized = normalize(self.train_X_all.double(), self.bounds.double()) + train_X_obj_normalized = normalize(self.train_X_obj.double(), self.bounds.double()) + + # Unit bounds for normalized space + unit_bounds = torch.stack([ + torch.zeros(self.n_dims, dtype=torch.float64, device=self.device), + torch.ones(self.n_dims, dtype=torch.float64, device=self.device), + ]) + + # Acquisition function + acquisition_function = qLogNoisyExpectedImprovement( + model=self.objective_model, + X_baseline=train_X_obj_normalized, + prune_baseline=True, + cache_root=True + ) + + # Step 1: Generate main EI candidate pool (larger than batch_size) + pool_q = self.batch_size * 5 # 40 candidates for batch_size=8 + if verbose: + print(f"Step 1: Generating main EI candidate pool (q={pool_q})...") + + candidates_pool, _ = optimize_acqf( + acq_function=acquisition_function, + bounds=unit_bounds, + q=pool_q, + num_restarts=40, + raw_samples=500, + options={"batch_limit": 5, "maxiter": 200} + ) + + if verbose: + print(f" Generated {pool_q} candidate pool") + + # Step 2: Compute metadata for all candidates + if verbose: + print("Step 2: Computing metadata (EI, P(valid), scores)...") + + metadata_pool = self._compute_metadata(candidates_pool, acquisition_function) + score_constrained = torch.tensor(metadata_pool['score_constrained'], dtype=torch.float64) + + if verbose: + print(f" EI_log range: [{metadata_pool['ei_log'].min():.4f}, {metadata_pool['ei_log'].max():.4f}]") + print(f" EI_raw range: [{metadata_pool['ei_raw'].min():.4f}, {metadata_pool['ei_raw'].max():.4f}]") + print(f" P(valid) range: [{metadata_pool['p_valid'].min():.4f}, {metadata_pool['p_valid'].max():.4f}]") + print(f" Score_constrained range: [{score_constrained.min():.4f}, {score_constrained.max():.4f}]") + + # Step 3: Filter by uniqueness and select top candidates + if verbose: + print("Step 3: Filtering duplicates and selecting candidates...") + + sorted_indices = torch.argsort(score_constrained, descending=True) + selected_indices = [] # Store indices in candidates_pool + selected_original = [] + selected_metadata = { + 'ei_log': [], 'ei_raw': [], 'p_valid': [], 'score_constrained': [], + 'mu_obj': [], 'sigma_obj': [], 'mu_valid_raw': [], + 'candidate_rank': [], 'source': [] + } + + rank = 1 + for idx in sorted_indices: + if len(selected_indices) >= self.batch_size: + break + + # Decode and snap + candidate_norm = candidates_pool[idx:idx+1] + candidate_orig = self._decode_and_snap(candidate_norm) + + # Create parameter tuple + sp = round(float(candidate_orig[0, 0]), 2) + T = int(round(float(candidate_orig[0, 1]))) + g = int(round(float(candidate_orig[0, 2]))) + v = int(round(float(candidate_orig[0, 3]))) + param_key = (sp, T, g, v) + + # Check if already seen + if param_key in self.seen_param_keys: + continue + + # Add to selected + self.seen_param_keys.add(param_key) + selected_indices.append(idx.item()) # Store as int for indexing + selected_original.append(candidate_orig) + + # Store metadata + selected_metadata['ei_log'].append(metadata_pool['ei_log'][idx]) + selected_metadata['ei_raw'].append(metadata_pool['ei_raw'][idx]) + selected_metadata['p_valid'].append(metadata_pool['p_valid'][idx]) + selected_metadata['score_constrained'].append(metadata_pool['score_constrained'][idx]) + selected_metadata['mu_obj'].append(metadata_pool['mu_obj'][idx]) + selected_metadata['sigma_obj'].append(metadata_pool['sigma_obj'][idx]) + selected_metadata['mu_valid_raw'].append(metadata_pool['mu_valid_raw'][idx]) + selected_metadata['candidate_rank'].append(rank) + selected_metadata['source'].append('main_ei') + rank += 1 + + n_selected_main = len(selected_indices) + if verbose: + print(f" Selected {n_selected_main} unique candidates from main pool") + + # Step 4: Sobol fallback with max-min distance exploration + if len(selected_indices) < self.batch_size: + if verbose: + print(f"Step 4: Sobol fallback with max-min distance exploration to fill remaining {self.batch_size - len(selected_indices)} slots...") + + k = self.batch_size - len(selected_indices) + + # Construct X_seen: previous data + this round's main EI selections + X_prev = train_X_all_normalized # (N_prev, d) - already normalized + if n_selected_main > 0: + X_round = candidates_pool[torch.tensor(selected_indices, dtype=torch.long, device=candidates_pool.device)] # (n_selected_main, d) - already normalized + X_seen = torch.cat([X_prev, X_round], dim=0) # (N_prev + n_selected_main, d) + else: + X_seen = X_prev # No main EI selections yet + + if verbose: + if n_selected_main > 0: + print(f" X_seen: {X_seen.shape[0]} points (previous: {X_prev.shape[0]}, this round: {X_round.shape[0]})") + else: + print(f" X_seen: {X_seen.shape[0]} points (previous: {X_prev.shape[0]}, this round: 0)") + + # Generate and filter Sobol pool + fallback_pool_size = self.batch_size * 20 # 160 for batch_size=8 + max_pool_retries = 5 + valid_sobol_pool_norm = [] + valid_sobol_param_keys = [] + + n_sobol = fallback_pool_size + pool_retry = 0 + + while len(valid_sobol_pool_norm) < k and pool_retry < max_pool_retries: + # Generate Sobol samples + sobol_raw = draw_sobol_samples( + bounds=unit_bounds, + n=1, + q=n_sobol + ).squeeze(0).double() # (n_sobol, d) + + # Filter duplicates + for i in range(sobol_raw.shape[0]): + candidate_norm = sobol_raw[i:i+1] # (1, d) + candidate_orig = self._decode_and_snap(candidate_norm) + + # Create parameter tuple + sp = round(float(candidate_orig[0, 0]), 2) + T = int(round(float(candidate_orig[0, 1]))) + g = int(round(float(candidate_orig[0, 2]))) + v = int(round(float(candidate_orig[0, 3]))) + param_key = (sp, T, g, v) + + # Check if already seen + if param_key not in self.seen_param_keys: + valid_sobol_pool_norm.append(candidate_norm) + valid_sobol_param_keys.append(param_key) + + if len(valid_sobol_pool_norm) < k: + n_sobol *= 2 + pool_retry += 1 + if verbose: + print(f" Pool retry {pool_retry}: Found {len(valid_sobol_pool_norm)}/{k} valid candidates, expanding pool to {n_sobol}") + + if len(valid_sobol_pool_norm) < k: + raise RuntimeError( + f"ConstrainedBO: could not generate {k} unique Sobol candidates " + f"after {max_pool_retries} pool expansion retries. " + f"Consider relaxing the uniqueness constraint or adjusting the search space." + ) + + # Stack valid pool + sobol_pool_norm = torch.cat(valid_sobol_pool_norm, dim=0) # (M, d) where M >= k + + if verbose: + print(f" Valid Sobol pool: {sobol_pool_norm.shape[0]} candidates") + + # Greedy max-min distance selection + selected_fallback_norm = [] + selected_fallback_param_keys = [] + remaining_pool = sobol_pool_norm.clone() + remaining_param_keys = valid_sobol_param_keys.copy() + X_seen_current = X_seen.clone() + + for i in range(k): + # Compute minimum distances to X_seen + distances = torch.cdist(remaining_pool, X_seen_current) # (M_remaining, N_seen) + dist_min, _ = distances.min(dim=1) # (M_remaining,) + + # Select candidate with maximum min-distance + best_idx = dist_min.argmax().item() + x_best = remaining_pool[best_idx:best_idx+1] # (1, d) + param_key_best = remaining_param_keys[best_idx] + + # Add to selected + selected_fallback_norm.append(x_best) + selected_fallback_param_keys.append(param_key_best) + X_seen_current = torch.cat([X_seen_current, x_best], dim=0) + + # Remove from remaining pool + mask = torch.ones(remaining_pool.shape[0], dtype=torch.bool, device=remaining_pool.device) + mask[best_idx] = False + remaining_pool = remaining_pool[mask] + remaining_param_keys = [rk for j, rk in enumerate(remaining_param_keys) if j != best_idx] + + if verbose and (i + 1) % max(1, k // 4) == 0: + print(f" Selected {i+1}/{k} fallback candidates (min distance: {dist_min[best_idx]:.4f})") + + # Stack selected fallback candidates + selected_fallback_norm = torch.cat(selected_fallback_norm, dim=0) # (k, d) + + # Compute metadata for selected fallback candidates + if verbose: + print(f" Computing metadata for {k} fallback candidates...") + + with torch.no_grad(): + # Objective GP posterior + objective_posterior = self.objective_model.posterior(selected_fallback_norm) + mu_obj_fallback = objective_posterior.mean.squeeze(-1) # (k,) + sigma_obj_fallback = objective_posterior.stddev.squeeze(-1) # (k,) + mu_obj_fallback_denorm = mu_obj_fallback * self.train_Y_std + self.train_Y_mean + + # EI + ei_log_fallback = acquisition_function(selected_fallback_norm.unsqueeze(1)).squeeze(-1) # (k,) + ei_raw_fallback = torch.clamp(torch.exp(ei_log_fallback), min=0.0) + + # Constraint GP posterior + constraint_posterior = self.constraint_model.posterior(selected_fallback_norm) + mu_valid_raw_fallback = constraint_posterior.mean.squeeze(-1) # (k,) + p_valid_fallback = torch.sigmoid(mu_valid_raw_fallback) + + # Constrained score + score_constrained_fallback = ei_raw_fallback * p_valid_fallback + + # Decode and snap for final output + selected_fallback_original = self._decode_and_snap(selected_fallback_norm) + + # Add to selected lists + for i in range(k): + param_key = selected_fallback_param_keys[i] + self.seen_param_keys.add(param_key) + selected_indices.append(len(selected_indices)) # Dummy index for Sobol + selected_original.append(selected_fallback_original[i:i+1]) + + # Store metadata + selected_metadata['ei_log'].append(ei_log_fallback[i].item()) + selected_metadata['ei_raw'].append(ei_raw_fallback[i].item()) + selected_metadata['p_valid'].append(p_valid_fallback[i].item()) + selected_metadata['score_constrained'].append(score_constrained_fallback[i].item()) + selected_metadata['mu_obj'].append(mu_obj_fallback_denorm[i].item()) + selected_metadata['sigma_obj'].append(sigma_obj_fallback[i].item()) + selected_metadata['mu_valid_raw'].append(mu_valid_raw_fallback[i].item()) + selected_metadata['candidate_rank'].append(rank) + selected_metadata['source'].append('sobol_fallback') + rank += 1 + + if verbose: + print(f" Selected {k} fallback candidates using max-min distance exploration") + + # Stack selected candidates + candidates_final = torch.cat(selected_original, dim=0).float() + + # Re-rank all selected candidates by score_constrained + final_scores = torch.tensor(selected_metadata['score_constrained'], dtype=torch.float64) + final_rank_order = torch.argsort(final_scores, descending=True) + + # Reorder candidates and metadata + candidates_final = candidates_final[final_rank_order] + for key in selected_metadata: + if key == 'candidate_rank': + # Re-assign ranks 1 to batch_size + selected_metadata[key] = list(range(1, self.batch_size + 1)) + else: + # Reorder by final_rank_order + arr = np.array(selected_metadata[key]) + selected_metadata[key] = arr[final_rank_order.cpu().numpy()].tolist() + + if verbose: + print(f"\nFinal {self.batch_size} candidates (ranked by score_constrained):") + for i in range(self.batch_size): + print(f" [{selected_metadata['candidate_rank'][i]}] [{selected_metadata['source'][i]}] " + f"Speed={candidates_final[i, 0]:.2f}, " + f"Temp={candidates_final[i, 1]:.1f}, " + f"Gap={candidates_final[i, 2]:.1f}, " + f"Volume={candidates_final[i, 3]:.1f}") + print(f" EI_log={selected_metadata['ei_log'][i]:.4f}, " + f"EI_raw={selected_metadata['ei_raw'][i]:.4f}, " + f"P(valid)={selected_metadata['p_valid'][i]:.4f}, " + f"Score={selected_metadata['score_constrained'][i]:.4f}") + + return candidates_final, selected_metadata + + def save_candidates_to_csv( + self, + candidates: torch.Tensor, + metadata: Optional[dict] = None, + next_round_num: Optional[int] = None, + solvent: str = "CB", + concentration: int = 40 + ) -> None: + """ + Save suggested candidates to CSV file. + + Args: + candidates: Tensor of shape (batch_size, n_dims) containing suggested candidates + metadata: Optional dictionary with EI, P_valid, Score values for each candidate + next_round_num: Round number for new candidates (default: round_num + 1) + solvent: Solvent value for new rows (default: "CB") + concentration: Concentration value for new rows (default: 40) + """ + df = pd.read_csv(BytesIO(self.csv_data)) + + # Determine next round number + if next_round_num is None: + next_round_num = self.round_num + 1 + + # Determine starting sample number + if 'Sample #' in df.columns and 'round#' in df.columns and len(df) > 0: + round_df = df[df['round#'] == next_round_num] + if len(round_df) > 0: + start_sample = int(round_df['Sample #'].max()) + 1 + else: + start_sample = 1 # New round, start from 1 + else: + start_sample = 1 + + # Convert candidates to numpy + candidates_np = candidates.cpu().numpy() + batch_size = candidates_np.shape[0] + + # Create new rows + new_rows = [] + for i in range(batch_size): + speed = float(candidates_np[i, 0]) + temperature = float(candidates_np[i, 1]) + gap = float(candidates_np[i, 2]) + volume = float(candidates_np[i, 3]) + + new_row = { + 'Unnamed: 0': 'PProDOT', + 'round#': next_round_num, + 'Sample #': start_sample + i, + 'Temperature': int(round(temperature)), + 'Speed': round(speed, 2), # Round to 2 decimal places to avoid float precision issues + 'gap': int(round(gap)), + 'Solvent': solvent, + 'Concentration': concentration, + 'Precursor Volume': int(round(volume)), + 'uvvis_max_abs': np.nan, + 'uniformity_score': np.nan, + } + + # Add metadata if provided + if metadata is not None: + # Required metadata + if 'ei_log' in metadata and i < len(metadata['ei_log']): + new_row['ei_log'] = metadata['ei_log'][i] + if 'ei_raw' in metadata and i < len(metadata['ei_raw']): + new_row['ei_raw'] = metadata['ei_raw'][i] + if 'p_valid' in metadata and i < len(metadata['p_valid']): + new_row['p_valid'] = metadata['p_valid'][i] + if 'score_constrained' in metadata and i < len(metadata['score_constrained']): + new_row['score_constrained'] = metadata['score_constrained'][i] + if 'candidate_rank' in metadata and i < len(metadata['candidate_rank']): + new_row['candidate_rank'] = int(metadata['candidate_rank'][i]) + if 'source' in metadata and i < len(metadata['source']): + new_row['source'] = metadata['source'][i] + + # Additional analysis metadata + if 'mu_obj' in metadata and i < len(metadata['mu_obj']): + new_row['mu_obj'] = metadata['mu_obj'][i] + if 'sigma_obj' in metadata and i < len(metadata['sigma_obj']): + new_row['sigma_obj'] = metadata['sigma_obj'][i] + if 'mu_valid_raw' in metadata and i < len(metadata['mu_valid_raw']): + new_row['mu_valid_raw'] = metadata['mu_valid_raw'][i] + + # Update seen_param_keys immediately + param_key = (round(speed, 2), int(round(temperature)), int(round(gap)), int(round(volume))) + self.seen_param_keys.add(param_key) + + for col in df.columns: + if col not in new_row: + new_row[col] = np.nan + + new_rows.append(new_row) + + new_df = pd.DataFrame(new_rows) + + # Ensure all columns from new_df are included (especially metadata columns) + # If metadata columns don't exist in df, add them with NaN + for col in new_df.columns: + if col not in df.columns: + df[col] = np.nan + + # Reorder new_df to match df columns (including newly added ones) + new_df = new_df[df.columns] + combined_df = pd.concat([df, new_df], ignore_index=True) + + if self.verbose: + print(f"\nSaved {batch_size} candidates") + print(f" Round: {next_round_num}, Samples: {start_sample} to {start_sample + batch_size - 1}") + if metadata is not None: + print(f" Metadata (EI, P_valid, Score) also saved") + + return combined_df + + def reload_data(self) -> None: + """Reload data from CSV file (useful after CSV is updated).""" + if self.verbose: + print(f"Reloading data from {self.csv_data}...") + self._load_data() + # Reset models to force refitting + self.objective_model = None + self.constraint_model = None diff --git a/aamp_app/Image_Processing/image_processing_changhyun.py b/aamp_app/Image_Processing/image_processing_changhyun.py new file mode 100644 index 0000000..937173b --- /dev/null +++ b/aamp_app/Image_Processing/image_processing_changhyun.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python3 +"""Simple image processing script using compute_uniformity_score. + +Processes images from 'data/Round0/Raw' directory. +""" + +import sys +from pathlib import Path + +# Add project root to Python path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +from typing import Dict, Any + +import pandas as pd +import cv2 +import numpy as np +from .Image_processing.scoring import compute_uniformity_score, _match_reference_for +from .Image_processing.pipeline import analyze_image +from .Image_processing.config import UniformityConfig + + +def _save_mask_with_footer( + mask: np.ndarray, + sample_roi: np.ndarray | None, + source_image: np.ndarray | None, + roi_coords: tuple[int, int, int, int], + image_name: str, + coverage_pct: float | None, + uniformity_value: float | None, + uvvis_max: float | None, + debug_info: dict | None, + output_path: Path, +): + if mask is None: + return + + def _prepare_panel( + img: np.ndarray, + label: str | None = None, + draw_rect: bool = False, + rect: tuple[int, int, int, int] | None = None, + ) -> np.ndarray: + if img.dtype != np.uint8: + img_disp = np.clip(img, 0, 255).astype(np.uint8) + else: + img_disp = img.copy() + if len(img_disp.shape) == 2: + img_disp = cv2.cvtColor(img_disp, cv2.COLOR_GRAY2BGR) + + if draw_rect and rect is not None: + x, y, w, h = rect + cv2.rectangle(img_disp, (x, y), (x + w, y + h), (0, 0, 255), 2) + + if label: + label_strip = np.full((30, img_disp.shape[1], 3), 255, dtype=np.uint8) + cv2.putText( + label_strip, + label, + (10, 20), + cv2.FONT_HERSHEY_SIMPLEX, + 0.6, + (0, 0, 0), + 1, + cv2.LINE_AA, + ) + panel = cv2.vconcat([label_strip, img_disp]) + else: + panel = img_disp + panel = cv2.copyMakeBorder(panel, 1, 1, 1, 1, cv2.BORDER_CONSTANT, value=(0, 0, 0)) + return panel + + def _pad_to_width(img: np.ndarray, width: int) -> np.ndarray: + if img.shape[1] >= width: + if img.shape[1] == width: + return img + scale = width / img.shape[1] + new_h = int(img.shape[0] * scale) + return cv2.resize(img, (width, new_h), interpolation=cv2.INTER_AREA) + pad_total = width - img.shape[1] + left = pad_total // 2 + right = pad_total - left + return cv2.copyMakeBorder(img, 0, 0, left, right, cv2.BORDER_CONSTANT, value=(255, 255, 255)) + + def _resize_to_height(img: np.ndarray, height: int) -> np.ndarray: + if img.shape[0] == height: + return img + scale = height / img.shape[0] + new_w = max(1, int(round(img.shape[1] * scale))) + interpolation = cv2.INTER_AREA if scale < 1 else cv2.INTER_CUBIC + return cv2.resize(img, (new_w, height), interpolation=interpolation) + + if len(mask.shape) == 2: + mask_binary = mask + else: + mask_binary = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY) + + if sample_roi is None: + sample_roi_color = np.zeros((mask_binary.shape[0], mask_binary.shape[1], 3), dtype=np.uint8) + else: + sample_roi_color = sample_roi + if sample_roi_color.dtype != np.uint8: + sample_roi_color = np.clip(sample_roi_color, 0, 255).astype(np.uint8) + if len(sample_roi_color.shape) == 2: + sample_roi_color = cv2.cvtColor(sample_roi_color, cv2.COLOR_GRAY2BGR) + + masked_roi = cv2.bitwise_and(sample_roi_color, sample_roi_color, mask=mask_binary) + mask_color = cv2.cvtColor(mask_binary, cv2.COLOR_GRAY2BGR) + + roi_x, roi_y, roi_w, roi_h = roi_coords + if source_image is None: + full_panel_img = np.zeros_like(sample_roi_color) + else: + if source_image.dtype != np.uint8: + full_panel_img = np.clip(source_image, 0, 255).astype(np.uint8) + else: + full_panel_img = source_image.copy() + + panel_full = _prepare_panel(full_panel_img, None, True, (roi_x, roi_y, roi_w, roi_h)) + panel_original = _prepare_panel(sample_roi_color, "Original ROI") + panel_masked = _prepare_panel(masked_roi, "Masked ROI (film=color, no film=black)") + panel_binary = _prepare_panel(mask_color, "Binary Mask (film=white, no film=black)") + + # Unify all panels to maximum height (panel 1 may have different height since it has no title) + max_height = max(panel_full.shape[0], panel_original.shape[0], panel_masked.shape[0], panel_binary.shape[0]) + panel_full = _resize_to_height(panel_full, max_height) + panel_original = _resize_to_height(panel_original, max_height) + panel_masked = _resize_to_height(panel_masked, max_height) + panel_binary = _resize_to_height(panel_binary, max_height) + + col1_width = max(panel_full.shape[1], panel_original.shape[1]) + col2_width = max(panel_masked.shape[1], panel_binary.shape[1]) + + panel_full = _pad_to_width(panel_full, col1_width) + panel_original = _pad_to_width(panel_original, col1_width) + panel_masked = _pad_to_width(panel_masked, col2_width) + panel_binary = _pad_to_width(panel_binary, col2_width) + + spacer_col = np.full((max_height, 10, 3), 255, dtype=np.uint8) + + top_row = cv2.hconcat([panel_full, spacer_col, panel_masked]) + bottom_row = cv2.hconcat([panel_original, spacer_col.copy(), panel_binary]) + + spacer_row = np.full((10, top_row.shape[1], 3), 255, dtype=np.uint8) + stacked = cv2.vconcat([top_row, spacer_row, bottom_row]) + + # Add title (from filename up to _mask) + title_text = output_path.stem.replace("_mask", "") + title_height = 50 + title_bar = np.full((title_height, stacked.shape[1], 3), 255, dtype=np.uint8) + cv2.putText( + title_bar, + title_text, + (10, 35), + cv2.FONT_HERSHEY_SIMPLEX, + 0.8, + (0, 0, 0), + 2, + cv2.LINE_AA, + ) + + # Create footer (divided into multiple columns) + debug_info = debug_info or {} + cov_display = (coverage_pct or 0.0) * 100 + uniformity_score = debug_info.get("uniformity_score", uniformity_value if uniformity_value is not None else 0.0) + + # Reference-based Uniformity statistics + ref_std = debug_info.get("ref_std", 0.0) + ref_entropy = debug_info.get("ref_entropy", 0.0) + sample_std = debug_info.get("sample_std", 0.0) + sample_entropy = debug_info.get("sample_entropy", 0.0) + delta_std = debug_info.get("delta_std", 0.0) + delta_entropy = debug_info.get("delta_entropy", 0.0) + std_sensitivity = debug_info.get("uniformity_std_sensitivity", 15.0) + ent_sensitivity = debug_info.get("uniformity_ent_sensitivity", 2.0) + use_model_ref = debug_info.get("use_model_reference", 0.0) > 0.5 # Convert float to bool + + uvvis_display = uvvis_max if uvvis_max is not None else 0.0 + threshold_val = debug_info.get("saturation_threshold", 0.0) + ref_mean = debug_info.get("ref_s_mean", 0.0) + + footer_height = 200 + footer = np.full((footer_height, stacked.shape[1], 3), 255, dtype=np.uint8) + + # Two-column layout + col_width = stacked.shape[1] // 2 + left_col_x = 15 + right_col_x = col_width + 15 + + font = cv2.FONT_HERSHEY_SIMPLEX + font_scale = 0.7 + color = (0, 0, 0) + thickness = 1 + line_height = 28 + y_start = 25 + + # Reference notation (whether Model Reference is used) + ref_label = "Ref" if not use_model_ref else "Model Ref" + + # Left column + left_lines = [ + f"File: {image_name}", + f"Coverage: {cov_display:.1f}%", + f"Uniformity: {uniformity_score:.3f}", + f" ({ref_label} Std: {ref_std:.2f}, Sample: {sample_std:.2f})", + f" ({ref_label} Ent: {ref_entropy:.2f}, Sample: {sample_entropy:.2f})", + f"UV-Vis max (300-800nm): {uvvis_display:.3f}", + ] + for idx, text in enumerate(left_lines): + y = y_start + idx * line_height + cv2.putText(footer, text, (left_col_x, y), font, font_scale, color, thickness, cv2.LINE_AA) + + # Right column + right_lines = [ + f"Threshold: {threshold_val:.1f}", + f"Ref Mean: {ref_mean:.1f}", + f"Delta Std: {delta_std:.2f} (K1: {std_sensitivity:.1f})", + f"Delta Ent: {delta_entropy:.2f} (K2: {ent_sensitivity:.1f})", + ] + if use_model_ref: + right_lines.append("Note: Model Reference used") + for idx, text in enumerate(right_lines): + y = y_start + idx * line_height + cv2.putText(footer, text, (right_col_x, y), font, font_scale, color, thickness, cv2.LINE_AA) + + combined = cv2.vconcat([title_bar, stacked, footer]) + cv2.imwrite(str(output_path), combined) + + +def process_single_directory( + main_dir: Path, + roi_x: int, + roi_y: int, + roi_width: int, + roi_height: int, + uvvis_abs_threshold: float, + coverage_threshold: float, + metadata_base: Dict[str, Any], +) -> None: + """Process images for a single directory.""" + + raw_dir = main_dir / "Raw" + reference_dir = main_dir / "blank" + uvvis_dir = main_dir / "UV-Vis" + initial_parameters_csv = main_dir / "PProDOT_CB_Campaign_parameters.csv" + output_csv = main_dir / "processing_results_simple.csv" + + print(f"\n{'='*60}") + print(f"Processing directory: {main_dir}") + print(f"{'='*60}") + + # Load initial parameters CSV if available + parameters_df = None + if initial_parameters_csv.exists(): + parameters_df = pd.read_csv(initial_parameters_csv) + if parameters_df.columns[0] == '' or parameters_df.columns[0].startswith('Unnamed'): + parameters_df = parameters_df.iloc[:, 1:] + parameters_df.columns = parameters_df.columns.str.strip() + + # Prepare metadata for compute_uniformity_score + metadata = metadata_base.copy() + metadata.update({ + "reference_dir": str(reference_dir.resolve()), + "uvvis_dir": str(uvvis_dir.resolve()), + }) + + # Find all image files + if not raw_dir.exists(): + print(f" Warning: Raw directory not found: {raw_dir}") + return + + image_exts = {'.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff'} + image_files = [f for f in raw_dir.iterdir() if f.is_file() and f.suffix.lower() in image_exts] + + if len(image_files) == 0: + print(f" Warning: No image files found in {raw_dir}") + return + + print(f"Found {len(image_files)} image files") + + debug_mask_dir = main_dir / "Debug_Masks" + debug_mask_dir.mkdir(parents=True, exist_ok=True) + + # Process each image + results = [] + for i, image_path in enumerate(sorted(image_files), 1): + print(f"[{i}/{len(image_files)}] Processing {image_path.name}...") + sample_roi = None + final_mask = None + debug_info = None + + # Extract round# and Sample # from filename (R0S01 -> round=0, sample=1) + import re + match = re.match(r'R(\d+)S(\d+)', image_path.stem, re.IGNORECASE) + round_num = int(match.group(1)) if match else None + sample_num = int(match.group(2)) if match else None + + # Compute uniformity score and full metrics + try: + # Use verbose=True for first image to diagnose issues + verbose = (i == 1) + uniformity_score = compute_uniformity_score( + image_path=str(image_path), + metadata=metadata, + verbose=verbose + ) + + # Also get full metrics for CSV + img = cv2.imread(str(image_path)) + if img is not None: + cfg = UniformityConfig( + input_dir=str(image_path.parent), + use_reference=True, + reference_dir=str(reference_dir.resolve()), + reference_name="background_normal_0deg", + roi_x=roi_x, + roi_y=roi_y, + roi_width=roi_width, + roi_height=roi_height, + uvvis_dir=str(uvvis_dir.resolve()), + uvvis_abs_threshold=uvvis_abs_threshold, + coverage_threshold=coverage_threshold, + coverage_k=metadata.get("coverage_k", 3.0), + uniformity_std_sensitivity=metadata.get("uniformity_std_sensitivity", 15.0), + uniformity_ent_sensitivity=metadata.get("uniformity_ent_sensitivity", 2.0), + compute_learning_features=True, + ) + + # Load reference image (required for ROI feature computation) + ref_path = _match_reference_for(image_path, cfg.reference_dir, cfg.reference_name) + ref_bgr = None + if ref_path is not None and ref_path.exists(): + ref_bgr = cv2.imread(str(ref_path)) + + ( + metrics, + sample_roi, + reference_roi, + final_mask, + debug_info, + ) = analyze_image(img, cfg, ref_bgr=ref_bgr, image_path=image_path) + + # Get uniformity score from metrics or debug_info (new CV-based calculation) + uniformity_score_from_metrics = metrics.get("uniformity_score") + uniformity_score_from_debug = debug_info.get("uniformity_score") if isinstance(debug_info, dict) else None + if uniformity_score_from_metrics is not None: + uniformity_score = uniformity_score_from_metrics + elif uniformity_score_from_debug is not None: + uniformity_score = uniformity_score_from_debug + + # Start with basic info + result = { + "round#": round_num, + "Sample #": sample_num, + "image_name": image_path.name, + "image_path": str(image_path), + "uniformity_score": uniformity_score, + } + + # Add all metrics + result.update(metrics) + else: + result = { + "round#": round_num, + "Sample #": sample_num, + "image_name": image_path.name, + "image_path": str(image_path), + "uniformity_score": uniformity_score, + } + except Exception as e: + print(f" Error: {e}") + import traceback + traceback.print_exc() + uniformity_score = None + result = { + "round#": round_num, + "Sample #": sample_num, + "image_name": image_path.name, + "image_path": str(image_path), + "uniformity_score": None, + } + + # Add parameters from CSV if available + if parameters_df is not None and round_num is not None and sample_num is not None: + param_row = parameters_df[ + (parameters_df["round#"] == round_num) & + (parameters_df["Sample #"] == sample_num) + ] + if not param_row.empty: + row = param_row.iloc[0] + result["Temperature"] = row.get("Temperature", None) + result["Speed"] = row.get("Speed", None) + result["gap"] = row.get("gap", None) + result["Solvent"] = row.get("Solvent", None) + result["Concentration"] = row.get("Concentration", None) + result["Precursor Volume"] = row.get("Precursor Volume", None) + + if isinstance(debug_info, dict): + result.update(debug_info) + + if final_mask is not None: + coverage_pct = result.get("coverage_percentage") + uvvis_max = result.get("uvvis_max_abs") + uniformity_val = uniformity_score if uniformity_score is not None else coverage_pct + threshold_val = (debug_info or {}).get("saturation_threshold", metadata.get("coverage_k", 0.0)) + mask_path = debug_mask_dir / f"{image_path.stem}_mask_th{threshold_val:.1f}.png" + _save_mask_with_footer( + final_mask, + sample_roi, + img, + (roi_x, roi_y, roi_width, roi_height), + image_path.name, + coverage_pct, + uniformity_val, + uvvis_max, + debug_info, + mask_path, + ) + + results.append(result) + + # Save to CSV with preferred column order + df = pd.DataFrame(results) + preferred_order = [ + "round#", + "Sample #", + "image_name", + "image_path", + "coverage_percentage", + "uvvis_max_abs", + "uniformity_score", + ] + remaining_cols = [col for col in df.columns if col not in preferred_order] + df = df[[col for col in preferred_order if col in df.columns] + remaining_cols] + df.to_csv(output_csv, index=False) + print(f"\nResults saved to {output_csv}") + return results + + +def main(): + """Main entry point.""" + + # ============================================ + # Configuration: Choose method for processing multiple directories + # ============================================ + + # Method 1: Specify directly as a list + main_dirs = [ + Path("Round0"), + # Path("data/Round0_1"), + # Path("data/Round0_2"), + # Path("data/Round0_3"), + # Path("data/Round1"), + # Path("data/Round2"), + # Path("data/Round3"), + # Path("data/Round4"), + # Path("data/Round5"), + # Path("data/PProDOT_2nd_dataset") + # Add desired directories + ] + + # Method 2: Automatically discover all Round* directories under data/ (comment out Method 1 first) + # data_base = Path("data") + # if data_base.exists(): + # main_dirs = [d for d in data_base.iterdir() if d.is_dir() and d.name.startswith("Round")] + # else: + # main_dirs = [] + + # Common settings + roi_x = 580 + roi_y = 400 + roi_width = 913 + roi_height = 415 + + # Thresholds + uvvis_abs_threshold = 0.1 + coverage_threshold = 0.1 + + # Common metadata (reference_dir and uvvis_dir are automatically set for each directory) + metadata_base = { + "roi_x": roi_x, + "roi_y": roi_y, + "roi_width": roi_width, + "roi_height": roi_height, + "uvvis_abs_threshold": uvvis_abs_threshold, + "coverage_threshold": coverage_threshold, + "coverage_k": 1.0, + "uniformity_std_sensitivity": 15.0, # K1: Sensitivity to Std difference + "uniformity_ent_sensitivity": 2.0, # K2: Sensitivity to Entropy difference + } + + # Process each directory + for main_dir in main_dirs: + if not main_dir.exists(): + print(f"Warning: Directory not found: {main_dir}, skipping...") + continue + + try: + process_single_directory( + main_dir=main_dir, + roi_x=roi_x, + roi_y=roi_y, + roi_width=roi_width, + roi_height=roi_height, + uvvis_abs_threshold=uvvis_abs_threshold, + coverage_threshold=coverage_threshold, + metadata_base=metadata_base, + ) + except Exception as e: + print(f"Error processing {main_dir}: {e}") + import traceback + traceback.print_exc() + continue + + print(f"\n{'='*60}") + print("All directories processed!") + print(f"{'='*60}") + + +if __name__ == "__main__": + main() diff --git a/aamp_app/Image_Processing/requirements.txt b/aamp_app/Image_Processing/requirements.txt new file mode 100644 index 0000000..2579f17 --- /dev/null +++ b/aamp_app/Image_Processing/requirements.txt @@ -0,0 +1,11 @@ +# Core dependencies +numpy>=1.21.0 +opencv-python>=4.5.0 +pandas>=1.3.0 +scikit-image>=0.18.0 + +# PyTorch and Bayesian Optimization +torch>=1.11.0 +botorch>=0.6.0 +gpytorch>=1.6.0 + diff --git a/aamp_app/app.py b/aamp_app/app.py index 6cd60ca..721513d 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -130,7 +130,8 @@ def load_env(file_path=".env"): dbc.NavItem(dbc.NavLink("Sampler", href="/sampler", external_link=True)), dbc.NavItem(dbc.NavLink("Recipe Builder", href="/recipe-builder", external_link=True)), dbc.NavItem(dbc.NavLink("Solution Map", href="/solution-map", external_link=True)), - dbc.NavItem(dbc.NavLink("Optimization", href="/bayesian-optimization", external_link=True)), + dbc.NavItem(dbc.NavLink("Autonomous Run", href="/bayesian-optimization", external_link=True)), + dbc.NavItem(dbc.NavLink("Manual Run", href="/manual-run", external_link=True)), # dbc.DropdownMenu( # children=[ # # dbc.DropdownMenuItem( @@ -2987,27 +2988,6 @@ def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_na campaign_result = mongo.db.campaigns.insert_one(campaign_doc) campaign_id = campaign_result.inserted_id - cc = MongoClient('mongodb://localhost:27017/') - dd = cc['diaogroup'] - ff = GridFS(dd) - pca_bytes = p1.to_image(format="png") - umap_bytes = p2.to_image(format="png") - pca_id = ff.put(pca_bytes, filename=f"{campaign_name}_pca.png") - umap_id = ff.put(umap_bytes, filename=f"{campaign_name}_umap.png") - pca_doc = { - "name": f"{campaign_name}_pca", - "image_id": pca_id, - "campaign_id": campaign_id - } - umap_doc = { - "name": f"{campaign_name}_umap", - "image_id": umap_id, - "campaign_id": campaign_id - } - coll = dd['sampler_plots'] - coll.insert_one(pca_doc) - coll.insert_one(umap_doc) - sets_to_insert = [{ "campaign_id": campaign_id, "polymer_name": polymer_name, @@ -3223,9 +3203,6 @@ def generate_recipes(n_clicks, selected_rows, parameter_sets, solution_positions prevent_initial_call=True ) def run_recipes_sequentially(n_clicks, generated_scripts): - if not generated_scripts: - raise PreventUpdate - log_components = [] interceptor = ConsoleInterceptor() diff --git a/aamp_app/pages/bayesian-optimization.py b/aamp_app/pages/bayesian-optimization.py index 137048c..fad5147 100644 --- a/aamp_app/pages/bayesian-optimization.py +++ b/aamp_app/pages/bayesian-optimization.py @@ -1,15 +1,12 @@ from dash import html, dcc, dash_table, callback, Input, Output, State import dash_bootstrap_components as dbc import dash -import numpy as np -import pandas as pd import io import contextlib -import matplotlib.pyplot as plt -import base64 from pymongo import MongoClient import gridfs -from bson import ObjectId +import os +import signal dash.register_page( __name__, @@ -18,8 +15,11 @@ title="Optimization" ) -client = MongoClient('mongodb://localhost:27017/') -db = client['diaogroup'] +def emergency_stop(): + os.kill(os.getpid(), signal.SIGINT) + +client = MongoClient('mongodb://aamp-user:b6b52bc6b85b8a53af21ae2883b3910d@aamp-mongodb-0.aamp.mmli1.ncsa.illinois.edu/aamp?replicaSet=aamp-mongodb&authSource=admin&tlsAllowInvalidCertificates=true&tls=true') +db = client['aamp'] fs = gridfs.GridFS(db) # def plot_optimization_results(optimizer, objective): @@ -91,110 +91,9 @@ # return encoded_image - -# def plot_optimization_results(optimizer, objective): -# """Create comprehensive plots of the optimization results.""" - -# history = optimizer.get_optimization_history() -# train_X, train_Y = optimizer.get_training_data() -# true_params, _ = objective.get_optimal_parameters() -# true_score = objective.evaluate_at_optimal() - -# # Create the visualization -# fig, axes = plt.subplots(2, 3, figsize=(18, 12)) - -# # 1. Optimization Progress -# iterations = [h['iteration'] for h in history] -# best_values = [h['best_value'] for h in history] - -# axes[0, 0].plot(iterations, best_values, 'b-o', linewidth=2, markersize=6) -# axes[0, 0].axhline(y=true_score, color='r', linestyle='--', alpha=0.7, -# label=f'True optimum: {true_score:.3f}') -# axes[0, 0].set_xlabel('Iteration') -# axes[0, 0].set_ylabel('Best Observed Value') -# axes[0, 0].set_title('Optimization Progress') -# axes[0, 0].legend() -# axes[0, 0].grid(True, alpha=0.3) - -# # 2. Improvement Rate -# # improvements = [best_values[i] - best_values[0] for i in range(len(best_values))] -# # axes[0, 1].plot(iterations, improvements, 'g-o', linewidth=2, markersize=6) -# # axes[0, 1].set_xlabel('Iteration') -# # axes[0, 1].set_ylabel('Improvement from Initial') -# # axes[0, 1].set_title('Cumulative Improvement') -# # axes[0, 1].grid(True, alpha=0.3) - -# # 3. Parameter Space Exploration (Concentration vs Print Speed) -# scatter = axes[0, 2].scatter(train_X[:, 0], train_X[:, 1], c=train_Y.squeeze(), -# cmap='viridis', alpha=0.6, s=50) -# axes[0, 2].scatter(optimizer.best_parameters[0], optimizer.best_parameters[1], -# c='red', s=200, marker='*', label='Best found', -# edgecolor='black', linewidth=2) -# axes[0, 2].scatter(true_params[0], true_params[1], c='orange', s=200, marker='*', -# label='True optimum', edgecolor='black', linewidth=2) -# axes[0, 2].set_xlabel('Concentration') -# axes[0, 2].set_ylabel('Print Speed (mm/s)') -# axes[0, 2].set_title('Parameter Space Exploration') -# axes[0, 2].legend() -# plt.colorbar(scatter, ax=axes[0, 2], label='Objective Value') - -# # 4. Gap Size vs Volume -# scatter2 = axes[1, 0].scatter(train_X[:, 2], train_X[:, 3], c=train_Y.squeeze(), -# cmap='viridis', alpha=0.6, s=50) -# axes[1, 0].scatter(optimizer.best_parameters[2], optimizer.best_parameters[3], -# c='red', s=200, marker='*', label='Best found', -# edgecolor='black', linewidth=2) -# axes[1, 0].scatter(true_params[2], true_params[3], c='orange', s=200, marker='*', -# label='True optimum', edgecolor='black', linewidth=2) -# axes[1, 0].set_xlabel('Gap Size (mm)') -# axes[1, 0].set_ylabel('Volume (μL)') -# axes[1, 0].set_title('🔍 Gap Size vs Volume') -# axes[1, 0].legend() -# plt.colorbar(scatter2, ax=axes[1, 0], label='Objective Value') - -# # 5. Objective Value Distribution -# # axes[1, 1].hist(train_Y.squeeze().numpy(), bins=20, alpha=0.7, color='skyblue', -# # edgecolor='black') -# # axes[1, 1].axvline(optimizer.best_observed_value, color='red', linestyle='--', -# # linewidth=2, label=f'Best: {optimizer.best_observed_value:.3f}') -# # axes[1, 1].axvline(true_score, color='orange', linestyle='--', linewidth=2, -# # label=f'True: {true_score:.3f}') -# # axes[1, 1].set_xlabel('Objective Value') -# # axes[1, 1].set_ylabel('Frequency') -# # axes[1, 1].set_title('Objective Value Distribution') -# # axes[1, 1].legend() -# # axes[1, 1].grid(True, alpha=0.3) - -# # 6. Parameter Convergence -# # param_names = ['gap_size', 'volume'] -# # eval_order = np.arange(len(train_X)) - -# # for i, name in enumerate(param_names): -# # color = plt.cm.Set1(i) -# # axes[1, 2].scatter(eval_order, train_X[:, i], alpha=0.6, s=30, -# # c=color, label=name) -# # axes[1, 2].axhline(y=true_params[i], color=color, linestyle='--', alpha=0.7) - -# # axes[1, 2].set_xlabel('Evaluation Order') -# # axes[1, 2].set_ylabel('Parameter Value') -# # axes[1, 2].set_title('Parameter Convergence') -# # axes[1, 2].legend() -# # axes[1, 2].grid(True, alpha=0.3) - -# plt.tight_layout() -# plt.show() - -# buf = io.BytesIO() -# fig.savefig(buf, format="png", bbox_inches='tight') -# buf.seek(0) -# encoded_image = base64.b64encode(buf.read()).decode("utf-8") -# buf.close() -# plt.close(fig) # Close the figure to prevent it from showing - -# return encoded_image - layout = html.Div( [ + html.Div(id="bo-emergency-stop-output", style={"display": "none"}), html.Div([ html.H1([ "Optimization", @@ -311,32 +210,40 @@ placeholder="e.g., 0.95" ) ], width=4), - - # dbc.Col([ - # html.Label("Number of Iterations (Required)"), - # dbc.Input(id="bo-maxiter", type="number", placeholder="e.g., 15", min=1) - # ], width=4), ], className="mb-3"), - dbc.Button("Generate and Save Optimizer Parameters", id="bo-generate-btn", color="primary", className="mb-3"), + dbc.ButtonGroup( + [ + dbc.Button("Generate and Save Optimizer Parameters", id="bo-generate-btn", color="primary", className="mb-3"), + dbc.Button("EMERGENCY STOP", id="bo-emergency-stop-btn", color="danger", className="mb-3") + ], + ), html.Pre(id="optimizer-output1", style={"whiteSpace": "pre-wrap", "border": "1px solid #ccc", "padding": "10px"}), ], className="container", ) +@callback( + Output("bo-emergency-stop-output", "children"), + Input("bo-emergency-stop-btn", "n_clicks"), + prevent_initial_call=True +) +def emergency_stop_callback(n_clicks): + emergency_stop() + return [] + @callback( Output("optimizer-output1", "children"), Input("bo-generate-btn", "n_clicks"), State("recipe-builder-campaign-dropdown", "value"), State("bo-batch-size", "value"), State("bo-target-objective", "value"), - # State("bo-maxiter", "value"), prevent_initial_call=True ) def generate_optimizer_parameters(n_clicks, camp, bs, target_objective): import torch - from optimizer import BayesianOptimizer, MockObjectiveFunction + from Image_Processing.constrained_bo_ver2 import ConstrainedBayesianOptimizer f = io.StringIO() # plot_images = [] @@ -345,39 +252,78 @@ def generate_optimizer_parameters(n_clicks, camp, bs, target_objective): if n_clicks > 0: collection = db['campaigns'] entry = collection.find_one({"campaign_name": camp}) + grid_out = fs.find_one({"campaign_id": entry['_id'], "filename": "PProDOT_CB_Campaign_parameters.csv"}) + data = grid_out.read() + # bounds = torch.tensor([ + # [min(entry['concentration_range']), max(entry['concentration_range'])], # concentration + # [min(entry['motor_speed']), max(entry['motor_speed'])], # print_speed + # [min(entry['printing_gap']), max(entry['printing_gap'])], # gap_size + # [min(entry['precursor_volume']), max(entry['precursor_volume'])] # volume + # ]).T bounds = torch.tensor([ - [min(entry['concentration_range']), max(entry['concentration_range'])], # concentration - [min(entry['motor_speed']), max(entry['motor_speed'])], # print_speed - [min(entry['printing_gap']), max(entry['printing_gap'])], # gap_size - [min(entry['precursor_volume']), max(entry['precursor_volume'])] # volume + [0.01, 20.0], # Speed + [25.0, 107.0], # Temperature + [50, 200], # Gap + [5, 15] # Volume ]).T - print(bounds) - # Create optimizer and objective function - optimizer = BayesianOptimizer(bounds=bounds, batch_size=bs) - objective = MockObjectiveFunction() + discrete_points = [ + torch.tensor([0.1, 0.5, 0.7, 1.0]), # Speed + torch.tensor(list(range(25, 108))), # Temperature + torch.tensor(list(range(50, 201))), # Gap + torch.tensor(list(range(5, 16))) # Volume + ] - # Run optimization - best_params, best_score, plot_images, ff = optimizer.optimize( - objective_function=objective, - n_iterations=200, - n_initial_points=10, - target=target_objective + # Create optimizer and objective function + optimizer = ConstrainedBayesianOptimizer( + bounds=bounds, + csv_data=data, + round_num=0, + batch_size=bs, + discrete_or_not=[False, True, True, True], + discrete_points=discrete_points ) print("\n\n") - for i, fig in enumerate(ff): - experiment_id = entry['_id'] - buf = io.BytesIO() - fig.savefig(buf, format="png") - buf.seek(0) - file_id = fs.put(buf.getvalue(), filename=f"sample_plot_{i}.png", metadata={"experiment_id": experiment_id}) - plots_collection = db['optimizer_plots'] - plot_doc = { - "name": f"sample_matplotlib_plot_{i}", - "experiment_id": experiment_id, - "file_id": file_id + candidates, metadata = optimizer.suggest() + + collection = db['sets'] + for i in range(bs): + set_doc = { + "campaign_id": entry['_id'], + "batch_no": optimizer.round_num, + "sample_no": i + 1, + "motor_speed": candidates[i][0].item(), + "temperature": candidates[i][1].item(), + "printing_gap": candidates[i][2].item(), + "precursor_volume": candidates[i][3].item(), + "ei_log": metadata["ei_log"][i], + "ei_raw": metadata["ei_raw"][i], + "p_valid": metadata["p_valid"][i], + "score_constrained": metadata["score_constrained"][i], + "source": metadata["source"][i], + "candidate_rank": metadata["candidate_rank"][i], + "mu_obj": metadata["mu_obj"][i], + "sigma_obj": metadata["sigma_obj"][i], + "mu_valid_raw": metadata["mu_valid_raw"][i], } - plots_collection.insert_one(plot_doc) + collection.insert_one(set_doc) + updated_df = optimizer.save_candidates_to_csv(candidates, metadata) + fs.delete(grid_out._id) + csv_bytes = updated_df.to_csv(index=False).encode('utf-8') + fs.put(csv_bytes, filename="PProDOT_CB_Campaign_parameters.csv", campaign_id=entry['_id']) + # for i, fig in enumerate(ff): + # experiment_id = entry['_id'] + # buf = io.BytesIO() + # fig.savefig(buf, format="png") + # buf.seek(0) + # file_id = fs.put(buf.getvalue(), filename=f"sample_plot_{i}.png", metadata={"experiment_id": experiment_id}) + # plots_collection = db['optimizer_plots'] + # plot_doc = { + # "name": f"sample_matplotlib_plot_{i}", + # "experiment_id": experiment_id, + # "file_id": file_id + # } + # plots_collection.insert_one(plot_doc) # plot = plot_optimization_results(optimizer, objective) # plot_images.append(plot) # plot_images.append("plot") @@ -385,113 +331,7 @@ def generate_optimizer_parameters(n_clicks, camp, bs, target_objective): log_text = f.getvalue() # Create image components from the base64-encoded plot images - image_components = [html.Img(src=f"data:image/png;base64,{img}", style={"width": "100%"}) for img in plot_images] + # image_components = [html.Img(src=f"data:image/png;base64,{img}", style={"width": "100%"}) for img in plot_images] # Return the log text and plot images as components - return image_components + [log_text] - -# @callback( -# Output("bo-generator-timer", "disabled"), -# Output("bo-generated-data", "data"), -# Output("bo-generated-table", "children"), -# Output("bo-save-btn", "disabled"), -# Input("bo-generate-btn", "n_clicks"), -# Input("bo-generator-timer", "n_intervals"), -# State("bo-status-store", "data"), # 🆕 check if paused -# State("bo-generated-data", "data"), -# State("bo-batch-size", "value"), -# prevent_initial_call=True -# ) -# def manage_bo_generation(n_clicks, n_intervals, status, current_data, num_batches): -# import numpy as np -# import pandas as pd -# from dash import dash_table, callback_context - -# if current_data is None: -# current_data = [] - -# triggered = callback_context.triggered_id - -# if triggered == "bo-generate-btn": -# # Reset state and begin generation -# return False, [], dash.no_update, True - -# if triggered == "bo-generator-timer": -# if status != "running": -# # 🧊 paused or idle — stop timer activity -# return dash.no_update, dash.no_update, dash.no_update, dash.no_update - -# if len(current_data) >= num_batches: -# df = pd.DataFrame(current_data) -# return True, current_data, dash_table.DataTable( -# columns=[{"name": i, "id": i} for i in df.columns], -# data=df.to_dict("records"), -# style_table={"overflowX": "auto"}, -# page_size=10 -# ), False - -# # ➕ Generate one new row -# new_row = { -# "concentration": round(np.random.uniform(1, 5), 2), -# "motor_speed": round(np.random.uniform(0.01, 20.0), 2), -# "precursor_volume": round(np.random.uniform(6.0, 12.0), 2), -# } - -# updated_data = current_data + [new_row] -# df = pd.DataFrame(updated_data) - -# return False, updated_data, dash_table.DataTable( -# columns=[{"name": i, "id": i} for i in df.columns], -# data=df.to_dict("records"), -# style_table={"overflowX": "auto"}, -# page_size=10 -# ), len(updated_data) < num_batches - -# return dash.no_update, dash.no_update, dash.no_update, dash.no_update - -# @callback( -# Output("bo-status-store", "data"), -# Output("bo-pause-btn", "children"), -# Output("bo-status-label", "children"), -# Input("bo-pause-btn", "n_clicks"), -# Input("bo-generate-btn", "n_clicks"), -# State("bo-status-store", "data"), -# prevent_initial_call=True -# ) -# def update_bo_status(pause_clicks, generate_clicks, current_status): -# from dash import callback_context - -# triggered = callback_context.triggered_id - -# if triggered == "bo-generate-btn": -# return "running", "Pause", "Running" - -# if triggered == "bo-pause-btn": -# if current_status == "running": -# return "paused", "Resume", "Paused" -# elif current_status == "paused": -# return "running", "Pause", "Running" - -# # fallback -# return "idle", "Pause", "Idle" - -# @callback( -# Output("recipe-builder-campaign-progress", "data"), -# Input("recipe-builder-campaign-dropdown", "value") -# ) -# def update_campaign_progress(campaign_name): -# if not campaign_name: -# return [] -# return get_param_sets_for_campaign(campaign_name) -# def get_param_sets_for_campaign(campaign): -# now = datetime.now() -# statuses = ["Pending", "Generated", "Executed"] -# return [ -# { -# "set_id": f"PS{i+1}", -# "status": 'Pending', -# "timestamp": (now - timedelta(minutes=i*5)).strftime("%Y-%m-%d %H:%M"), -# "solution_pos": 'A1', -# } -# for i in range(5) -# ] \ No newline at end of file + return log_text \ No newline at end of file diff --git a/aamp_app/pages/home.py b/aamp_app/pages/home.py index bd50e74..1d557e4 100644 --- a/aamp_app/pages/home.py +++ b/aamp_app/pages/home.py @@ -37,7 +37,8 @@ "Sampler": "/sampler", "Recipe Builder": "/recipe-builder", "Solution Map": "/solution-map", - "Optimization": "/bayesian-optimization", + "Autonomous Run": "/bayesian-optimization", + "Manual Run": "/manual-run", # "Options": "/options", } diff --git a/aamp_app/pages/manual-run.py b/aamp_app/pages/manual-run.py new file mode 100644 index 0000000..dcdb112 --- /dev/null +++ b/aamp_app/pages/manual-run.py @@ -0,0 +1,201 @@ +import torch +import pandas as pd +from pathlib import Path + +from dash import html, dcc, dash_table, callback, Input, Output, State +import dash_bootstrap_components as dbc +import dash +import io +import contextlib +from Image_Processing.image_processing_changhyun import * +from Image_Processing.constrained_bo_ver2 import ConstrainedBayesianOptimizer + +dash.register_page( + __name__, + path="/manual-run", + name="Manual Run", + title="Manual Run" +) + +layout = html.Div([ + html.Div([ + html.H2("Manual Bayesian Optimization Run"), + # Additional layout components can be added here + html.Div([ + dbc.Row([ + dbc.Col([ + html.H5("Upload Training Data"), + dcc.Upload( + id='manual-upload-data', + children=html.Div([ + 'Drag and Drop or ', + html.A('Select a CSV File') + ]), + style={ + 'width': '100%', + 'height': '60px', + 'lineHeight': '60px', + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center', + 'margin': '10px' + }, + multiple=False, + accept='.csv' + ), + html.Div(id='manual-upload-data-output') + ], width=12) + ], className="mb-3"), + dbc.Button("Run Optimization", id="manual-run-optimization-btn", color="primary"), + html.Pre(id="manual-optimization-output", style={"whiteSpace": "pre-wrap", "border": "1px solid #ccc", "padding": "10px", "marginTop": "10px"}) + ]) + ]), + html.Div( + [ + html.H2("Image Processing"), + dbc.Input(id="data-path-input", placeholder="Enter input data path...", type="text"), + dbc.Button("Process Image", id="process-image-btn", color="primary", className="mt-2"), + html.Table(id="manual-image-processing-output", style={"whiteSpace": "pre-wrap", "border": "1px solid #ccc", "padding": "10px", "marginTop": "10px"}) + ] + ) +]) + +@callback( + Output('manual-optimization-output', 'children'), + Input('manual-run-optimization-btn', 'n_clicks'), + State('manual-upload-data', 'contents'), + State('manual-upload-data', 'filename') +) +def run_manual_optimization(n_clicks, contents, filename): + if n_clicks is None or contents is None: + return "No data uploaded yet." + + f = io.StringIO() + bounds = torch.tensor([ + [0.01, 20.0], # Speed + [25.0, 107.0], # Temperature + [50, 200], # Gap + [5, 15] # Volume + ]).T + + # Define discrete points for each parameter + discrete_points = [ + torch.tensor([0.1, 0.5, 0.7, 1.0]), # Speed (if discrete) + torch.tensor(list(range(25, 108))), # Temperature + torch.tensor(list(range(50, 201))), # Gap + torch.tensor(list(range(5, 16))) # Volume + ] + + csv_path = 'Round0/PProDOT_CB_Campaign_parameters.csv' + optimizer = ConstrainedBayesianOptimizer( + bounds=bounds, + csv_path=csv_path, + round_num=0, + batch_size=8, + discrete_or_not=[False, True, True, True], + discrete_points=discrete_points + ) + + # Suggest candidates + with contextlib.redirect_stdout(f): + candidates, metadata = optimizer.suggest() + print("\n" + "=" * 60) + print("CONSTRAINED BO CANDIDATES READY") + print("=" * 60) + log = f.getvalue() + return log + +@callback( + Output('manual-image-processing-output', 'children'), + Input('process-image-btn', 'n_clicks'), + State('data-path-input', 'value') +) +def img_proc(n_clicks, data_path): + if n_clicks is None: + return "Image processing not started yet." + + main_dirs = [ + Path("Image_Processing/Round0"), + # Path("data/Round0_1"), + # Path("data/Round0_2"), + # Path("data/Round0_3"), + # Path("data/Round1"), + # Path("data/Round2"), + # Path("data/Round3"), + # Path("data/Round4"), + # Path("data/Round5"), + # Path("data/PProDOT_2nd_dataset") + # Add desired directories + ] + + # Method 2: Automatically discover all Round* directories under data/ (comment out Method 1 first) + # data_base = Path("data") + # if data_base.exists(): + # main_dirs = [d for d in data_base.iterdir() if d.is_dir() and d.name.startswith("Round")] + # else: + # main_dirs = [] + + # Common settings + roi_x = 580 + roi_y = 400 + roi_width = 913 + roi_height = 415 + + # Thresholds + uvvis_abs_threshold = 0.1 + coverage_threshold = 0.1 + + # Common metadata (reference_dir and uvvis_dir are automatically set for each directory) + metadata_base = { + "roi_x": roi_x, + "roi_y": roi_y, + "roi_width": roi_width, + "roi_height": roi_height, + "uvvis_abs_threshold": uvvis_abs_threshold, + "coverage_threshold": coverage_threshold, + "coverage_k": 1.0, + "uniformity_std_sensitivity": 15.0, # K1: Sensitivity to Std difference + "uniformity_ent_sensitivity": 2.0, # K2: Sensitivity to Entropy difference + } + + # Process each directory + for main_dir in main_dirs: + if not main_dir.exists(): + print(f"Warning: Directory not found: {main_dir}, skipping...") + continue + + results = None + try: + results = process_single_directory( + main_dir=main_dir, + roi_x=roi_x, + roi_y=roi_y, + roi_width=roi_width, + roi_height=roi_height, + uvvis_abs_threshold=uvvis_abs_threshold, + coverage_threshold=coverage_threshold, + metadata_base=metadata_base, + ) + except Exception as e: + print(f"Error processing {main_dir}: {e}") + import traceback + traceback.print_exc() + continue + + print(f"\n{'='*60}") + print("All directories processed!") + print(f"{'='*60}") + + # Create DataFrame for display + df_results = pd.DataFrame(results) + # only need a few columns for display + display_columns = ["round#", "Sample #", "image_name", "uniformity_score"] + df_display = df_results[display_columns] + return dash_table.DataTable( + data=df_display.to_dict('records'), + columns=[{"name": i, "id": i} for i in df_display.columns], + page_size=10, + style_table={'overflowX': 'auto'}, + style_cell={'textAlign': 'left'}, + ) \ No newline at end of file From d48531ce348dea1f381f4cd144cc334353d874e8 Mon Sep 17 00:00:00 2001 From: Weiqi Zhang Date: Sun, 15 Feb 2026 17:07:47 -0600 Subject: [PATCH 110/125] Switchc to DB for Dev --- aamp_app/pages/bayesian-optimization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aamp_app/pages/bayesian-optimization.py b/aamp_app/pages/bayesian-optimization.py index fad5147..39aae49 100644 --- a/aamp_app/pages/bayesian-optimization.py +++ b/aamp_app/pages/bayesian-optimization.py @@ -18,8 +18,8 @@ def emergency_stop(): os.kill(os.getpid(), signal.SIGINT) -client = MongoClient('mongodb://aamp-user:b6b52bc6b85b8a53af21ae2883b3910d@aamp-mongodb-0.aamp.mmli1.ncsa.illinois.edu/aamp?replicaSet=aamp-mongodb&authSource=admin&tlsAllowInvalidCertificates=true&tls=true') -db = client['aamp'] +client = MongoClient('mongodb://aamp-dev:dcb22e50d7f310850e748c22e6278e4b@aamp-mongodb-0.aamp.mmli2.ncsa.illinois.edu/aamp?replicaSet=aamp-mongodb&authSource=admin&tlsAllowInvalidCertificates=true&tls=true') +db = client['aamp-dev'] fs = gridfs.GridFS(db) # def plot_optimization_results(optimizer, objective): From ffdf5d180234b1a81cc95880a74ca8d0aa42a825 Mon Sep 17 00:00:00 2001 From: Hwang Date: Mon, 16 Feb 2026 10:09:03 -0600 Subject: [PATCH 111/125] edited requirements.txt --- aamp_app/solution_map.json | 24 ++++++++++++------------ requirements.txt | Bin 490 -> 600 bytes 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/aamp_app/solution_map.json b/aamp_app/solution_map.json index 8797320..8dd664a 100644 --- a/aamp_app/solution_map.json +++ b/aamp_app/solution_map.json @@ -24,10 +24,10 @@ "status": "occupied" }, "A5": { - "polymer": null, - "solvent": null, - "concentration": null, - "status": "empty" + "polymer": "Chang", + "solvent": "CF", + "concentration": 2, + "status": "occupied" }, "B1": { "polymer": "P3MEEMT", @@ -36,16 +36,16 @@ "status": "occupied" }, "B2": { - "polymer": null, - "solvent": null, - "concentration": null, - "status": "empty" + "polymer": "Chang", + "solvent": "CF", + "concentration": 10, + "status": "occupied" }, "B3": { - "polymer": null, - "solvent": null, - "concentration": null, - "status": "empty" + "polymer": "Chang", + "solvent": "CF", + "concentration": 15, + "status": "occupied" }, "B4": { "polymer": null, diff --git a/requirements.txt b/requirements.txt index e2b7613e524e8f506622c8c859bfb06048476633..0c0e65d8eacd4663b4ec26a63b84049120c29557 100644 GIT binary patch delta 118 zcmaFGe1m1fE5 Date: Mon, 16 Feb 2026 12:25:43 -0600 Subject: [PATCH 112/125] revisited ximea camera device/commands. --- aamp_app/commands/ximea_camera_commands.py | 144 +++++++- aamp_app/devices/ximea_camera.py | 399 ++++++++++++++++++--- 2 files changed, 478 insertions(+), 65 deletions(-) diff --git a/aamp_app/commands/ximea_camera_commands.py b/aamp_app/commands/ximea_camera_commands.py index 02e92f6..6c79a45 100644 --- a/aamp_app/commands/ximea_camera_commands.py +++ b/aamp_app/commands/ximea_camera_commands.py @@ -11,6 +11,7 @@ class XimeaCameraParentCommand(Command): def __init__(self, receiver: XimeaCamera, **kwargs): super().__init__(receiver, **kwargs) + class XimeaCameraInitialize(XimeaCameraParentCommand): """Initialize camera by opening communication with camera and optionally set defaults.""" @@ -20,6 +21,7 @@ def __init__(self, receiver: XimeaCamera, **kwargs): def execute(self) -> None: self._result = CommandResult(*self._receiver.initialize()) + class XimeaCameraDeinitialize(XimeaCameraParentCommand): """Deinitialize camera by stopping any acquisition and closing communication with camera.""" @@ -30,27 +32,122 @@ def __init__(self, receiver: XimeaCamera, reset_init_flag: bool = True, **kwargs def execute(self) -> None: self._result = CommandResult(*self._receiver.deinitialize(self._params['reset_init_flag'])) +class XimeaCameraUpdateBackground(XimeaCameraParentCommand): + def __init__( + self, + receiver: XimeaCamera, + save_to_file: bool = True, + filename: str = None, + exposure_time: Optional[int] = None, + gain: Optional[float] = None, + **kwargs): + super().__init__(receiver, **kwargs) + self._params['save_to_file'] = save_to_file + self._params['filename'] = filename + self._params['exposure_time'] = exposure_time + self._params['gain'] = gain + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.update_background(self._params['save_to_file'], self._params['filename'], + self._params['exposure_time'], self._params['gain'])) + + +class XimeaCameraUpdateCompRef(XimeaCameraParentCommand): + def __init__( + self, + receiver: XimeaCamera, + save_to_file: bool = True, + filename: str = None, + exposure_time: Optional[int] = None, + gain: Optional[float] = None, + **kwargs): + super().__init__(receiver, **kwargs) + self._params['save_to_file'] = save_to_file + self._params['filename'] = filename + self._params['exposure_time'] = exposure_time + self._params['gain'] = gain + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.update_completeness_ref(self._params['save_to_file'], self._params['filename'], + self._params['exposure_time'], self._params['gain'])) + + +class XimeaCameraCheckComp(XimeaCameraParentCommand): + def __init__( + self, + receiver: XimeaCamera, + save_to_file: bool = True, + filename: str = None, + exposure_time: Optional[int] = None, + gain: Optional[float] = None, + **kwargs): + super().__init__(receiver, **kwargs) + self._params['save_to_file'] = save_to_file + self._params['filename'] = filename + self._params['exposure_time'] = exposure_time + self._params['gain'] = gain + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.check_completeness(self._params['save_to_file'], self._params['filename'], + self._params['exposure_time'], self._params['gain'])) + + +class XimeaCameraUpdateVertBound(XimeaCameraParentCommand): + def __init__( + self, + receiver: XimeaCamera, + exposure_time: Optional[int] = None, + gain: Optional[float] = None, + **kwargs): + super().__init__(receiver, **kwargs) + self._params['exposure_time'] = exposure_time + self._params['gain'] = gain + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.update_vertical_bound(self._params['exposure_time'], self._params['gain'])) + + class XimeaCameraGetImage(XimeaCameraParentCommand): - """Get image and save to file, display image, or both. No filename = timestamped filename. No exposure or gain = use defaults.""" - + """Get image and save to file, display image, or both, check uniform in certain range. No filename = timestamped filename. No exposure or gain = use defaults.""" + def __init__( - self, - receiver: XimeaCamera, - save_to_file: bool = True, - filename: str = None, - exposure_time: Optional[int] = None, - gain: Optional[float] = None, - show_pop_up: bool = False, + self, + receiver: XimeaCamera, + save_to_file: bool = True, + filename: str = None, + check_uniform: bool = True, + y_upper: int = 100, + y_length: int = 700, + x_upper: int = None, + x_length: int = None, + exposure_time: Optional[int] = None, + gain: Optional[float] = None, + show_pop_up: bool = False, **kwargs): super().__init__(receiver, **kwargs) self._params['save_to_file'] = save_to_file self._params['filename'] = filename + self._params['check_uniform'] = check_uniform + self._params['y_upper'] = y_upper + self._params['y_length'] = y_length + self._params['x_upper'] = x_upper + self._params['x_length'] = x_length self._params['exposure_time'] = exposure_time self._params['gain'] = gain self._params['show_pop_up'] = show_pop_up def execute(self) -> None: - self._result = CommandResult(*self._receiver.get_image(self._params['save_to_file'], self._params['filename'], self._params['exposure_time'], self._params['gain'], self._params['show_pop_up'])) + self._result = CommandResult(*self._receiver.get_image(self._params['save_to_file'], self._params['filename'], + self._params['check_uniform'], self._params['y_upper'], + self._params['y_length'], self._params['x_upper'], + self._params['x_length'], + self._params['exposure_time'], self._params['gain'], + self._params['show_pop_up'])) + class XimeaCameraSetDefaultExposure(XimeaCameraParentCommand): """Set the default exposure time to use if no exposure time is passed when getting image.""" @@ -62,9 +159,12 @@ def __init__(self, receiver: XimeaCamera, exposure_time: int, **kwargs): def execute(self) -> None: self._receiver.default_exposure_time = self._params['exposure_time'] if self._receiver.default_exposure_time == self._params['exposure_time']: - self._result = CommandResult(True, "Default exposure time successfully set to " + str(self._params['exposure_time'])) + self._result = CommandResult(True, "Default exposure time successfully set to " + str( + self._params['exposure_time'])) else: - self._result = CommandResult(False, "Failed to set the default exposure time to " + str(self._params['exposure_time']) + ". The default exposure time must be > 0.") + self._result = CommandResult(False, "Failed to set the default exposure time to " + str( + self._params['exposure_time']) + ". The default exposure time must be > 0.") + class XimeaCameraSetDefaultGain(XimeaCameraParentCommand): """Set the default gain to use if no gain is passed when getting image.""" @@ -79,7 +179,9 @@ def execute(self) -> None: self._result = CommandResult(True, "Default gain successfully set to " + str(self._params['gain'])) else: # unsure what the upper limit is at the moment - self._result = CommandResult(False, "Failed to set the default gain to " + str(self._params['gain']) + ". The default gain must be >= 0 and has an upper limit.") + self._result = CommandResult(False, "Failed to set the default gain to " + str( + self._params['gain']) + ". The default gain must be >= 0 and has an upper limit.") + class XimeaCameraUpdateWhiteBal(XimeaCameraParentCommand): """Update the white balance coefficients using the current camera feed.""" @@ -90,26 +192,32 @@ def __init__(self, receiver: XimeaCamera, exposure_time: int = None, gain: Optio self._params['gain'] = gain def execute(self) -> None: - self._result = CommandResult(*self._receiver.update_white_balance(self._params['exposure_time'], self._params['gain'])) + self._result = CommandResult( + *self._receiver.update_white_balance(self._params['exposure_time'], self._params['gain'])) + class XimeaCameraSetManualWhiteBal(XimeaCameraParentCommand): """Set any of the red, green, and blue white balance coefficients manually.""" - def __init__(self, receiver: XimeaCamera, wb_kr: Optional[float] = None, wb_kg: Optional[float] = None, wb_kb: Optional[float] = None, **kwargs): + def __init__(self, receiver: XimeaCamera, wb_kr: Optional[float] = None, wb_kg: Optional[float] = None, + wb_kb: Optional[float] = None, **kwargs): super().__init__(receiver, **kwargs) self._params['wb_kr'] = wb_kr self._params['wb_kg'] = wb_kg self._params['wb_kb'] = wb_kb - + def execute(self) -> None: - self._result = CommandResult(self._receiver.set_white_balance_manually(self._params['wb_kr'], self._params['wb_kg'], self._params['wb_kb'])) + self._result = CommandResult( + self._receiver.set_white_balance_manually(self._params['wb_kr'], self._params['wb_kg'], + self._params['wb_kb'])) + class XimeaCameraResetWhiteBal(XimeaCameraParentCommand): """Reset the red, green, and blue white balance coefficients to defaults.""" def __init__(self, receiver: XimeaCamera, **kwargs): super().__init__(receiver, **kwargs) - + def execute(self) -> None: self._result = CommandResult(*self._receiver.reset_white_balance_rgb_coeffs()) diff --git a/aamp_app/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py index 0d6bdc4..9231a67 100644 --- a/aamp_app/devices/ximea_camera.py +++ b/aamp_app/devices/ximea_camera.py @@ -6,10 +6,14 @@ from .device import Device, check_initialized +import numpy as np +import cv2 + + # Unsure how cooperative xiapi.Camera is so did not use multiple inheritance # will need to figure out the method resolution order if using multiple inheritance class XimeaCamera(Device): - save_directory = '../data/imaging/' + save_directory = 'data/imaging/' def __init__(self, name: str): super().__init__(name) @@ -19,32 +23,31 @@ def __init__(self, name: str): # could consider a namedtuple to keep descriptive context of each param while still being able to use an iterative # for now, individual params self._default_imgdataformat = 'XI_RGB24' + # self._default_imgdataformat = 'XI_MONO8' self._default_exposure_time = 50000 self._default_gain = 0.0 # default wb coeffs below technically constants, leaving uncaptilaized + self._default_wb_kr = 1.531 self._default_wb_kg = 1.0 self._default_wb_kb = 1.305 - # self.set_default_params() # done in initalize because cam is not yet open here - def get_init_args(self) -> dict: - args_dict = { - "name": self.name, - } - return args_dict - - def update_init_args(self, args_dict: dict): - self.name = args_dict["name"] + self.background = None + self.completeness_ref = None + + self.boundaryx_0 = 350 + self.boundaryx_1 = 1000 + # self.set_default_params() # done in initalize because cam is not yet open here # no setter for imgdataformat at the moment @property def default_imgdataformat(self) -> str: return self._default_imgdataformat - + @property def default_exposure_time(self) -> int: return self._default_exposure_time - + @default_exposure_time.setter def default_exposure_time(self, exposure_time: int): if exposure_time > 0: @@ -59,7 +62,7 @@ def default_gain(self, gain: float): # there is an upper limit for this but not 100% sure what it is if gain >= 0.0: self._default_gain = gain - + # no setter for default wb coeffs @property def default_wb_kr(self) -> float: @@ -87,11 +90,21 @@ def initialize(self, set_defaults: bool = True) -> Tuple[bool, str]: # set defaults if flag is True. This should be True the very first time in order to set the default params, but not enforced if set_defaults: self.set_default_params() + print(self.cam.get_exposure_minimum()) + print(self.cam.get_exposure_maximum()) + print(self.cam.get_exposure_increment()) + print("Gain min:", self.cam.get_gain_minimum()) + print("Gain max:", self.cam.get_gain_maximum()) + print("Gain increment:", self.cam.get_gain_increment()) + print("Auto white balance?", self.cam.is_auto_wb()) + self.cam.disable_aeag() + # self.cam.enable_auto_wb() + # print() self._is_initialized = True except xiapi.Xi_error as inst: self._is_initialized = False return (False, "Failed to connect and initialize: " + str(inst)) - + if set_defaults: return (True, "Successfully initialized camera by opening communications and setting defaults.") else: @@ -109,17 +122,114 @@ def deinitialize(self, reset_init_flag: bool = True) -> Tuple[bool, str]: return (True, "Successfully deinitialized camera, communication closed.") + @check_initialized + def update_background( + self, + save_to_file: bool = True, + filename: str = None, + exposure_time: Optional[int] = None, + gain: Optional[float] = None) -> Tuple[bool, str]: + if exposure_time is None: + exposure_time = self._default_exposure_time + if gain is None: + gain = self._default_gain + + try: + self.cam.set_exposure(exposure_time) + self.cam.set_gain(gain) + + img = xiapi.Image() + self.cam.start_acquisition() + self.cam.get_image(img) + self.cam.stop_acquisition() + except xiapi.Xi_error as inst: + return (False, "Error while getting image: " + str(inst)) + + data = img.get_image_data_numpy(invert_rgb_order=True) + self.background = data + img = Image.fromarray(self.background, 'RGB') + + if save_to_file: + + if filename is None: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = timestamp + fullfilename = self.save_directory + filename + img.save(fullfilename + '.bmp') + + return (True, "Successfully update background") + + # @check_initialized + # def is_uniform( + # self, + # save_to_file: bool = True, + # exposure_time: Optional[int] = None, + # gain: Optional[float] = None) -> float: + # if exposure_time is None: + # exposure_time = self._default_exposure_time + # if gain is None: + # gain = self._default_gain + + # try: + # self.cam.set_exposure(exposure_time) + # self.cam.set_gain(gain) + # img = xiapi.Image() + # self.cam.start_acquisition() + # self.cam.get_image(img) + # self.cam.stop_acquisition() + # except xiapi.Xi_error as inst: + # return (False, "Error while getting image: " + str(inst)) + # data = img.get_image_data_numpy(invert_rgb_order=True) + + # # define a square area, check uniform + # x_upper = 680 + # y_upper = 540 + # length = 500 + # checking_area = data[y_upper : y_upper+length, x_upper: x_upper + length, :] + # std = 0 + # for i in range (3): + # avg = np.average(checking_area[:,:,i]) + # sqaure_diff = (checking_area[:,:,i] - avg) ** 2 + # std += np.sqrt(np.average(sqaure_diff)) + # score = 1 - std / (127.5 * 3) + + # checking_area = data[y_upper : y_upper+length, x_upper: x_upper + length, :] + # img_focus = Image.fromarray(checking_area, 'RGB') + + # if save_to_file: + + # if filename is None: + # timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + # filename = timestamp + # fullfilename = self.save_directory + filename + # img_focus.save(fullfilename + '_focus' + '.bmp') + + # return score + @check_initialized def get_image( - self, - save_to_file: bool = True, - filename: str = None, - exposure_time: Optional[int] = None, - gain: Optional[float] = None, + self, + save_to_file: bool = True, + filename: str = None, + check_uniform: bool = True, + y_upper: int = 20, + y_length: int = 1000, + x_upper: int = None, + x_length: int = None, + exposure_time: Optional[int] = None, + gain: Optional[float] = None, show_pop_up: bool = False) -> Tuple[bool, str]: # if not self._is_initialized: # return (False, "Camera is not initialized") + + + if x_upper is None: + x_upper = self.boundaryx_0 + if x_length is None: + x_length = self.boundaryx_1 - self.boundaryx_0 + print('x_upper:', x_upper) + print('x_length:', x_length) if exposure_time is None: exposure_time = self._default_exposure_time if gain is None: @@ -128,20 +238,44 @@ def get_image( try: self.cam.set_exposure(exposure_time) self.cam.set_gain(gain) - img = xiapi.Image() self.cam.start_acquisition() # exposure time is in microsec, timeout is in millisec, timeout is set to double the exposure time - self.cam.get_image(img, timeout=(int(exposure_time / 1000 * 2))) + # self.cam.get_image(img, timeout=(int(exposure_time / 1000 * 2))) + self.cam.get_image(img) + # data_raw = img.get_image_data_raw() + # data_0 = list(data_raw) + # print("first 10 pixels: " + str(data_0[:10])) self.cam.stop_acquisition() except xiapi.Xi_error as inst: return (False, "Error while getting image: " + str(inst)) + # print("Current image format:", self.cam.get_imgdataformat()) + data = img.get_image_data_numpy(invert_rgb_order=True) img = Image.fromarray(data, 'RGB') + # print("data:",data.dtype) + print("shape: ", data.shape) + # print("first 10 value:", data[0:10,0,:]) + + if check_uniform: + if self.background is None: + checking_area = data[y_upper: y_upper + y_length, x_upper: x_upper + x_length, :] + else: + corrected_image = cv2.subtract(self.background, data) + print("shape of corrected_image", corrected_image.shape) + checking_area = corrected_image[y_upper: y_upper + y_length, x_upper: x_upper + x_length] + checking_area[checking_area < 0] = 0 + std = 0 + for i in range(3): + std += (np.std(checking_area[:, :, i])) ** 2 + score = 1 - np.sqrt(std) / (220.836) + img_focus = Image.fromarray(checking_area, 'RGB') + print("uniformity score is ", score) + if save_to_file: - + if filename is None: timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = timestamp @@ -149,45 +283,214 @@ def get_image( img.save(fullfilename + '.bmp') settings = ["image data format = " + self.cam.get_imgdataformat() + "\n", - "exposure (us) = " + str(self.cam.get_exposure()) + "\n", - "gain = " + str(self.cam.get_gain()) + "\n", - "wb_kr = " + str(self.cam.get_wb_kr()) + "\n", - "wb_kg = " + str(self.cam.get_wb_kg()) + "\n", - "wb_kb = " + str(self.cam.get_wb_kb()) + "\n"] + "exposure (us) = " + str(self.cam.get_exposure()) + "\n", + "gain = " + str(self.cam.get_gain()) + "\n", + "wb_kr = " + str(self.cam.get_wb_kr()) + "\n", + "wb_kg = " + str(self.cam.get_wb_kg()) + "\n", + "wb_kb = " + str(self.cam.get_wb_kb()) + "\n"] with open(fullfilename + '.txt', 'w') as file: - file.writelines(settings) + file.writelines(settings) + + if check_uniform: + img_focus.save(fullfilename + '_checkunifrom' + '.bmp') + settings = ["This sample is " + str(score * 100) + "% uniform, (1 - std / 127.5) " + "\n", + "checking area:" "\n", + "x from " + str(x_upper) + " to " + str(x_upper + x_length) + "\n", + "y from " + str(y_upper) + " to " + str(y_upper + y_length) + "\n"] + with open(fullfilename + '_checkunifrom' + '.txt', 'w') as file: + file.writelines(settings) return (True, "Successfully saved image and settings to " + str(fullfilename) + ".bmp") if show_pop_up: img.show() - return (True, "Successfully took image but did not save.") + return (True, "Successfully took image but did not save.") + + @check_initialized + def update_vertical_bound( + self, + exposure_time: Optional[int] = None, + gain: Optional[float] = None) -> Tuple[bool, str]: + if self.background is None: + return (False, "background image is required, call update_background first") + if exposure_time is None: + exposure_time = self._default_exposure_time + if gain is None: + gain = self._default_gain + + try: + self.cam.set_exposure(exposure_time) + self.cam.set_gain(gain) + + img = xiapi.Image() + self.cam.start_acquisition() + # exposure time is in microsec, timeout is in millisec, timeout is set to double the exposure time + self.cam.get_image(img) + self.cam.stop_acquisition() + except xiapi.Xi_error as inst: + return (False, "Error while getting image: " + str(inst)) + + data = img.get_image_data_numpy(invert_rgb_order=True) + gray_scale_ref = cv2.subtract(cv2.cvtColor(self.background, cv2.COLOR_RGB2GRAY), + cv2.cvtColor(data, cv2.COLOR_RGB2GRAY)) + gray_scale_ref[gray_scale_ref < 5] = 0 + gray_scale_ref[gray_scale_ref >= 5] = 100 + boundary = np.abs(gray_scale_ref[:, 1:] - gray_scale_ref[:, :-1]) + img = Image.fromarray(boundary, 'L') + img.save(self.save_directory + 'z_bbs' + '.bmp') + print('boundary.shape', boundary.shape) + boundary = boundary.astype(np.int32) + boundary = np.sum(boundary, axis=0) + + # remainder = len(boundary) % 10 + # if remainder != 0: + # padding = 10 - remainder + # boundary_pad = np.pad(boundary, (0,padding), "constant") + # else: + # boundary_pad = boundary + # boundary_reshape = (boundary_pad.reshape(-1,10)).sum(axis=1) + + boundary_x = np.argsort(boundary) + print('boundary_x:', boundary_x[-10:]) + print(boundary.argmin(), boundary.argmax(), boundary.min(), boundary.max()) + print(boundary[boundary_x[-10:]]) + + self.boundaryx_0 = boundary_x[-1] + boundaryx_1_idx = -1 + while (np.abs(boundary_x[boundaryx_1_idx] - self.boundaryx_0) < 600): + boundaryx_1_idx -= 1 + self.boundaryx_1 = boundary_x[boundaryx_1_idx] + + if self.boundaryx_1 < self.boundaryx_0: + temp = self.boundaryx_1 + self.boundaryx_1 = self.boundaryx_0 + self.boundaryx_0 = temp + print('self.boundaryx_0:', self.boundaryx_0) + print('self.boundaryx_1:', self.boundaryx_1) + self.boundaryx_0 += 60 + self.boundaryx_1 -= 60 + + return (True, "Successfully update vertical boundary") + + @check_initialized + def update_completeness_ref( + self, + save_to_file: bool = True, + filename: str = None, + exposure_time: Optional[int] = None, + gain: Optional[float] = None) -> Tuple[bool, str]: + if self.background is None: + return (False, "background image is required, call update_background first") + if exposure_time is None: + exposure_time = self._default_exposure_time + if gain is None: + gain = self._default_gain + + try: + self.cam.set_exposure(exposure_time) + self.cam.set_gain(gain) + + img = xiapi.Image() + self.cam.start_acquisition() + # exposure time is in microsec, timeout is in millisec, timeout is set to double the exposure time + self.cam.get_image(img) + self.cam.stop_acquisition() + except xiapi.Xi_error as inst: + return (False, "Error while getting image: " + str(inst)) + + data = img.get_image_data_numpy(invert_rgb_order=True) + gray_scale_ref = cv2.subtract(cv2.cvtColor(self.background, cv2.COLOR_RGB2GRAY), + cv2.cvtColor(data, cv2.COLOR_RGB2GRAY)) + gray_scale_ref[gray_scale_ref < 5] = 0 + gray_scale_ref[gray_scale_ref >= 5] = 100 + self.completeness_ref = gray_scale_ref + img = Image.fromarray(self.completeness_ref, 'L') + + if save_to_file: + + if filename is None: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = timestamp + fullfilename = self.save_directory + filename + img.save(fullfilename + '.bmp') + + return (True, "Successfully update completeness reference") + + @check_initialized + def check_completeness( + self, + save_to_file: bool = True, + filename: str = None, + exposure_time: Optional[int] = None, + gain: Optional[float] = None) -> Tuple[bool, str]: + if self.completeness_ref is None: + return (False, "completeness_ref image is required, call update_completeness_ref first") + if exposure_time is None: + exposure_time = self._default_exposure_time + if gain is None: + gain = self._default_gain + + try: + self.cam.set_exposure(exposure_time) + self.cam.set_gain(gain) + + img = xiapi.Image() + self.cam.start_acquisition() + # exposure time is in microsec, timeout is in millisec, timeout is set to double the exposure time + self.cam.get_image(img) + self.cam.stop_acquisition() + except xiapi.Xi_error as inst: + return (False, "Error while getting image: " + str(inst)) + + data = img.get_image_data_numpy(invert_rgb_order=True) + gray_scale_image = cv2.subtract(cv2.cvtColor(self.background, cv2.COLOR_RGB2GRAY), + cv2.cvtColor(data, cv2.COLOR_RGB2GRAY)) + gray_scale_image[gray_scale_image < 5] = 0 + gray_scale_image[gray_scale_image >= 5] = 100 + similarity = 1 - np.sum(np.abs(gray_scale_image - self.completeness_ref)) / ( + np.sum(np.ones_like(gray_scale_image)) * 100) + img = Image.fromarray(gray_scale_image, 'L') + + if save_to_file: + + if filename is None: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = timestamp + fullfilename = self.save_directory + filename + img.save(fullfilename + '.bmp') + + return (True, "This sample's shape is " + str(similarity * 100) + "% similar to the reference.") @check_initialized def update_white_balance(self, exposure_time: int = None, gain: Optional[float] = None) -> Tuple[bool, str]: # if not self._is_initialized: # return (False, "Camera is not initialized") - + try: self.cam.set_manual_wb(1) except xiapi.Xi_error as inst: return (False, "Failed to set white balance: " + str(inst)) - was_successful, result_message = self.get_image(save_to_file=False, filename=None, exposure_time=exposure_time, gain=gain) + was_successful, result_message = self.get_image(save_to_file=False, filename=None, exposure_time=exposure_time, + gain=gain) if not was_successful: return (was_successful, result_message) - return (True, "Successfully updated white balance coefficients with image: wb_kr, wb_kg, wb_kb = " + str(self.get_white_balance_rgb_coeffs())) - - @check_initialized # is this necessary - def set_white_balance_manually(self, wb_kr: Optional[float] = None, wb_kg: Optional[float] = None, wb_kb: Optional[float] = None) -> Tuple[bool, str]: + return (True, "Successfully updated white balance coefficients with image: wb_kr, wb_kg, wb_kb = " + str( + self.get_white_balance_rgb_coeffs())) + + @check_initialized # is this necessary + def set_white_balance_manually(self, wb_kr: Optional[float] = None, wb_kg: Optional[float] = None, + wb_kb: Optional[float] = None) -> Tuple[bool, str]: # if not self._is_initialized: # return (False, "Camera is not initialized") - + if wb_kr is None and wb_kg is None and wb_kb is None: - return (True, "No white balance coefficients were changed. Coefficients are currently: wb_kr, wb_kg, wb_kb = " + str(self.get_white_balance_rgb_coeffs())) + return (True, + "No white balance coefficients were changed. Coefficients are currently: wb_kr, wb_kg, wb_kb = " + str( + self.get_white_balance_rgb_coeffs())) try: if wb_kr is not None: self.cam.set_wb_kr(wb_kr) @@ -196,24 +499,26 @@ def set_white_balance_manually(self, wb_kr: Optional[float] = None, wb_kg: Optio if wb_kb is not None: self.cam.set_wb_kb(wb_kb) except: - return (False, "Error in setting white balance coefficients. Coefficients are currently: wb_kr, wb_kg, wb_kb = " + str(self.get_white_balance_rgb_coeffs())) - - return (True, "Successfully set white balance coefficients manually: wb_kr, wb_kg, wb_kb = " + str(self.get_white_balance_rgb_coeffs())) - - @check_initialized # is this necessary + return (False, + "Error in setting white balance coefficients. Coefficients are currently: wb_kr, wb_kg, wb_kb = " + str( + self.get_white_balance_rgb_coeffs())) + + return (True, "Successfully set white balance coefficients manually: wb_kr, wb_kg, wb_kb = " + str( + self.get_white_balance_rgb_coeffs())) + + @check_initialized # is this necessary def reset_white_balance_rgb_coeffs(self) -> Tuple[bool, str]: self.cam.set_wb_kr(self._default_wb_kr) self.cam.set_wb_kg(self._default_wb_kg) self.cam.set_wb_kb(self._default_wb_kb) - return (True, "Successfully reset white balance coefficients to defaults. Coefficients are currently: wb_kr, wb_kg, wb_kb = " + str(self.get_white_balance_rgb_coeffs())) + return (True, + "Successfully reset white balance coefficients to defaults. Coefficients are currently: wb_kr, wb_kg, wb_kb = " + str( + self.get_white_balance_rgb_coeffs())) - @check_initialized # is this necessary + @check_initialized # is this necessary def get_white_balance_rgb_coeffs(self) -> List[float]: wb = [] wb.append(self.cam.get_wb_kr()) wb.append(self.cam.get_wb_kg()) wb.append(self.cam.get_wb_kb()) return wb - - - From 3c5ee5db595c528ea8833ea34b33a02e787a2863 Mon Sep 17 00:00:00 2001 From: Weiqi Zhang Date: Mon, 23 Feb 2026 13:23:09 -0600 Subject: [PATCH 113/125] Update sobol sampling --- .gitignore | 2 + aamp_app/app.py | 560 ++++++++----- aamp_app/optimizer/QUICKSTART.md | 169 ---- aamp_app/optimizer/README.md | 255 ------ aamp_app/optimizer/__init__.py | 15 - aamp_app/optimizer/bayesian_optimizer.py | 289 ------- aamp_app/optimizer/demo/demo.py | 312 ------- aamp_app/optimizer/demo/example_usage.py | 282 ------- .../demo/model_integration_example.py | 156 ---- aamp_app/optimizer/demo/run_demo.py | 55 -- aamp_app/optimizer/model_objective_factory.py | 313 ------- .../optimizer/notebooks/demo_notebook.ipynb | 462 ----------- .../notebooks/interactive_notebook.ipynb | 299 ------- .../notebooks/model_integration_demo.ipynb | 775 ------------------ .../notebooks/tutorial_notebook.ipynb | 70 -- aamp_app/optimizer/objective_function.py | 199 ----- aamp_app/optimizer/optimization_progress.png | Bin 228134 -> 0 bytes aamp_app/optimizer/requirements.txt | 17 - aamp_app/optimizer/setup.py | 53 -- aamp_app/pages/sampler.py | 2 +- 20 files changed, 377 insertions(+), 3908 deletions(-) delete mode 100644 aamp_app/optimizer/QUICKSTART.md delete mode 100644 aamp_app/optimizer/README.md delete mode 100644 aamp_app/optimizer/__init__.py delete mode 100644 aamp_app/optimizer/bayesian_optimizer.py delete mode 100644 aamp_app/optimizer/demo/demo.py delete mode 100644 aamp_app/optimizer/demo/example_usage.py delete mode 100644 aamp_app/optimizer/demo/model_integration_example.py delete mode 100644 aamp_app/optimizer/demo/run_demo.py delete mode 100644 aamp_app/optimizer/model_objective_factory.py delete mode 100644 aamp_app/optimizer/notebooks/demo_notebook.ipynb delete mode 100644 aamp_app/optimizer/notebooks/interactive_notebook.ipynb delete mode 100644 aamp_app/optimizer/notebooks/model_integration_demo.ipynb delete mode 100644 aamp_app/optimizer/notebooks/tutorial_notebook.ipynb delete mode 100644 aamp_app/optimizer/objective_function.py delete mode 100644 aamp_app/optimizer/optimization_progress.png delete mode 100644 aamp_app/optimizer/requirements.txt delete mode 100644 aamp_app/optimizer/setup.py diff --git a/.gitignore b/.gitignore index 245e210..45a097b 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,5 @@ pw.py .DS_Store blank.yaml pw.txt +ximea_linux_sp_beta.tgz +package \ No newline at end of file diff --git a/aamp_app/app.py b/aamp_app/app.py index 721513d..f2503aa 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -60,7 +60,6 @@ def load_env(file_path=".env"): mongo_db_name, ) -p1, p2 = None, None try: db_list = mongo.client.list_database_names() @@ -2562,6 +2561,8 @@ def update_temperature_options(selected_solvents): return {'display': 'block'}, temp_options +from scipy.stats import qmc + @app.callback( Output("sampler-results-table", "children"), Output("sampler-alert", "children"), @@ -2606,6 +2607,283 @@ def update_temperature_options(selected_solvents): ], prevent_initial_call=True, ) +# def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string, mw, pdi, +# solvents, toggle_values, toggle_ids, temp_dropdown_values, +# temp_dropdown_ids, temp_min_values, temp_min_ids, +# temp_max_values, temp_max_ids, concentration_range, +# concentration_toggle, concentration_min, concentration_max, +# printing_gaps, printing_gap_toggle, printing_gap_min, +# printing_gap_max, precursor_vol, precursor_vol_toggle, +# precursor_vol_min, precursor_vol_max, motor_speeds, +# motor_speed_toggle, motor_speed_min, motor_speed_max, +# sampling_method, num_samples): +# if not solvents: +# return None, "Please select at least one solvent.", True, "danger", True + +# try: +# temp_toggles = {} +# temp_discrete_values = {} +# temp_min = {} +# temp_max = {} + +# for toggle_value, toggle_id in zip(toggle_values, toggle_ids): +# param = toggle_id["param"] +# if param.startswith("temp-"): +# solvent = param.replace("temp-", "") +# temp_toggles[solvent] = toggle_value + +# for dropdown_value, dropdown_id in zip(temp_dropdown_values, temp_dropdown_ids): +# solvent = dropdown_id["index"] +# temp_discrete_values[solvent] = dropdown_value + +# for min_value, min_id in zip(temp_min_values, temp_min_ids): +# solvent = min_id["index"] +# temp_min[solvent] = min_value + +# for max_value, max_id in zip(temp_max_values, temp_max_ids): +# solvent = max_id["index"] +# temp_max[solvent] = max_value + +# parameter_sets = [] +# sample_count = 1 + +# for solvent in solvents: +# is_temp_continuous = temp_toggles.get(solvent, False) + +# for _ in range(num_samples): +# if is_temp_continuous: +# min_temp = temp_min.get(solvent, TEMP_CHOICES_C[solvent][0]) +# max_temp = temp_max.get(solvent, TEMP_CHOICES_C[solvent][1]) +# temperature = round(random.uniform(min_temp, max_temp)) +# else: +# temp_options = temp_discrete_values.get(solvent, TEMP_CHOICES_D[solvent]) +# temperature = round(random.choice(temp_options)) + +# if concentration_toggle: +# concentration = round(random.uniform(concentration_min, concentration_max), 2) +# else: +# concentration = random.choice(concentration_range) if concentration_range else random.choice(CONCEN_D) + +# if printing_gap_toggle: +# printing_gap = round(random.uniform(printing_gap_min, printing_gap_max)) +# else: +# printing_gap = random.choice(printing_gaps) if printing_gaps else random.choice(PRINT_GAP_D) + +# if precursor_vol_toggle: +# precursor_volume = round(random.uniform(precursor_vol_min, precursor_vol_max), 1) +# else: +# precursor_volume = random.choice(precursor_vol) if precursor_vol else random.choice(PREC_VOL_D) + +# if motor_speed_toggle: +# log_speed_min = math.log10(motor_speed_min) +# log_speed_max = math.log10(motor_speed_max) +# log_speed = log_speed_min + random.random() * (log_speed_max - log_speed_min) +# motor_speed = round(10 ** log_speed, 2) +# else: +# motor_speed = random.choice(motor_speeds) if motor_speeds else random.choice(MOTOR_SPEEDS_D) + +# # make all the parameters normalized between 0 and 1 using min-max normalization +# if motor_speed_toggle: +# log_speed_min = math.log10(motor_speed_min) +# log_speed_max = math.log10(motor_speed_max) +# motor_speed_norm = (math.log10(motor_speed) - log_speed_min) / (log_speed_max - log_speed_min) if log_speed_max - log_speed_min != 0 else 0 +# else: +# motor_speeds_list = motor_speeds if motor_speeds else MOTOR_SPEEDS_D +# log_min = math.log10(min(motor_speeds_list)) +# log_max = math.log10(max(motor_speeds_list)) +# motor_speed_norm = (math.log10(motor_speed) - log_min) / (log_max - log_min) if log_max - log_min != 0 else 0 + +# if is_temp_continuous: +# min_temp = temp_min.get(solvent, TEMP_CHOICES_C[solvent][0]) +# max_temp = temp_max.get(solvent, TEMP_CHOICES_C[solvent][1]) +# temperature_norm = (temperature - min_temp) / (max_temp - min_temp) +# else: +# temp_options = temp_discrete_values.get(solvent, TEMP_CHOICES_D[solvent]) +# temperature_norm = (temperature - min(temp_options)) / (max(temp_options) - min(temp_options)) + +# if concentration_toggle: +# concentration_norm = (concentration - concentration_min) / (concentration_max - concentration_min) if concentration_max - concentration_min != 0 else 0 +# else: +# concentration_list = concentration_range if concentration_range else CONCEN_D +# concentration_norm = (concentration - min(concentration_list)) / (max(concentration_list) - min(concentration_list)) if max(concentration_list) - min(concentration_list) != 0 else 0 + +# if printing_gap_toggle: +# printing_gap_norm = (printing_gap - printing_gap_min) / (printing_gap_max - printing_gap_min) if printing_gap_max - printing_gap_min != 0 else 0 +# else: +# printing_gaps_list = printing_gaps if printing_gaps else PRINT_GAP_D +# printing_gap_norm = (printing_gap - min(printing_gaps_list)) / (max(printing_gaps_list) - min(printing_gaps_list)) if max(printing_gaps_list) - min(printing_gaps_list) != 0 else 0 + +# if precursor_vol_toggle: +# precursor_volume_norm = (precursor_volume - precursor_vol_min) / (precursor_vol_max - precursor_vol_min) if precursor_vol_max - precursor_vol_min != 0 else 0 +# else: +# precursor_vol_list = precursor_vol if precursor_vol else PREC_VOL_D +# precursor_volume_norm = (precursor_volume - min(precursor_vol_list)) / (max(precursor_vol_list) - min(precursor_vol_list)) if max(precursor_vol_list) - min(precursor_vol_list) != 0 else 0 + +# parameter_set = { +# "sample_no": sample_count, +# "campaign_name": campaign_name, +# "polymer_name": polymer_name, +# "smiles_string": smiles_string, +# "mw": mw, +# "pdi": pdi, +# "motor_speed": motor_speed, +# "temperature": temperature, +# "concentration": concentration, +# "printing_gap": printing_gap, +# "precursor_volume": precursor_volume, +# "solvent": solvent, +# "motor_speed_norm": motor_speed_norm, +# "temperature_norm": temperature_norm, +# "concentration_norm": concentration_norm, +# "printing_gap_norm": printing_gap_norm, +# "precursor_volume_norm": precursor_volume_norm +# } + +# parameter_sets.append(parameter_set) +# sample_count += 1 + +# df = pd.DataFrame(parameter_sets) +# df = df.sort_values(by=["solvent", "temperature"], ascending=[True, True]) +# parameter_sets = df.to_dict(orient="records") + +# table = dash_table.DataTable( +# id="sampler-results", +# columns=[ +# {"name": "Sample No", "id": "sample_no"}, +# {"name": "Motor Speed", "id": "motor_speed"}, +# {"name": "Temperature", "id": "temperature"}, +# {"name": "Concentration", "id": "concentration"}, +# {"name": "Printing Gap", "id": "printing_gap"}, +# {"name": "Precursor Volume", "id": "precursor_volume"}, +# {"name": "Solvent", "id": "solvent"}, +# ], +# data=parameter_sets, +# style_table={"overflowX": "auto"}, +# ) + +# if len(df) >= 2: +# X = df[['motor_speed_norm', 'temperature_norm', 'concentration_norm', +# 'printing_gap_norm', 'precursor_volume_norm']].values + +# pca = PCA(n_components=2) +# pca_result = pca.fit_transform(X) + +# reducer = umap.UMAP(random_state=42, n_neighbors=min(5, len(df)-1)) +# umap_result = reducer.fit_transform(X) + +# n_background = 1000 +# background_points = np.random.rand(n_background, X.shape[1]) + +# background_pca = pca.transform(background_points) +# background_umap = reducer.transform(background_points) + +# pca_fig = go.Figure() + +# x_min, x_max = background_pca[:,0].min(), background_pca[:,0].max() +# y_min, y_max = background_pca[:,1].min(), background_pca[:,1].max() + +# xi = np.linspace(x_min, x_max, 100) +# yi = np.linspace(y_min, y_max, 100) +# xi, yi = np.meshgrid(xi, yi) + +# positions = np.vstack([xi.ravel(), yi.ravel()]) +# values = np.vstack([background_pca[:,0], background_pca[:,1]]) +# kernel = gaussian_kde(values) +# z = np.reshape(kernel(positions).T, xi.shape) + +# pca_fig.add_trace(go.Contour( +# z=z, +# x=xi[0], +# y=yi[:,0], +# colorscale='Blues', +# showscale=False, +# opacity=0.5, +# name='Parameter Space Density' +# )) + +# for solvent in df['solvent'].unique(): +# mask = df['solvent'] == solvent +# pca_fig.add_trace(go.Scatter( +# x=pca_result[mask, 0], +# y=pca_result[mask, 1], +# mode='markers', +# marker=dict(size=8), +# name=solvent +# )) + +# pca_fig.update_layout( +# title='PCA Visualization of Parameter Sets', +# xaxis_title='PCA Component 1', +# yaxis_title='PCA Component 2' +# ) + +# umap_fig = go.Figure() + +# x_min, x_max = background_umap[:,0].min(), background_umap[:,0].max() +# y_min, y_max = background_umap[:,1].min(), background_umap[:,1].max() + +# xi = np.linspace(x_min, x_max, 100) +# yi = np.linspace(y_min, y_max, 100) +# xi, yi = np.meshgrid(xi, yi) + +# positions = np.vstack([xi.ravel(), yi.ravel()]) +# values = np.vstack([background_umap[:,0], background_umap[:,1]]) +# kernel = gaussian_kde(values) +# z = np.reshape(kernel(positions).T, xi.shape) + +# umap_fig.add_trace(go.Contour( +# z=z, +# x=xi[0], +# y=yi[:,0], +# colorscale='Blues', +# showscale=False, +# opacity=0.5, +# name='Parameter Space Density' +# )) + +# for solvent in df['solvent'].unique(): +# mask = df['solvent'] == solvent +# umap_fig.add_trace(go.Scatter( +# x=umap_result[mask, 0], +# y=umap_result[mask, 1], +# mode='markers', +# marker=dict(size=8), +# name=solvent +# )) + +# umap_fig.update_layout( +# title='UMAP Visualization of Parameter Sets', +# xaxis_title='UMAP Component 1', +# yaxis_title='UMAP Component 2' +# ) + +# res = html.Div([ +# table +# ]) +# plots = html.Div([ +# html.Div([ +# dcc.Graph(figure=pca_fig) +# ], className="col-md-6"), +# html.Div([ +# dcc.Graph(figure=umap_fig) +# ], className="col-md-6"), +# ], className="row") +# global p1, p2 +# p1 = pca_fig +# p2 = umap_fig +# else: +# res = html.Div([ +# html.P("Not enough data points for visualization. Generate more samples."), +# html.H3("Generated Parameter Sets"), +# table +# ]) +# plots = None + + +# return res, f"Generated {len(parameter_sets)} parameter sets using simple random sampling.", True, "success", False, plots +# except Exception as e: +# print(f"Error generating parameter sets: {e}") +# return None, f"Failed to generate parameter sets: {str(e)}", True, "danger", True, None def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string, mw, pdi, solvents, toggle_values, toggle_ids, temp_dropdown_values, temp_dropdown_ids, temp_min_values, temp_min_ids, @@ -2616,109 +2894,142 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string precursor_vol_min, precursor_vol_max, motor_speeds, motor_speed_toggle, motor_speed_min, motor_speed_max, sampling_method, num_samples): + if not solvents: - return None, "Please select at least one solvent.", True, "danger", True - + return None, "Please select at least one solvent.", True, "danger", True, None + try: temp_toggles = {} temp_discrete_values = {} temp_min = {} temp_max = {} - + for toggle_value, toggle_id in zip(toggle_values, toggle_ids): param = toggle_id["param"] if param.startswith("temp-"): solvent = param.replace("temp-", "") temp_toggles[solvent] = toggle_value - + for dropdown_value, dropdown_id in zip(temp_dropdown_values, temp_dropdown_ids): - solvent = dropdown_id["index"] - temp_discrete_values[solvent] = dropdown_value - + temp_discrete_values[dropdown_id["index"]] = dropdown_value + for min_value, min_id in zip(temp_min_values, temp_min_ids): - solvent = min_id["index"] - temp_min[solvent] = min_value - + temp_min[min_id["index"]] = min_value + for max_value, max_id in zip(temp_max_values, temp_max_ids): - solvent = max_id["index"] - temp_max[solvent] = max_value + temp_max[max_id["index"]] = max_value + + # --------------------------- + # Sobol Setup + # --------------------------- + if sampling_method == "sobol": + dim = 5 + sobol_engine = qmc.Sobol(d=dim, scramble=True) + sobol_points = sobol_engine.random(n=num_samples * len(solvents)) + sobol_index = 0 parameter_sets = [] sample_count = 1 - + for solvent in solvents: is_temp_continuous = temp_toggles.get(solvent, False) - + for _ in range(num_samples): + + if sampling_method == "sobol": + u = sobol_points[sobol_index] + sobol_index += 1 + + # ---------------- MOTOR SPEED ---------------- + if motor_speed_toggle: + log_min = math.log10(motor_speed_min) + log_max = math.log10(motor_speed_max) + + if sampling_method == "sobol": + log_speed = log_min + u[0] * (log_max - log_min) + else: + log_speed = log_min + random.random() * (log_max - log_min) + + motor_speed = round(10 ** log_speed, 2) + else: + motor_speed = random.choice(motor_speeds) if motor_speeds else random.choice(MOTOR_SPEEDS_D) + + # ---------------- TEMPERATURE ---------------- if is_temp_continuous: min_temp = temp_min.get(solvent, TEMP_CHOICES_C[solvent][0]) max_temp = temp_max.get(solvent, TEMP_CHOICES_C[solvent][1]) - temperature = round(random.uniform(min_temp, max_temp)) + + if sampling_method == "sobol": + temperature = round(min_temp + u[1] * (max_temp - min_temp)) + else: + temperature = round(random.uniform(min_temp, max_temp)) else: temp_options = temp_discrete_values.get(solvent, TEMP_CHOICES_D[solvent]) - temperature = round(random.choice(temp_options)) + temperature = random.choice(temp_options) + # ---------------- CONCENTRATION ---------------- if concentration_toggle: - concentration = round(random.uniform(concentration_min, concentration_max), 2) + if sampling_method == "sobol": + concentration = round( + concentration_min + u[2] * (concentration_max - concentration_min), 2 + ) + else: + concentration = round(random.uniform(concentration_min, concentration_max), 2) else: concentration = random.choice(concentration_range) if concentration_range else random.choice(CONCEN_D) + # ---------------- PRINTING GAP ---------------- if printing_gap_toggle: - printing_gap = round(random.uniform(printing_gap_min, printing_gap_max)) + if sampling_method == "sobol": + printing_gap = round( + printing_gap_min + u[3] * (printing_gap_max - printing_gap_min) + ) + else: + printing_gap = round(random.uniform(printing_gap_min, printing_gap_max)) else: printing_gap = random.choice(printing_gaps) if printing_gaps else random.choice(PRINT_GAP_D) - + + # ---------------- PRECURSOR VOLUME ---------------- if precursor_vol_toggle: - precursor_volume = round(random.uniform(precursor_vol_min, precursor_vol_max), 1) + if sampling_method == "sobol": + precursor_volume = round( + precursor_vol_min + u[4] * (precursor_vol_max - precursor_vol_min), 1 + ) + else: + precursor_volume = round(random.uniform(precursor_vol_min, precursor_vol_max), 1) else: precursor_volume = random.choice(precursor_vol) if precursor_vol else random.choice(PREC_VOL_D) - if motor_speed_toggle: - log_speed_min = math.log10(motor_speed_min) - log_speed_max = math.log10(motor_speed_max) - log_speed = log_speed_min + random.random() * (log_speed_max - log_speed_min) - motor_speed = round(10 ** log_speed, 2) - else: - motor_speed = random.choice(motor_speeds) if motor_speeds else random.choice(MOTOR_SPEEDS_D) - - # make all the parameters normalized between 0 and 1 using min-max normalization - if motor_speed_toggle: - log_speed_min = math.log10(motor_speed_min) - log_speed_max = math.log10(motor_speed_max) - motor_speed_norm = (math.log10(motor_speed) - log_speed_min) / (log_speed_max - log_speed_min) if log_speed_max - log_speed_min != 0 else 0 - else: - motor_speeds_list = motor_speeds if motor_speeds else MOTOR_SPEEDS_D - log_min = math.log10(min(motor_speeds_list)) - log_max = math.log10(max(motor_speeds_list)) - motor_speed_norm = (math.log10(motor_speed) - log_min) / (log_max - log_min) if log_max - log_min != 0 else 0 + # ---------------- NORMALIZATION ---------------- + log_min = math.log10(motor_speed_min) if motor_speed_toggle else math.log10(min(motor_speeds if motor_speeds else MOTOR_SPEEDS_D)) + log_max = math.log10(motor_speed_max) if motor_speed_toggle else math.log10(max(motor_speeds if motor_speeds else MOTOR_SPEEDS_D)) + motor_speed_norm = (math.log10(motor_speed) - log_min) / (log_max - log_min) if log_max - log_min != 0 else 0 if is_temp_continuous: - min_temp = temp_min.get(solvent, TEMP_CHOICES_C[solvent][0]) - max_temp = temp_max.get(solvent, TEMP_CHOICES_C[solvent][1]) - temperature_norm = (temperature - min_temp) / (max_temp - min_temp) + temperature_norm = (temperature - min_temp) / (max_temp - min_temp) if max_temp - min_temp != 0 else 0 else: - temp_options = temp_discrete_values.get(solvent, TEMP_CHOICES_D[solvent]) - temperature_norm = (temperature - min(temp_options)) / (max(temp_options) - min(temp_options)) + temp_list = temp_discrete_values.get(solvent, TEMP_CHOICES_D[solvent]) + temperature_norm = (temperature - min(temp_list)) / (max(temp_list) - min(temp_list)) if max(temp_list) - min(temp_list) != 0 else 0 if concentration_toggle: concentration_norm = (concentration - concentration_min) / (concentration_max - concentration_min) if concentration_max - concentration_min != 0 else 0 else: - concentration_list = concentration_range if concentration_range else CONCEN_D - concentration_norm = (concentration - min(concentration_list)) / (max(concentration_list) - min(concentration_list)) if max(concentration_list) - min(concentration_list) != 0 else 0 + c_list = concentration_range if concentration_range else CONCEN_D + concentration_norm = (concentration - min(c_list)) / (max(c_list) - min(c_list)) if max(c_list) - min(c_list) != 0 else 0 if printing_gap_toggle: printing_gap_norm = (printing_gap - printing_gap_min) / (printing_gap_max - printing_gap_min) if printing_gap_max - printing_gap_min != 0 else 0 else: - printing_gaps_list = printing_gaps if printing_gaps else PRINT_GAP_D - printing_gap_norm = (printing_gap - min(printing_gaps_list)) / (max(printing_gaps_list) - min(printing_gaps_list)) if max(printing_gaps_list) - min(printing_gaps_list) != 0 else 0 + pg_list = printing_gaps if printing_gaps else PRINT_GAP_D + printing_gap_norm = (printing_gap - min(pg_list)) / (max(pg_list) - min(pg_list)) if max(pg_list) - min(pg_list) != 0 else 0 if precursor_vol_toggle: precursor_volume_norm = (precursor_volume - precursor_vol_min) / (precursor_vol_max - precursor_vol_min) if precursor_vol_max - precursor_vol_min != 0 else 0 else: - precursor_vol_list = precursor_vol if precursor_vol else PREC_VOL_D - precursor_volume_norm = (precursor_volume - min(precursor_vol_list)) / (max(precursor_vol_list) - min(precursor_vol_list)) if max(precursor_vol_list) - min(precursor_vol_list) != 0 else 0 + pv_list = precursor_vol if precursor_vol else PREC_VOL_D + precursor_volume_norm = (precursor_volume - min(pv_list)) / (max(pv_list) - min(pv_list)) if max(pv_list) - min(pv_list) != 0 else 0 - parameter_set = { + parameter_sets.append({ "sample_no": sample_count, "campaign_name": campaign_name, "polymer_name": polymer_name, @@ -2736,150 +3047,27 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string "concentration_norm": concentration_norm, "printing_gap_norm": printing_gap_norm, "precursor_volume_norm": precursor_volume_norm - } - - parameter_sets.append(parameter_set) + }) + sample_count += 1 - + df = pd.DataFrame(parameter_sets) - df = df.sort_values(by=["solvent", "temperature"], ascending=[True, True]) - parameter_sets = df.to_dict(orient="records") + df = df.sort_values(by=["solvent", "temperature"]) table = dash_table.DataTable( id="sampler-results", - columns=[ - {"name": "Sample No", "id": "sample_no"}, - {"name": "Motor Speed", "id": "motor_speed"}, - {"name": "Temperature", "id": "temperature"}, - {"name": "Concentration", "id": "concentration"}, - {"name": "Printing Gap", "id": "printing_gap"}, - {"name": "Precursor Volume", "id": "precursor_volume"}, - {"name": "Solvent", "id": "solvent"}, - ], - data=parameter_sets, + columns=[{"name": col.replace("_", " ").title(), "id": col} + for col in ["sample_no", "motor_speed", "temperature", + "concentration", "printing_gap", + "precursor_volume", "solvent"]], + data=df.to_dict("records"), style_table={"overflowX": "auto"}, ) - if len(df) >= 2: - X = df[['motor_speed_norm', 'temperature_norm', 'concentration_norm', - 'printing_gap_norm', 'precursor_volume_norm']].values - - pca = PCA(n_components=2) - pca_result = pca.fit_transform(X) - - reducer = umap.UMAP(random_state=42, n_neighbors=min(5, len(df)-1)) - umap_result = reducer.fit_transform(X) - - n_background = 1000 - background_points = np.random.rand(n_background, X.shape[1]) - - background_pca = pca.transform(background_points) - background_umap = reducer.transform(background_points) - - pca_fig = go.Figure() - - x_min, x_max = background_pca[:,0].min(), background_pca[:,0].max() - y_min, y_max = background_pca[:,1].min(), background_pca[:,1].max() - - xi = np.linspace(x_min, x_max, 100) - yi = np.linspace(y_min, y_max, 100) - xi, yi = np.meshgrid(xi, yi) - - positions = np.vstack([xi.ravel(), yi.ravel()]) - values = np.vstack([background_pca[:,0], background_pca[:,1]]) - kernel = gaussian_kde(values) - z = np.reshape(kernel(positions).T, xi.shape) - - pca_fig.add_trace(go.Contour( - z=z, - x=xi[0], - y=yi[:,0], - colorscale='Blues', - showscale=False, - opacity=0.5, - name='Parameter Space Density' - )) - - for solvent in df['solvent'].unique(): - mask = df['solvent'] == solvent - pca_fig.add_trace(go.Scatter( - x=pca_result[mask, 0], - y=pca_result[mask, 1], - mode='markers', - marker=dict(size=8), - name=solvent - )) - - pca_fig.update_layout( - title='PCA Visualization of Parameter Sets', - xaxis_title='PCA Component 1', - yaxis_title='PCA Component 2' - ) - - umap_fig = go.Figure() - - x_min, x_max = background_umap[:,0].min(), background_umap[:,0].max() - y_min, y_max = background_umap[:,1].min(), background_umap[:,1].max() - - xi = np.linspace(x_min, x_max, 100) - yi = np.linspace(y_min, y_max, 100) - xi, yi = np.meshgrid(xi, yi) - - positions = np.vstack([xi.ravel(), yi.ravel()]) - values = np.vstack([background_umap[:,0], background_umap[:,1]]) - kernel = gaussian_kde(values) - z = np.reshape(kernel(positions).T, xi.shape) - - umap_fig.add_trace(go.Contour( - z=z, - x=xi[0], - y=yi[:,0], - colorscale='Blues', - showscale=False, - opacity=0.5, - name='Parameter Space Density' - )) - - for solvent in df['solvent'].unique(): - mask = df['solvent'] == solvent - umap_fig.add_trace(go.Scatter( - x=umap_result[mask, 0], - y=umap_result[mask, 1], - mode='markers', - marker=dict(size=8), - name=solvent - )) - - umap_fig.update_layout( - title='UMAP Visualization of Parameter Sets', - xaxis_title='UMAP Component 1', - yaxis_title='UMAP Component 2' - ) - - res = html.Div([ - table - ]) - plots = html.Div([ - html.Div([ - dcc.Graph(figure=pca_fig) - ], className="col-md-6"), - html.Div([ - dcc.Graph(figure=umap_fig) - ], className="col-md-6"), - ], className="row") - global p1, p2 - p1 = pca_fig - p2 = umap_fig - else: - res = html.Div([ - html.P("Not enough data points for visualization. Generate more samples."), - html.H3("Generated Parameter Sets"), - table - ]) - plots = None + return html.Div([table]), \ + f"Generated {len(df)} parameter sets using {sampling_method} sampling.", \ + True, "success", False, None - - return res, f"Generated {len(parameter_sets)} parameter sets using simple random sampling.", True, "success", False, plots except Exception as e: print(f"Error generating parameter sets: {e}") return None, f"Failed to generate parameter sets: {str(e)}", True, "danger", True, None diff --git a/aamp_app/optimizer/QUICKSTART.md b/aamp_app/optimizer/QUICKSTART.md deleted file mode 100644 index 0adc1d2..0000000 --- a/aamp_app/optimizer/QUICKSTART.md +++ /dev/null @@ -1,169 +0,0 @@ -# Quick Start Guide - -Get up and running with the Bayesian Optimization package in minutes! - -## Installation - -1. **Install dependencies:** -```bash -pip install -r requirements.txt -``` - -2. **Verify installation:** -```bash -python -c "import torch, botorch, gpytorch; print('All dependencies installed successfully')" -``` - -## Run the Demo - -### Option 1: Simple Demo -```bash -python run_demo.py -``` - -### Option 2: From Python -```python -from optimizer import run_optimization_demo - -# Run with default settings -best_params, best_score = run_optimization_demo() -print(f"Best parameters: {best_params}") -print(f"Best score: {best_score:.4f}") -``` - -## Basic Usage - -### Simple Optimization -```python -import torch -from optimizer import BayesianOptimizer, MockObjectiveFunction - -# 1. Define parameter bounds -bounds = torch.tensor([ - [0.1, 1.0], # concentration - [10.0, 100.0], # print_speed - [0.05, 0.5], # gap_size - [5.0, 25.0] # volume -]).T - -# 2. Create optimizer and objective -optimizer = BayesianOptimizer(bounds=bounds, batch_size=8) -objective = MockObjectiveFunction(noise_std=0.1) - -# 3. Run optimization -best_params, best_score = optimizer.optimize( - objective_function=objective, - n_iterations=20, - n_initial_points=10 -) - -print(f"Best found: {best_params} with score {best_score:.4f}") -``` - -### Custom Objective Function -```python -def my_objective(X): - """Custom objective function""" - # Your optimization logic here - # X is a tensor of shape (batch_size, 4) - # Return tensor of shape (batch_size, 1) - - # Example: simple quadratic function - result = -(X - 0.5).pow(2).sum(dim=1, keepdim=True) - return result - -# Use with optimizer -optimizer = BayesianOptimizer(bounds=bounds, batch_size=8) -best_params, best_score = optimizer.optimize( - objective_function=my_objective, - n_iterations=15 -) -``` - -## Key Parameters - -| Parameter | Description | Default | Tips | -|-----------|-------------|---------|------| -| `bounds` | Parameter bounds tensor (2, n_dims) | Required | Define your search space | -| `batch_size` | Candidates per iteration | 8 | Larger = more parallel evaluation | -| `n_iterations` | Optimization iterations | 20 | More iterations = better results | -| `n_initial_points` | Initial random samples | 10 | Should be ≥ 2 * dimensions | -| `noise_std` | Objective function noise | 0.1 | Match your actual noise level | - -## Common Patterns - -### Pattern 1: Quick Optimization -```python -# For quick tests and experimentation -best_params, best_score = optimizer.optimize( - objective_function=objective, - n_iterations=10, - n_initial_points=5, - verbose=True -) -``` - -### Pattern 2: Production Optimization -```python -# For production use with more iterations -best_params, best_score = optimizer.optimize( - objective_function=objective, - n_iterations=50, - n_initial_points=20, - verbose=True -) -``` - -### Pattern 3: Manual Control -```python -# For custom control over the optimization loop -optimizer.generate_initial_data(10, objective) - -for i in range(20): - optimizer.fit_model() - candidates = optimizer.optimize_acquisition() - values = objective(candidates) - optimizer.update_training_data(candidates, values) - print(f"Iteration {i+1}: Best = {optimizer.best_observed_value:.4f}") -``` - -## Expected Output - -The demo will show: -- Parameter space information -- Optimization progress for each iteration -- Best parameters found -- Comparison with true optimum -- Convergence analysis -- Visualization plots (if matplotlib available) - -## Troubleshooting - -### Import Errors -```bash -# Install missing packages -pip install torch botorch gpytorch numpy matplotlib -``` - -### Slow Performance -- Reduce `batch_size` (e.g., 4 instead of 8) -- Reduce `n_iterations` for testing -- Use smaller `n_initial_points` - -### Memory Issues -- Reduce `batch_size` -- Use CPU instead of GPU: `torch.set_default_device('cpu')` - -## Next Steps - -1. **Read the full README.md** for detailed documentation -2. **Check example_usage.py** for advanced usage patterns -3. **Modify the objective function** for your specific problem -4. **Adjust bounds and parameters** for your optimization problem - -## Need Help? - -- Check the full documentation in `README.md` -- Look at examples in `example_usage.py` -- Review the source code for detailed comments -- The package is designed to be self-contained and well-documented \ No newline at end of file diff --git a/aamp_app/optimizer/README.md b/aamp_app/optimizer/README.md deleted file mode 100644 index dbc16b5..0000000 --- a/aamp_app/optimizer/README.md +++ /dev/null @@ -1,255 +0,0 @@ -# Bayesian Optimization Package - -A complete, self-contained Python package for batch Bayesian Optimization using BoTorch and GPyTorch libraries. This package demonstrates how to optimize a noisy, black-box objective function with 4 continuous parameters using batch optimization. - -## Features - -- **Batch Optimization**: Suggests 8 new candidates per iteration for parallel evaluation -- **Noisy Objective Handling**: Uses qLogNoisyExpectedImprovement acquisition function (numerically stable) -- **Gaussian Process Surrogate**: SingleTaskGP model for function approximation -- **Real-world Simulation**: Mock objective function with realistic noise and single maximum -- **Well-documented**: Comprehensive comments and easy-to-understand code - -## Problem Definition - -### Objective -Maximize a noisy, black-box objective function representing a printing process optimization. - -### Parameters -- `concentration`: [0.1, 1.0] - Material concentration -- `print_speed`: [10.0, 100.0] - Printing speed (mm/s) -- `gap_size`: [0.05, 0.5] - Gap size between layers (mm) -- `volume`: [5.0, 25.0] - Volume per drop (μL) - -### Configuration -- **Batch Size**: 8 candidates per iteration -- **Iterations**: 20 optimization rounds -- **Initial Points**: 10 random samples - -## Installation - -1. Install the required dependencies: -```bash -pip install -r requirements.txt -``` - -2. Import and use the package: -```python -from optimizer import run_optimization_demo - -# Run the complete optimization workflow -best_params, best_score = run_optimization_demo() -``` - -## Usage Example - -```python -import torch -from optimizer import BayesianOptimizer, MockObjectiveFunction - -# Define search space bounds -bounds = torch.tensor([ - [0.1, 1.0], # concentration - [10.0, 100.0], # print_speed - [0.05, 0.5], # gap_size - [5.0, 25.0] # volume -]).T - -# Create optimizer and objective function -optimizer = BayesianOptimizer(bounds=bounds, batch_size=8) -objective = MockObjectiveFunction() - -# Run optimization -best_params, best_score = optimizer.optimize( - objective_function=objective, - n_iterations=20, - n_initial_points=10 -) - -print(f"Best parameters: {best_params}") -print(f"Best score: {best_score:.4f}") -``` - -## 📓 Jupyter Notebooks - -The package includes three interactive Jupyter notebooks: - -### 1. **Tutorial Notebook** (`tutorial_notebook.ipynb`) -- **Perfect for beginners** - step-by-step explanation of Bayesian optimization -- Covers theory, implementation, and best practices -- Interactive examples and visualizations - -### 2. **Demo Notebook** (`demo_notebook.ipynb`) -- **Complete demonstration** of the optimization workflow -- Comprehensive analysis and visualization -- Real-world manufacturing example - -### 3. **Interactive Notebook** (`interactive_notebook.ipynb`) -- **Experimentation platform** with easy parameter adjustment -- Compare different optimization settings -- Real-time visualization of results - -To use the notebooks: -```bash -# Install Jupyter if not already installed -pip install jupyter - -# Launch Jupyter -jupyter notebook - -# Open any of the .ipynb files -``` - -## Architecture - -### Core Components - -1. **BayesianOptimizer**: Main optimization class handling the GP model and acquisition function -2. **MockObjectiveFunction**: Simulates a realistic black-box objective with noise -3. **Demo Script**: Complete workflow demonstration - -### Workflow - -1. **Initialization**: Generate initial training dataset (10 random points) -2. **Optimization Loop** (20 iterations): - - Fit SingleTaskGP model to current data - - Define qLogNoisyExpectedImprovement acquisition function - - Optimize acquisition function to find 8 new candidates - - Evaluate candidates and update training data -3. **Results**: Report best parameters and score - -## Key Libraries Used - -- **BoTorch**: State-of-the-art Bayesian optimization library -- **GPyTorch**: Gaussian process library for surrogate modeling -- **PyTorch**: Tensor operations and automatic differentiation - -## Files Structure - -``` -optimizer/ -├── __init__.py # Package initialization -├── bayesian_optimizer.py # Core optimization class -├── objective_function.py # Mock objective function -├── demo.py # Complete demo script -├── run_demo.py # Simple executable script -├── example_usage.py # Advanced usage examples -├── setup.py # Package setup -├── requirements.txt # Dependencies -├── README.md # This file -├── QUICKSTART.md # Quick start guide -├── demo_notebook.ipynb # Complete demonstration notebook -├── interactive_notebook.ipynb # Interactive experimentation -└── tutorial_notebook.ipynb # Step-by-step tutorial -``` - -## Performance Notes - -- The mock objective function simulates realistic noise levels -- qLogNEI acquisition function is optimized for noisy objectives with improved numerical stability -- Batch optimization allows for parallel evaluation of candidates -- GPU acceleration available through PyTorch/BoTorch - -## License - -This package is part of the Polyprint project and serves as an educational example for Bayesian optimization in manufacturing processes. - -## Model Integration with Scikit-Learn - -The optimizer now supports direct integration with trained scikit-learn models through the `model_objective_factory` module. This allows you to use your trained models as objective functions for Bayesian optimization. - -### Using Your Trained Model - -#### 1. Create Objective Function from Model - -```python -from model_objective_factory import create_objective_from_model -import joblib - -# Load your trained model and scaler -model = joblib.load('your_trained_model.pkl') -scaler = joblib.load('your_trained_scaler.pkl') - -# Create BoTorch-compatible objective function -objective_fn = create_objective_from_model(model, scaler) -``` - -#### 2. Run Optimization - -```python -from bayesian_optimizer import BayesianOptimizer -import torch - -# Define parameter bounds -bounds = torch.tensor([ - [0.1, 10.0, 0.05, 5.0], # Lower bounds: [concentration, print_speed, gap_size, volume] - [1.0, 100.0, 0.5, 25.0] # Upper bounds: [concentration, print_speed, gap_size, volume] -], dtype=torch.float64) - -# Initialize optimizer -optimizer = BayesianOptimizer(bounds=bounds, batch_size=8, seed=42) - -# Run optimization -best_params, best_score = optimizer.optimize( - objective_function=objective_fn, - n_iterations=20, - n_initial_points=10, - verbose=True -) - -print(f"Best parameters: {best_params}") -print(f"Best success probability: {best_score:.4f}") -``` - -### How It Works - -The `create_objective_from_model` function creates a wrapper that: - -1. **Accepts BoTorch tensors**: Input shape `(batch_size, 4)` with parameters `[concentration, print_speed, gap_size, volume]` - -2. **Performs feature engineering**: Automatically creates the 11 features your model expects: - - `gap_size_squared` - - `solvent_CF` (fixed at 0.5) - - `print_speed_squared` - - `concentration_squared` - - `concentration` - - `gap_size` - - `print_speed_gap_size` - - `print_speed` - - `concentration_print_speed` - - `print_speed_volume` - - `volume` - -3. **Applies scaling**: Uses your trained `StandardScaler` to normalize features - -4. **Returns probabilities**: Uses `model.predict_proba()` to get success probabilities - -5. **Outputs BoTorch tensors**: Returns shape `(batch_size, 1)` tensor of success probabilities - -### Running the Demo - -```bash -# Test the model factory -python model_objective_factory.py - -# Test full integration with Bayesian optimization -python model_integration_example.py -``` - -### Key Benefits - -- **Seamless Integration**: Direct compatibility between scikit-learn models and BoTorch -- **Automatic Feature Engineering**: No need to manually recreate feature transformations -- **Batch Evaluation**: Efficient evaluation of multiple parameter combinations -- **Clean Architecture**: Factory pattern separates model logic from optimization logic -- **Robust**: Handles tensor conversions, scaling, and error checking automatically - -### Requirements - -Your scikit-learn model must: -- Be a trained `LogisticRegression` or similar classifier -- Have a `predict_proba()` method -- Expect exactly 11 features in the specified order -- Be paired with a fitted `StandardScaler` - -The factory pattern makes it easy to extend support for other model types in the future. \ No newline at end of file diff --git a/aamp_app/optimizer/__init__.py b/aamp_app/optimizer/__init__.py deleted file mode 100644 index a318b02..0000000 --- a/aamp_app/optimizer/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Bayesian Optimization Package - -A complete, self-contained Python package for batch Bayesian Optimization -using BoTorch and GPyTorch libraries. -""" - -__version__ = "1.0.0" -__author__ = "Polyprint Project" - -from .bayesian_optimizer import BayesianOptimizer -from .objective_function import MockObjectiveFunction -from .demo.demo import run_optimization_demo - -__all__ = ["BayesianOptimizer", "MockObjectiveFunction", "run_optimization_demo"] \ No newline at end of file diff --git a/aamp_app/optimizer/bayesian_optimizer.py b/aamp_app/optimizer/bayesian_optimizer.py deleted file mode 100644 index 5ea37cb..0000000 --- a/aamp_app/optimizer/bayesian_optimizer.py +++ /dev/null @@ -1,289 +0,0 @@ -import torch -import numpy as np -from typing import Callable, Tuple, Optional, List - -# BoTorch imports for Bayesian optimization -from botorch.models import SingleTaskGP -from botorch.fit import fit_gpytorch_mll -from botorch.acquisition import qLogNoisyExpectedImprovement -from botorch.optim import optimize_acqf -from botorch.utils.transforms import normalize, unnormalize - -# GPyTorch imports for Gaussian process components -from gpytorch.mlls import ExactMarginalLogLikelihood -from gpytorch.kernels import ScaleKernel, RBFKernel -from gpytorch.priors import GammaPrior - -def plot_optimization_results(optimizer, objective): - """Create selective plots of the optimization results (1, 3, and 4 only).""" - - import matplotlib.pyplot as plt - import numpy as np - import base64 - import io - - history = optimizer.get_optimization_history() - train_X, train_Y = optimizer.get_training_data() - true_params, _ = objective.get_optimal_parameters() - true_score = objective.evaluate_at_optimal() - - # Create a 1x3 subplot layout for the three selected plots - fig, axes = plt.subplots(1, 3, figsize=(21, 6)) # Wider layout - - ### 1. Optimization Progress (axes[0]) - iterations = [h['iteration'] for h in history] - best_values = [h['best_value'] for h in history] - - axes[0].plot(iterations, best_values, 'b-o', linewidth=2, markersize=6) - axes[0].axhline(y=true_score, color='r', linestyle='--', alpha=0.7, - label=f'True optimum: {true_score:.3f}') - axes[0].set_xlabel('Iteration') - axes[0].set_ylabel('Best Observed Value') - axes[0].set_title('Optimization Progress') - axes[0].legend() - axes[0].grid(True, alpha=0.3) - - ### 3. Parameter Space Exploration (axes[1]) - scatter = axes[1].scatter(train_X[:, 0], train_X[:, 1], c=train_Y.squeeze(), - cmap='viridis', alpha=0.6, s=50) - axes[1].scatter(optimizer.best_parameters[0], optimizer.best_parameters[1], - c='red', s=200, marker='*', label='Best found', - edgecolor='black', linewidth=2) - axes[1].scatter(true_params[0], true_params[1], c='orange', s=200, marker='*', - label='True optimum', edgecolor='black', linewidth=2) - axes[1].set_xlabel('Concentration') - axes[1].set_ylabel('Print Speed (mm/s)') - axes[1].set_title('Parameter Space Exploration') - axes[1].legend() - plt.colorbar(scatter, ax=axes[1], label='Objective Value') - - ### 4. Gap Size vs Volume (axes[2]) - scatter2 = axes[2].scatter(train_X[:, 2], train_X[:, 3], c=train_Y.squeeze(), - cmap='viridis', alpha=0.6, s=50) - axes[2].scatter(optimizer.best_parameters[2], optimizer.best_parameters[3], - c='red', s=200, marker='*', label='Best found', - edgecolor='black', linewidth=2) - axes[2].scatter(true_params[2], true_params[3], c='orange', s=200, marker='*', - label='True optimum', edgecolor='black', linewidth=2) - axes[2].set_xlabel('Gap Size (mm)') - axes[2].set_ylabel('Volume (μL)') - axes[2].set_title('🔍 Gap Size vs Volume') - axes[2].legend() - plt.colorbar(scatter2, ax=axes[2], label='Objective Value') - - plt.tight_layout() - plt.show() - - buf = io.BytesIO() - fig.savefig(buf, format="png", bbox_inches='tight') - buf.seek(0) - encoded_image = base64.b64encode(buf.read()).decode("utf-8") - buf.close() - plt.close(fig) - - return encoded_image, fig - -class BayesianOptimizer: - - def __init__( - self, - bounds: torch.Tensor, - batch_size: int = 8, - noise_variance: float = 0.01, - seed: int = 42 - ): - self.bounds = bounds.double() - self.batch_size = batch_size - self.noise_variance = noise_variance - self.seed = seed - - torch.manual_seed(seed) - np.random.seed(seed) - - assert bounds.shape[0] == 2, "Bounds must have shape (2, n_dims)" - assert (bounds[1] > bounds[0]).all(), "Upper bounds must be greater than lower bounds" - - self.n_dims = bounds.shape[1] - - self.train_X = None - self.train_Y = None - self.model = None - - self.iteration_history = [] - self.best_observed_value = -float('inf') - self.best_parameters = None - - def generate_initial_data(self, n_points: int, objective_function: Callable) -> None: - print(f"Generating {n_points} initial training points...") - - initial_X = self._generate_random_points(n_points) - initial_Y = objective_function(initial_X) - - self.train_X = initial_X - self.train_Y = initial_Y - - # Print all initial parameter sets and their scores - # print("Initial parameter sets and scores:") # <-- ADDED PRINT - # for i in range(n_points): - # print(f" [{i+1}] Params: {initial_X[i].tolist()}, Score: {initial_Y[i].item():.4f}") # <-- ADDED PRINT - - best_idx = torch.argmax(initial_Y) - self.best_observed_value = initial_Y[best_idx].item() - self.best_parameters = initial_X[best_idx] - - print(f"Initial best score: {self.best_observed_value:.4f}") - print(f"Initial best parameters: {self.best_parameters}") - - def _generate_random_points(self, n_points: int) -> torch.Tensor: - unit_points = torch.rand(n_points, self.n_dims, dtype=torch.float64) - lower_bounds = self.bounds[0] - upper_bounds = self.bounds[1] - scaled_points = lower_bounds + (upper_bounds - lower_bounds) * unit_points - return scaled_points - - def fit_model(self) -> None: - if self.train_X is None or self.train_Y is None: - raise ValueError("No training data available. Call generate_initial_data first.") - - train_X_double = self.train_X.double() - train_Y_double = self.train_Y.double() - - train_X_normalized = normalize(train_X_double, self.bounds.double()) - train_Y_normalized = (train_Y_double - train_Y_double.mean()) / train_Y_double.std() - - self.model = SingleTaskGP( - train_X_normalized, - train_Y_normalized, - covar_module=ScaleKernel( - RBFKernel( - lengthscale_prior=GammaPrior(2.0, 0.5), - ard_num_dims=self.n_dims - ), - outputscale_prior=GammaPrior(2.0, 0.5) - ) - ) - - self.model.train() - mll = ExactMarginalLogLikelihood(self.model.likelihood, self.model) - fit_gpytorch_mll(mll) - self.model.eval() - - def optimize_acquisition(self) -> torch.Tensor: - if self.model is None: - raise ValueError("Model not fitted. Call fit_model first.") - - train_X_double = self.train_X.double() - train_X_normalized = normalize(train_X_double, self.bounds.double()) - - acquisition_function = qLogNoisyExpectedImprovement( - model=self.model, - X_baseline=train_X_normalized, - prune_baseline=True, - cache_root=True - ) - - unit_bounds = torch.stack([torch.zeros(self.n_dims), torch.ones(self.n_dims)]).double() - candidates, _ = optimize_acqf( - acq_function=acquisition_function, - bounds=unit_bounds, - q=self.batch_size, - num_restarts=20, - raw_samples=200, - options={"batch_limit": 5, "maxiter": 200} - ) - - candidates_unnormalized = unnormalize(candidates, self.bounds.double()) - return candidates_unnormalized.float() - - def update_training_data(self, new_X: torch.Tensor, new_Y: torch.Tensor) -> None: - self.train_X = torch.cat([self.train_X, new_X], dim=0) - self.train_Y = torch.cat([self.train_Y, new_Y], dim=0) - - current_best_idx = torch.argmax(new_Y) - current_best_value = new_Y[current_best_idx].item() - - if current_best_value > self.best_observed_value: - self.best_observed_value = current_best_value - self.best_parameters = new_X[current_best_idx] - - def optimize( - self, - objective_function: Callable, - n_iterations: int = 20, - n_initial_points: int = 10, - verbose: bool = True, - target: float = 0.95 - ) -> Tuple[torch.Tensor, float]: - - img = [] - l = [] - - if verbose: - print("=" * 60) - print("BAYESIAN OPTIMIZATION STARTING") - print("=" * 60) - print(f"Parameters: {self.n_dims} dimensions") - print(f"Batch size: {self.batch_size}") - print(f"Iterations: {n_iterations}") - print(f"Initial points: {n_initial_points}") - print("=" * 60) - - self.generate_initial_data(n_initial_points, objective_function) - - for iteration in range(n_iterations): - if verbose: - print(f"\nIteration {iteration + 1}/{n_iterations}") - print("-" * 40) - print("Fitting GP model...") - - self.fit_model() - - if verbose: - print("Optimizing acquisition function...") - candidates = self.optimize_acquisition() - - if verbose: - print(f"Evaluating {self.batch_size} candidates...") - candidate_values = objective_function(candidates) - - # Print all evaluated candidates and their scores - print("New candidate parameter sets and scores:") # <-- ADDED PRINT - for i in range(self.batch_size): - print(f" [{i+1}] Params: {candidates[i].tolist()}, Score: {candidate_values[i].item():.4f}") # <-- ADDED PRINT - - self.update_training_data(candidates, candidate_values) - - iteration_info = { - 'iteration': iteration + 1, - 'best_value': self.best_observed_value, - 'best_params': self.best_parameters.clone() - } - self.iteration_history.append(iteration_info) - - if verbose: - print(f"Current best score: {self.best_observed_value:.4f}") - print(f"Current best parameters: {self.best_parameters}") - encoded_img, fig = plot_optimization_results(self, objective_function) - img.append(encoded_img) - l.append(fig) - - if self.best_observed_value >= target: - if verbose: - print(f"Target objective {target} reached. Stopping optimization.") - break - - if verbose: - print("\n" + "=" * 60) - print("OPTIMIZATION COMPLETED") - print("=" * 60) - print(f"Final best score: {self.best_observed_value:.4f}") - print(f"Final best parameters: {self.best_parameters}") - print(f"Total evaluations: {len(self.train_X)}") - - return self.best_parameters, self.best_observed_value, img, l - - def get_optimization_history(self) -> List[dict]: - return self.iteration_history - - def get_training_data(self) -> Tuple[torch.Tensor, torch.Tensor]: - return self.train_X, self.train_Y diff --git a/aamp_app/optimizer/demo/demo.py b/aamp_app/optimizer/demo/demo.py deleted file mode 100644 index 79a3526..0000000 --- a/aamp_app/optimizer/demo/demo.py +++ /dev/null @@ -1,312 +0,0 @@ -""" -Bayesian Optimization Demo Script - -This script demonstrates a complete Bayesian optimization workflow for -optimizing a printing process with 4 continuous parameters. - -The script includes: -- Parameter space definition -- Mock objective function evaluation -- Bayesian optimization with batch candidates -- Results visualization and analysis -""" - -import torch -import numpy as np -import matplotlib.pyplot as plt -from typing import Tuple, List -import time - -# Import our custom modules -from ..bayesian_optimizer import BayesianOptimizer -from ..objective_function import MockObjectiveFunction - - -def print_header(title: str, width: int = 80) -> None: - """Print a formatted header for console output.""" - print("\n" + "=" * width) - print(f"{title:^{width}}") - print("=" * width) - - -def print_parameter_info() -> None: - """Print information about the optimization parameters.""" - print("\nPARAMETER SPACE:") - print("-" * 50) - print("Parameter | Range | Description") - print("-" * 50) - print("concentration | [0.1, 1.0] | Material concentration") - print("print_speed | [10.0, 100.0] | Printing speed (mm/s)") - print("gap_size | [0.05, 0.5] | Gap size between layers (mm)") - print("volume | [5.0, 25.0] | Volume per drop (μL)") - print("-" * 50) - - -def format_parameters(params: torch.Tensor) -> str: - """Format parameters for display.""" - if params.dim() == 1: - concentration, print_speed, gap_size, volume = params - return (f"concentration={concentration:.3f}, " - f"print_speed={print_speed:.1f}, " - f"gap_size={gap_size:.3f}, " - f"volume={volume:.1f}") - else: - return f"Tensor of shape {params.shape}" - - -def visualize_optimization_progress( - iteration_history: List[dict], - true_optimum: float, - save_plot: bool = False -) -> None: - """ - Visualize the optimization progress over iterations. - - Args: - iteration_history: List of iteration information - true_optimum: True optimal value for comparison - save_plot: Whether to save the plot to file - """ - try: - iterations = [info['iteration'] for info in iteration_history] - best_values = [info['best_value'] for info in iteration_history] - - plt.figure(figsize=(12, 8)) - - # Plot best observed value over iterations - plt.subplot(2, 1, 1) - plt.plot(iterations, best_values, 'b-o', linewidth=2, markersize=6) - plt.axhline(y=true_optimum, color='r', linestyle='--', - label=f'True optimum: {true_optimum:.3f}') - plt.xlabel('Iteration') - plt.ylabel('Best Observed Value') - plt.title('Optimization Progress: Best Value vs Iteration') - plt.legend() - plt.grid(True, alpha=0.3) - - # Plot improvement over iterations - plt.subplot(2, 1, 2) - improvements = [best_values[i] - best_values[0] for i in range(len(best_values))] - plt.plot(iterations, improvements, 'g-o', linewidth=2, markersize=6) - plt.xlabel('Iteration') - plt.ylabel('Improvement from Initial Best') - plt.title('Cumulative Improvement Over Iterations') - plt.grid(True, alpha=0.3) - - plt.tight_layout() - - if save_plot: - plt.savefig('optimization_progress.png', dpi=300, bbox_inches='tight') - print("Plot saved as 'optimization_progress.png'") - - plt.show() - - except ImportError: - print("Matplotlib not available. Skipping visualization.") - except Exception as e: - print(f"Error creating visualization: {e}") - - -def compare_with_true_optimum( - best_params: torch.Tensor, - best_score: float, - objective_function: MockObjectiveFunction -) -> None: - """ - Compare the optimization results with the true optimum. - - Args: - best_params: Best parameters found by optimization - best_score: Best score achieved - objective_function: The objective function to get true optimum - """ - print_header("COMPARISON WITH TRUE OPTIMUM") - - # Get true optimal parameters and score - true_params, true_max_score = objective_function.get_optimal_parameters() - true_score_noiseless = objective_function.evaluate_at_optimal() - - print(f"\nTRUE OPTIMUM:") - print(f"Parameters: {format_parameters(true_params)}") - print(f"Score (noiseless): {true_score_noiseless:.4f}") - print(f"Max possible score: {true_max_score:.4f}") - - print(f"\nOPTIMIZED RESULT:") - print(f"Parameters: {format_parameters(best_params)}") - print(f"Score (noisy): {best_score:.4f}") - - # Calculate parameter differences - param_diff = torch.norm(best_params - true_params).item() - score_diff = abs(best_score - true_score_noiseless) - - print(f"\nCOMPARISON:") - print(f"Parameter L2 distance: {param_diff:.4f}") - print(f"Score difference: {score_diff:.4f}") - print(f"Score gap from true optimum: {(true_score_noiseless - best_score):.4f}") - - # Performance assessment - if param_diff < 5.0: # Reasonable threshold for parameter space - print("✓ Parameters are close to true optimum") - else: - print("⚠ Parameters are far from true optimum") - - if score_diff < 0.2: # Reasonable threshold considering noise - print("✓ Score is close to true optimum") - else: - print("⚠ Score is far from true optimum") - - -def analyze_convergence(iteration_history: List[dict]) -> None: - """ - Analyze convergence properties of the optimization. - - Args: - iteration_history: List of iteration information - """ - print_header("CONVERGENCE ANALYSIS") - - best_values = [info['best_value'] for info in iteration_history] - - # Find when best improvements occurred - improvements = [] - for i in range(1, len(best_values)): - improvement = best_values[i] - best_values[i-1] - if improvement > 0.01: # Significant improvement threshold - improvements.append((i+1, improvement)) - - print(f"\nSIGNIFICANT IMPROVEMENTS (> 0.01):") - if improvements: - for iteration, improvement in improvements: - print(f"Iteration {iteration}: +{improvement:.4f}") - else: - print("No significant improvements found") - - # Calculate convergence metrics - final_best = best_values[-1] - initial_best = best_values[0] - total_improvement = final_best - initial_best - - print(f"\nCONVERGENCE METRICS:") - print(f"Initial best: {initial_best:.4f}") - print(f"Final best: {final_best:.4f}") - print(f"Total improvement: {total_improvement:.4f}") - - # Check for convergence in last 5 iterations - if len(best_values) >= 5: - last_5_values = best_values[-5:] - convergence_variance = np.var(last_5_values) - print(f"Variance in last 5 iterations: {convergence_variance:.6f}") - - if convergence_variance < 0.001: - print("✓ Optimization appears to have converged") - else: - print("⚠ Optimization may not have fully converged") - - -def run_optimization_demo( - n_iterations: int = 20, - n_initial_points: int = 10, - batch_size: int = 8, - noise_std: float = 0.1, - visualize: bool = True, - seed: int = 42 -) -> Tuple[torch.Tensor, float]: - """ - Run a complete Bayesian optimization demonstration. - - Args: - n_iterations: Number of optimization iterations - n_initial_points: Number of initial random points - batch_size: Batch size for candidate generation - noise_std: Standard deviation of noise in objective function - visualize: Whether to create visualization plots - seed: Random seed for reproducibility - - Returns: - Tuple of (best_parameters, best_score) - """ - print_header("BAYESIAN OPTIMIZATION DEMO") - print(f"Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S')}") - print_parameter_info() - - # Define parameter bounds (using float32 for consistency) - bounds = torch.tensor([ - [0.1, 1.0], # concentration - [10.0, 100.0], # print_speed - [0.05, 0.5], # gap_size - [5.0, 25.0] # volume - ], dtype=torch.float32).T # Transpose to get shape (2, 4) - - print(f"\nOPTIMIZATION CONFIGURATION:") - print(f"Dimensions: {bounds.shape[1]}") - print(f"Batch size: {batch_size}") - print(f"Iterations: {n_iterations}") - print(f"Initial points: {n_initial_points}") - print(f"Noise std: {noise_std}") - print(f"Random seed: {seed}") - - # Create objective function and optimizer - objective_function = MockObjectiveFunction(noise_std=noise_std, seed=seed) - optimizer = BayesianOptimizer( - bounds=bounds, - batch_size=batch_size, - seed=seed - ) - - # Run optimization - print_header("RUNNING OPTIMIZATION") - start_time = time.time() - - best_params, best_score = optimizer.optimize( - objective_function=objective_function, - n_iterations=n_iterations, - n_initial_points=n_initial_points, - verbose=True - ) - - end_time = time.time() - optimization_time = end_time - start_time - - print_header("OPTIMIZATION RESULTS") - print(f"Optimization completed in {optimization_time:.2f} seconds") - print(f"Total function evaluations: {len(optimizer.train_X)}") - print(f"Best score: {best_score:.4f}") - print(f"Best parameters: {format_parameters(best_params)}") - - # Detailed analysis - compare_with_true_optimum(best_params, best_score, objective_function) - analyze_convergence(optimizer.get_optimization_history()) - - # Visualization - if visualize: - print_header("VISUALIZATION") - true_optimum = objective_function.evaluate_at_optimal() - visualize_optimization_progress( - optimizer.get_optimization_history(), - true_optimum, - save_plot=True - ) - - print_header("DEMO COMPLETED") - return best_params, best_score - - -def main(): - """Main function to run the demo.""" - # Example 1: Standard optimization - print("Running standard optimization demo...") - best_params, best_score = run_optimization_demo() - - # Example 2: Quick optimization with fewer iterations - print("\n" + "="*80) - print("Running quick optimization demo (10 iterations)...") - run_optimization_demo( - n_iterations=10, - n_initial_points=5, - batch_size=4, - visualize=False - ) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/aamp_app/optimizer/demo/example_usage.py b/aamp_app/optimizer/demo/example_usage.py deleted file mode 100644 index e1798be..0000000 --- a/aamp_app/optimizer/demo/example_usage.py +++ /dev/null @@ -1,282 +0,0 @@ -""" -Example Usage Scripts for Bayesian Optimization Package - -This script demonstrates various ways to use the Bayesian optimization package -for different optimization scenarios. -""" - -import torch -import numpy as np -from optimizer import BayesianOptimizer, MockObjectiveFunction - - -def example_1_basic_usage(): - """ - Example 1: Basic usage with default parameters - """ - print("="*60) - print("EXAMPLE 1: Basic Usage") - print("="*60) - - # Define parameter bounds - bounds = torch.tensor([ - [0.1, 1.0], # concentration - [10.0, 100.0], # print_speed - [0.05, 0.5], # gap_size - [5.0, 25.0] # volume - ]).T - - # Create optimizer and objective function - optimizer = BayesianOptimizer(bounds=bounds, batch_size=8) - objective = MockObjectiveFunction(noise_std=0.1) - - # Run optimization - best_params, best_score = optimizer.optimize( - objective_function=objective, - n_iterations=10, - n_initial_points=5, - verbose=True - ) - - print(f"Best parameters: {best_params}") - print(f"Best score: {best_score:.4f}") - return best_params, best_score - - -def example_2_custom_parameters(): - """ - Example 2: Custom optimization parameters - """ - print("\n" + "="*60) - print("EXAMPLE 2: Custom Parameters") - print("="*60) - - # Define parameter bounds - bounds = torch.tensor([ - [0.1, 1.0], # concentration - [10.0, 100.0], # print_speed - [0.05, 0.5], # gap_size - [5.0, 25.0] # volume - ]).T - - # Create optimizer with custom parameters - optimizer = BayesianOptimizer( - bounds=bounds, - batch_size=4, # Smaller batch size - noise_variance=0.05, # Lower noise assumption - seed=123 # Different random seed - ) - - # Create objective with different noise level - objective = MockObjectiveFunction(noise_std=0.05, seed=123) - - # Run optimization with custom settings - best_params, best_score = optimizer.optimize( - objective_function=objective, - n_iterations=15, - n_initial_points=8, - verbose=True - ) - - print(f"Best parameters: {best_params}") - print(f"Best score: {best_score:.4f}") - return best_params, best_score - - -def example_3_step_by_step(): - """ - Example 3: Step-by-step optimization (manual control) - """ - print("\n" + "="*60) - print("EXAMPLE 3: Step-by-Step Optimization") - print("="*60) - - # Define parameter bounds - bounds = torch.tensor([ - [0.1, 1.0], # concentration - [10.0, 100.0], # print_speed - [0.05, 0.5], # gap_size - [5.0, 25.0] # volume - ]).T - - # Create optimizer and objective function - optimizer = BayesianOptimizer(bounds=bounds, batch_size=6) - objective = MockObjectiveFunction(noise_std=0.1) - - # Step 1: Generate initial data - print("Step 1: Generating initial data...") - optimizer.generate_initial_data(8, objective) - - # Step 2: Manual optimization loop - n_iterations = 5 - for i in range(n_iterations): - print(f"\nIteration {i+1}/{n_iterations}") - - # Fit model - print(" Fitting GP model...") - optimizer.fit_model() - - # Get next candidates - print(" Getting next candidates...") - candidates = optimizer.optimize_acquisition() - - # Evaluate candidates - print(" Evaluating candidates...") - values = objective(candidates) - - # Update training data - optimizer.update_training_data(candidates, values) - - print(f" Current best: {optimizer.best_observed_value:.4f}") - - print(f"\nFinal best parameters: {optimizer.best_parameters}") - print(f"Final best score: {optimizer.best_observed_value:.4f}") - - return optimizer.best_parameters, optimizer.best_observed_value - - -def example_4_analysis(): - """ - Example 4: Detailed analysis of optimization results - """ - print("\n" + "="*60) - print("EXAMPLE 4: Detailed Analysis") - print("="*60) - - # Define parameter bounds - bounds = torch.tensor([ - [0.1, 1.0], # concentration - [10.0, 100.0], # print_speed - [0.05, 0.5], # gap_size - [5.0, 25.0] # volume - ]).T - - # Create optimizer and objective function - optimizer = BayesianOptimizer(bounds=bounds, batch_size=8) - objective = MockObjectiveFunction(noise_std=0.1) - - # Run optimization - best_params, best_score = optimizer.optimize( - objective_function=objective, - n_iterations=8, - n_initial_points=6, - verbose=False - ) - - # Get optimization history - history = optimizer.get_optimization_history() - train_X, train_Y = optimizer.get_training_data() - - print(f"Optimization Summary:") - print(f"- Total evaluations: {len(train_X)}") - print(f"- Best score: {best_score:.4f}") - print(f"- Best parameters: {best_params}") - - # Analyze convergence - print(f"\nConvergence Analysis:") - best_values = [h['best_value'] for h in history] - improvements = [best_values[i] - best_values[i-1] for i in range(1, len(best_values))] - - print(f"- Initial best: {best_values[0]:.4f}") - print(f"- Final best: {best_values[-1]:.4f}") - print(f"- Total improvement: {best_values[-1] - best_values[0]:.4f}") - print(f"- Average improvement per iteration: {np.mean(improvements):.4f}") - - # Compare with true optimum - true_params, _ = objective.get_optimal_parameters() - true_score = objective.evaluate_at_optimal() - - param_distance = torch.norm(best_params - true_params).item() - score_gap = true_score - best_score - - print(f"\nComparison with True Optimum:") - print(f"- True optimal score: {true_score:.4f}") - print(f"- Found score: {best_score:.4f}") - print(f"- Score gap: {score_gap:.4f}") - print(f"- Parameter distance: {param_distance:.4f}") - - return best_params, best_score - - -def example_5_custom_objective(): - """ - Example 5: Using a custom objective function - """ - print("\n" + "="*60) - print("EXAMPLE 5: Custom Objective Function") - print("="*60) - - def custom_objective(X): - """ - Custom objective function example. - This function has a different optimal point than the mock function. - """ - # Ensure X is 2D - if X.dim() == 1: - X = X.unsqueeze(0) - - # Extract parameters - concentration = X[:, 0] - print_speed = X[:, 1] - gap_size = X[:, 2] - volume = X[:, 3] - - # Custom objective: minimize print time while maintaining quality - # Quality decreases if parameters are too far from optimal ranges - quality = 1.0 - 0.5 * ((concentration - 0.8)**2 + - (print_speed - 60.0)**2 / 1000.0 + - (gap_size - 0.3)**2 * 4.0 + - (volume - 12.0)**2 / 100.0) - - # Add noise - noise = torch.randn(X.shape[0], 1) * 0.05 - - return quality.unsqueeze(1) + noise - - # Define parameter bounds - bounds = torch.tensor([ - [0.1, 1.0], # concentration - [10.0, 100.0], # print_speed - [0.05, 0.5], # gap_size - [5.0, 25.0] # volume - ]).T - - # Create optimizer - optimizer = BayesianOptimizer(bounds=bounds, batch_size=6) - - # Run optimization with custom objective - best_params, best_score = optimizer.optimize( - objective_function=custom_objective, - n_iterations=10, - n_initial_points=6, - verbose=True - ) - - print(f"Best parameters: {best_params}") - print(f"Best score: {best_score:.4f}") - - return best_params, best_score - - -def main(): - """ - Run all examples - """ - print("BAYESIAN OPTIMIZATION EXAMPLES") - print("These examples demonstrate different ways to use the package.") - print("Each example shows a different aspect of the optimization workflow.") - - # Run examples - example_1_basic_usage() - example_2_custom_parameters() - example_3_step_by_step() - example_4_analysis() - example_5_custom_objective() - - print("\n" + "="*60) - print("ALL EXAMPLES COMPLETED") - print("="*60) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/aamp_app/optimizer/demo/model_integration_example.py b/aamp_app/optimizer/demo/model_integration_example.py deleted file mode 100644 index 53b19ce..0000000 --- a/aamp_app/optimizer/demo/model_integration_example.py +++ /dev/null @@ -1,156 +0,0 @@ -import torch -import numpy as np -from model_objective_factory import create_objective_from_model, create_mock_model_and_scaler -from bayesian_optimizer import BayesianOptimizer - -def demonstrate_model_integration(): - """ - Demonstrate how to integrate a scikit-learn model with the Bayesian optimization framework. - """ - - print("=" * 60) - print("Model Integration with Bayesian Optimization") - print("=" * 60) - - # Step 1: Create or load a trained scikit-learn model - print("\n1. Creating trained scikit-learn model...") - model, scaler = create_mock_model_and_scaler() - print(f"Model created with {model.coef_.shape[1]} features") - - # Step 2: Create objective function from the model - print("\n2. Creating objective function from model...") - objective_fn = create_objective_from_model(model, scaler) - print("Objective function created successfully!") - - # Step 3: Set up Bayesian optimization - print("\n3. Setting up Bayesian optimization...") - - # Define parameter bounds (same as in the demo) - # BayesianOptimizer expects bounds in shape (2, n_dims) where first row is lower bounds - bounds = torch.tensor([ - [0.1, 10.0, 0.05, 5.0], # Lower bounds: [concentration, print_speed, gap_size, volume] - [1.0, 100.0, 0.5, 25.0] # Upper bounds: [concentration, print_speed, gap_size, volume] - ], dtype=torch.float64) - - # Initialize optimizer - optimizer = BayesianOptimizer( - bounds=bounds, - batch_size=4, # Smaller batch for demonstration - seed=42 - ) - - print(f"Optimizer initialized with bounds:") - print(f" Concentration: [{bounds[0,0]:.1f}, {bounds[1,0]:.1f}]") - print(f" Print Speed: [{bounds[0,1]:.1f}, {bounds[1,1]:.1f}]") - print(f" Gap Size: [{bounds[0,2]:.3f}, {bounds[1,2]:.3f}]") - print(f" Volume: [{bounds[0,3]:.1f}, {bounds[1,3]:.1f}]") - - # Step 4: Run optimization - print("\n4. Running Bayesian optimization...") - - # Run optimization - best_params, best_score = optimizer.optimize( - objective_function=objective_fn, - n_iterations=5, # Fewer iterations for demonstration - n_initial_points=6, - verbose=True - ) - - # Display results - print(f"\nOptimization completed!") - print(f"Best parameters found:") - print(f" Concentration: {best_params[0]:.3f}") - print(f" Print Speed: {best_params[1]:.1f}") - print(f" Gap Size: {best_params[2]:.3f}") - print(f" Volume: {best_params[3]:.1f}") - print(f"Best success probability: {best_score:.4f}") - - # Step 5: Analyze results - print("\n5. Analyzing optimization results...") - - # Get training data (all evaluated points) - train_X, train_Y = optimizer.get_training_data() - - print(f"Total evaluations: {len(train_Y)}") - print(f"Best score: {train_Y.max().item():.4f}") - print(f"Mean score: {train_Y.mean().item():.4f}") - print(f"Score improvement: {(train_Y.max() - train_Y.min()).item():.4f}") - - # Show top 3 parameter combinations - print("\nTop 3 parameter combinations:") - sorted_indices = torch.argsort(train_Y.flatten(), descending=True) - - for i, idx in enumerate(sorted_indices[:3]): - params = train_X[idx] - score = train_Y[idx].item() - print(f" {i+1}. Score: {score:.4f} | " - f"Conc: {params[0]:.3f}, Speed: {params[1]:.1f}, " - f"Gap: {params[2]:.3f}, Vol: {params[3]:.1f}") - - return optimizer, best_params, best_score - -def test_with_real_model_workflow(): - """ - Example of how to use this with a real trained model. - This shows the workflow you would follow with your actual model. - """ - - print("\n" + "=" * 60) - print("Real Model Integration Workflow") - print("=" * 60) - - print("\nExample workflow for using your trained model:") - print("1. Load your trained model:") - print(" import joblib") - print(" model = joblib.load('your_trained_model.pkl')") - print(" scaler = joblib.load('your_trained_scaler.pkl')") - - print("\n2. Create objective function:") - print(" from model_objective_factory import create_objective_from_model") - print(" objective_fn = create_objective_from_model(model, scaler)") - - print("\n3. Set up and run optimization:") - print(" from bayesian_optimizer import BayesianOptimizer") - print(" optimizer = BayesianOptimizer(objective_function=objective_fn, ...)") - print(" best_params, best_score, results = optimizer.optimize()") - - print("\n4. The optimization will:") - print(" - Handle batch evaluation of parameter combinations") - print(" - Automatically perform feature engineering for each combination") - print(" - Use your model's predict_proba to get success probabilities") - print(" - Find optimal parameters that maximize success probability") - - print("\nFeature Engineering Details:") - print("- Input: [concentration, print_speed, gap_size, volume]") - print("- Automatically creates 11 features in the correct order:") - print(" 1. gap_size_squared") - print(" 2. solvent_CF (fixed at 0.5)") - print(" 3. print_speed_squared") - print(" 4. concentration_squared") - print(" 5. concentration") - print(" 6. gap_size") - print(" 7. print_speed_gap_size") - print(" 8. print_speed") - print(" 9. concentration_print_speed") - print(" 10. print_speed_volume") - print(" 11. volume") - print("- Applies scaling using your trained StandardScaler") - print("- Returns success probabilities from your model") - -if __name__ == "__main__": - # Run the demonstration - optimizer, best_params, best_score = demonstrate_model_integration() - - # Show the real model workflow - test_with_real_model_workflow() - - print("\n" + "=" * 60) - print("Integration demonstration completed!") - print("=" * 60) - - print("\nKey Benefits:") - print("- Seamless integration between scikit-learn models and BoTorch") - print("- Automatic feature engineering and scaling") - print("- Batch evaluation for efficient optimization") - print("- Clean separation of concerns using factory pattern") - print("- Compatible with existing Bayesian optimization framework") \ No newline at end of file diff --git a/aamp_app/optimizer/demo/run_demo.py b/aamp_app/optimizer/demo/run_demo.py deleted file mode 100644 index c47f293..0000000 --- a/aamp_app/optimizer/demo/run_demo.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple script to run the Bayesian Optimization demo. - -This script can be run directly to see the complete optimization workflow. -It handles imports and provides a clean interface for users. -""" - -import sys -import os - -# Add the parent directory to the Python path -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -def main(): - """Main function to run the demo.""" - try: - # Import and run the demo - from optimizer.demo.demo import run_optimization_demo - - print("Starting Bayesian Optimization Demo...") - print("This may take a few minutes to complete.") - print("Press Ctrl+C to interrupt if needed.\n") - - # Run the demo with default parameters - best_params, best_score = run_optimization_demo( - n_iterations=20, - n_initial_points=10, - batch_size=8, - noise_std=0.1, - visualize=True, - seed=42 - ) - - print(f"\nDemo completed successfully!") - print(f"Best parameters found: {best_params}") - print(f"Best score achieved: {best_score:.4f}") - - except ImportError as e: - print(f"Import error: {e}") - print("Please ensure all required packages are installed:") - print("pip install -r requirements.txt") - sys.exit(1) - - except KeyboardInterrupt: - print("\nDemo interrupted by user.") - sys.exit(0) - - except Exception as e: - print(f"Error running demo: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/aamp_app/optimizer/model_objective_factory.py b/aamp_app/optimizer/model_objective_factory.py deleted file mode 100644 index 6a73b66..0000000 --- a/aamp_app/optimizer/model_objective_factory.py +++ /dev/null @@ -1,313 +0,0 @@ -import numpy as np -import torch -from sklearn.linear_model import LogisticRegression -from sklearn.preprocessing import StandardScaler -from typing import Callable, Tuple -import warnings -warnings.filterwarnings('ignore') - -def create_objective_from_model(model: LogisticRegression, scaler: StandardScaler = None) -> Callable: - """ - Factory function that creates a BoTorch-compatible objective function from a trained scikit-learn model. - - Parameters: - ----------- - model : LogisticRegression - A trained scikit-learn LogisticRegression model - scaler : StandardScaler, optional - A fitted StandardScaler to normalize features. If None, no scaling is applied. - - Returns: - -------- - Callable - An objective function that accepts (batch_size, 4) tensor of base parameters - and returns (batch_size, 1) tensor of success probabilities - """ - - # Determine expected number of features from the model or scaler - if scaler is not None: - n_features_expected = scaler.n_features_in_ - else: - n_features_expected = model.coef_.shape[1] - - print(f"Model expects {n_features_expected} features") - - def objective_function(X: torch.Tensor) -> torch.Tensor: - """ - Objective function that evaluates parameter combinations using the trained model. - - Parameters: - ----------- - X : torch.Tensor - Input tensor of shape (batch_size, 4) containing base parameters: - [concentration, print_speed, gap_size, volume] - - Returns: - -------- - torch.Tensor - Output tensor of shape (batch_size, 1) containing success probabilities - """ - - # Convert to numpy for feature engineering - X_np = X.detach().cpu().numpy() - batch_size = X_np.shape[0] - - # Extract base parameters - concentration = X_np[:, 0] # Column 0: concentration - print_speed = X_np[:, 1] # Column 1: print_speed - gap_size = X_np[:, 2] # Column 2: gap_size - volume = X_np[:, 3] # Column 3: volume - - # Create comprehensive feature matrix based on typical polymer printing feature engineering - features = create_feature_matrix(concentration, print_speed, gap_size, volume, n_features_expected) - - # Apply feature scaling if provided - if scaler is not None: - features = scaler.transform(features) - - # Get success probabilities using the trained model - # predict_proba returns [P(class=0), P(class=1)], we want P(class=1) - success_probabilities = model.predict_proba(features)[:, 1] - - # Convert back to PyTorch tensor and reshape for BoTorch - result = torch.tensor(success_probabilities, dtype=torch.float32, device=X.device) - result = result.unsqueeze(-1) # Shape: (batch_size, 1) - - return result - - return objective_function - -def create_feature_matrix(concentration, print_speed, gap_size, volume, n_features_expected): - """ - Create feature matrix with comprehensive feature engineering to match trained model. - """ - batch_size = len(concentration) - - if n_features_expected == 11: - # Original 11-feature structure - features = np.zeros((batch_size, 11)) - features[:, 0] = gap_size ** 2 # gap_size_squared - features[:, 1] = 0.5 # solvent_CF (fixed constant) - features[:, 2] = print_speed ** 2 # print_speed_squared - features[:, 3] = concentration ** 2 # concentration_squared - features[:, 4] = concentration # concentration - features[:, 5] = gap_size # gap_size - features[:, 6] = print_speed * gap_size # print_speed_gap_size - features[:, 7] = print_speed # print_speed - features[:, 8] = concentration * print_speed # concentration_print_speed - features[:, 9] = print_speed * volume # print_speed_volume - features[:, 10] = volume # volume - - else: - # Extended feature structure for larger models - features = np.zeros((batch_size, n_features_expected)) - - # Base parameters - features[:, 0] = concentration - features[:, 1] = print_speed - features[:, 2] = gap_size - features[:, 3] = volume - - # Interaction terms - features[:, 4] = concentration * print_speed - features[:, 5] = concentration * gap_size - features[:, 6] = concentration * volume - features[:, 7] = print_speed * gap_size - features[:, 8] = print_speed * volume - features[:, 9] = gap_size * volume - - # Squared terms - features[:, 10] = concentration ** 2 - features[:, 11] = print_speed ** 2 - features[:, 12] = gap_size ** 2 - features[:, 13] = volume ** 2 - - # Solvent features (assuming multiple solvents were used in training) - if n_features_expected >= 18: - # Add solvent dummy variables - features[:, 14] = 0.5 # solvent_CB (fixed) - features[:, 15] = 0.0 # solvent_CF - features[:, 16] = 0.0 # solvent_anisole - features[:, 17] = 0.0 # solvent_p-xylene - - # If even more features, add higher-order interactions - if n_features_expected > 18: - for i in range(18, min(n_features_expected, 25)): - # Add more complex interactions or polynomial terms - if i == 18: - features[:, i] = concentration * print_speed * gap_size - elif i == 19: - features[:, i] = concentration * print_speed * volume - elif i == 20: - features[:, i] = concentration * gap_size * volume - elif i == 21: - features[:, i] = print_speed * gap_size * volume - elif i == 22: - features[:, i] = concentration ** 3 - elif i == 23: - features[:, i] = print_speed ** 3 - elif i == 24: - features[:, i] = gap_size ** 3 - else: - features[:, i] = 1.0 # constant term - - return features - -def create_mock_model_and_scaler() -> Tuple[LogisticRegression, StandardScaler]: - """ - Create a mock LogisticRegression model and StandardScaler for demonstration purposes. - - Returns: - -------- - Tuple[LogisticRegression, StandardScaler] - A tuple containing the trained model and fitted scaler - """ - - # Set random seed for reproducibility - np.random.seed(42) - - # Create dummy training data - n_samples = 1000 - n_features = 11 - - # Generate realistic parameter ranges for the dummy data - # These approximate the ranges from the actual polymer printing experiments - X_dummy = np.random.rand(n_samples, n_features) - - # Scale features to realistic ranges based on the feature definitions - X_dummy[:, 0] = X_dummy[:, 0] * 0.25 # gap_size_squared (0 to 0.25) - X_dummy[:, 1] = 0.5 # solvent_CF (constant) - X_dummy[:, 2] = X_dummy[:, 2] * 10000 # print_speed_squared (0 to 10000) - X_dummy[:, 3] = X_dummy[:, 3] * 1.0 # concentration_squared (0 to 1.0) - X_dummy[:, 4] = X_dummy[:, 4] * 1.0 # concentration (0 to 1.0) - X_dummy[:, 5] = X_dummy[:, 5] * 0.5 # gap_size (0 to 0.5) - X_dummy[:, 6] = X_dummy[:, 6] * 50 # print_speed_gap_size (0 to 50) - X_dummy[:, 7] = X_dummy[:, 7] * 100 # print_speed (0 to 100) - X_dummy[:, 8] = X_dummy[:, 8] * 100 # concentration_print_speed (0 to 100) - X_dummy[:, 9] = X_dummy[:, 9] * 2500 # print_speed_volume (0 to 2500) - X_dummy[:, 10] = X_dummy[:, 10] * 25 # volume (0 to 25) - - # Create synthetic target variable with some realistic patterns - # Higher success probability for moderate concentration, speed, and gap_size - concentration = X_dummy[:, 4] - print_speed = X_dummy[:, 7] - gap_size = X_dummy[:, 5] - volume = X_dummy[:, 10] - - # Create a synthetic success probability based on realistic patterns - # Success is higher for moderate values of key parameters - success_prob = ( - 0.3 + # Base probability - 0.4 * np.exp(-((concentration - 0.6) ** 2) / 0.2) + # Optimal concentration around 0.6 - 0.2 * np.exp(-((print_speed - 45) ** 2) / 800) + # Optimal print_speed around 45 - 0.1 * np.exp(-((gap_size - 0.2) ** 2) / 0.08) # Optimal gap_size around 0.2 - ) - - # Add some noise - success_prob += np.random.normal(0, 0.1, n_samples) - success_prob = np.clip(success_prob, 0, 1) - - # Convert to binary outcomes - y_dummy = (success_prob > 0.5).astype(int) - - # Fit the scaler - scaler = StandardScaler() - X_dummy_scaled = scaler.fit_transform(X_dummy) - - # Train the logistic regression model - model = LogisticRegression(random_state=42, max_iter=1000) - model.fit(X_dummy_scaled, y_dummy) - - # Print model performance for verification - accuracy = model.score(X_dummy_scaled, y_dummy) - print(f"Mock model training accuracy: {accuracy:.3f}") - - return model, scaler - -def test_objective_function(): - """ - Test the objective function with sample inputs to verify correct operation. - """ - - # Create mock model and scaler - model, scaler = create_mock_model_and_scaler() - - # Create objective function - objective_fn = create_objective_from_model(model, scaler) - - # Test with a batch of parameter combinations - # Parameters: [concentration, print_speed, gap_size, volume] - test_params = torch.tensor([ - [0.5, 50.0, 0.2, 15.0], # Near optimal values - [0.1, 10.0, 0.05, 5.0], # Lower values - [0.9, 90.0, 0.45, 25.0], # Higher values - [0.6, 45.0, 0.2, 15.0] # Optimal values - ], dtype=torch.float32) - - print(f"Input tensor shape: {test_params.shape}") - print(f"Input parameters:\n{test_params}") - - # Call objective function - results = objective_fn(test_params) - - print(f"Output tensor shape: {results.shape}") - print(f"Success probabilities:\n{results}") - - # Verify that results are valid probabilities - assert torch.all(results >= 0), "All probabilities should be non-negative" - assert torch.all(results <= 1), "All probabilities should be <= 1" - assert results.shape == (4, 1), f"Expected shape (4, 1), got {results.shape}" - - print("All tests passed!") - - return objective_fn, test_params, results - -if __name__ == "__main__": - print("=" * 60) - print("Model Objective Factory Demonstration") - print("=" * 60) - - print("\n1. Creating and training mock LogisticRegression model...") - model, scaler = create_mock_model_and_scaler() - - print(f"\nModel coefficients shape: {model.coef_.shape}") - print(f"Model intercept: {model.intercept_[0]:.3f}") - print(f"Number of features expected: {model.coef_.shape[1]}") - - print("\n2. Creating objective function from model...") - objective_fn = create_objective_from_model(model, scaler) - print("Objective function created successfully!") - - print("\n3. Testing objective function with sample parameter combinations...") - - # Create sample input tensor (batch_size=2, 4 parameters) - sample_params = torch.tensor([ - [0.6, 45.0, 0.2, 15.0], # Near optimal combination - [0.2, 80.0, 0.4, 10.0] # Suboptimal combination - ], dtype=torch.float32) - - print(f"Sample input tensor shape: {sample_params.shape}") - print(f"Sample parameters:") - print(f" Batch 1: concentration={sample_params[0,0]:.1f}, print_speed={sample_params[0,1]:.1f}, gap_size={sample_params[0,2]:.1f}, volume={sample_params[0,3]:.1f}") - print(f" Batch 2: concentration={sample_params[1,0]:.1f}, print_speed={sample_params[1,1]:.1f}, gap_size={sample_params[1,2]:.1f}, volume={sample_params[1,3]:.1f}") - - # Call objective function - results = objective_fn(sample_params) - - print(f"\nResults tensor shape: {results.shape}") - print(f"Success probabilities:") - print(f" Batch 1: {results[0,0]:.4f}") - print(f" Batch 2: {results[1,0]:.4f}") - - print("\n4. Running comprehensive test...") - test_objective_function() - - print("\n" + "=" * 60) - print("Demonstration completed successfully!") - print("=" * 60) - - print("\nUsage Summary:") - print("- Use create_objective_from_model(model, scaler) to create an objective function") - print("- The returned function accepts (batch_size, 4) tensors of [concentration, print_speed, gap_size, volume]") - print("- It returns (batch_size, 1) tensors of success probabilities") - print("- The function handles all necessary feature engineering and scaling internally") \ No newline at end of file diff --git a/aamp_app/optimizer/notebooks/demo_notebook.ipynb b/aamp_app/optimizer/notebooks/demo_notebook.ipynb deleted file mode 100644 index 4172631..0000000 --- a/aamp_app/optimizer/notebooks/demo_notebook.ipynb +++ /dev/null @@ -1,462 +0,0 @@ -{ - "cells": [ - { - "cell_type": "raw", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "# Bayesian Optimization Demo\n", - "\n", - "This notebook demonstrates the complete Bayesian optimization workflow for optimizing a printing process with 4 continuous parameters.\n", - "\n", - "## Overview\n", - "- **Objective**: Maximize a noisy, black-box objective function\n", - "- **Parameters**: concentration, print_speed, gap_size, volume\n", - "- **Method**: Batch Bayesian optimization with qNoisyExpectedImprovement\n", - "- **Batch Size**: 8 candidates per iteration\n", - "- **Iterations**: 20 optimization rounds\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "All imports successful!\n", - "PyTorch version: 2.7.1+cu126\n" - ] - } - ], - "source": [ - "import torch\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "from typing import List, Tuple\n", - "import time\n", - "import warnings\n", - "warnings.filterwarnings('ignore')\n", - "import sys\n", - "import os\n", - "sys.path.append(os.path.abspath(os.path.join(os.getcwd(), '..')))\n", - "\n", - "# Import our optimization package\n", - "from bayesian_optimizer import BayesianOptimizer\n", - "from objective_function import MockObjectiveFunction\n", - "\n", - "# Set up matplotlib for better plots\n", - "plt.style.use('default')\n", - "plt.rcParams['figure.figsize'] = (12, 8)\n", - "plt.rcParams['font.size'] = 12\n", - "\n", - "print(\"All imports successful!\")\n", - "print(f\"PyTorch version: {torch.__version__}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Parameter Space:\n", - "==================================================\n", - "concentration [ 0.10, 1.00]\n", - "print_speed [ 10.00, 100.00] (mm/s)\n", - "gap_size [ 0.05, 0.50] (mm)\n", - "volume [ 5.00, 25.00] (μL)\n", - "\n", - "Total dimensions: 4\n" - ] - } - ], - "source": [ - "## 2. Parameter Space Definition\n", - "\n", - "# Define the 4-dimensional parameter space for our printing process optimization:\n", - "# Define parameter bounds\n", - "bounds = torch.tensor([\n", - " [0.1, 1.0], # concentration\n", - " [10.0, 100.0], # print_speed\n", - " [0.05, 0.5], # gap_size\n", - " [5.0, 25.0] # volume\n", - "]).T\n", - "\n", - "# Parameter names for plotting\n", - "param_names = ['concentration', 'print_speed', 'gap_size', 'volume']\n", - "param_units = ['', 'mm/s', 'mm', 'μL']\n", - "\n", - "print(\"Parameter Space:\")\n", - "print(\"=\" * 50)\n", - "for i, (name, unit) in enumerate(zip(param_names, param_units)):\n", - " lower, upper = bounds[0, i], bounds[1, i]\n", - " unit_str = f\" ({unit})\" if unit else \"\"\n", - " print(f\"{name:<15} [{lower:>6.2f}, {upper:>6.2f}]{unit_str}\")\n", - " \n", - "print(f\"\\nTotal dimensions: {bounds.shape[1]}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Starting Bayesian Optimization...\n", - "============================================================\n", - "Batch size: 8\n", - "Iterations: 20\n", - "Initial points: 10\n", - "True optimal score: 0.9993\n", - "============================================================\n", - "BAYESIAN OPTIMIZATION STARTING\n", - "============================================================\n", - "Parameters: 4 dimensions\n", - "Batch size: 8\n", - "Iterations: 20\n", - "Initial points: 10\n", - "============================================================\n", - "Generating 10 initial training points...\n", - "Initial best score: 0.4601\n", - "Initial best parameters: tensor([ 0.1793, 73.1064, 0.3305, 13.7456], dtype=torch.float64)\n", - "\n", - "Iteration 1/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.4601\n", - "Current best parameters: tensor([ 0.1793, 73.1064, 0.3305, 13.7456], dtype=torch.float64)\n", - "\n", - "Iteration 2/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.4601\n", - "Current best parameters: tensor([ 0.1793, 73.1064, 0.3305, 13.7456], dtype=torch.float64)\n", - "\n", - "Iteration 3/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.4728\n", - "Current best parameters: tensor([ 1.0000, 76.9107, 0.3270, 16.2047])\n", - "\n", - "Iteration 4/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.4728\n", - "Current best parameters: tensor([ 1.0000, 76.9107, 0.3270, 16.2047])\n", - "\n", - "Iteration 5/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.4728\n", - "Current best parameters: tensor([ 1.0000, 76.9107, 0.3270, 16.2047])\n", - "\n", - "Iteration 6/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.4916\n", - "Current best parameters: tensor([ 0.4224, 10.0000, 0.3638, 16.2034])\n", - "\n", - "Iteration 7/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.4916\n", - "Current best parameters: tensor([ 0.4224, 10.0000, 0.3638, 16.2034])\n", - "\n", - "Iteration 8/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.4916\n", - "Current best parameters: tensor([ 0.4224, 10.0000, 0.3638, 16.2034])\n", - "\n", - "Iteration 9/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.4916\n", - "Current best parameters: tensor([ 0.4224, 10.0000, 0.3638, 16.2034])\n", - "\n", - "Iteration 10/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.5518\n", - "Current best parameters: tensor([ 0.2830, 45.5636, 0.2645, 7.9626])\n", - "\n", - "Iteration 11/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.9580\n", - "Current best parameters: tensor([ 0.5732, 41.5476, 0.2469, 14.6669])\n", - "\n", - "Iteration 12/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.9580\n", - "Current best parameters: tensor([ 0.5732, 41.5476, 0.2469, 14.6669])\n", - "\n", - "Iteration 13/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 0.9646\n", - "Current best parameters: tensor([ 0.6178, 40.2137, 0.2170, 13.4322])\n", - "\n", - "Iteration 14/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 1.0939\n", - "Current best parameters: tensor([ 0.6122, 42.8861, 0.1982, 12.8696])\n", - "\n", - "Iteration 15/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 1.2429\n", - "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", - "\n", - "Iteration 16/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 1.2429\n", - "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", - "\n", - "Iteration 17/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 1.2429\n", - "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", - "\n", - "Iteration 18/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 1.2429\n", - "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", - "\n", - "Iteration 19/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 1.2429\n", - "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", - "\n", - "Iteration 20/20\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 8 candidates...\n", - "Current best score: 1.2429\n", - "Current best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", - "\n", - "============================================================\n", - "OPTIMIZATION COMPLETED\n", - "============================================================\n", - "Final best score: 1.2429\n", - "Final best parameters: tensor([ 0.6034, 40.9089, 0.1743, 12.6525])\n", - "Total evaluations: 170\n", - "\n", - "Optimization completed in 198.91 seconds\n", - "Best score: 1.2429\n", - "Score gap from true optimum: -0.2436\n" - ] - } - ], - "source": [ - "## 3. Run Optimization\n", - "#Execute the complete Bayesian optimization workflow:\n", - "# # Configuration parameters\n", - "BATCH_SIZE = 8\n", - "N_ITERATIONS = 20\n", - "N_INITIAL_POINTS = 10\n", - "NOISE_VARIANCE = 0.01\n", - "SEED = 42\n", - "\n", - "# Create optimizer and objective function\n", - "optimizer = BayesianOptimizer(\n", - " bounds=bounds,\n", - " batch_size=BATCH_SIZE,\n", - " noise_variance=NOISE_VARIANCE,\n", - " seed=SEED\n", - ")\n", - "\n", - "objective = MockObjectiveFunction(noise_std=0.1, seed=SEED)\n", - "\n", - "# Get true optimum for comparison\n", - "true_params, true_max_score = objective.get_optimal_parameters()\n", - "true_score = objective.evaluate_at_optimal()\n", - "\n", - "print(\"Starting Bayesian Optimization...\")\n", - "print(\"=\" * 60)\n", - "print(f\"Batch size: {BATCH_SIZE}\")\n", - "print(f\"Iterations: {N_ITERATIONS}\")\n", - "print(f\"Initial points: {N_INITIAL_POINTS}\")\n", - "print(f\"True optimal score: {true_score:.4f}\")\n", - "\n", - "# Run optimization\n", - "start_time = time.time()\n", - "\n", - "best_params, best_score = optimizer.optimize(\n", - " objective_function=objective,\n", - " n_iterations=N_ITERATIONS,\n", - " n_initial_points=N_INITIAL_POINTS,\n", - " verbose=True\n", - ")\n", - "\n", - "end_time = time.time()\n", - "optimization_time = end_time - start_time\n", - "\n", - "print(f\"\\nOptimization completed in {optimization_time:.2f} seconds\")\n", - "print(f\"Best score: {best_score:.4f}\")\n", - "print(f\"Score gap from true optimum: {(true_score - best_score):.4f}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABcwAAASgCAYAAADFDAuCAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3XlcVNX7B/DPnWGXXUFBNsVdwT39Wgjihpobau4LaplpaZbm8su9NNukrKxcwFTcDTW1zERNyyXLNNMMBXdlkU0EYeb+/pgYGWdAmBm4M8zn/Xr58s69597zzAMMZx7OnCuIoiiCiIiIiIiIiIiIiMjCyaQOgIiIiIiIiIiIiIjIFLBgTkREREREREREREQEFsyJiIiIiIiIiIiIiACwYE5EREREREREREREBIAFcyIiIiIiIiIiIiIiACyYExEREREREREREREBYMGciIiIiIiIiIiIiAgAC+ZERERERERERERERABYMCciIiIiIiIiIiIiAsCCORFRmYWFhUEQhArtY8yYMRAEAUlJSRXaT1nFxMRAEATExMRIHQoRERERkZogCAgLC6vQPhISEiAIAubPn1+h/RARkWlhwZyITN7p06cRFRWFunXrwt7eHs7OzggKCsL06dNx8+ZNo/VjasXqymCObwKKivjF/9na2qJOnTqIiorCxYsXpQ6RiIiISFIXL17Eq6++imbNmsHFxQU2Njbw9vZGr169sHr1auTl5UkdoklISkqCIAgYM2aM1KGUWdH4lyyPJb5fJZKKldQBEBGVRBRFzJw5E8uWLYOVlRW6du2KQYMG4dGjRzh+/Dg++OADfP7554iNjcXAgQMrPJ5169YhNze3QvtYsmQJZs6cidq1a1doP2XVv39/tG/fHl5eXlKHoqV58+bo168fACAzMxMJCQmIiYnBli1bkJCQgLZt20obIBEREZEEFi5ciAULFkCpVKJ9+/YYPXo0nJyccPfuXRw5cgTjx4/HF198gdOnT0sdqsl75pln8Pfff6NGjRpSh0JERJWIBXMiMlkLFy7EsmXLEBAQgD179qBp06Yax7dv344RI0ZgyJAh+OGHHxAeHl6h8fj5+VXo9QHAy8vLpIrTLi4ucHFxkToMnVq0aKExM14URURFRSE2NhYzZ87EwYMHpQuOiIiISALvvPMO5s2bB19fX2zduhXt2rXTarN//34s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wTzANQByGZerJSIqjVEK5qIowsbGRuex+/fvw9bW1hjdEBGRGWPBnIjIeKZNm4bXX38dvr6+qFOnDry8vGBtbS11WEREZIJSclPwoOABAC7HQkRUFka56WdwcDB27typ89j+/fvRunVrY3RDRERmjAVzIiLjWb58OW7fvg0AqFOnDn7//XeJIyIiIlPFG34SEZWPUWaYT5kyBcOGDUO1atUwcuRIAMC1a9fw008/Yc2aNdi2bZsxuiEiIjNWvGAeGChdHEREVYGbmxvu3r0LQPVpzydv8klERFSkeME80I0DcSKipzFKwXzw4MFITEzE/Pnz8cknnwAABgwYACsrKyxYsAC9e/c2RjdERGTGigrmMhng5ydtLERE5q59+/YYN24cnnnmGQDAG2+8AVdXV51tBUFAfHx8JUZHRESmhDPMiYjKxygFcwCYPXs2Ro0ahe+//x53795FjRo10L17d/j7+xurCyIiMmOJiar//fwALrNLRGSYzz//HFOnTsVff/0FQRDw77//lnjfIM4+JyKybIn3E9XbLJgTET2dUQrmCoUCcrkcPj4+GDdunDEuSUREVcj9+0BGhmqb65cTERnO399ffQ8hmUyGb7/9Vj3bnIiIqLjiM8zruNWRMBIiIvNglJt++vj4YMaMGbhw4YIxLkdERFUMb/hJRFRxDh06hCZNmkgdBhERmaiignkNhxpwtnWWOBoiItNnlIJ5x44d8emnnyIoKAjt27fHV199haysLGNcmoiIqgAWzImIKk5oaCgcHR2lDoOIiExQXmEebmbdBMDlWIiIysooBfPNmzfjzp07WLFiBURRxMsvvwwvLy+MHDkSBw8eNEYXRERkxooXzAMDpYuDiIieLicnB1OnToW3tzfs7OzQokULbNq0qdzX+b//+z8IgoBmzZpVQJRERFQWyRnJECECAALdOBAnIioLoxTMAcDFxQUTJ07EiRMncP78ebzyyis4ePAgunXrhjp1uEYWEZEl4wxzIiLzERkZidjYWMybNw/79u1D27ZtMXToUGzcuLHM1/jjjz/wwQcfoGbNmhUYKRERPU3x9cs5w5yIqGyMctPPJzVp0gTLli1DaGgoXnnlFVy7dq0iuiEiIjORmPh4mwVzIiLTtXfvXhw4cAAbN27E0KFDAQCdOnVCcnIypk+fjsGDB0Mul5d6jcLCQkRFRWHChAk4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mBsLABRXDRo0kCSNHDlSI0eOdLrPX/voAIDijYI5ALjOLQVzwzBUunRpp9vOnj0rX19fdzQDAChCrl2TDh3KWI6KkiwWc+MBgOJq7NixsvBDFgBwgxsL5lEVokyMBACKHrcUzBs1aqRFixapa9eumbatXLlSTZs2dUczAIAi5PBhyTagkelYAKDgjB8/3uwQAAAexlYwL+1dWqHlQk2OBgCKFrcUzIcNG6b+/furbNmyeuSRRyRJhw4d0tq1a/XJJ59owYIF7mgGAFCEJCZeX6ZgDgCF49KlS0pKSlKVKlXk4+OWrj4AoIgxDMNeMI8MipS3l7fJEQFA0eLljpP06dNHr732mqZOnapmzZpJkh588EGNGTNGEyZM0L333uuOZgAARQgP/ASAwrNu3TrdcccdKleunCIiIvTzzz9Lkp566iklJCSYHB0AoDCdvHBSF65dkMT85QDgCrcUzCXppZde0r59+/TRRx/p9ddf13//+1/9/vvvGj16tLuaAAAUIRTMAaBwrF27Vvfcc48uX76s559/Xlar1b6tUqVKio2NNS84AEChc3jgZxAdcQDIK7fepxkWFqYhQ4a485QAgCKKgjkAFI6xY8eqW7duWrx4sdLS0jR58mT7tsaNG2vmzJkmRgcAKGwOBXNGmANAnrllhPnPP/+sjRs32l9fuHBB//jHP9SyZUuNHTtWhmG4oxkAQBFiK5h7e0s1apgbCwAUZz/++KOefPJJSZLFYnHYFhISopMnT5oRFgDAJDcWzKMqRpkYCQAUTW4pmD/33HNaunSp/fVLL72k6dOn6+rVq5o4caLef/99dzQDAChCbAXzGjWkUqXMjQUAijMfHx9du3bN6baTJ0+qXLlyhRwRAMBM+84xwhwA8sMtBfNdu3apVatWkjKexjx79mxNmDBB27dv1wsvvKBPPvnEHc0AAIqIs2elc+cylpmOBQAKVrNmzfS///3P6bYFCxbojjvuKOSIAABmunGEec2gmiZGAgBFk1sK5ufOnVOlSpUkSTt27NDZs2f10EMPSZI6duyofTdOZAsAKPYSE68vUzAHgII1evRoLVq0SD179tQXX3whi8WirVu36umnn9aCBQs0atQos0MEABSixKSMzniIf4jK+XKXEQDklVse+hkcHKzDhw9LktatW6cqVaqodu3akqSrV68yhzkAlDA88BMACs/d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FBoaqhYtWuTpfIZhaMOGDQoKClJwcHCejgUAZO/GB37WqkBHHADcxS1DUapUqaJLly45FMw/++wzbd++XXfffXeeHjQEAChaKJgDgLkMw9AzzzyjDz74wOFhRl5eXvrHP/6h9957L9fn6tq1qzp16qShQ4cqOTlZtWvXVnx8vFauXKlZs2bJ29tbkjRkyBDFxcUpMTFRERERkqT7779fjRs3VpMmTRQcHKxjx44pNjZWGzZs0NSpU902Ch4AkIGCOQAUDLf0Wh955BGVLVtWsbGxkqT33ntPw4cPlyS9/fbbWrJkibp16+aOpgAAHsZWMPf2lmrUMDcWACiJ3n33XU2bNk1Dhw7VwIEDFRoaqmPHjikuLk7Tpk1TzZo1NWLEiFyfLyEhQWPGjNHYsWOVlJSkunXrKj4+Xn379rXvk56ervT0dBmGYV935513asGCBXr//feVnJysoKAg3X777Vq6dKm6d+/u1vcMAHAsmEdViDIxEgAoXtxSMP/uu+/01ltv2V+/9957GjBggN5//30NGTJE77zzDgVzACiGDOP6Qz8jIiQGDwJA4fv444/1zDPPKCYmxr6uevXqatasmby9vTV9+vQ8FcwDAgIUExPjcL6/io2NtQ+WsRk1apRGjRqV5/gBAK5hhDkAFAy3zKx+6tQpVa9eXVLGfOb79u3TM888o8DAQA0ZMkS7du1yRzMAAA9z9qyUnJyxzHQsAGCOffv2qUePHk639ejRQ/tunDsLAFBs3Fgwr1mhpomRAEDx4paCub+/v86fPy9J2rRpkwICAnT77bdLkvz8/JSamuqOZgAAHsY2ulyiYA4AZilfvrwOHjzodNvBgwcVGBhYyBEBAApD4tmMznjlspUVUDrA5GgAoPhwS8G8YcOGmjp1qnbu3Klp06apffv2slgskqRDhw6patWq7mgGAOBheOAnAJivU6dOevnll/XDDz84rP/pp580btw4de7c2aTIAAAF5dK1SzqWckwS07EAgLu5ZbbZV155RT169FCTJk1UunRprVmzxr5t2bJluu2229zRDADAw1AwBwDzTZw4UevXr1fz5s1Vv359VatWTcePH9evv/6q0NBQTZw40ewQAQBuduDcAfsyBXMAcC+3FMw7dOig3377TT/88IOaNGmiWjdUTTp06KAmTZq4oxkAgIe5sWAeFWVeHABQkoWHh+un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841PPvnEkGT07t3bmD9/vjF//nzjwoULhmEYxm+//WYEBAQYd911l7F8+XIjISHBaNCggREaGmqcPHnS4Xze3t5Ghw4dHNatW7fO8PHxMXr27GmsXr3amD17thEeHm40aNDAuHz5cqG914KQU/4uXrxoNGvWzLBYLEZMTEymn4N//PGHw/n+mr+NGzcanTt3Nj744APjyy+/NL744gtj6NCh9v3S09ML+y27TU65O3DggNGqVSvjvffeM5YvX26sWLHCGD16tOHn52fccsstRmpqqsP5StK1l5t/tzZdunQxypQpY5w7dy7L85WU6y63/RJ+5qE4oB+eP/TF849+eO7QF3cd/XDX0Q/PH/ririkJfXEK5siVtm3bGpKy/LJ57LHHDEnG/v377euGDx9u1K9f3yhXrpzh4+NjhIaGGgMGDDD27NljwjspfBMnTjSaNGlilC9f3vD29jZCQkKMnj17Gt99953Dfs5yZxiGceLECePRRx81KlasaPj5+RktW7Z06NyXFJ06dTLKli1rJCcnO93OtWcYERERWf4bvTEv27ZtMzp27Gj4+/sbgYGBxgMPPJCpk2kYhiHJaNu2bab1X375pdGyZUvDz8/PqFixovHoo48af/75ZwG+s8KRU/7279+f7c/Bxx57zOF8f83f3r17jW7duhnVq1c3fH19DT8/P6Nhw4bGG2+8UeQ7mjnlLikpyejZs6cRGRlplClTxihdurRx0003GaNGjXLa4SxJ115u/90eOnTI8PLyMh599NFsz1dSrrvc9ksMg595KProh+cPffH8ox+eO/TFXUc/3HX0w/OHvrhrSkJf3PL/QQEAAAAAAAAAUKIxhzkAAAAAAAAAAKJgDgAAAAAAAACAJArmAAAAAAAAAABIomAOAAAAAAAAAIAkCuYAAAAAAAAAAEiiYA4AAAAAAAAAgCQK5gAAAAAAAAAASKJgDgAAAAAAAACAJArmAFDkxcbGymKxaNu2bZKk5cuXa/z48eYGlUMckZGRGjhwYKHGAwAAALgT/XAAKJ4omANAMbN8+XJNmDDB7DCyjWPRokV65ZVXCjkiAAAAoODQDweA4sHH7AAAAEXDxYsX5e/v75Zz3XrrrW45DwAAAFDc0Q8HgMLFCHMAKEYGDhyoqVOnSpIsFov968CBA5IkwzA0bdo0NWnSRGXKlFGFChXUq1cv7du3z+E87dq1U4MGDbRx40a1atVK/v7+Gjx4sCRp3rx5uueee1StWjWVKVNG9erV0+jRo3XhwoVcx+HsVtBDhw5pwIABqly5snx9fVWvXj3961//ktVqte9z4MABWSwWvfPOO/r3v/+tmjVrKiAgQHfccYe+/fZbd6YSAAAAyDX64QBQfDDCHACKkVdeeUUXLlzQggUL9M0339jXV6tWTZL05JNPKjY2Vs8++6zeeustJSUl6dVXX1WrVq20Y8cOValSxX7M8ePHNWDAAI0aNUpvvvmmvLwy/sa6d+9edevWTcOHD1fZsmW1e/duvfXWW/ruu++0du3aXMXxV6dOnVKrVq109epVvfbaa4qMjNTSpUv1/PPPKzExUdOmTXPYf+rUqapbt66mTJlib69bt27av3+/ypcvn/9EAgAAAHlAP5x+OIDig4I5ABQjUVFR9s52y5YtHbZ9++23mj59uv71r3/pueees69v06aNbr75Zv373//WW2+9ZV+flJSk+fPnq0OHDg7nefnll+3LhmHozjvvVL169dS2bVv9/PPPatSoUbZxOPPvf/9bR48e1datW9W8eXNJUufOnZWenq4PPvhAw4cP180332zfv1y5clq6dKm8vb0lSaGhoWrevLlWrFihvn375ipXAAAAgLvQD6cfDqD4YEoWACghli5dKovFogEDBigtLc3+VbVqVTVu3Fjr16932L9ChQqZOumStG/fPvXv319Vq1aVt7e3SpUqpbZt20qSfvvtN5diW7t2rerXr2/vpNsMHDhQhmHYR8zYdO/e3d5Jl6RGjRpJkg4ePOhS+wAAAEBBoR8OAEULI8wBoIT4888/ZRiGw+2eN6pVq5bDa2e3baampqpNmzby8/PT66+/rptvvln+/v46fPiwoqOjdenSJZdiO3PmjCIjIzOtDw0NtW+/UXBwsMNrX19fSXK5fQAAAKCg0A8HgKKFgjkAlBCVKlWSxWLRpk2b7B3bG/11ncViybTP2rVrdezYMa1fv94+mkWSzp07l6/YgoODdfz48Uzrjx07Zo8dAAAAKIrohwNA0cKULABQzGQ1yqNHjx4yDENHjx7V7bffnumrYcOGOZ7b1nn/a6f+ww8/zHUcznTs2FG//vqrtm/f7rD+008/lcViUfv27XM8BwAAAGAm+uEAUDwwwhwAihlbh/utt95S165d5e3trUaN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77+fZd7+avDgwfLx8dGsWbOc9p0lafr06ZKkxx9/XF5eXvr99981evRo3X777QoJCZGvr68iIiL0xBNP6NChQ7lqV8p+Ssob+75/lZfPRK6qVauWli1bprp162r79u368MMPc4z9ypUrevfdd3XrrbeqQoUK8vf3V3h4uO69916tXr1a0vXPQZK0YcMGh2vE9nnhxikfd+/erV69eikkJEReXl72fOQ0Lco333yju+++W+XLl1e5cuXUuXNnbdu2LdN+2f37+OvnGFevb5vLly9r4sSJatiwofz9/RUYGKg2bdo4na71xhwcOHBAffv2VaVKleTn56emTZs6/dwJIHuMMAeKAMMwHF6PHj1aXl5eatGihapXr65z587pq6++0ogRI/Tdd99pzpw5Ts/z7LPPavPmzerevbu6desmb29vSRmd2WHDhqlVq1bq1KmTQkJCdPz4cS1evFg9evTQhx9+qL/97W+Zznft2jV16tRJSUlJuv/++3X16lXFx8frwQcf1JdffqmYmBht375dXbt2la+vrxYsWKCnn35alSpVUp8+fRzO9eqrr2rcuHEKDg5W9+7dVblyZf3888965513tHz5cm3ZskXly5dXZGSkxo0bZ58T3HZLoCQ1adLEvrxy5UpFR0crLS1NPXr0UO3atXXkyBElJCRo2bJlWrdunW677bZc58hdSpcurXnz5unWW2/VoEGD9NNPPyk8PFySNHPmTM2aNUudOnXS6NGjMx2bkJCgFStWqGfPnmrXrp1++uknLVy4UOvWrdOWLVtUp06dHNt/4YUXNHnyZIWEhOjhhx9W2bJltXz5co0ZM0YrV67UmjVrVLp0aYdjrly5og4dOujs2bPq3LmzAgIC7J26jRs3atKkSWrfvr0efPBBlS1bVnv37tWCBQv0xRdf6Ouvv7Z/X9q1a6dz584pJiZGjRs31gMPPGBv48bvnTNbt25Vp06dlJqaqvvvv1/16tXTb7/9ptmzZ2vx4sVavXq10/kkp02bpi+++EL33Xef2rZtq61bt+qzzz7TTz/9pJ9//tlh5AsAAIAnmTlzptLS0tS7d281bNgwy/0ef/xxvfrqq/r999+1YcMGtW/f3mG7O/qQM2fO1BNPPCE/Pz/dd999CgsL0969e/Xxxx9ryZIl+vbbbx1G8e7fv1/t27fXwYMH1bRpU3sxf8+ePXr33Xf197//XUFBQRo3bpxiY2N18OBB+9SUkrJ9VtGjjz6ql19+WXFxcXr22Wczbf/f//4nq9Wqxx57zL7u2rVruvfee7Vq1SrVrVtX/fv3l5+fn9atW6dnnnlG3377rWbNmpVjHqpWraoePXro888/V0JCgvr37++w/dq1a/r000/l7e2tQYMGScrI/wcffKD27durVatWKl26tHbt2qUZM2boiy++0A8//KCwsLAc23aFq5+JXOHv76/nn39ejz/+uGbNmuX0e3OjRx99VJ999pkaNGigRx99VGXKlNGxY8e0efNmrVq1Sp06dVKTJk00btw4TZgwQREREQ4DpP46sOmPP/5Qy5YtVadOHQ0YMECpqakqV65cjnFv3bpVEydO1N13362nnnpKf/zxhxISErRx40Z9+eWXatOmjSvpcPn6ljIGWd1zzz3atGmT6tevr6eeekoXL17U/Pnz1a9fP/3444966623Mh138OBBNW/eXLVq1dIjjzyipKQkzZs3Tw888IBWr16tjh07uvRegBLJAOARJBnO/kmuXbvW8PLyMiwWi7F//37DMAzjjz/+yLRfenq68fDDDxuSjG+++cZh22OPPWZIMkJDQ419+/ZlOvby5cvG4cOHM61PSkoy6tWrZ1SoUMG4ePGiw7aIiAhDktGjRw/j8uXL9vUbN240JBnly5c3br/9duPs2bP2bfv37zdKlSplNGnSJNN7lGTceeedxrlz5xy2zZw505BkDBs2LFP7ERERmWK2xR0UFGRUqlTJ+O233xy27dq1yyhbtmymGHLKUXbatm1rSDIee+wxY9y4cU6/jh8/7nDMvHnzDElG69atjbS0NOPXX381/P39jSpVqhgnTpxwmgNJxpIlSxy2TZkyxZBkdOjQwen7sV0zhmEYmzdvNiQZERERxp9//mlff+3aNaNbt26GJOP11193OI/t+9yxY0cjNTU103v/888/jeTk5Ezrf/jhB8Pf39/o3Lmzw/r9+/fbc+XMunXrDEnGuHHj7OvS09ONOnXqGJKMuXPnOuw/Z84cQ5Jx8803G+np6fb148aNMyQZ5cqVM37++WeHY/r16+f0XAAAAJ6kffv2hiTjo48+ynFfW//mtddes69zVx9yz549RqlSpYybbrrJOHbsmMP+X331leHl5WXcf//9DutbtWplSDLefPPNTLGeOnXKuHTpkv21rS+dFWf9/k6dOhmSjJ07d2bav27dukapUqWM06dP29fZ+obDhg0z0tLS7OvT0tKMwYMHG5KMRYsWZRnDjZYvX25IMtq3b59p22effWZIMu677z77uiNHjjh8XrrxPF5eXsaTTz7psN5Zf9gwsv/8Y3t/69ats69z5TNRVmx9+Kzat/njjz8MSYa3t7dx7dq1LGM/d+6cYbFYjKZNmzp8P2xu/N4ZRsZn5bZt22YbmyTjxRdfdLqPs2vMlmdJxn/+8x+HbZ9//rkhyahdu7bDZwxn/z7+er6/ft9cub7feOMN+2ftG/N44sQJIzw83JBkbNq0yWkOxo8f73CulStXGpKMLl26ZBkDgMwomAMewvYLzlZgfemll4xevXoZPj4+hiRjxIgROZ5j27ZthiRjwoQJDuttv9jffffdPMf1zjvvGJKMDRs2OKy3FVITExMzHVOzZk1DkvHVV19l2ta+fXvD29vboWP0wAMPGJKMX375xWkMTZo0MUJCQjK1n1WHzfYBYOrUqU63Dx8+3JBk7Nq1y74uPzmydYKy+/rxxx8zHffkk08akox//vOfRoMGDQwvLy9j9erVmfazfdj56wcaw8jo5EdFRRmSjAMHDmR6Pzd25oYMGWJIMqZPn57pPLt37za8vLyMmjVrOqy3fZ+dxZ+THj16GL6+vsbVq1ft61wpmG/atMn+BxVnbB/IbrxGbR8aXn755Uz72/5A889//jPP7wkAAKCw1KtXz5BkrFixIsd9R40aZUgyhg4dal/nrj6kre+8bNkyp20/8MADhpeXl3H+/HnDMK5/JmnSpIlDsTErrhQUZ82aZUgynn/+eYf13377rSHJ6Nmzp31denq6ERwcbFSrVs1pcfbs2bOGxWIxevXqlWOstvNFREQYFovF2Lt3r8O2e+65x+kfKLLSoEGDTP1vdxXMXflMlJXcFswvXrxo//xz4wCdv8aenJxsSDJatWplWK3WHNvPTcG8SpUqTv8wYRjZF8z/WhT/6zHr16+3ryusgnlUVJRhsViMPXv2ZNr/o48+MiQZgwYNsq+z5SAyMtLpNV6jRg0jODg4yxgAZMaULICHmTBhgqSMOc6CgoLUunVrDRkyRAMGDLDvc+bMGb399ttavny59u3bpwsXLjic4+jRo7skeUMAAQAASURBVE7P7WzKCptffvlFb7/9tjZu3Kjjx49nmpPP2TmDgoJUq1atTOtDQ0O1f/9+NW3a1Om29PR0nThxQtWrV5eUMWdcqVKl9NlnnzmN7erVqzp16pTOnDmj4ODgLN+DzTfffCNJ+umnn7KcC1vKmM/vr3NBZpejnKxbty5Pc55PmTJFW7Zs0b/+9S9J0ksvvaS77747y/3btm2baZ23t7dat26txMRE/fjjj9k+FPbHH3+UpEy36UpSnTp1FBYWpv379+vcuXMKCgqyb/P19VXjxo2zPO+yZcv0wQcfaNu2bTp9+rTS0tIctp8+fTpfD4/KLm5Juvvuu7VlyxZt375dd911l8O222+/PdP+tilwzp4963JMAAAABc34/2kZs5t72ca2j7N989uHtPWt169fr++++y7T9pMnT8pqtWrv3r1q2rSpvv32W0lS586d5eVVMI9Ni46OVmBgoGbNmqVJkybZp1H89NNPJclh6o7ff/9dZ86c0U033aTXXnvN6fnKlCmj3bt356ptLy8vDR48WOPGjdOMGTPszx46ePCg1qxZo7CwMHXt2tW+v2EYmj17tmJjY7Vjxw6dPXvW4blTf50O0V3y85nIVbm5ViWpXLlyuvfee7VkyRLdeuutevDBB9W6dWu1aNFC/v7+LrXduHFjl6ZbbNOmjdPrtF27dtqwYYN+/PFHp/+GCkpKSooSExMVFhamm2++OdN22+fF7du3Z9rWpEkTp1OKhoeH268HALlDwRzwMLaOcVbOnTunZs2aaf/+/WrevLkeffRRVaxYUT4+Pvb5oa9cueL02KpVqzpd/+2336pDhw5KS0tTx44ddd999ykwMFBeXl766aeftHjxYqfnLF++vNPz+fj4ZLndtu3atWv2dWfOnFFaWpr9jwVZSU1NzVXB/MyZM5KuP3Anu/P9VVY5Kgh+fn7q3r27du7cKR8fHz311FPZ7l+lShWn620xnz9/Ptvjbduzeo/VqlXToUOHdP78eYeCeZUqVbLs/L733nsaNmyYKlSooE6dOqlGjRry9/eXxWLR559/rh07dmR5PeZWbuK+cb8bZXcNOntALgAAgKeoVq2adu/ercOHD+e4r+2B5s4GKeS3D2nrW7/99tvZ7mfrW587d06S7INjCkKZMmXUu3dvzZgxQ19++aW6du2qq1evau7cuQoJCXEoWNvi37t3b7afN5x9NsjKkCFD9OqrryouLk6vvfaa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XBUrVszUyXOmVatWGjVqlCZPnqwGDRqoV69eKlu2rFasWKFdu3apdevWGjlyZK7fr5eXl5599llNmjRJDRs21P3336+rV69q3bp1SkpKUvv27TP9YScgIEAtWrTQxo0bNWDAAN10003y9vbWfffdl+WtohaLRXFxcerUqZP69Omj+++/X3Xr1tWePXv0+eefq1y5cvr0008L7KFSAAAAZhk/frwuXLigf//732rcuLE6d+6sW265RdeuXdOWLVu0detWlSlTRvHx8erQoYPTc+S3D1m3bl198sknGjx4sG655RZ16dJFN998s65du6ZDhw5p06ZNCgkJcXho5qxZs9SuXTuNGjVKn332mdq2bSvDMLR37159+eWX2r17t72Y3LFjR82fP1/R0dHq2rWrypQpo4iICD3yyCM5xvboo49q/PjxGjdunK5du5ZpdLnNK6+8oh07duiDDz7QkiVL1KFDB1WvXl0nT57U3r179fXXX+uNN95Q/fr1c2zTxtvbW4MHD9arr76qb775RlWrVtW9997rsE/VqlXVt29fzZ07V02aNNE999yj8+fPa/Xq1fLz81OTJv/H3n3HZ1Xe/x9/nXvf2XsvIOwpGxVlOFChjmoVa93VWltbbbVOHNVarVa7vq36s8XWURfWSisOBLQKAiJbQBkhCWTvcSe57/v8/ghEYwYBc+fOeD95nAePnHOd63zunNzrc67zuSawcePGLh3vhhtu4G9/+xvXXXcdy5cvJz09nU2bNvHRRx8xf/78NhdFAvGdqKKioqUsqNfrpby8nE2bNrF69Wr8fj/z5s3jmWeeOeLkm/n5+UyfPp2RI0cyceJE0tPTqaqqYunSpRQUFPCjH/2oVfmfuXPn8s9//pOzzz6b4447DpvNxkknncRJJ53U5dg7Mm/ePH72s5/x5ptvMn78eL744guWLFmCy+Xi6aefbvUd4+yzz2b48OG88MIL5OXlMW3aNPbv38/rr7/O2WefzUsvvdSm/2P5+/75z3/Om2++yeuvv8748eM588wzqaur4+WXX6aoqIhbbrlFg4VEAs0UkV4BMLv6lNy2bZu5YMECMz4+3gwJCTEnTpxoPvXUU+bevXtNwLzssstatb/ssstMwNy7d2+Hfb7xxhvmtGnTzLCwMDMyMtI89dRTzVWrVpl/+9vfTMD829/+1qp9ZmammZmZ2W5f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5ET/8IkTJ5KdnY3b7aZfv36Vbh07dqy0D71ez8CBA3njjTcA2LZtG1A2WhuoMGL7TIwZM4Y1a9aUF8hPWLx4MVartbzfeevWrQH466+/Kmz39ddfn/GxR40axZ49e9i5c2eF5Z988skZ71OIk7355pvMmzePG2+8kW+//bbCBMITJ07k22+/9WC6upefn8/333/Pxx9/TG5urqfjCCGEEEII0WjVetLPrVu38vjjj1e5rnv37jz66KNnHUrUv+TEbD56dx2pyTkEBHkTHhUAmkZBvo2fvv2LIwfTuPaW0QQG+3g6qhBCnJK3tzfvv/8+6//YwIH9+wA40XU7H/A/6XOAzp278N577zVYvt27d5f3Jc/OzubLL7/kxx9/ZMqUKeVtR6ZNm8bHH3/MRRddxF133cWAAQMwGo0kJSWxdu1aLr74YqZMmcLbb7/NmjVrmDBhAq1ataK0tJQPPvgAKJs8E8pGo8fExLBixQrGjBlDUFAQISEh5YXt0/X444+zcuVKRo0axdy5cwkKCuLjjz/m22+/5fnnny+f8LN///507NiR2bNn43K5CAwMZPny5fz+++9n/DW7++67+eCDD5gwYQJPPfUU4eHhfPzxx+zfv/+M93nO05SyW33tu4l5/fXXuffee3n++ecr9fBu3749hw4d8lCyuncujKQXQgghhBCirtR6hLmfnx8//fRTlet++ukn/Bpx/xlRxu1ys/yTDaSn5BHTJozAIB+MRj1Gk4HgUF9axgRz9FAGq77c4umoQghx2o4cOVJeLO8LmIALKZsIdBxgBvoc33bfvr0cPXq0wbJdf/31DB48mMGDB3P11Vezbds2XnrpJZYsWVK+jV6v5+uvv+bhhx/myy+/ZMqUKVxyySU8++yzWCwWunfvDpSNqHe5XDz++OOMHz+ea6+9lszMTL7++msuuOCC8v29//77WK1WJk+eTP/+/XniiSdqnbtjx4788ccfdOzYkdtuu41LLrmE3bt3s3DhQu6///4K2b/55hs6derErbfeynXXXYfZbOb1118/469ZREQEv/zyC126dGHmzJlcc801WCyWs9qnECc7cuQIF154YZXrfH19ycvLa9hA9eRcG0kvhBBCiGbEWYjiKgRnoaeTiLpUWFjWw7yw8T6vtR5hfu211/LCCy/gcrmYPn06ERERpKWl8fHHH/PKK69wbxPoQ3OuizuYRuLRLMKj/NHpKo8IMxj1BIX4sH93EumpeYRHBjR8SCGEqKUvv/yy/GNfoCdwooP290APoNs/tr/vvvvqNdOMGTOYMWPGaW9vMBi47777asw1aNCgCo+1OmPGjClv0XI6Ti6inaxbt26n1Vqlffv2fP/996fc77p166q8/6JFiyot69y5Mz/88MNpZxU1U6DeJktqeuPLwd/fn/T09CrXxcfHE9ZMemWeSyPphRBCCNG8KKu6Em5LRvNqAVOSPB1H1JXOnSE5GVq0gKTG+bzWumD+9NNPk5GRwYsvvshLL71UvlzTNK655hqefvrpOg0o6l5CXCZOpwuLl6nabXz9vUg8mklCXIYUzIVoZjRNIyk1j8LiUowGPa2iAjGbjZ6OddaWLVtW/vG6KtZn/WP5smXL6r1gLoRovMaMGcPzzz/PxRdfXD5xraIouFwu3nrrrWpHnzc158pIeiGEEEIIIepKrQvmBoOBRYsW8dBDD7FmzRpycnIIDg5m5MiRdOrUqT4yijrmdLo41VgwRSkbh+ZyumvcTgjRtOw+kMLaPw5w9Fg2docLvU4hJMiHQX3aMGJQe8ymWv9aaBSSkpLYuHFjpeWTJ0/m+eef5/777+ebb76psG7Dhg0kJSURHR3dUDGF8Czt+K2+9t3EzJ8/n/79+9OlSxemTJmCoii8/vrrbN++ncTERD777DNPR6wT58pIeiGEEEIIIepKrXuYn9CxY0dmzpzJI488wq233irF8ibEP8AbBVDV6t/dOh0udDoFvwBrwwUTQtSrDduOsPCzP9gfl46vt5noyADCgn3JK7CxfPV2Pl7+Jw6ny9Mxz8g/W5Sc6J/91Vdf0bFjR1asWMF///tfzGZzhe2WL1/ekDGFEI1Iu3btWL9+PZ07d+bNN99E0zQWL15MSEgIv/32G61atfJ0xDpxYiR9cXFx+bLmOJJeCCGEEEKIunJGBXO73c4777zDVVddxQUXXFDe+3DFihUcOXKkTgOKutelZ0t8/b3IyymqdpvszELCIvzp0CWqAZMJIepLRlYhK77fiaZBTIsgvK1m9DodJpOBiFA/wkP82PJXIn9saZqv4fv27Sv/uHPnzmzatInbbrvt+NUyZcWh22+/nT///LPCCd69e/c2eFYhROPRpUsXVq9eTWFhIUlJSRQUFPDDDz/QuXNnT0erM/PnzychIYEuXbpw3333lY+kHzBgAIcPH+axxx7zdEQhhBBCCCEalVoXzLOysujXrx8zZ87kl19+4eeff6bw+KymX331FQsWLKjzkKJuBQb7MGh4R4oKSsnPK6kweZqmaWRnFuJWNYaf3w1TM+hrLISA7XuOkVdQQniIb5XrrV4mTEYDG7YeweVqeq2YbrvtNkaPHs3s2bPZsmULPXr0qHK7nj17smXLFmbPns3o0aO57bbbGjipEKKxWLlyJaqqAmVXpURFReHl5eXhVHXvXBlJL4QQQgghRF2pdbPaOXPmkJeXV16QMJn+njhy1KhRPPfcc3UaUNSP8yf2wmF38ufvh8jNLsJsNqJpGg67Cx9/L8Zf0oeBwzp4OqYQoo7sPZSKxWwsH3FdlUB/LzKyC0nLLCA6MrAB0529bt268fPPP5/Wtt7e3rzwwgv1nEiIxkfhVDOYnN2+m5rJkycTHh7Oddddx4wZM5rVqPJ/OjGS3m63k52dTWBgYLM8OSCEEEIIIURdqHXBfOXKlTz33HP06dMHt7viKMTo6GiSkpLqLJyoPwajnslXDqT3wLbs3HKUlGM5KDqF1m3D6NG3NRFRTatYJoSomcPpRq+r+aIinU6Hqmq43GoDpRJCCM/59ttvWbRoEa+99hoLFixgwIAB3HDDDUybNg1f36qvxmnqToykF0IIIYQQQlSv1gXzgoICYmJiqlzndDpxuZrmhHHnIkVRaBUbSqvYUE9HEULUs4gQXxKTsmvcpsTmwGI24u/bfEYdrl+/nnfeeYdbbrmF8847z9NxhPAs7fitvvbdxIwfP57x48eTl5fHJ598wocffsgtt9zC3XffzdSpU7n++usZPXq0p2Oetfnz59e4XlEU6WMuhBBCCCHESWpdMI+NjWXDhg1VvoHYtGkTHTt2rJNgQggh6k6f7q3Y8lcipaVOLJbKcxNomkZufgnDBrQj0N/qgYR1T1VVpk2bRlJSEuvWrSM+Ph7dKUbZi4ahaRqaBopCjW2CRB2TgnmVAgICmDVrFrNmzWLfvn0sXLiQDz/8kCVLljSLgSBPPPFEjeulYC6EEEIIIURFta4cXH311Tz33HOsWLGifLJIRVHYvHkzr776Ktdee22dhxRCCHF2OreLoGuHSJIz8iixOSqsc7lVjqXmEhzozbCB7TyUsO5t2LChvE3YsWPH2Lhxo4cTCZfTTUF+CekpeaQl55CWnEtebjEOR9MvSoraKyoq4u677yYqKgqLxUKvXr349NNPT/v+K1asYMSIEfj5+eHt7U3Xrl159913zziPpmkcO3aMY8eOUVBQUGFS9KZMVdVKt6ysLP73v//RrVs34uPjPR1RCCGEEKJK2rDlZPf9Bm3Yck9HEXVpxQr444+y/xupWo8wf+CBB1i/fj1TpkwhMLCsz/WFF15IdnY248aN46677qrzkEIIIc6OwaBn+pQBKArsOZRGelYhJqMet1tFVTXCw/yYNqkfraKCPB21zixbtqzS50OGDPFQGlFa6iQ3qxCXy42iKOh0CqqqUZBXQnFRKYFBPli9zZ6OKRrQ1KlT2bx5M88++ywdOnTgk08+4aqrrkJVVaZPn17jfZ999lkeeeQRbr31Vh566CGMRiP79+/H4XDUeL+qHD58mEWLFrF48WKSk5OJiori3nvv5frrrz/Th9boBQUFccMNN5CRkcGdd97J8uXyJlQIIYQQjVBQX5yuDAgK83QSUZf69vV0glOqdcHcaDSyatUqli5dyrfffkt6ejohISFMnDiRadOmyeXuQghRjbycYpITs3G53AQG+RDdOrhBXzP9fCzcdNVQDsVn8NfeJLJzi7FYjHRqG0GPzi3wtjafYqWmaVUWzBcsWNBgLUBO9zhr165l5MiR9RumAZWUlPD8888zcuTI8sflcrnJzSpkyacfc9/9d7Lpz520atkKKDuZ43S6yM0uQq/XYa6iZZBoflatWsWPP/5YXiQHGDVqFAkJCdx///1ceeWV6PX6Ku+7detWHnnkEZ555hnmzJlTvnzMmDG1yrBw4UIWLlzI+vXrMZlMTJ48meuvv54LLrjgnPl7dsCAATz99NOejiGEEEIIIUSjUuuCOZQVAaZNm8a0adPqOo8QQjQ7BXkl/LRyJzu3HKUw34amaVi8TMS0DWXU+B507NqiwbLo9To6tY2gU9uIBjumJ2zZsoXExMQKyxISEti6dSv9+vVrkAwbNmyo8PmTTz7J2rVrWbNmTYXlXbp0aZA8DaWkpIR58+YBlBfMbcV2XC43F144nk6dfiA8LPzvOyhgNBpwOJwUF9ulYF6fGlEP8+XLl+Pj48Pll19eYfn111/P9OnT+fPPP6u9IuT111/HbDZzxx13nGlaAG688UZ69+7Nq6++ytVXX11+5eS5ZOfOnfj4+Hg6hhBCCCGEEI3KGRXMAfbv388vv/xCVlYWN954IxEREaSkpBAYGIiXl1ddZhRCiCarIK+ED99aQ9z+NAKCrLSMDUZRFGzFDg7uTSE5MYfLrhtCz36xno7arJw8unxgW/gz7u/lDVUwHzRoUIXPQ0ND0el0lZb/U0lJCVZr85h49YTiYgeKohASEkJISEjlDZSykzmlJXbcAVb0+nNjdO+5bPfu3XTu3BmDoeKfoj169ChfX13B/Ndff6Vz584sW7aMJ598ksOHDxMZGck111zD/PnzMZlMp5Vhx44d5cdrzhYvXlxpmd1u56+//uKDDz7gmmuu8UAqIYQQQojTkLwSc246OMOh5WRPpxF1ZeVKsNnAywsmTvR0mirV+h2p2+3mxhtvpGvXrsycOZO5c+eSkpICwC233MIzzzxT5yGFaGxUVSX+UDqrvtjMJ++uY/n/rWfX1ngcdqeno4lGZs2qv4jbn0Z062ACg33Q6XQoioLVx0yr2BAcdicrP9tMUaHN01GbFJfLxVVXXUVoaGh5Efbk24svvgiAToH3by77H2DBggVVbh8aGsr06dNxuRp28smRI0fSrVs3fv31V4YMGYLVauWGG24Ayq7meuKJJyrdp3Xr1syYMaPCsrS0NG655Raio6MxmUzExsYyb96803o8qqry/PPP06lTJ8xmM2FhYVx33XXlE6b+M+tvv/3GoEGD8PLyokWLFjz22GO43W4A4uPjCQ0NBWDevHkoioKiKNx510wUncKnSz8hIiqQxGN/j/6fculERowazNZtW5h8yXh8fLxp3bo1CxcuBODbb7+lT58+WK1WunfvzurVqyvkmjFjBq1bt670uJ544olKbXEUReH2229n4cKFdOzYES8vL/r168fGjRvRNI0XXniB2NhYfHx8GD16NIcPHz7l109UVFBQUOFmt9ur3C47O5ugoMpzJpxYlp2dXe0xkpOTOXToEHfeeSd33nknP/30EzNmzGDBggW16jt+LhTLoexn5J+3W265hffff5+rr76aBQsWeDqiEEIIIUSVlC2zCNx9M8qWWZ6OIurSrbfCFVeU/d9I1XqE+X/+8x8++eQTXnjhBcaNG0e3bt3K140fP55FixYxf/78Og0pRGNSXFjKl//3B3t3JmK3OdAb9KhulfVr99OiVTCX/es8WsaGejqmaAQK8krYueUo/oFeGI2Ve/EqikJ4VABJ8dns3pbIoBEdPZCyaTp8+DCffvrpKbcb3gm6Rpf9v25fWaG9ukLckiVLmDt3Lp06darruDVKTU3lmmuuYc6cOTz99NO17p2clpbGgAED0Ol0zJ07l7Zt27Jhwwaeeuop4uPjywvP1Zk5cybvvvsut99+OxMnTiQ+Pp7HHnuMdevWsW3btgojwtPS0pg2bRoPPvgg8+fP59tvv+Wpp54iNzeX119/ncjISFavXs24ceO48cYbuemmm8ru6DahadX37MjMyOCe+25n5q130KVrB9588w1uuOEGjh07xhdffMHDDz+Mv78/8+fP55JLLuHIkSNERUXV6ut0wsqVK9m+fTvPPvssiqLwwAMPMGHCBP71r39x5MgRXn/9dfLz87n33nu59NJL2bFjR4P1vW8OWrZsWeHzxx9/vMoTP1Bzn/+a1qmqSmFhIUuWLClvDzhq1CiKi4t55ZVXmDdvHu3atavyvvPnz+emm24iKirqlH+vKorCY489VuM2TcHRo0crLbNYLISHh1extRBCCCGEEKLWBfNFixbx2GOPce+995aPKDshNja2yj/KhWgunE4Xny38jb+2HCU03J/wqIDyN/VOh4uko5l88u46brjrAkIj/D2cVnhacmI2BfkltGgVXO02er0ORQfxcelSMK+FDh06cPnll/P5559XWB5ghRDfso9DfOHFq8s+XnA13L4IsgrLPs8qhLySivu8/PLL6dChQ/0Gr0JOTg6ff/45o0ePPqP7P/HEE+Tm5rJnzx5atSqbSHPMmDF4eXkxe/Zs7r///mr7pO/fv593332XWbNm8d///rd8ee/evRk4cCAvv/wy//nPf8qXZ2dns2LFCiZPLrsc8oILLsBms/HWW28xZ84cWrVqRd/jM55HR0eXt58pyCshP7e42j7XObk5LP5wKYMGDSQ41JeBAwcQFhbGs88+y+HDh8uL41FRUfTq1Ytly5adcf9qu93ODz/8gLe3N1BWFL3kkktYu3Yt27ZtK39Nz8zM5O6772b37t107979jI7V6DRAD/Njx47h5+dXvthsrnoy4eDg4CpPXuXk5ABUOfr85PumpaVx4YUXVlg+fvx4XnnlFbZt21ZtwfyJJ55g3LhxREVFVVvIP6G5FMxjYmI8HUEIIYQQQogmpdYtWZKTkxk8eHCV6ywWC4WFhWcdSojG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6+vR4vHApNUiNzaWD/okIiIiIiIiIoogTJgTBZnJqD1aNlZ0OYrY5/NDAmA0aEMbHIWURqXCsPj4cIdBRAOVODoFa9tERERERIGUeCbc9nJoo1LDHQkF0syZQG0tEMH5ESbMiYJs+JAUrNm4D44WN6JM+k7XqatvRnycGUMHJYY4OiIiGigk0ToFa9tERERERIEkpi5HfXU1EhMTT3jnPvUhb78d7ghOiLUAiIIsMy0WI4eloLrWAXcnJVeaHC44XV6cMWkQDHqOMCciIiIiIiIiIgoXjjAnCjJJknDZRafB7fFhz4EKSBJgNukgFAF7sxtqjQqzpg3D7DPywh0qERH1ZyzJQkREREREdEJMmBOFQJRZjxuunI4f9pZi07YCVFY3AioJk8en4LRx2Rg+OAWyzBuMiIiIiCJBY2MjHA5Hl8+fCRaPxwOtNrR3HBYVFcHn44PnKTg8HjeKiopC3q7FYkFCQkLI2yUiov6BCXOiENFp1Zg0LhuTxmXD51cgSxKT5EREREQRpra2Fn/7+wvYe/gIhAjd7RMejxslhYXIyh0EtTp0l2nOlmaUV1bB6/WErE0aGOwNNhQcyccDjzwGnU4X0rajDHq89spLTJoT9XHSurMR11wGyZQGnL0u3OFQoMyZA1RVAUlJwLrI7FcmzInCQK3i4wOIiIiIIlFTUxOcHg9mXnML4lIyQtbuoR2bUfTCUky/6makZQ8KabsfvLAUfr8/ZG3SwOBqaYas0WDmtYtC+jddV1GKDW+9iKamJibMifo6+0FonGUQSnO4I6FAOngQKCsDGhvDHUmXmDAnIiIiGgAkAFKQBsvyfinqj+KS05GclROy9mrKS462mxqWdomCJdR/00RERL3FYa5EREREREREREREROAIcyIiIqKBQYjWKVjbJiIiIiIi6gc4wpyIiIiIiIiIiIiICEyYExEREREREREREREBYMKciIiIiIiIiIiIiAgAa5gTERERDQzi6BSsbRMREREREfUDHGFORERERERERERERASOMCciIiIaEKSjU7C2TUREREQUSGLkH2FvqIQ5Opnnm/3J4sWAwwGYzeGOpEtMmBMREREREREREVFkGXwzWqqrYU5MDHckFEg33xzuCE6ICXMiIiKigUA5OgVr20RERERERP0Aa5gTEREREREREREREYEjzImIiIgGEBHuAIiIiIiI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vVPtq7/97W8CgDj//PPFf/7zH/HZZ5+Je++9V6jVanH22WeHY1cGhE8//VS8//774rXXXhMAxGWXXSbef/998f7774vm5mYhROf95XK5xMiRI0VGRoZ4++23xZo1a8T8+fOFWq0WGzZsCNfuEFEAnOpxnMeFwOH1RPjxOiH8eP4ffjyvjww8Xw+/U+2DgfC5zIQ59Zjdbhe33367SE5OFlqtVowZM0asWLGi3TrXXXedACAKCgra5mVlZQkAnU5ZWVmh3YkB4lT7qjM8EQ6u3vRVXV2duOWWW0RSUpLQaDRi6NCh4i9/+Yvw+/0h3IOBozd99cEHH4jp06eL+Ph4YTKZxMiRI8UjjzwiHA5HCPdgYOnus+dY/3TVX5WVleKXv/yliI2NFXq9Xpx++ulizZo1od8JIgqo3hzHeVwIDF5PhB+vE8KP5//hx/P6yMDz9fA71T4YCJ/LkhBHC14REREREREREREREQ1grGFORERERERERERERAQmzImIiIiIiIiIiIiIADBhTkREREREREREREQEgAlzIiIiIiIiIiIiIiIATJgTEREREREREREREQFgwpyIiIiIiIiIiIiICAAT5kREREREREREREREAJgwJyIiIiIiIiIiIiICwIQ5EYXIDz/8gIULFyInJwd6vR5msxkTJkzAU089BZvNFu7wAmrv3r146KGHUFhYGJTtf/rpp3jooYc6XZadnY3rr78+KO0SEREREUWy7777DpdddhlSUlKg1WqRnJyMSy+9FN9++22HdR966CFIkoTa2toTbnfWrFmYNWtWECJu9c477+CZZ57pdJkkSV2e+wfDzp07IUkS7rvvvi7XOXToECRJwu23397j7R77fRMR9QVMmBNR0P3jH//AxIkTsXnzZvz+97/H6tWrsXLlSlx22WV46aWXcOONN4Y7xIDau3cvHn744aAmzB9++OFOl61cuRIPPvhgUNolIiIiIopUzz33HM444wyUlpbiqaeewtq1a7F06VKUlZVh+vTp+Pvf/37K237hhRfwwgsvBDDa9rpLmH/77be46aabgtb2T40dOxYTJ07Em2++Cb/f3+k6r7/+OgD0u+s4IqJj1OEOgIj6t2+//RaLFi3C3Llz8Z///Ac6na5t2dy5c3H33Xdj9erVYYww/FpaWmA0GgOyrfHjxwdkO0REREREfcXXX3+NO++8ExdccAFWrlwJtfrHVMcVV1yB+fPn44477sD48eNxxhlnnPT2R4wYEchwT8rpp58e8jZvvPFG/OY3v8GqVaswb968dsv8fj/efPNNTJw4EWPHjg15bEREocAR5kQUVI899hgkScIrr7zSLll+jFarxcUXXwwAUBQFTz31FPLy8qDT6ZCYmIhf/vKXKC0tbfeaWbNmYdSoUdi8eTNmzJgBo9GI3NxcPPHEE1AUpd26DQ0NuPvuu5Gbm9u2zQsuuAD79+9vW8fj8eDRRx9tazchIQELFy5ETU1Nu21lZ2dj3rx5WL16NSZMmACDwYC8vDy89tprbessW7YMl112GQBg9uzZkCQJkiRh2bJl7WL/4osvMG3aNBiNRtxwww0AgPfeew/nnHMOUlJSYDAYMHz4cNx3331obm5u2/7111+P559/HgDati1JUtto9s5KshQXF+Oaa65BYmIidDodhg8fjqeffrrd76qwsBCSJGHp0qX461//ipycHJjNZkydOhXfffdd551LRERERBQBHn/8cUiShBdffLFdshwA1Go1XnjhBUiShCeeeKLDa0tKSrBgwQJYLBZYrVZcc801Ha4DOivJ0tNrCKB1BPnUqVNhNpthNpsxbtw4vPrqq23b/uSTT1BUVNTu/P6Y40uyHCuXcuy1x1u1ahUkScJ///vftnmHDh3CVVdd1e464Ni1RHeuuuoqGAyGtpHkx/vss89QVlZ2UtcwXemq3Exn1zSVlZW45ZZbkJ6eDq1Wi5ycHDz88MPw+XwnbIeI6GRxhDkRBY3f78e6deswceJEZGRknHD9RYsW4ZVXXsFtt92GefPmobCwEA8++CA2bNiAbdu2IT4+vm3dyspKXH311bj77ruxZMkSrFy5Evfffz9SU1Pxy1/+EgBgt9sxffp0FBYW4t5778WUKVPgcDjwxRdfoKKiAnl5eVAUBZdccgm+/PJL3HPPPZg2bRqKioqwZMkSzJo1C1u2bIHBYGhrd+fOnbj77rtx3333ISkpCf/85z9x4403YvDgwTjzzDNx4YUX4rHHHsMf/vAHPP/885gwYQIAYNCgQW3bqKiowDXXXIN77rkHjz32GGS59bvLQ4cO4YILLsCdd94Jk8mE/fv348knn8T333+PdevWAQAefPBBNDc349///ne7WowpKSmd/k5ramowbdo0eDwePPLII8jOzsbH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", - "text/plain": [ - "

" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "## 4. Results Analysis and Visualization\n", - "\n", - "# Analyze the optimization results and create comprehensive visualizations:\n", - "# # Get optimization history and training data\n", - "history = optimizer.get_optimization_history()\n", - "train_X, train_Y = optimizer.get_training_data()\n", - "\n", - "# Create comprehensive visualization\n", - "fig, axes = plt.subplots(2, 2, figsize=(15, 12))\n", - "\n", - "# 1. Optimization progress\n", - "iterations = [h['iteration'] for h in history]\n", - "best_values = [h['best_value'] for h in history]\n", - "\n", - "axes[0, 0].plot(iterations, best_values, 'b-o', linewidth=2, markersize=6)\n", - "axes[0, 0].axhline(y=true_score, color='r', linestyle='--', alpha=0.7, \n", - " label=f'True optimum: {true_score:.3f}')\n", - "axes[0, 0].set_xlabel('Iteration')\n", - "axes[0, 0].set_ylabel('Best Observed Value')\n", - "axes[0, 0].set_title('Optimization Progress')\n", - "axes[0, 0].legend()\n", - "axes[0, 0].grid(True, alpha=0.3)\n", - "\n", - "# 2. Improvement over iterations\n", - "improvements = [best_values[i] - best_values[0] for i in range(len(best_values))]\n", - "axes[0, 1].plot(iterations, improvements, 'g-o', linewidth=2, markersize=6)\n", - "axes[0, 1].set_xlabel('Iteration')\n", - "axes[0, 1].set_ylabel('Improvement from Initial')\n", - "axes[0, 1].set_title('Cumulative Improvement')\n", - "axes[0, 1].grid(True, alpha=0.3)\n", - "\n", - "# 3. Parameter exploration (2D projection)\n", - "scatter = axes[1, 0].scatter(train_X[:, 0], train_X[:, 1], c=train_Y.squeeze(), \n", - " cmap='viridis', alpha=0.6, s=50)\n", - "axes[1, 0].scatter(best_params[0], best_params[1], c='red', s=200, marker='*', \n", - " label='Best found', edgecolor='black', linewidth=2)\n", - "axes[1, 0].scatter(true_params[0], true_params[1], c='orange', s=200, marker='*', \n", - " label='True optimum', edgecolor='black', linewidth=2)\n", - "axes[1, 0].set_xlabel('Concentration')\n", - "axes[1, 0].set_ylabel('Print Speed (mm/s)')\n", - "axes[1, 0].set_title('Parameter Exploration')\n", - "axes[1, 0].legend()\n", - "plt.colorbar(scatter, ax=axes[1, 0], label='Objective Value')\n", - "\n", - "# 4. Objective value distribution\n", - "axes[1, 1].hist(train_Y.squeeze().numpy(), bins=20, alpha=0.7, color='skyblue', edgecolor='black')\n", - "axes[1, 1].axvline(best_score, color='red', linestyle='--', linewidth=2, label=f'Best: {best_score:.3f}')\n", - "axes[1, 1].axvline(true_score, color='orange', linestyle='--', linewidth=2, label=f'True: {true_score:.3f}')\n", - "axes[1, 1].set_xlabel('Objective Value')\n", - "axes[1, 1].set_ylabel('Frequency')\n", - "axes[1, 1].set_title('Objective Value Distribution')\n", - "axes[1, 1].legend()\n", - "axes[1, 1].grid(True, alpha=0.3)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "polyprintenv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.2" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/aamp_app/optimizer/notebooks/interactive_notebook.ipynb b/aamp_app/optimizer/notebooks/interactive_notebook.ipynb deleted file mode 100644 index 31e709b..0000000 --- a/aamp_app/optimizer/notebooks/interactive_notebook.ipynb +++ /dev/null @@ -1,299 +0,0 @@ -{ - "cells": [ - { - "cell_type": "raw", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "# Interactive Bayesian Optimization\n", - "\n", - "This notebook provides an interactive interface for experimenting with different Bayesian optimization settings.\n", - "\n", - "## Quick Start\n", - "1. Run the setup cells\n", - "2. Adjust parameters in the configuration section\n", - "3. Execute the optimization\n", - "4. Analyze results with interactive plots\n", - "\n", - "## Features\n", - "- **Interactive Parameter Tuning**: Easily modify optimization settings\n", - "- **Real-time Visualization**: See results as they develop\n", - "- **Comparison Tools**: Compare different optimization runs\n", - "- **Export Results**: Save findings for later analysis\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import torch\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "from typing import List, Tuple, Dict\n", - "import time\n", - "import warnings\n", - "warnings.filterwarnings('ignore')\n", - "\n", - "# Import our optimization package\n", - "from bayesian_optimizer import BayesianOptimizer\n", - "from objective_function import MockObjectiveFunction\n", - "\n", - "# Set up plotting\n", - "plt.style.use('default')\n", - "plt.rcParams['figure.figsize'] = (12, 8)\n", - "plt.rcParams['font.size'] = 12\n", - "\n", - "# Global variables for storing results\n", - "optimization_results = []\n", - "current_optimizer = None\n", - "current_objective = None\n", - "\n", - "print(\"Interactive Bayesian Optimization Setup Complete!\")\n", - "print(\"Now you can configure and run optimizations in the cells below.\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# EXPERIMENT CONFIGURATION - MODIFY THESE VALUES!\n", - "\n", - "# Optimization Settings\n", - "BATCH_SIZE = 8 # Number of candidates per iteration (try: 4, 8, 12, 16)\n", - "N_ITERATIONS = 15 # Number of optimization iterations (try: 10, 15, 20, 30)\n", - "N_INITIAL_POINTS = 10 # Initial random samples (try: 5, 10, 15, 20)\n", - "NOISE_STD = 0.1 # Objective function noise level (try: 0.05, 0.1, 0.2)\n", - "SEED = 42 # Random seed for reproducibility (try: 42, 123, 456)\n", - "\n", - "# Experiment Name (for tracking different runs)\n", - "EXPERIMENT_NAME = \"default_run\"\n", - "\n", - "# Display configuration\n", - "print(\"CURRENT CONFIGURATION\")\n", - "print(\"=\" * 50)\n", - "print(f\"Experiment Name: {EXPERIMENT_NAME}\")\n", - "print(f\"Batch Size: {BATCH_SIZE}\")\n", - "print(f\"Iterations: {N_ITERATIONS}\")\n", - "print(f\"Initial Points: {N_INITIAL_POINTS}\")\n", - "print(f\"Noise Level: {NOISE_STD}\")\n", - "print(f\"Random Seed: {SEED}\")\n", - "print(f\"Total Evaluations: {N_INITIAL_POINTS + N_ITERATIONS * BATCH_SIZE}\")\n", - "print(\"=\" * 50)\n", - "print(\"=->Change values above and re-run this cell to update configuration!\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def run_optimization_experiment():\n", - " \"\"\"Run a complete optimization experiment with current settings.\"\"\"\n", - " global current_optimizer, current_objective\n", - " \n", - " print(f\"Starting Run: {EXPERIMENT_NAME}\")\n", - " print(\"=\" * 60)\n", - " \n", - " # Define parameter bounds\n", - " bounds = torch.tensor([\n", - " [2, 20], # concentration\n", - " [0.01, 20], # print_speed\n", - " [50.0, 100.0], # gap_size\n", - " [6.0, 12.0] # volume\n", - " ]).T\n", - " \n", - " # Create optimizer and objective function\n", - " current_optimizer = BayesianOptimizer(\n", - " bounds=bounds,\n", - " batch_size=BATCH_SIZE,\n", - " noise_variance=0.01,\n", - " seed=SEED\n", - " )\n", - " \n", - " current_objective = MockObjectiveFunction(noise_std=NOISE_STD, seed=SEED)\n", - " \n", - " # Get true optimum for comparison\n", - " true_params, _ = current_objective.get_optimal_parameters()\n", - " true_score = current_objective.evaluate_at_optimal()\n", - " \n", - " print(f\"True optimal score: {true_score:.4f}\")\n", - " print(f\"Starting optimization...\")\n", - " \n", - " # Run optimization\n", - " start_time = time.time()\n", - " \n", - " best_params, best_score = current_optimizer.optimize(\n", - " objective_function=current_objective,\n", - " n_iterations=N_ITERATIONS,\n", - " n_initial_points=N_INITIAL_POINTS,\n", - " verbose=False # Reduce output for cleaner notebook\n", - " )\n", - " \n", - " end_time = time.time()\n", - " optimization_time = end_time - start_time\n", - " \n", - " # Store results\n", - " result = {\n", - " 'experiment_name': EXPERIMENT_NAME,\n", - " 'batch_size': BATCH_SIZE,\n", - " 'n_iterations': N_ITERATIONS,\n", - " 'n_initial_points': N_INITIAL_POINTS,\n", - " 'noise_std': NOISE_STD,\n", - " 'seed': SEED,\n", - " 'best_params': best_params,\n", - " 'best_score': best_score,\n", - " 'true_score': true_score,\n", - " 'optimization_time': optimization_time,\n", - " 'total_evaluations': len(current_optimizer.train_X),\n", - " 'optimizer': current_optimizer,\n", - " 'objective': current_objective\n", - " }\n", - " \n", - " optimization_results.append(result)\n", - " \n", - " print(\"\\nOPTIMIZATION COMPLETE\")\n", - " print(\"=\" * 60)\n", - " print(f\"Time: {optimization_time:.2f} seconds\")\n", - " print(f\"Total evaluations: {result['total_evaluations']}\")\n", - " print(f\"Best score: {best_score:.4f}\")\n", - " print(f\"True optimal: {true_score:.4f}\")\n", - " print(f\"Gap: {(true_score - best_score):.4f}\")\n", - " \n", - " # Parameter comparison\n", - " param_names = ['concentration', 'print_speed', 'gap_size', 'volume']\n", - " print(f\"\\nBest Parameters Found:\")\n", - " for i, (name, value) in enumerate(zip(param_names, best_params)):\n", - " print(f\" {name}: {value:.3f}\")\n", - " \n", - " return result\n", - "\n", - "# Run the experiment\n", - "experiment_result = run_optimization_experiment()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def plot_optimization_results(optimizer, objective):\n", - " \"\"\"Create comprehensive plots of the optimization results.\"\"\"\n", - " \n", - " history = optimizer.get_optimization_history()\n", - " train_X, train_Y = optimizer.get_training_data()\n", - " true_params, _ = objective.get_optimal_parameters()\n", - " true_score = objective.evaluate_at_optimal()\n", - " \n", - " # Create the visualization\n", - " fig, axes = plt.subplots(2, 3, figsize=(18, 12))\n", - " \n", - " # 1. Optimization Progress\n", - " iterations = [h['iteration'] for h in history]\n", - " best_values = [h['best_value'] for h in history]\n", - " \n", - " axes[0, 0].plot(iterations, best_values, 'b-o', linewidth=2, markersize=6)\n", - " axes[0, 0].axhline(y=true_score, color='r', linestyle='--', alpha=0.7, \n", - " label=f'True optimum: {true_score:.3f}')\n", - " axes[0, 0].set_xlabel('Iteration')\n", - " axes[0, 0].set_ylabel('Best Observed Value')\n", - " axes[0, 0].set_title('Optimization Progress')\n", - " axes[0, 0].legend()\n", - " axes[0, 0].grid(True, alpha=0.3)\n", - " \n", - " # 2. Improvement Rate\n", - " improvements = [best_values[i] - best_values[0] for i in range(len(best_values))]\n", - " axes[0, 1].plot(iterations, improvements, 'g-o', linewidth=2, markersize=6)\n", - " axes[0, 1].set_xlabel('Iteration')\n", - " axes[0, 1].set_ylabel('Improvement from Initial')\n", - " axes[0, 1].set_title('Cumulative Improvement')\n", - " axes[0, 1].grid(True, alpha=0.3)\n", - " \n", - " # 3. Parameter Space Exploration (Concentration vs Print Speed)\n", - " scatter = axes[0, 2].scatter(train_X[:, 0], train_X[:, 1], c=train_Y.squeeze(), \n", - " cmap='viridis', alpha=0.6, s=50)\n", - " axes[0, 2].scatter(optimizer.best_parameters[0], optimizer.best_parameters[1], \n", - " c='red', s=200, marker='*', label='Best found', \n", - " edgecolor='black', linewidth=2)\n", - " axes[0, 2].scatter(true_params[0], true_params[1], c='orange', s=200, marker='*', \n", - " label='True optimum', edgecolor='black', linewidth=2)\n", - " axes[0, 2].set_xlabel('Concentration')\n", - " axes[0, 2].set_ylabel('Print Speed (mm/s)')\n", - " axes[0, 2].set_title('Parameter Space Exploration')\n", - " axes[0, 2].legend()\n", - " plt.colorbar(scatter, ax=axes[0, 2], label='Objective Value')\n", - " \n", - " # 4. Gap Size vs Volume\n", - " scatter2 = axes[1, 0].scatter(train_X[:, 2], train_X[:, 3], c=train_Y.squeeze(), \n", - " cmap='viridis', alpha=0.6, s=50)\n", - " axes[1, 0].scatter(optimizer.best_parameters[2], optimizer.best_parameters[3], \n", - " c='red', s=200, marker='*', label='Best found', \n", - " edgecolor='black', linewidth=2)\n", - " axes[1, 0].scatter(true_params[2], true_params[3], c='orange', s=200, marker='*', \n", - " label='True optimum', edgecolor='black', linewidth=2)\n", - " axes[1, 0].set_xlabel('Gap Size (mm)')\n", - " axes[1, 0].set_ylabel('Volume (μL)')\n", - " axes[1, 0].set_title('🔍 Gap Size vs Volume')\n", - " axes[1, 0].legend()\n", - " plt.colorbar(scatter2, ax=axes[1, 0], label='Objective Value')\n", - " \n", - " # 5. Objective Value Distribution\n", - " axes[1, 1].hist(train_Y.squeeze().numpy(), bins=20, alpha=0.7, color='skyblue', \n", - " edgecolor='black')\n", - " axes[1, 1].axvline(optimizer.best_observed_value, color='red', linestyle='--', \n", - " linewidth=2, label=f'Best: {optimizer.best_observed_value:.3f}')\n", - " axes[1, 1].axvline(true_score, color='orange', linestyle='--', linewidth=2, \n", - " label=f'True: {true_score:.3f}')\n", - " axes[1, 1].set_xlabel('Objective Value')\n", - " axes[1, 1].set_ylabel('Frequency')\n", - " axes[1, 1].set_title('Objective Value Distribution')\n", - " axes[1, 1].legend()\n", - " axes[1, 1].grid(True, alpha=0.3)\n", - " \n", - " # 6. Parameter Convergence\n", - " param_names = ['concentration', 'print_speed', 'gap_size', 'volume']\n", - " eval_order = np.arange(len(train_X))\n", - " \n", - " for i, name in enumerate(param_names):\n", - " color = plt.cm.Set1(i)\n", - " axes[1, 2].scatter(eval_order, train_X[:, i], alpha=0.6, s=30, \n", - " c=color, label=name)\n", - " axes[1, 2].axhline(y=true_params[i], color=color, linestyle='--', alpha=0.7)\n", - " \n", - " axes[1, 2].set_xlabel('Evaluation Order')\n", - " axes[1, 2].set_ylabel('Parameter Value')\n", - " axes[1, 2].set_title('Parameter Convergence')\n", - " axes[1, 2].legend()\n", - " axes[1, 2].grid(True, alpha=0.3)\n", - " \n", - " plt.tight_layout()\n", - " plt.show()\n", - " \n", - " return fig\n", - "\n", - "# Create the plots\n", - "if current_optimizer is not None:\n", - " fig = plot_optimization_results(current_optimizer, current_objective)\n", - " print(\"Plots created successfully!\")\n", - "else:\n", - " print(\"No optimization results to plot. Run the optimization first!\")\n" - ] - } - ], - "metadata": { - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/aamp_app/optimizer/notebooks/model_integration_demo.ipynb b/aamp_app/optimizer/notebooks/model_integration_demo.ipynb deleted file mode 100644 index 329e1da..0000000 --- a/aamp_app/optimizer/notebooks/model_integration_demo.ipynb +++ /dev/null @@ -1,775 +0,0 @@ -{ - "cells": [ - { - "cell_type": "raw", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "# Model Integration Demo: Scikit-Learn to BoTorch Optimization\n", - "\n", - "This notebook demonstrates how to integrate a pretrained scikit-learn logistic regression model with BoTorch for Bayesian optimization of polymer printing parameters.\n", - "\n", - "## Overview\n", - "\n", - "We'll demonstrate:\n", - "1. Loading a pretrained logistic regression model\n", - "2. Using the model objective factory to create a BoTorch-compatible objective function\n", - "3. Running Bayesian optimization to find optimal printing parameters\n", - "4. Analyzing and visualizing the optimization results\n", - "\n", - "## Background\n", - "\n", - "The logistic regression model was trained on polymer printing data with features:\n", - "- **Base parameters**: concentration, print_speed, gap_size, volume\n", - "- **Engineered features**: polynomial terms, interactions, and solvent indicators\n", - "- **Target**: Binary success/failure outcome\n", - "\n", - "The model expects exactly 18 features in a specific order and uses a StandardScaler for normalization.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Libraries imported successfully\n", - "PyTorch version: 2.7.1+cu126\n", - "NumPy version: 2.2.3\n", - "Pandas version: 2.2.3\n" - ] - } - ], - "source": [ - "# Import required libraries\n", - "import sys\n", - "import os\n", - "sys.path.append('..') # Add parent directory to path to import our modules\n", - "\n", - "import numpy as np\n", - "import pandas as pd\n", - "import torch\n", - "import matplotlib.pyplot as plt\n", - "import seaborn as sns\n", - "from sklearn.linear_model import LogisticRegression\n", - "from sklearn.preprocessing import StandardScaler\n", - "import joblib\n", - "import warnings\n", - "warnings.filterwarnings('ignore')\n", - "\n", - "# Set up plotting style\n", - "plt.style.use('seaborn-v0_8')\n", - "sns.set_palette(\"husl\")\n", - "\n", - "# Set random seeds for reproducibility\n", - "np.random.seed(42)\n", - "torch.manual_seed(42)\n", - "\n", - "print(\"Libraries imported successfully\")\n", - "print(f\"PyTorch version: {torch.__version__}\")\n", - "print(f\"NumPy version: {np.__version__}\")\n", - "print(f\"Pandas version: {pd.__version__}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Model path: /home/vpalacio/Documents/data/polyprint/PProDOT_Samples/supervised_analysis/RandomForest_model.joblib\n", - "Scaler path: /home/vpalacio/Documents/data/polyprint/PProDOT_Samples/supervised_analysis/feature_scaler.joblib\n" - ] - } - ], - "source": [ - "# Load your pretrained model and scaler\n", - "# Update these paths to point to your actual trained model files\n", - "MODEL_PATH = \"/home/vpalacio/Documents/data/polyprint/PProDOT_Samples/supervised_analysis/RandomForest_model.joblib\"\n", - "SCALER_PATH = \"/home/vpalacio/Documents/data/polyprint/PProDOT_Samples/supervised_analysis/feature_scaler.joblib\"\n", - "\n", - "print(f\"Model path: {MODEL_PATH}\")\n", - "print(f\"Scaler path: {SCALER_PATH}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading pretrained model and scaler...\n", - "Model loaded successfully: RandomForestClassifier\n", - "Scaler loaded successfully: StandardScaler\n", - "\n", - "Model Information:\n", - " Model type: RandomForestClassifier\n", - "Error loading model: 'RandomForestClassifier' object has no attribute 'coef_'\n", - "Please ensure the files are valid joblib/pickle files.\n" - ] - } - ], - "source": [ - "# Load the pretrained model and scaler\n", - "print(\"Loading pretrained model and scaler...\")\n", - "\n", - "try:\n", - " # Load the trained logistic regression model\n", - " model = joblib.load(MODEL_PATH)\n", - " print(f\"Model loaded successfully: {type(model).__name__}\")\n", - " \n", - " # Load the trained scaler\n", - " scaler = joblib.load(SCALER_PATH)\n", - " print(f\"Scaler loaded successfully: {type(scaler).__name__}\")\n", - " \n", - " # Display model information\n", - " print(f\"\\nModel Information:\")\n", - " print(f\" Model type: {type(model).__name__}\")\n", - " print(f\" Number of features: {model.coef_.shape[1]}\")\n", - " print(f\" Model intercept: {model.intercept_[0]:.4f}\")\n", - " print(f\" Scaler mean: {scaler.mean_[:5]}...\") # Show first 5 values\n", - " print(f\" Scaler scale: {scaler.scale_[:5]}...\") # Show first 5 values\n", - " \n", - "except FileNotFoundError as e:\n", - " print(f\"Error: Could not find file - {e}\")\n", - " print(\"Please check the file paths and try again.\")\n", - "except Exception as e:\n", - " print(f\"Error loading model: {e}\")\n", - " print(\"Please ensure the files are valid joblib/pickle files.\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Creating objective function from pretrained model...\n", - "Model expects 18 features\n", - "Objective function created successfully\n", - "\n", - "Testing objective function with sample parameters...\n", - "Test input shape: torch.Size([2, 4])\n", - "Test parameters:\n", - " Sample 1: concentration=0.6, print_speed=45.0, gap_size=0.2, volume=15.0\n", - " Sample 2: concentration=0.3, print_speed=80.0, gap_size=0.1, volume=20.0\n", - "\n", - "Test results shape: torch.Size([2, 1])\n", - "Success probabilities:\n", - " Sample 1: 0.8181\n", - " Sample 2: 0.8181\n", - "\n", - "Objective function is working correctly\n" - ] - } - ], - "source": [ - "# Import the model objective factory\n", - "from model_objective_factory import create_objective_from_model\n", - "\n", - "# Create the objective function from your pretrained model\n", - "print(\"Creating objective function from pretrained model...\")\n", - "\n", - "objective_function = create_objective_from_model(model, scaler)\n", - "print(\"Objective function created successfully\")\n", - "\n", - "# Test the objective function with a sample input\n", - "print(\"\\nTesting objective function with sample parameters...\")\n", - "test_params = torch.tensor([\n", - " [0.6, 45.0, 0.2, 15.0], # Moderate values\n", - " [0.3, 80.0, 0.1, 20.0], # Different combination\n", - "], dtype=torch.float32)\n", - "\n", - "print(f\"Test input shape: {test_params.shape}\")\n", - "print(f\"Test parameters:\")\n", - "print(f\" Sample 1: concentration={test_params[0,0]:.1f}, print_speed={test_params[0,1]:.1f}, gap_size={test_params[0,2]:.1f}, volume={test_params[0,3]:.1f}\")\n", - "print(f\" Sample 2: concentration={test_params[1,0]:.1f}, print_speed={test_params[1,1]:.1f}, gap_size={test_params[1,2]:.1f}, volume={test_params[1,3]:.1f}\")\n", - "\n", - "# Call the objective function\n", - "test_results = objective_function(test_params)\n", - "print(f\"\\nTest results shape: {test_results.shape}\")\n", - "print(f\"Success probabilities:\")\n", - "print(f\" Sample 1: {test_results[0,0]:.4f}\")\n", - "print(f\" Sample 2: {test_results[1,0]:.4f}\")\n", - "\n", - "print(\"\\nObjective function is working correctly\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Parameter bounds:\n", - " Concentration: [20.0, 100.0] mg/mL\n", - " Print Speed: [0.0, 20.0] mm/s\n", - " Gap Size: [50.000, 200.000] mm\n", - " Volume: [4.0, 15.0] μL\n", - "\n", - "Bayesian optimizer initialized with:\n", - " Batch size: 6\n", - " Parameter dimensions: 4\n", - " Seed: 42\n" - ] - } - ], - "source": [ - "# Import the Bayesian optimizer\n", - "from bayesian_optimizer import BayesianOptimizer\n", - "\n", - "# Define parameter bounds based on typical polymer printing ranges\n", - "bounds = torch.tensor([\n", - " [20.0, 0.0, 50, 4.0], # Lower bounds: [concentration, print_speed, gap_size, volume]\n", - " [100.0, 20.0, 200, 15.0] # Upper bounds: [concentration, print_speed, gap_size, volume]\n", - "], dtype=torch.float64)\n", - "\n", - "print(\"Parameter bounds:\")\n", - "print(f\" Concentration: [{bounds[0,0]:.1f}, {bounds[1,0]:.1f}] mg/mL\")\n", - "print(f\" Print Speed: [{bounds[0,1]:.1f}, {bounds[1,1]:.1f}] mm/s\")\n", - "print(f\" Gap Size: [{bounds[0,2]:.3f}, {bounds[1,2]:.3f}] mm\")\n", - "print(f\" Volume: [{bounds[0,3]:.1f}, {bounds[1,3]:.1f}] μL\")\n", - "\n", - "# Initialize the Bayesian optimizer\n", - "optimizer = BayesianOptimizer(\n", - " bounds=bounds,\n", - " batch_size=6, # Evaluate 6 candidates per iteration\n", - " seed=42\n", - ")\n", - "\n", - "print(f\"\\nBayesian optimizer initialized with:\")\n", - "print(f\" Batch size: {optimizer.batch_size}\")\n", - "print(f\" Parameter dimensions: {optimizer.n_dims}\")\n", - "print(f\" Seed: {optimizer.seed}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Starting Bayesian optimization...\n", - "This will find the optimal printing parameters to maximize success probability.\n", - "============================================================\n", - "============================================================\n", - "BAYESIAN OPTIMIZATION STARTING\n", - "============================================================\n", - "Parameters: 4 dimensions\n", - "Batch size: 6\n", - "Iterations: 12\n", - "Initial points: 8\n", - "============================================================\n", - "Generating 8 initial training points...\n", - "Initial best score: 0.9881\n", - "Initial best parameters: tensor([ 27.0488, 14.0236, 143.5105, 8.8101], dtype=torch.float64)\n", - "\n", - "Iteration 1/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9881\n", - "Current best parameters: tensor([ 27.0488, 14.0236, 143.5105, 8.8101], dtype=torch.float64)\n", - "\n", - "Iteration 2/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9886\n", - "Current best parameters: tensor([ 20.0000, 16.0668, 123.0113, 9.5847])\n", - "\n", - "Iteration 3/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9886\n", - "Current best parameters: tensor([ 20.0000, 16.0668, 123.0113, 9.5847])\n", - "\n", - "Iteration 4/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9886\n", - "Current best parameters: tensor([ 20.0000, 16.0668, 123.0113, 9.5847])\n", - "\n", - "Iteration 5/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9886\n", - "Current best parameters: tensor([ 20.0000, 16.0668, 123.0113, 9.5847])\n", - "\n", - "Iteration 6/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9897\n", - "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", - "\n", - "Iteration 7/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9897\n", - "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", - "\n", - "Iteration 8/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9897\n", - "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", - "\n", - "Iteration 9/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9897\n", - "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", - "\n", - "Iteration 10/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9897\n", - "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", - "\n", - "Iteration 11/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9897\n", - "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", - "\n", - "Iteration 12/12\n", - "----------------------------------------\n", - "Fitting GP model...\n", - "Optimizing acquisition function...\n", - "Evaluating 6 candidates...\n", - "Current best score: 0.9897\n", - "Current best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", - "\n", - "============================================================\n", - "OPTIMIZATION COMPLETED\n", - "============================================================\n", - "Final best score: 0.9897\n", - "Final best parameters: tensor([ 20.0000, 8.2326, 126.2246, 10.1650])\n", - "Total evaluations: 80\n", - "============================================================\n", - "Optimization completed\n", - "Best success probability: 0.9897\n", - "Best parameters:\n", - " Concentration: 20.000 mg/mL\n", - " Print Speed: 8.2 mm/s\n", - " Gap Size: 126.225 mm\n", - " Volume: 10.2 μL\n" - ] - } - ], - "source": [ - "# Run Bayesian optimization\n", - "print(\"Starting Bayesian optimization...\")\n", - "print(\"This will find the optimal printing parameters to maximize success probability.\")\n", - "print(\"=\" * 60)\n", - "\n", - "# Run the optimization\n", - "best_params, best_score = optimizer.optimize(\n", - " objective_function=objective_function,\n", - " n_iterations=12, # Number of optimization iterations\n", - " n_initial_points=8, # Number of initial random points\n", - " verbose=True\n", - ")\n", - "\n", - "print(\"=\" * 60)\n", - "print(\"Optimization completed\")\n", - "print(f\"Best success probability: {best_score:.4f}\")\n", - "print(f\"Best parameters:\")\n", - "print(f\" Concentration: {best_params[0]:.3f} mg/mL\")\n", - "print(f\" Print Speed: {best_params[1]:.1f} mm/s\")\n", - "print(f\" Gap Size: {best_params[2]:.3f} mm\")\n", - "print(f\" Volume: {best_params[3]:.1f} μL\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Optimization Summary:\n", - " Total evaluations: 80\n", - " Best score: 0.9897\n", - " Worst score: 0.3873\n", - " Mean score: 0.9386\n", - " Score improvement: 0.6024\n", - "\\nTop 5 parameter combinations:\n", - " 1. Score: 0.9897 | Conc: 20.000, Speed: 8.2, Gap: 126.225, Vol: 10.2\n", - " 2. Score: 0.9888 | Conc: 20.000, Speed: 5.9, Gap: 145.855, Vol: 9.2\n", - " 3. Score: 0.9886 | Conc: 20.000, Speed: 19.1, Gap: 121.952, Vol: 8.5\n", - " 4. Score: 0.9886 | Conc: 20.000, Speed: 16.1, Gap: 123.011, Vol: 9.6\n", - " 5. Score: 0.9883 | Conc: 24.166, Speed: 20.0, Gap: 130.074, Vol: 6.4\n", - "\\nCreating visualizations...\n" - ] - } - ], - "source": [ - "# Get all evaluated points during optimization\n", - "train_X, train_Y = optimizer.get_training_data()\n", - "\n", - "print(f\"Optimization Summary:\")\n", - "print(f\" Total evaluations: {len(train_Y)}\")\n", - "print(f\" Best score: {train_Y.max().item():.4f}\")\n", - "print(f\" Worst score: {train_Y.min().item():.4f}\")\n", - "print(f\" Mean score: {train_Y.mean().item():.4f}\")\n", - "print(f\" Score improvement: {(train_Y.max() - train_Y.min()).item():.4f}\")\n", - "\n", - "# Find top 5 parameter combinations\n", - "print(f\"\\\\nTop 5 parameter combinations:\")\n", - "sorted_indices = torch.argsort(train_Y.flatten(), descending=True)\n", - "for i, idx in enumerate(sorted_indices[:5]):\n", - " params = train_X[idx]\n", - " score = train_Y[idx].item()\n", - " print(f\" {i+1}. Score: {score:.4f} | \"\n", - " f\"Conc: {params[0]:.3f}, Speed: {params[1]:.1f}, \"\n", - " f\"Gap: {params[2]:.3f}, Vol: {params[3]:.1f}\")\n", - "\n", - "# Convert to numpy for plotting\n", - "eval_params = train_X.cpu().numpy()\n", - "eval_scores = train_Y.cpu().numpy().flatten()\n", - "\n", - "print(f\"\\\\nCreating visualizations...\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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/DDDMiaTCaPReNfKpKoqFosFg0F+8oUQ4r9Kp9M53Z8CAwOpVKkS0dHRzJ07l7fffpsSJUrkad13+z6WU1arlQMHDtCmTZs7Wk9m+9O9e3e+/fZb1q9fT48ePTJMX7NmDdWqVaNWrVp3tP1/i4SEBFavXs1LL73E888/r31funRpHnroIV544QUOHDhAmTJlCrCU917hwoWdrsUyZcrg5+dHnz592LhxI08//XSGZe6Xa8wVX1/fTKel/9vh5s2bnDt37h6USggh7k/pn8dKlSpF48aNqV+/Pm+99RZVqlShe/fugC0ImBvp22/SM5vN6PX6u9Ye4+o5LKt7REG5efMmTZs2pWzZspnOs3z5curVq8e7776rfRcUFETt2rW5desWBw8e5PHHH78Xxb1v3I9tXXciu/bC9Nff/v37adGihfb5bl1HQuSG9NgT4l9gwYIFPPLII05BPUf9+/enXLlyLFq0CIDU1FSqVavGF198wdSpU2nSpAm1atWiT58+nD9/HrB1Q588eTLXr1+nWrVqzJkzJ0M6nRUrVlCtWjVOnjzJc889R61atXj00Uf58ccfuXLlCv3796d27dq0atWK9evXa+VJ32U9fbd++3/ff/+9tsy2bdt4/vnnadCgAaGhoXTu3JmNGzdq01u1asWWLVtYv3691n3fVSrOLVu20KNHD2rVqkWdOnV47rnn2Llzpzb9r7/+olq1auzatYs33niDevXq0ahRI0aMGJHpG/rZ8fHxoXTp0ly+fBm4nWJgw4YNdOzYkWbNmuW4fAB79uyhS5cuhISE0KpVK1asWMHEiRN55JFHnOpjwoQJjBo1itq1a/P7778DcOPGDd555x2aNGlCcHAwTzzxBGFhYU7r37VrF7169aJBgwbUqVOHzp07s27dOm36rVu3GDVqFI888gjBwcG0aNGCCRMmkJycnKf6EUIIUXDsKfsuXboEgMViYfbs2bRr145atWrRtGlThgwZwsWLF7Vl7PfXP//8k8cee0xrgMrJsnl9dgA4cuQI/fv3JzQ0lNq1a9OrVy8thdLFixepUaMG8fHxvPfee06pCLdv307Pnj2pXbs2devW5aWXXnJ6Nshsf9ILDg6mRo0aTs8nduHh4ezfv19b9tChQ/Tv359GjRpRu3ZtOnTowLfffpvpccgsZeGwYcMypBJavnw57du319JCjR07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tum5557TKaecohtuuEHdunXTE088oQsuuEC33367nnvuObtDBAAAiIqwW3HecsstuuaaayRJl112ma666iq99lpp7/grr7wystEBkVTDij3W2QISkDWxb2m/abDGHmKE8RMAINm4nQ6N6J6tKQs3aq/XJ5fTkNdnqp7LqbN7tJLb6ZDHX/k6eLXRrl17Pffcy3rxxWd1ySXna9euncrKaq5Bg07Q+edfLLfbrTvvnKB//es+DRlyoho2bKSjjx6oMWOurtHzH3XUAJ133oW67bYbtXv3Lh12WDfdddf/SZJ69uyjK6+8Wg8/PFEFBVvVsmUrXX/9zerS5dAaPfewYWfoueee0i+/LNKVV15bYf95512k22+/Raeddrxyczvpttvu1Ntvv65//et+ZWQ01AknnKwvvvhU5513jp57LrhqcejQM7R58yZdffXl8nhK1LlzFz355DPKyMioUWx1xddff60pU6bo888/V+PGjXXmmWdqxIgRatNmf+XhIYccor///e+65JJLbIwUAAAgOgyzlv0g1q9fryVLlqh169Y67LDDIhVXVOTnh/7EWyQYhuR2O+Xx+CrkjBJRPJ5vypzv5dxS2mqlaPiJkuWTmEH7hp0ouSvmvOPxnGuLc7Y7mthIhnN2rlijlAVLJEkleV3lz82R2+2U8fG3cuRvkyQVnXGS5Az9qfJEkAyvs1UkzzcrK/KTXYyfSiXbv8u6hutvL66/vbj+seP1+fXD2u3asrNEzTNS1DOnsdwuB9ffZrG4B6IxhpKkLl26qHfv3ho5cqSOP/74SqsshwwZolmzZkUlhupEc/xUU7zP1Q7Xr3a4frXD9asdrt+Bs/PanffBOdpatFXN0ppVut7eihW/65ttXrVs3zG2we3T56Jhar57u1Jat9GOVyrGGInrV9PxU9gVe5JUUlKiBQsWaNOmTapXr55atWqlQw+t2afPANtYK/b85e8saytO2vEBCccRumIv6Gsq9hBljJ8AAMnI5XSob/tMu8NAApk9e7batWunkpKSQFJv165datCgQdDj7ErqAQCA8C3K/1kbd29Qq/Rsu0OpVPPflyujYIt8BQV2hxJ+Ym/RokW69NJLtX379qDtrVu31n/+8x8dfPDBkYoNiCxHDdfYY50tIPFYE/bWxL5JUh+xwfgJAAAgMpxOp0477TSNHTtWJ598siTp7bff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z1qxR7UuooTxEwAAKO+II/ICrT8RvltuuUU33nijTj31VElSv379ZJqmBg4cqH/84x82RwcAABB9NU7sPf7446pfv75effVVeb1ePfroo3riiSc0fvz4aMYHRE65Nfb2f83kPpAUqqjYoxUnooXxEwAAQGQ1bNhQzzzzjP744w+tXLlSDodDHTp0UIcOHewODQAAHKCXB7+hEn+JUhwpdodSqRm3P6gj6vuV3T7X7lBqntj79ttv9cILL6h9+/aSpHvvvVdjxoxhYgrxw1FZxZ7lMST2gMS17z0gsLYea+whBhg/AQAAREfHjh3VsWPHoG1+v18OxvYAAMSdw5v3sDuEam05qIt2Zbrkze1kdyg1T+wVFBQEJqWk0gFUfn5+NGICoqOSVpyGScUekBTK7u+y+z8osce9j+hg/AQAABBZnTt3llHF/91//fXXGEYDAAAQezVO7JVnGIZMa9UTUNexxh6Q1EyHQ4a0vwWnSStOxB7jJwAAgNq54447ghJ7fr9f69ev16effqpLLrnExsgAAABi44ATe0DcsQz8jUoSeyYVe0DicpSr2PPtr9jj3gcAAADiw1//+teQ28866yw98sgjGjFiRIwjAgAAtfXxqg9V7C1WqitVJ7YfbHc4IeX+92tlpniV8vtvKjnR3hhrnNjzer165JFHgj5l7vP59PDDDwc9bty4cZGLDoggsyYVe0zuA4nL2FeVV9aCk4o9xADjJwAAgNjo0KGDfvvtN7vDAAAAB+DGOddp4+4NapWeXWcTe8c/OVEZBVvka5WtbfGS2GvRooVmzZoVtK158+Z67733At8bhsHEFOoua9LOb03sVfIYAImlfMVe0Bp7JPYQHYyfAABAmYkT71VJSYn++c+77Q4lIf3000/auXOn3WEAAABEXY0Te59//nk04wCir7KKPevkPok9IHHtS94Zfr9kmqV/B/Zx7yM6GD8BABB7a9as1ksvPaf583/Qzp071aBBAx12WDddeOFo/eUvnaN23I8++kBvv/2G1q9fJ5/Pp1atWmnYsLN0xhmlrSFvvnl81I6dTPr3719hm8fj0Y4dOzRq1CgbIgIAAIgt1thD8ghK7Fkm9IPa8TG5DySs8sl9P/c+AABAovntt+UaO/ZSDRt2ll544VU1bdpMmzdv0uuvv6zLLx+tp556XgcfHPnk3jffzNHDD0/UXXfd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v19TpkyRY984PiMjQ126dNGECRNsjg4AAByIeWumac7KVzVvzTS7Q6lU5y8/VvNZM1Rv+lS7Qwk/sTdt2jSdccYZkqQ9e/Zo5MiReu2113TjjTfqrbfeiniAQKSYqmSNPT+JPSAplF9nk1aciCHGTwAAAJExd+5c3X///UFr6qWmpuqWW27R999/b3N0AAAA0Rf2TObkyZP16KOPSpJmz54tt9ut999/Xy+++KJeffXVSMcHRI6jksSe9WsHiT0gYVVoxcm9j9hh/AQAABAZBQUFysrKqrA9LS1NxcXFNkQEAAAQW2En9tatW6djjz1WkvTtt9/q5JNPlsvl0iGHHKL169dHPEAgYspX6wS+Zp0tICkYll95pp+KPcQU4ycAAIDI6NChg7788ssK299880116NAh9gEBAADEmCvcH6hXr56Ki4tVr149/fe//9VZZ50lSSoqKpLT6Yx4gEDEWJN21gl9WnECyaFCxR5JfcQO4ycAAIDIuOKKK3Tttddq0KBB8vl8uvvuu/XLL79o8eLFgQ4JAAAAiSzsxN6RRx6pu+66S263W4ZhqFevXpKkN954Q507d454gEDEBFXsWXeQ2AOSgrUqz9zfitM0DO59RB3jJwAAgMg48cQT9eqrr2ry5Mnq1KmTFi5cqA4dOui2225Tt27d7A4PAAAg6sJO7N1666268847tXXrVj3++ONyOp3atm2bnnrqKU2aNCkaMQKREbTG3v5KHYOKPSApmJb72/D7Zfj9pWl91tdDDDB+AgAAqD2/368lS5aoW7duevjhh+0OBwAAwBZhJ/Zat26tZ599NmhbZmamvvrqK6WlpUUsMCDiKqvYs6y3Z5LYAxJXhVac++591tdDDDB+AgCg7jvrrCH6298u0LBhZ9kdCirhcDh0/vnna8GCBbQzBwAASeuAZjM3bNigJ598Urfccktg22+//RaxoICosFbrWCr2rIk9KvaABBbUitO/v3KX+x4xwvgJAIDouuqqyzRx4r0h93366Uc64YQB2rNnd4yjQqQNGTJEkydPlmma1T8YAAAgAYWd2FuwYIFOPvlkffTRR3rvvfckSWvXrtW5556rTz/9NOIBAhFTg4o9WvIBCcwoX7G3L7FHxR5igPETAADRN2TIMH3++Sfau7e4wr4PPnhPgwadoPr1022IDJGUn5+vF154QX369NEZZ5yh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BYiIiIiIiIiIiJSAj483Tr32RsGiJX67rEhNDEGatfv/5u8LJyBXPF9IETJS/9khr9c/UXo/g9nx/QcA758/gp6xCYqXUcxn8L2nL0pa5lfIvrPbyru98CPuGwy0C+ba5+05zZuIQlFh0CpeAhF7Dys0FjbsERERERERERERKYmCRUugSJlyv11OTQ2QZsOF7bwuvfVPRNkjPZ/B7Pr+C/bzgX4BU4V9BwT7+ShkvznBO/QFQmP8kV+3qKJDSVMhTw8YfA+C5Pt3RYfCZ+wRERERERERERERERERKQM27BEREREREREREREREREpATbsERERERERERERERERESkBNuwRERERERERERERERERKQE27BEREREREREREf2fr68vBg0ahCpVqsDBwQHLly+HVCpVdFhEREREAAANRQdARERERERERESUGwiCgNGjR6N8+fK4desWvn//jsGDB6NAgQIYOHCgosMjIiIiYo89IiIiIiIiIiIiAHj58iU8PDwwa9YsGBkZwczMDEOHDsXhw4cVHRoRERERAPbYIyIiIiIiIiIiAgC8efMGxYsXh7GxsWxapUqV4O3tjcjISOjr66drOyJRNgUI4Ju/7+/3DyTezi8FhCzet0+k6vcTEIkADQ11iMUSCMkq0MfHB99+KG5YVmWp/7Tqj9KH9Ze29HwGs+v7DwBCgwMQnxAPXR3dLN5y7tj/7+ouO/cvFUtk/wZ4f0x1GUXXv1Qqlv0/tfN80rTszAFk+xIEfj0QERERERERERFt2rQJ165dw/Hjx2XTPn/+jGbNmuHatWsoWbKkAqMjIiIi4lCcREREREREREREREREREqBDXtEREREREREREQAChQogLCwMLlpoaGhAAATExMFREREREQkjw17REREREREREREAKytrfH161dZYx4AuLu7o3z58tDT01NgZERERESJ2LBHREREREREREQEoGLFirCxscGCBQsQEREBDw8PbN26Fb169VJ0aEREREQAAJEgCIKigyAiIiIiIiIiIsoNAgICMHv2bDx8+BB6enro2bMnRo8ereiwiIiIiACwYY+IiIiIiIiIiIiIiIhIKXAoTiIiIiIiIiIiIiIiIiIlwIY9IiIiIiIiIiIiIiIiIiXAhj0iIiIiIiIiIiIiIiIiJcCGPSIiIiIiIiIiIiIiIiIlwIa9TPL19cWgQYNQ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cmPXW1hYYMiQIbJn1mVFXU+bNg137tzBvXv3ZOv4+vqicePGmDt3Lnr06AFBEDB+/Hj8+++/MDExwbVr19C/f3/ExcXhyJEjcvWwdetW7N27Fz9+/ICtrS1mz56N0NBQjB07Furq6jh79iyCg4MxcuRIfPv2Dd27d0evXr3k9pf0Hli2bBnc3NwQFxcHMzMzDBw4UG4Yi5/r41d1R0RElNWYY8nL6hwrtRwlLX369EmRl/ycz5w4cQLTp0/HhQsXUK5cOdlyjo6OsLW1xerVqwEAO3bswPr166Gjo4ONGzeiSpUqqe4zvcff0dERNWvWRNmyZbF//36EhISgcuXKmDdvnuy4AMD9+/fh4uKCN2/eQE1NDeXLl8fgwYPRvHlz2TI+Pj5YsWIFHjx4gNjYWBQrVgxOTk4YOnSobH9v377FvHnz8OrVKxgaGqJDhw4wNTXF4sWL8erVK2hqav62PomISLkwJ5GXXdd9Xrx4gX379uHJkycIDg6Gjo4OypYti/r166Nnz55yPfRu376NxYsXw9fXF2XKlMH48eNRtWpV9O3bFwEBAVi1ahWeP3+O9evX4/Dhw5g/fz7ev3+P/PnzY/Dgwejbt+8vY3ny5An27duH58+f49u3b1BTU0PJkiXh6OiIwYMHy56HlzyfSsqFUjN69GiMGTMGQOLzGVevXo2nT59CKpWidOnS6NGjh+x6DRGpLjbsERERERERUZ6X1LC3ZMkSRYdCREREuUjSjcm/GlqUiCgncShOIiIiIiIiIiIiIiIiIiXAhj0iIiIiIiIiIiIiIiIiJcChOImIiIiIiIiIiIiIiIiUAHvsERERERERERERERERESkBNuwRERERERERERERERERKQE27BEREREREREREREREREpATbsERERERERERERERERESkBNuwRERERERERERERERERKQENRQeQk4KDf2Tr9rW01BEfL8nWfeQmLK9qY3lVG8ur2ljerGVqapBt21YGzJ+yFsur2lhe1cbyqr68VmbmUNknu/On7KBK739VKgugWuVhWXInVSoLoFrlycqy5OR5OTZgULZuX6fI9mzdvjJgj70sIhLJ/6vqWF7VxvKqNpZXtbG8pEzy2vFjeVUby6vaWF7Vl9fKnNfKS7+mSu8HVSoLoFrlYVlyJ1UqC6Ba5VGlslDWy1M99oiIiIiIiIiIiIiIiCh7SCEoOgSVxx57REREREREREREREREREqAPfaIiIiIiIiIiIiIiIgo06SQKjoElccee0RERERERERERERERERKgD32iIiIiIiIiIiIiIiIKNMEPmMv27HHHhEREREREREREREREZESYI89IiIiIiIiIiIiIiIiyjT22Mt+7LFHREREREREREREREREpAQU3rB3584d1K5dG87Ozr9ddvfu3WjUqBFsbGzQpUsXvH79OgciJCIiypiwsFCcP38WYWGhig6FVJSq5U/8zBAREVF2U7X8iYiIKLeSCEK2/pGCG/b++ecfLFiwAKVLl/7tslevXsWaNWuwePFiPHz4EA0aNMCwYcMQHR2dA5ESERGl36BBfTFgQC8MGtRP0aGQClLF/GngQH5miIiIKPuoYv5ERESUWwnZ/EcKbtjT1tbGsWPH0pVYHT16FJ07d0atWrWgq6uLUaNGQSQS4fr16zkQ6e+FhYXi7NkzvNOciCiP8/b+hDt3bgEA7tz5F58/eysyHFJBqpQ/AfzMEP2MPViJiLKequVPRHkRcyQiov8otGGvb9++MDAwSNeyb968QeXKlWWvRSIRLC0tc81wCAMH9kWfPj14pzkRUR53/vzZX77Oap07t0WDBvZwdKwNR8c6aNu2OWbMmIqgoMAs2f65c6cRFhaWrmUFQcCOHVvRsWNrNG1aH0OG9MOLF89k88PDwzBnznQ0a9YALVo0wpIl8xEXF5vm9q5du4wePTrC0bE2+vTpCjc3V7l9bdmyAW3bNke9eg4YN24kvn71++N9KRNVyp8A4OzZ03Kvc/oz065dc8yePV0hn5m4uFisXLkU7ds3//9npi/c3B6mufyHD+8xbtwING/eAO3bt8CaNSuQkJAgm3/y5DF0794RTZrURZcu7bF//+5k+4rDmjUr0L59CzRtWg8DB/bC/ft3/7iclH3Y65uIKOupWv5ElBcxRyJSHlII2fpHueAZe+kVGhoKY2NjuWlGRkYICQnJ0HZEoqz/+/z5vzvNb9/+Fz4+3tmyn9z2l131mVv/WF7V/mN5VfsvJ8t7/vxpufPO+fOns71szs6TcfPmfdy8eQ87d+5DWFgYli1bmOltS6USuLisQkREWLqWP3x4Py5cOItVq1xw6dIN1K1bD9OmTUR0dCREImDx4r8RGhqKw4dPYN++w/D0/ICNG9elui0Pj7dY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nfXy/AFrqvT4q47PrtLjywN27FUSef1C/1CMV9wYr7gxXnFjvJXvv3xTCmD7qTIxXnFjvOLGeMXvvxYz21BVix17NUdMsQDiioex1LyhQwfh7NnfAAC9AJzB++eqFAD0/P/XAODk5IygoB9rJI8VJa9lU5TKjqVaO/b6r67S4686OadKjy8PuMYeEREREREREREREZFIpaam4NKli9LXv+F/kyUK+F+nHgD8/vsFpKamVGPuiKpWXFwcxowZAwsLC9jZ2WH16tWQSCQlvufFixewtLSEn59fNeWyfNixR0REREREREREREQkUmfO/IqsrCyZbcrKynBz+wLKysoy27OysqRP9hF9GKGKf8qRE0HAlClToKOjg0uXLmH//v345ZdfsHv37hLf5+PjU2iK1NqEHXtERERERERERERERCJ16tRJmdctWrTEqVNnsH379/j113No0aJliemJ5NXdu3cRGRmJRYsWQVtbG61bt8a4ceNw8ODBYt9z6dIlPH78GI6OjtWY0/Jhxx4RERERERERERERkUj980+M9Hc3t0G4cCEUHTpYAwCsrTvi4sVQuLl9IU0TE/OkurNIYiIRqvanHCIiImBgYID69etLtxkbGyMmJgZpaWmF0mdkZMDb2xtLly4t9DRrbcKOPSIiIiIiIiIiIiIikVq82BvOzn3h778T/v47oampJbNfU1ML/v674O+/E87OfbF4sXcN5ZSociUlJUFbW1tmW97rpKSkQum3bNmCjh07wsbGplry96Fqb5cjERERERERERERERFVSPfujujeveRpBRUUFPD554Px+eeDqylXJF7le6qutoiKisKxY8dw8mTtn4qWT+wRERERERERERERERGRqDRo0ADJycky2/Ke1NPV1ZXZvnTpUsyYMaPQ9tqIT+wRERERERERERERERFRhQnlXAevKpmamuLZs2dISkqCjo4OAODOnTto06YN1NXVpeni4+MRHh6OR48eYfXq1QCA9PR0KCoq4sKFCzh27FiN5L847NgjIiIiIiIiIiIiIiIiUWnfvj3MzMzg4+ODJUuW4Pnz5wgICMCkSZMAAM7OzvDx8YGlpSUuXbok896VK1eicePG+PLLL2si6yVixx4RERERERERERERERFVnFB7ntgDgI0bN2Lx4sXo2rUr1NXV4enpCU9PTwDAkydPkJ6eDiUlJTRu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cfbs2Qo1Igs2QufMmYOEhATs27evMrJZJq9fv0aPHj2wbNky9O/fv9rOS0RERJUjLi4OPXr0wN69e2Fra1vT2ZGRnp4OJycnjB8/vlBHZG2SlZWFXr16YfDgwZg4cWJNZ4eIiKjK5a3JV533HypLTbYvhgwZgrZt28LHx6daz0tE4sepOImIqtjQoUOhrq6OwMDADz7G7t270bVrV9y/fx8SiQTXrl3D2bNn4eTkVIk5Ld2WLVtgaGiIfv36Vet5iYiISPzq1auHWbNmYfv27UhKSqrp7BQrKCgISkpKGDlyZE1nhYiIiEpRU+2L3377DTExMZg6dWq1nZOI/js4FScRURVTVVXFli1b4OHhAWtra+k89uUxbNgwvHjxAhMnTsSbN2+gr6+PCRMmwNPTswpyXLQzZ87gzJkzOHz4cJFz6BMRERFV1MCBA3Hr1i3MmTMHO3bs+OB1jqvK7du3sX37duzbtw/16tWr6ewQERFRGVR3+yI2NhZLlizBhg0buIwJEVUJTsVJREREREREREREREREJAf4yAURERERERERERERERGRHGDHHhEREREREREREREREZEcYMceERERERERERERERERkRxgxx4RERERERERERERERGRHGDHHhEREREREREREREREZEcYMceERERERERERERERERkRxgxx4RERERERERERERERGRHGDHHhEREREREREREREREZEcYMceERERERERERERERERkRz4PyndLDwJE5RHAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Visualizations created successfully\n" - ] - } - ], - "source": [ - "# Create comprehensive visualizations\n", - "fig, axes = plt.subplots(2, 3, figsize=(18, 12))\n", - "fig.suptitle('Bayesian Optimization Results with Pretrained Model', fontsize=16, fontweight='bold')\n", - "\n", - "# 1. Optimization progress\n", - "ax1 = axes[0, 0]\n", - "ax1.plot(range(len(eval_scores)), eval_scores, 'o-', alpha=0.7, linewidth=2, markersize=6)\n", - "ax1.set_xlabel('Evaluation Number')\n", - "ax1.set_ylabel('Success Probability')\n", - "ax1.set_title('Optimization Progress')\n", - "ax1.grid(True, alpha=0.3)\n", - "ax1.axhline(y=best_score, color='red', linestyle='--', alpha=0.7, label=f'Best: {best_score:.4f}')\n", - "ax1.legend()\n", - "\n", - "# 2. Parameter distributions\n", - "param_names = ['Concentration', 'Print Speed', 'Gap Size', 'Volume']\n", - "colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red']\n", - "\n", - "ax2 = axes[0, 1]\n", - "for i, (param_name, color) in enumerate(zip(param_names, colors)):\n", - " # Normalize parameters to [0,1] for comparison\n", - " param_values = eval_params[:, i]\n", - " param_min = bounds[0, i].item()\n", - " param_max = bounds[1, i].item()\n", - " normalized_values = (param_values - param_min) / (param_max - param_min)\n", - " \n", - " ax2.scatter(normalized_values, eval_scores, alpha=0.6, label=param_name, \n", - " color=color, s=30)\n", - "\n", - "ax2.set_xlabel('Normalized Parameter Value')\n", - "ax2.set_ylabel('Success Probability')\n", - "ax2.set_title('Parameter Values vs Success Probability')\n", - "ax2.legend()\n", - "ax2.grid(True, alpha=0.3)\n", - "\n", - "# 3. Score distribution\n", - "ax3 = axes[0, 2]\n", - "ax3.hist(eval_scores, bins=15, alpha=0.7, color='skyblue', edgecolor='black')\n", - "ax3.axvline(x=best_score, color='red', linestyle='--', linewidth=2, label=f'Best: {best_score:.4f}')\n", - "ax3.axvline(x=np.mean(eval_scores), color='green', linestyle='--', linewidth=2, label=f'Mean: {np.mean(eval_scores):.4f}')\n", - "ax3.set_xlabel('Success Probability')\n", - "ax3.set_ylabel('Frequency')\n", - "ax3.set_title('Distribution of Success Probabilities')\n", - "ax3.legend()\n", - "ax3.grid(True, alpha=0.3)\n", - "\n", - "# 4-6. Individual parameter analysis\n", - "for i, param_name in enumerate(param_names[:3]):\n", - " ax = axes[1, i]\n", - " \n", - " # Create scatter plot\n", - " param_values = eval_params[:, i]\n", - " scatter = ax.scatter(param_values, eval_scores, c=eval_scores, cmap='viridis', \n", - " alpha=0.7, s=50, edgecolors='black', linewidth=0.5)\n", - " \n", - " # Highlight best point\n", - " best_idx = np.argmax(eval_scores)\n", - " ax.scatter(param_values[best_idx], eval_scores[best_idx], \n", - " color='red', s=150, marker='*', edgecolors='black', linewidth=2,\n", - " label=f'Best: {param_values[best_idx]:.3f}')\n", - " \n", - " # Get units for each parameter\n", - " units = ['mg/mL', 'mm/s', 'mm', 'μL']\n", - " ax.set_xlabel(f'{param_name} ({units[i]})')\n", - " ax.set_ylabel('Success Probability')\n", - " ax.set_title(f'Success vs {param_name}')\n", - " ax.legend()\n", - " ax.grid(True, alpha=0.3)\n", - " \n", - " # Add colorbar to the last subplot\n", - " if i == 2:\n", - " plt.colorbar(scatter, ax=ax, label='Success Probability')\n", - "\n", - "plt.tight_layout()\n", - "plt.show()\n", - "\n", - "print(\"Visualizations created successfully\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Performing parameter sensitivity analysis...\n", - "Analyzing sensitivity for Concentration...\n", - " Range tested: [20.000, 36.000]\n", - " Score range: [0.9789, 0.9897]\n", - " Sensitivity: 0.000677 (score change per unit parameter change)\n", - "Analyzing sensitivity for Print Speed...\n", - " Range tested: [4.233, 12.233]\n", - " Score range: [0.9645, 0.9897]\n", - " Sensitivity: 0.003155 (score change per unit parameter change)\n", - "Analyzing sensitivity for Gap Size...\n", - " Range tested: [96.225, 156.225]\n", - " Score range: [0.9688, 0.9906]\n", - " Sensitivity: 0.000363 (score change per unit parameter change)\n", - "Analyzing sensitivity for Volume...\n", - " Range tested: [7.965, 12.365]\n", - " Score range: [0.9754, 0.9897]\n", - " Sensitivity: 0.003243 (score change per unit parameter change)\n" - ] - }, - { - "data": { - "image/png": "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e/L8889z4sSJAt9H3nrYGzdu5MEHH+S6666jefPmdOnShYcffpg//vjjPJF0+vbbb8nIyMBsNvPEE0/g7++P1WrNt/+Cey34t99+m/nz53P11Vdz2223GetkZ2fz3nvvcdttt9GqVSuio6Pp06cP06ZNIzEx0e31Bg8eTKNGjejWrVuh23E99h3r9+rVi8zMTCZNmkRMTAzNmzdn4MCBbN26NV+blyxZwg033EDz5s3p1auX2/nGEwsXLmTnzp0A3HvvvTz55JNUrlwZgMDAQO644w5efvllwH5OnTZtmvHc9957z3gfO3fuZP78+fTq1YvmzZvTs2dPt2Nt7NixdOrUiZycHADuuecet2OmoH3AcRw3a9aMrKwsnnvuOTp06EDbtm159NFHSU5OJi0tjXHjxtG+fXtat27NuHHjyMzMdHuPnn4XFIfzfb+dOXOGyZMnG3Fq3bo1d9xxBx9++KERG4eCaqK7ngtPnDjBggUL6NWrF82aNaNXr158/vnnbvE7V8xFRESk9FI5FxERkXLEkSAESE5OBmDlypU8+uij5Obm4uPjg5+fH//88w9vvvkmW7ZsYf78+fleJyUlhTFjxhAfH28kf7OyshgyZIiRdHKMpFy3bh3r169n/vz5tG/fPt9rLVu2jPfffx+LxYLVauXXX39l1KhR3HfffUybNs1YvnHjRh5//HE+/vhj47mLFi1iwoQJ2Gw2TCYTFouFnTt3MmHCBI4fP87DDz+Mv78/YWFhRrItICDA+A9g+/btDB48mNTUVMBem/vw4cPMnj2buLg43n33XXx83McdHD58mHHjxpGbm3vOesKudXXXr1/P8ePHiYyMNJbddNNN3HTTTfme9+qrrzJz5kzAXo/cx8eH33//nYceeojnn3+e22+/Pd9zVq1axdy5czGbzVitVo4cOcLLL79MtWrVjNHA+/fvp3///qSkpAD28hFJSUls2rSJTZs2MXbsWP71r38V+F7MZjPh4eEkJCQA9rI0QUFBhb7/66+/nokTJ2K1Wvnhhx+46667jMf27dvH3r17AejVq5dbDXQHX19fwsPDSUpKIjc3F7PZTEhICCEhIdx2223MnTvXeN+u5XkcSX6Am2++ucC2uVq2bBkAdevWpVGjRnTv3p0PPviAZcuW8cgjjxT6vMTERO655x6OHTtGTk4ONpuNlStXkpqa6vbj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IlI2KYkuIiJSRoWHhxt/O2oMx8TEcN999xkjbTMzMxkwYECRR9G5vrbDp59+ylNPPVXkdromxlxHQbvW/nVd7kiKuJY0cdS/zcuR/CqM62ukp6fnm7wRKDA5UtD7P5fw8HCGDx/O8OHDyczMZOvWrXz//fd8/PHHpKWlcfLkSd566y2eeuoptzYV1v6C2uQot+DgKAPi6rrrruO5557j1Vdf5cyZM/z111/89ddfLFy4kNDQUF566SW3uxAuVlhYGJ06dWLt2rWsWbOGCRMmsGbNGmMkrqNW+4UICQnh+uuvZ+nSpaxatYoxY8YYI/Tr1q1rJMULk5mZybfffgvY61oXlIA9duwYmzdvpl27dgW+N1eu8Xbso7t37+aOO+4ocL/yVKdOnahRowZHjhxh1apVdO7cmR9//BGw363hemzMmDGD8ePH8+OPPxp3LKxdu5ZXXnmFrl278tprrxlzARTksccew2q1smjRIjIzM9myZQtbtmxhzpw51K1bl1mzZlGnTp1Cn3/8+HE2b94M2EfLFxTT33//ncOHDxvlNlzlPa5c75wo6Ecr13r8ruteKNdtFPR5uk407Chj4pD3+CtMeHi4UU/bMaFtYY4dO2b8XdCPIHnj5dqGi9nnHC703Hyh3wVF4cn3m+v3QkHnbMf+U5R9x5PzrIiIiJRNSqKLiIiUUd9///05J70Ee11nRwI9JiaGKVOmEBERwYYNG85Z+qGgEcuzZ882HpsxYwYxMTFYLBYGDRrkcb3gonB9bwsXLjTqqV/oa4wePZphw4Z59Lyijth2THwK9qRLu3btaNeuHd27dzfKgDhKkri2ad26dUbZneJyxx13cNtttxEXF8eWLVvYvHkz69atIykpiUceeYSvv/6amjVrFtv2evfuzdq1azl8+DD//POPMVK2du3a5010n89tt93G0qVL2bdvH3v27DFKxjhqvJ/LmjVrPEqeffXVVwUm0T3xwQcfGMnMESNG8MADDxAcHMxrr73mUWkUsO9rt956KzNmzGD16tUcO3bMKIeR931GRkYye/ZsTp48ycaNG/n999/54YcfOHDgAKtXr+b55593uwslLz8/P8aPH89jjz3G5s2b+e2331i/fj2///47+/btY/jw4axYsaLQuweWLVvmNnq6IDabjWXLlhV4rOU9rlyTuI6JgV05SvFA/uRm3vIuBT2/qIKCgoy/8/5wd74f7BzMZjPNmjXjt99+4++//8432bArR2miChUqFDiqPO97cv1x7Xzn/kvpcnwXePL95vpDR0Gfj6NETt4fREREREQKoproIiIi5dihQ4eMv/v370/FihUxmUz8+eefxvLzJcXyvladOnXo2rUrFouFrKwsYyJQT1/HU456yOBe2gXsIzhdJ7J05TqRo+tI2bw1hE+dOnXRo1tfffVVbr31Vlq3bl3gqFPXkjiO0Z2Fva+cnByOHz9+UaUQsrOzOXDgAKdPn6Z169YMHTqUmTNnGgldq9VqTAh4PueaENNVjx49jFIUjrrrYJ80tSgK2l7Hjh2NEc2LFi0yym0UpZRLQEAACxYsYOHChW7/OcqXrFy5ssAJYD3hmNAQ4O677zaSfq4lgjw5Lvr374/JZOLkyZPMnz8fsJfRyJvcT0xMZMeOHVSpUoWbbrqJ8ePHs3LlSqMEhWOUeGEyMjLYtWuXUef70UcfZeHChUbCe+/evQXW5nb46quvjLbljefChQupV68ecO5a865CQkKoW7cuYC9hlLem/2+//QbYE8aO13aU2ImPj3e7qyMuLs6jbZ6La4mX7du3uz3mqMftCUc9/5ycHP73v/8VuM63335rlBm54YYbCpyrwPH+HVwngXbEzZVjHoRL7XJ/FxSmadOmxg8+eZP3Bw4cMOYjcMwfcSlcrpiLiIjIpackuoiISDnmOrGdI3m6efNmZsyYYSQ+PZ0EzvFax48f5/jx42RkZDBu3DhjJO7p06dJSkoqtrZ369bNuJV+9uzZxoSjX375JV26dCE6OtqYFNS1hMWWLVuwWq1YrVZq1aplJFBWrlzJunXrAHtCvU+fPrRp04aJEydecBtDQkLYsWMHaWlpjBo1yq1G7/Hjx3n++eeNdR2TArrWCX/ttdc4deoUNpuNOXPm0LlzZ5o1a3bBCcF+/fpx/fXX89hjjxmjeHNyctizZ4+xTqVKlc75Go6Y//HHH6Smpp43qe8o6QLw7rvvGvuAp6VcHNv7559/OH36tNv2TCaTkZBcsGABAC1btnRLdhYkJSXFGBF/zTXX0KZNG1q2bOn2n2OUd3x8vJH4L6q8x5fNZuO9995j/fr1xnJPjq9atWoZCXPH+7zpppvcRoSvXLmS9u3bc+utt7pNTnvy5EljhLKjRnlBTpw4Qdu2bbn55pt59dVXjR+h0tPTjR8D/P39Cx21u2/fPiOx3KNHj3zxbNmypTH55s6dO42E6vnceeedgL2UyvPPP09qairp6elMmzbNaNftt99uTCLrSB5nZ2fzzjvvYLPZ2LNnD3PmzPFoe+fSsGFD4zNdt24dq1evJjc3l2+++cbYnzzRr18/WrRoAcAnn3zCxIkTOXHiBGD/IWPx4sU8/vjjgH0feuyxxwp8nU8//ZRNmzaRm5vL119/bexXtWrVMn6gcz33OUa2F0dN8nO53N8FhYmMjDTmDPjll1/46KOPyM7O5tSpU0yYMME4RgYOHFis2/VGzEVEROTSUxJdRESkHOvcubORFJs/fz4tWrTg7rvvZuDAgTRv3hywJ6g9KY/hSIqmpaXRrVs32rZty7p16/jvf/8L2GtQx8TE8OWXXxZL2ytWrMjYsWMB+4R/119/PS1btjSST126dKF3796APXnoKIuyceNGWrZsyYQJEwCYOHEiQUFBZGVlMWTIEFq0aEHfvn1JSkqiZs2ajBw58oLbeN9999G1a1fAPnK1f//+NG/enJYtW9K5c2cjFu3bt+f//u//AHuibujQoYB99GxMTAwtW7bklVdeAexJRUcCrqgeeughfH192bx5M1dffTVt2rShRYsWvPDCCwB06NDhvKVLHJMN7tmzh3bt2jFkyJDzbtcxCaoj6XnllVe6jbj3ZHsJCQnExMTkm1C1X79+mEwmI+nrySj0b7/91li/R48eBa7TrVs3o7yIpyOn83L9oWDkyJG0bNmSF198kcmTJxs/Uj344IPExsae97Vuu+02gELfZ7du3YzyOI899hjNmzenTZs2dO7cmb1792KxWBg+fHihr1+1alVjItt3333XmJS0devWxvt/4IEHCq0B7RiFDoXH1LXevuv653LPPfcYPzAtWbKEdu3a0aZNGyMp3rx5cx599FFj/TvuuMOI7Ztvvknr1q3p06cPN910k7GOp3dR5OXj48OoUaMAe1L0wQcfJDo6moceesj4gcATFouFGTNmGJ/XRx99xLXXXkvr1q1p1aoV48ePJy0tjTp16jBnzhy3CWxdtWzZksGDBxMdHc0jjzxivK/Ro0cb6zRq1Mj4seWtt96iadOmxT4Bal6X+7vgXCZOnGhMfjpx4kRatmzJNddcY/zgcNdddxW6v14ob8RcRERELj0l0UVERMqxSpUqMWvWLFq2bElQUBCVKlVi9OjRjBkzhv/85z9ERUXh5+dHrVq1zvtao0aN4l//+heRkZH4+fnRoUMHPvzwQ2699VZuuukmAgICCA0NPedo2KIaNGgQb7/9Nu3btzdKZTRs2JAxY8bwxhtvuNVYnjJlCldccQUWi4WIiAjq168P2JNwixYtok+fPlSsWJGcnByioqL4v//7Pz766CO30cRF5UiWTZs2ja5duxqvlZ2dTZUqVYiJieGFF15g/vz5buUaxowZw0svvUR0dDQBAQH4+vrSrFkznn32WSMRdSF69uzJ3Llz6dGjB5GRkWRlZVGhQgWio6OZMGECc+bMOW+994kTJ9K0aVMsFguhoaEF1mrOy7WkCxStlMt//vMf2rdvj7+/PyEhITRp0sTt8aioKONHBbPZnC/JXhBHAtfX19f4kSOvypUrG3X2XZPuRRETE8OkSZOoV68eAQEB1KtXjzfeeIM+ffrwxBNPEBoaSmBgYIGTbObVs2dPY7R148aNufLKK90et1gszJs3j1GjRtGwYUMCAwOxWq3Url2bW265hU8++cRIRhfmqaeeYtKkSbRt25bw8HAyMjKMCRtnzpzJQw89VOhzHYn28PBw2rZtW+A6zZs3p1q1am7rn4/ZbGbmzJmMGzeOJk2aYLFYsFgsNGzYkMcee4wPPvjArVZ5zZo1eeONN2jYsKFxrI8ePZrHHnvMGCGckZHh0bYLcuedd/LUU08Z58Z69eoxZcoU444IT1WuXJmPP/6Yl156iZiYGCpVqkRWVhaVKlWiTZs2TJgwgSVLlhQ4OavDsGHDGD16NFWrVsVisXDllVcybdo0t2Ogfv36PPnkk1SpUgU/Pz9q1KhB9erVL/j9e8Ib3wWFqVatGp9++in33XefcYdKSEgI7dq1Y9q0aRd1p1FhvBFzERERufRMtstVlE5EREREpJglJyfTuXNn0tLS6NGjB2+++aa3m3RJrF+/3pjs96mnnuLee+/1boPEK5YsWcK4ceMAeO+99+jQoYOXWyQiIiJSPpi93QARERERkaKKj48nJSWFF198kbS0NADuv/9+L7eqeOXk5JCUlMTBgweNEbPh4eFGaRcREREREbk8lEQXERERkVLntttuM2qsg71GeJs2bbzYouJ39OhRtzriYC/1ExIS4qUWiYiIiIiUT0qii4iIiEipExERwYkTJ6hYsSI33XQTjzzyiLebVOzMZjPh4eGkpqZSs2ZNHnjgAY1CFxERERHxAtVEFxEREREREREREREphI+3GyAiIiIiIiIiIiIiUlIpiS4iIiIiIiIiIiIiUggl0UVERERERERERERECqEkuoiIiIiIiIiIiIhIIZREFxEREREREREREREphJLoIiIiIiIiIiIiIiKFUBJdRERERERERERERKQQSqKLiIiIiIiIiIiIiBRCSXQRERERERERERERkUIoiS4iZd7WrVt5+OGH6datG82aNaNjx44MHDiQTz/91GttatSoEVOnTi308bFjx3LNNdd4/HrTp0+nUaNGZGZmFkfzzuuff/5h3LhxdO3alWbNmnH11VczePBgPvvss8uy/dKuW7duPPbYY95uhoiIiEiJo7578VmyZAmNGjVy+69Zs2b07NmTZ599ltOnT5/3NS5lv/XYsWNMmjSJXr160bJlS9q0acNNN93Ea6+9dtmuazy1ceNGGjVqxMaNG73dFBHxEpPNZrN5uxEiIpfKxo0buf/+++nTpw933303kZGRJCQk8MUXXzB//nzGjh3Lv/71r8verpMnTxIUFERwcDAA999/P9dffz133XUXAMnJyVitVipWrOjR66WmppKWlkaVKlUA2LJlCw899BA//fRTsbd9zZo1PPLII7Rr147777+fOnXqEB8fz1dffcW7775Lnz59eOWVV4p9u5fTM888Q1hYGGPGjLno18rOzqZt27Z8+umn1K9fH4AzZ85gsVioUKHCRb++iIiISFmhvnvx9t2XLFnCuHHj+PDDD6lTpw4AGRkZ7Ny5k5dffhmbzcbSpUsJDAws9DUupN+6dOlS3n//fRYtWnTO17355pupXr06o0aN4oorriArK4sNGzYwdepUOnTowMyZMz1/s5fYxo0bueeee3jvvffo0KGDt5sjIl5g9nYDREQupY8++ogqVaowZcoUTCYTADVq1KBJkyakpKSwfft2r7TL0WEGyM3NJS4ujuuvv95YVtTkanBwsNGpB/j9998vuo0FOXnyJKNHj+a6667jtddeM2IaFRVFs2bNqF27Nv/973/p378/MTExl6QNl8Pvv/9Oly5dCn3carVisVg8eq2dO3eSnp7utszTCywRERGR8kR990sjIiLC7T3UqlWL0NBQ7rnnHr755htuueWWfM9x9HcvpN/qyfv5+uuvOXnyJAsWLKB27drG8vr162M2m/n88885c+aM+s0iUmKonIuIlGlWq5WcnBysVmu+x55//vl8t2V+8MEH9OnTxyhRMmHCBJKTk43HR48ezS233MLmzZvp168f0dHRdOvWzW2Uhc1mY+bMmfTq1Yvo6Gg6duzIqFGjOHjwoLGO45bQQ4cO0bhxY1JSUpg4cSKNGjUC3G8JHTRoEAMGDMjX/nfeeYfGjRtz/Phxt1tCx44dy+TJkzl16hSNGjXi2WefpXnz5kyfPj3fawwZMoT+/fsDzts9//nnn0LjuWjRItLT0xk3bpxxYeNq4MCBfPfdd24J9NWrV3PHHXcQHR1Ny5YtGTRoEBs2bDAe/+mnn2jUqBGbNm1izJgxtGnThg4dOjB69GhSUlKM9bKysnj55Zfp3Lkz0dHR9O3bl6+++spt+z///DMDBw6kRYsWtG7dmmHDhrm9n0WLFtGoUSP++usvhg8fTsuWLbnmmmv473//S3Z2tvHZ7N69m9mzZ9OoUSMOHTrE9OnTadu2rfHeHn30UQDS09OZNGkS3bp1o3nz5nTu3JmnnnqK+Ph4I6a33XYbADfccAODBw8G8t8Wm5ycTGxsLDExMTRr1owuXbrwwgsvuCXfBw4cyMiRI1m1ahV9+vShefPm9OnTh++++67Qz0tERESkNFHfvXj77ufiaPvRo0cBCu3vuvZb9+3bR6NGjVi+fDmTJk2iQ4cOtGnThmHDhnH8+HEABg8ezAcffEBcXByNGjViyZIlBW7f8RlnZGTke+zOO+/ko48+MhLoo0ePpm/fvqxbt46+ffvSrFkzunfvnq+U5IEDB3jooYdo164dzZs3p3///qxevdptnbS0NCZNmkTnzp1p1qwZ119/PbNmzcK1SENKSgpPPPEEbdq0oXXr1jzyyCNG/15Eyi8l0UWkTLvuuus4ceIEgwYNYtWqVW6d6rzefvttnn/+eW655RaWLl3Kiy++yNq1a/n3v/9trGOxWDhz5gyvv/4648eP56uvvqJVq1ZMnDjR6Gh/8sknvP322zzxxBN8/fXXzJo1i5SUFIYNG5Zvm9WrV2fx4sUAjBkzhnXr1uVbp2/fvmzdupXDhw+7LV++fDkdO3YkMjLSbfnTTz9Nnz59qFixIuvWrWP06NH07NmTzz77zK1zGB8fz4YNG7j99tsBe5J33bp11K1bt9AYbdy4kauuuopq1aoV+LiPjw81a9Y0/r1+/XpGjBhBw4YNWbRoEQsWLKBq1aoMHTqUnTt3AmA222+Keumll+jQoQOff/45zzzzDMuWLWP+/PnGa02YMIEvvviC2NhYvvrqK26++WbGjBljJJE3b97M0KFDqVGjBosWLWL+/Pmkp6fzf//3f5w5c8ZtW7GxsfTr14+vvvqKIUOGsGDBApYuXQpgfAb/93//x7p166hevToAOTk5fPDBB8ycOZOJEycCMGnSJL744gsmTpzIypUrmTZtGhs2bGDChAlGTJ966ikAPvzwwwIvhgBGjBjBd999R2xsLMuWLeOJJ57gs88+Y+zYscY6FouFv//+m0WLFjF16lQ+//xzqlSpwhNPPOH2Y4OIiIhIaaW+e/H23c/lwIEDAG79+oL6u64cfekZM2YQGRnJ4sWLef3119m0aROvvfYaYE/Gt2rViqZNm7Ju3TpuuOGGArd/zTXXYLFYuP/++/n44485duxYoW01m80cO3aMd955h+eff54vvviCNm3aMG7cOLZu3QpAQkICgwYN4sCBA7z11lt8/vnntG7dmpEjR7oN4HnooYdYunQpY8eOZdmyZQwZMoTp06fz5ptvGus899xzrFq1iokTJ7JkyRKuvvpqpkyZ4llgRaTMUhJdRMq0AQMG8OCDD7J7925GjRpF+/btufXWW5kyZQp79+411rNarcyePZsbbriBBx98kHr16tGlSxeefvppNm7cSFxcnLHuiRMneOaZZ2jTpg21a9fmX//6Fzk5Ofzxxx8AbN++nerVq9O9e3dq1KhBdHQ0r776KlOmTCE3N9etfb6+vsYIi5CQELfbLB169+6NxWJh5cqVxrKDBw/yxx9/FHjrZYUKFQgICMDHx4cqVaoQHBzMXXfdxeHDh906kN988w1ms5m+ffsCEBAQQJUqVfD19S00nidOnDCSyp6YM2cONWvW5LnnnuOqq66iSZMmvPTSSwQGBvL++++7rduhQwcGDBhArVq1uOmmm7jyyiuNTvGJEyf44osvGDVqFN27d6d27do88MAD3HvvvZw8eRKAWbNmERkZyeTJk2nUqBHR0dG88sorJCcn88knn7ht64YbbqB3797UrFmTf/3rXwQHBxvbcnwGgYGBbvFIS0vjnnvuoVmzZsY6o0ePZunSpXTu3JkaNWrQrl07+vTpw7p167DZbAQEBBi390ZERBAeHp4vRr///ju//PILY8aM4frrr6dOnTrceOONDB06lJUrVxqjg8A++dLkyZNp2rQp9evX5+677yYlJYU9e/Z4/JmIiIiIlFTquxdv370gNpuNv//+m+eee46qVavSo0cP47GC+rsFueKKK3jggQeoXbs2MTExdOzY0ehLh4eHY7FYMJvNVKlShYCAgAJfo0GDBkyZMgWbzUZsbCxdunShe/fuxmfoymQykZSUxJNPPkl0dDT169fnueeeIzAwkC+//BKw/xhy6tQpXnnlFdq2bUv9+vV5+umnadiwIXPmzAHsk9auW7eO//znP9xwww3UqVOHgQMHcueddzJ//nyysrJIT09n+fLl3HXXXfTt25e6dety55130rVr1yLFWUTKHiXRRaTMe+yxx1i3bh2vvvoqAwYMID09nTlz5nDjjTeyYMECAPbs2UNycnK+SWI6deoE2Cf7cQgKCqJhw4bGv8PCwgBITEwEoGvXruzfv597772XTz75hCNHjhAREUGzZs3w8Sn6aTciIoKYmBi3jvjy5csJCgqiZ8+eHr1G27ZtufLKK/n000/dXqN3795FquFosVgoynzU27Zto23btm6lXwICArjqqqvYsWOH27otW7Z0+3dYWJgR0z/++IPc3FyaNm3qts64ceMYOHAgAHFxcbRu3dqtVnmVKlW48sor3T6/vNsymUxu2zqXZs2auf3barXy2muv0b17d9q0aUOrVq149913SUtLIysr67yvBxgXHG3btnVb3qpVK2w2m1uc6tSp41YXMu++JyIiIlLaqe9efH13h9tuu41WrVrRqlUrmjdvzi233EJERAQffPABISEhbuvm7e8W5Fz99qLo06cPa9as4Z133uGBBx6gYsWKLFmyhHvuuYcxY8a4XXcEBwdz1VVXGf/29/enXr16xo8rcXFxVKlShfr16xvrmEwmOnbsaOwPjh9XCtpvkpOT+euvv9i/fz9ZWVn54tC6desivz8RKVs0saiIlAuhoaHccMMNxu2Ef/zxB0888QQvvPACvXr1Mm4VnTRpEi+++GK+5ztGO4O9I+7KkSB2dPK6dOnCe++9x/vvv89LL71EcnIyTZs25ZlnnqFVq1YX1P6+ffsyevRojh49SvXq1Vm+fDk9e/bM15ZzufPOO5k6dSrJyclkZmbyyy+/8N577xWpHVWr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ESNG8MYbbzBkyBBmzJhR4DpBQUFMnTqVF198kZtvvpm6devy6KOP0rp1a3777TfGjBnDK6+8QkxMDO3bt+e1117jm2++4eOPP873Wm+88QZTp04lNjaW+Ph4IiMjGTx4MKNGjSpSu0VERERKM/Xdi7/v7g333HMPcXFxDBkyhIceeoh777033zq9e/emSpUqfPDBB4wfP55Tp07h4+NDrVq16N27N0OHDnUr6xMeHs4DDzzAhAkT2L9/P9WrV2fatGnGZKPh4eEsWLCAqVOnMnz4cDIyMqhRowb33HMPw4YNM15n+vTpvPrqqzz77LOcOXOGiIgIunTpwujRo416+c8++yyxsbE89dRTAFx77bVMnDiR++67j+zs7EsZOhEpwUw2zUYmIlJubN++nf79+7NgwQLatGnj7eaIiIiIiEgh1He3Gzt2LJs2beL777/3dlNEpBzTSHQRkXLg9OnT7N27l6eeeoqePXuW6064iIiIiEhJpr67iEjJoyS6iEg58NRTT7Fhwwa6devGc8895+3miIiIiIhIIdR3FxEpeVTORURERERERERERESkEIVPDy0iIiIiIiIiIiIiUs4piS4iIiIiIiIiIiIiUggl0UVERERERERERERECqEkuoiIiIiIiIiIiIhIIczebkBpcfJksle26+fnS1ZWjle2XRopXp5TrDynWBWN4uU5xapoFC/PKVae82asqlSp4JXtlgVF7ZvrmCgaxctzipXnFKuiUbw8p1gVjeLlOcXKc6U9Vp70yzUSvQQzmdz/L+emeHlOsfKcYlU0ipfnFKuiUbw8p1h5TrEqH/Q5F43i5TnFynOKVdEoXp5TrIpG8fKcYuW58hIrJdFFRERERERERERERAqhJLqIiIiIiIiIiIiISCG8mkQ/dOgQQ4YMoWXLlnTq1IkpU6aQm5ubb72srCymTZtG165dadmyJffccw8HDx40Hj9x4gSPPfYYnTp1ok2bNowbN46MjIwib6ekCZzyIgFPPUnglBe93RQRERERERERKSWCprxI8DPjCFI+QUSkWHgtiW6z2Rg1ahQRERH88MMPfPjhh6xYsYL58+fnW3fWrFksW7aMOXPmsGHDBho2bMjIkSONRPiYMWNISUlh+fLlrFy5kn379jF58uQib6ekCXj/XfxnvEHA++96uykiIiIiIiIiUkoEfPAuQW+/ScAH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i2cjckRERDylkegi4qp58+YcOXKE+Ph4IiIiANi6dSsNGjQgODjYbd3t27eTlJREp06dAPukpAkJCbRu3dpYJzfXWTvbZrORk5ODj48PO3fu5PPPP2fs2LHG43v27MHPz4+qVasWqc1FvUiz2cr26KjiZrPln1RU8SuY9i3PKVZFczHxSpnyKmRkQEBAuYi59q2iUbw8p1h5rqzHShOLlmDWnn3IvrU/1p59vN0UEREpYzQSXURcNW7cmOjoaCZNmkRSUhK7du1i1qxZ3H333QD07t2bzZs3A7Br1y7GjBnDgQMHSExMZOrUqfTq1YuaNWsSHBxMu3btmDlzJmfOnCErK4vZs2djNptp06YNERERLFy4kFmzZpGVlcW+fft4/fXXueuuu/D1LbvlBkqrrVs1qahIaZXVsw9ZN/cjS/kEEZFioSS6iIhIOeSaRNdIdBEBeP3110lOTubaa6/lX//6FwMHDmTQoEEA7N27l7S0NAD69etH3759GTBgAN26daNatWq88MILxutMmzaNsLAwbrrpJjp16sSaNWt4++23qVSpEpGRkcyaNYvvvvuODh06cP/993PdddcxZswYr7xnObe4OOcPGy1alN1yECIiIiLnY7LZyvJA++Jz8mTyZd+mYwIQq1W3TnpC8fKcYuU5xapoFC/PeTtW8fHQqFEFALp1y+bjj9MvfyOKwNvxKk0UK895O1ZVqlS4/BstI4rSN/f251zauMYrJiaIXbt8sVhs7NmTgr+/t1tXsmjf8pxiVTSKl+cUq6JRvDynWHmuLMTKk365aqKXYL5xv+Gbm0Oujy/Z0a283RwRESlDQkPBZLJhs5lUzkVERPJJTYW//rLfuNy4ca4S6CKljDnuN8jKAj8/slsonyAicrGURC/BQgffhe/RI+RUr8GZuJ3ebo6IiJQhvr4QFgYJCSrnIiIi+R086ENurv374aqrVA9dpLQJvUf5BBGR4qSa6CIiIuWUoy66RqKLiEheqanOv8PCSum92SIiIiLFREl0ERGRcioiwpFEh1wNMhQRERdpac4fWIOClEQXERGR8k1JdBERkXLKMRLdZjORfPnnzxYRkRIsLc35d1CQ99ohIiIiUhIoiS4iIlJOOUaig+qii4iIO41EFxEREXFSEl1ERKSccoxEB9VFFxERd6mprkl0LzZEREREpARQEl1ERKScck2iayS6iIi4ci/nopHoIiIiUr4piS4iIlJOuZZz0Uh0ERFx5V7OxYsNERERESkBlEQXEREppzQSXURECqOR6CIiIiJOSqKLiIiUUxqJLiIihdHEoiIiIiJOZm83QAqX8PMvmH19yM7J9XZTRESkDAoLc/6tkegiIuLKfSS699ohIhcm/qdfwGYDk/p4IiLFQUn0EswWUgEsvtisOaDBHyIiUsw0El1ERAqjkegipZstpIK3myAiUqaonIuIiEg55VoTXUl0ERFxpZHoIiIiIk5KoouIiJRT7hOLerEhIiJS4riORA8O1kh0ERERKd9UzqUEC3jrDcypKfgGh5D+4ChvN0dERMoYPz97YiQ11aSR6CIi4sY1iR4Y6MWGiMgFCXzrDUzJSdgqhJI+QvkEEZGLpSR6CRb41hv4Hj1CTvUaSqKLiMglERFhT6JrYlEREXHlKOcSEGDD19e7bRGRoguc6ZJPUBJdROSiqZyLiIhIOeYo6ZKQYMKmu/VFROSs1FT7j6uaVFRERERESXQREZFyLSLCnhyxWk2kpnq5MSIiUmI4RqJrUlERERERJdFFRETKNdfJRVUXXUREHBw10TUSXURERERJdBERkXLNNYmuuugiIgJgs2kkuoiIiIgrJdFFRETKMUc5F9BIdBERscvMhNxcjUQXERERcVASXUREpBxTORcREcnLMQodNBJdREREBJREFxERKdciIpx/q5yLiIgApKY6vw80El1ERERESXQREZFyTSPRRUQkr9RU598aiS4iIiICZm83QAqXHd0CW82a5FSs5O2miIhIGeVaE10j0UVEBCAtTSPRRUq77OgW5NaIIrdyZW83RUSkTFASvQRL/mAhFosvVmsOqO8qIiKXgPtIdO+1Q0RESg73kei6EBEpjZLeX+jtJoiIlCkq5yIiIlKOaSS6iIjkpYlFRURERNwpiS4iIlKOuY5ET0xUEl1ERFTORURERCQvJdFFRETKscBACAiwJ0g0El1EREATi4qIiIjkpZroJViF/7sT3zOnyalYSfXMRETkkgkPt3HsmImEBCXRRUREI9FFyoLQwXfic+oUuZUrK58gIlIMlEQvwcxb4/A9egRT9RreboqIiJRhERE2jh1DSXQREQFUE12kLHDkE3KUTxARKRYq5yIiIlLOOeqip6WZyMjwcmNERMTrUlM1El1ERETElZLoIiIi5ZwmFxUREVcaiS4iIiLiTkl0ERGRci4iwplE1+SiIiLimkQPDtZIdBEREREl0UVERMq5sDDn36qLLiIiKuciIiIi4k5JdBERkXJOI9FFRMSVyrmIiIiIuFMSXUREpJxzrYmekOC9doiISMmQlqaR6CIiIiKulEQXEREp5zQSXUREXGkkuoiIiIg7JdFFRETKOfeR6Eqii4iUd46a6BaLDYvFy40RERERKQHMRX2CzWbDZNIF9uWQPmIU5tQUsoNDvN0UEREpwzQSXaR0Ur9cLhXHSHSNQhcpvdIfHIUpOQlbhVBvN0VEpEwochL9uuuu47bbbuO2224jKirqUrRJzsoYMQqLxRerNQdUilBERC4RjUQXKZ3UL5dLxZlE10WISGmVPmKUt5sgIlKmFLmcy4ABA/j666+5/vrrGTJkCN988w3Z2dmXom0iIiJyGWgkukjppH65XCqOci4aiS4iIiJiZ7LZbBc0vGD79u0sXbqUFStWkJ2dTb9+/bj99tupW7duMTexZDh5Mvmyb9NkwhiJfmGfUvmieHlOsfKcYlU0ipfnSlKsbDaoUSOEnBwT0dE5fPtt2vmfdJmVpHiVdIqV57wdqypVKhTL65S3fjkUrW/u7c+5tDGZoHr1ELKzTTRvnsN335W874SSQvuW5xSrolG8PKdYFY3i5TnFynNlIVae9MsvOInuYLPZWL58Oc8++yxJSUlcffXVPPLII0RHR1/My5Y43kii+6QmY/b1ITsnl9zg4rnIKsvKwkF7uShWnlOsikbx8lxJi1WTJsGcOuVD7dq5bN6c6u3m5FPS4lWSKVae83asiiuJ7lBe+uWgJPqlZLVCVJR93+zYMZsvv0z3cotKLu1bnlOsiqY44mVKSbaPlDCZsIWU3XyC9q2iUbw8p1h5rizEypN+eZHLuThkZ2ezYsUKhg0bxpNPPkmlSpUYO3YsTZo04d577+Xzzz8/72scOnSIIUOG0LJlSzp16sSUKVPIzc3Nt15WVhbTpk2ja9eutGzZknvuuYeDBw8aj585c4YxY8bQqVMn2rZty+DBg9m+fbvba8yYMYOYmBhatWrFfffd5/b8kir86naE1a5O+NXtvN0UEREp4xx10VXORaT0KY5+uYhDmsvAc5VzESm9Iq5pR+X6NYm4RvkEEZHiUOSJRf/55x8++eQTvvjiC5KTk+nZsyfz5s2jXTvniblt27aMHz+eW2+9tdDXsdlsjBo1igYNGvDDDz9w+vRphg4dSqVKlbj//vvd1p01axbLli1jzpw5REVFMXXqVEaOHMkXX3yBj48PEydOJDU1lRUrVhAUFMSbb77JsGHDWLt2Lb6+vnz44YesWbOGRYsWERwczEsvvcT8+fN55plnivr2RUREyqTwcPv/k5NNWK1gsXi1OSLigeLql4u4Sktz/piqiUVFRERE7IqcRL/xxhupU6cOQ4YMoX///kRERORbp0uXLqSkpJzzdbZt28auXbuYP38+YWFhhIWFMWzYMObNm5cvib569WoGDBhA/fr1AXj88cdp06YNcXFxtGrVih07dnDfffcRfjYD0LdvX2bOnMnJkyepVq0ac+fO5dVXX6VGjRoAvPjii0V92yIiImWa6+SiiYkmKldW4kSkpCuufrmIK41EFxEREcmvyOVc5s6dy8qVKxkyZIhbRz09PZ3ly5cb//7tt9/O+Tp//vknUVFRRuIboEmTJuzbty9fR99ms+Faut1iseDv78+OHTsAuO6661ixYgUnT54kIyODzz77jCZNmhAZGcmxY8c4duwY+/fvp2fPnnTo0IFHH32U+Pj4or51ERGRMstRzgUgIcF77RARzxVXv1zEVWqqRqKLiIiI5FXkkegjRowgLi4u3/KMjAxiY2O54YYbPHqd+Ph4wsLC3JY5/h0fH09ISIixvEuXLixevJgePXpQo0YN5s2bh9VqJTExEbCPTB8xYgQxMTEA1KhRgzlz5mAymTh+/Dgmk4lvv/2WhQsXkpGRwcMPP8yECROYPn16kd67yYtlYr257dLCESPF6vwUK88pVkWjeHmupMXKdSR6QoIJk6lkJU5KWrxKMsXKc6U9VsXVLxdx5V7OxYsNERERESlBPE6iL168mE8++YSsrCwGDhyY7/ETJ04QdIl6WcOHDychIYH77rsPs9nM4MGDqV27NmazvfkTJ04EYO3atYSGhjJv3jyGDBnCsmXLsFqtWK1WxowZY4zQefjhh3nggQfIzMzE39/fozb4+flekvd2Lq4XdhbL5d9+aWMyga+vLyYTpXY24MtFsfKcYlU0ipfnSlqsKlVy/p2c7IvFUrKyiiUtXiWZYuW50horb/bLpexzL+dSig4MERERkUvI4yR6586dycjIYNu2bdSrVy/f440bNy7ShEWVKlUi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/DIhISHMnz8fs9lMmzZtqFSp0jm3U565JtFTUryVRHduNyhISXSR0swWHExuiPIJIiLFxaMker9+/dw664WxWq0X3SBxSlj/KxaLr71+mfqwIiJyiWliUZGS71L2y19//XUmTJjAtddeS3BwMIMGDWLQoEEA7N27l7Szw5T79evH7t27GTBgANnZ2XTv3p0JEyYYrzNt2jQmT57MTTfdRGZmJo0aNeLtt9+mUqVK591OeVahgvPvlBTvtCE11fl3UJB32iAixSP+51+93QQRkTLFoyS6p7eDLly48KIaIyIiIt7jPrGoFxsiIoW6lP3yatWqMWvWrAIf27Vrl/G3yWRi7NixjB07tsB1q1atyrRp0y5oO+WZa01075Vz0Uh0ERERkYIUeWJRsNda3L17N5mZmcayo0ePMnnyZO68885ia5yIiIhcPn5+9hq4qakmjUQXKSXULy87SlpNdI1EFxEREXEqchL9p59+YtSoUaSnp2MymbCdnXbVZDJx0003FXsDRURE5PKJiLAn0TWxqEjJp3552eJezkUj0UVERERKkiIn0V955RXuuecebr31Vm6++WaWL1/Otm3b+OKLL3j66acvRRvLraCJ4zEnJWIJDSM1dpK3myMiIuVAeLiNQ4fsNdFtNvCg9LKIeIn65WWL+0h077QhNdU1ie6dNohI8QieOB5TYgK2sHBSJyqfICJysYqcRN+3bx+LFi3C19cXk8lErVq1qFWrFhEREcTGxvL6669finaWS/5LPsH36BF8q9dQEl1ERC6LiAh7EsdqNZGaCiEhXm6QiBRK/fKyxbUmuvdGojv/1kh0kdLN/zN7PiGneg0l0UVEioFPUZ9gNpvJyMgAIDg4mJSzU8d37NiRn3/+uXhbJyIiIpeV6+SiiYkahi5SkqlfXra4/mipiUVFRERESpYiJ9FbtmzJmDFjyMjIoF69erz22mucPn2aFStWYLFYLkUbRURE5DJxTaKrLrpIyaZ+edniPhLdO23QxKIiIiIiBStyEn3cuHEcOXIEgOHDh7No0SJiYmIYPXo0gwYNKvYGioiIyOXjKOcC9rroIlJyqV9etvj7g5+f/RxcEkaiBwdrJLqIiIiIQ5FrotetW5cvvvgCgC5durBixQr++OMPoqKiaNasWbE3UERERC4fjUQXKT3ULy97KlSwcfq0yYtJdOffGokuIiIi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SpUqBAQEWO178OBB4uLiaNKkCZB2UdKYmBjq1q1r2cdsNrNz504mT55s9dgjR46wfv16xo4da9l2/PhxfHx8KF26tF1jtneRZjYXrOqoTB10iIszULJk3gw+MTHjtVKkiNkq8VaQ4uVMipXtFCv7OCpehSHmem3ZR/GynWJlO3ePld1JdIBz585RtWpVWrRoAcDRo0c5ffo0FStWdOjgRERERFyZtzesXn2NTz7xtvQUdjaDATw8PDCZTC41iV22zIfoaAOXL6vowpEcNS+vXr06tWrVYtKkSYSHh3Pu3DkiIiIYMmQIAB06dGDSpEnUr1+fyMhIZs6cyapVqwgODmbGjBm0b9+eChUqWI538eJFLl++TJUqVazOExISwurVqylevDh9+/bl7NmzzJ49m549e960Yr2wCQrKeAMnJEDJknlznvSLikJaT3QRERERycruJPru3bsZPHgwkydP5pFHHgHgp59+4u2332bBggXqiy4iIiKFSpkyZoYNS3b2MCzSe5+mt2RwFd9/70V0tCfR0QaMRlC+9PY5el4+e/Zsxo8fT/PmzQkICKBXr1706tULgBMnTnD1f38p6tq1K3/99Rfdu3cnNTWVtm3bMn78eKtjXbx4EcBS1Z4uNDSUiIgIZsyYwfz58wkJCaFjx44MGzbslmLgzgIDM97A8fEGIG/e0Jn/AFikiAt9aIiIiIi4ELuT6G+//TYjRoygY8eOlm19+vTB29ubGTNmsHbtWocOUEREREQKvvRWFGazgStXDJQqpWTd7XL0vLxMmTJERERke19kZKTltsFgYOzYsVYtWW4UFhZm9ZjMGjRowOrVq+0aW2GUfmFRSLu4aF5RJbqIiIjIzdmdRD969Ch9+vTJ0mfxiSeeYPr06Q4bmMD13k/jlRBPamDQzXcWERERcWElSmQkzaOilER3BM3L3Zt1JXrenUeV6CLu6fp/nsYQF4e5aFFnD0VExC3YnUQPCgrixIkTVK5c2Wr7kSNHslx0SG7PtdGvWL6OnUff3hQRERHJFyVKmCy31RfdMTQvd2/WPdHzqxJdiw4Rd3F19CvOHoKIiFuxO4neuXNnBg8ezFNPPUWFChUwmUz8/fffrFq1ii5duuTBEEVERESkoMtcia4kumNoXu7eMifR03qi543ExIzbauciIiIikj27k+gjRozA39+fiIgIrly5AkDx4sX5z3/+w8CBAx0+QBEREREp+NJ7okNaOxe5fZqXu7fAwIzbedvORZXoIiIiIjdjdxLd09OTIUOGMGTIEBISEgAIzDzDExERERG5gZLojqd5uXvL3BM9b9u5ZNxWJbqIiIhI9jzsfYDZbGbRokX89ttvBAYGEhgYyKZNm1i4cGFejK9QC6lVjeBiAYTUqubsoYiIiIjcFrVzcTzNy92bM3qiBwSoEl3EXRS/vxqlShel+P3KJ4iIOILdSfR58+axbNkyzOaMCVbx4sVZtWoVc+bMcejgRERERMQ9ZK5EVxLdMTQvd29BQRm387InunU7lzw7jYiIiEiBZncSfd26dXzwwQfUq1fPsq1JkyYsWbKEdevWOXRwIiIiIuIeMleiq52LY2he7t4yt3PJ257oGbfVE11EREQke3Yn0a9cucJdd92VZXvFihWJjo52xJhERERExM34+2ck6FSJ7hial7u3zO1c8q8SXUl0ERERkezYnUSvXLkyn3/+eZbtH330EZUqVXLIoERERETE/aS3dFES3TE0L3dvAQEZt3VhURERERHn8rL3ASNGjOD5559n+fLlVKxYEZPJxIkTJzh79izz5s3LizGKiIiIiBsoWdLMyZNw5YoBoxE8PZ09ooJN83L35umZdqHPxEQDCQl5dx5VoouIiIjcnN2V6C1atODLL7+kefPmeHh44OnpSbt27fjyyy9p2bJlXoxRRERERNxAel90s9nAlSuqRr9dmpe7v/S+6HnbziXjtirRRURERLJndyU6pPVZHDNmjKPHIiIiIiJuLPPFRS9fNlCqlKpeb5fm5e4tKMjMhQt53c4l7dheXmZ8fPLsNCIiIiIFmt1J9FdeeSXH+4xGI9OnT7+tAYmIiIiIe7oxiS63R/Ny9xcUlPbf+Hgwm8GQB2+b9Ep0VaGLiIiI5MzuJPrx48etfjaZTJw7d47r16/TsGFDhw1MRERERNxLyZImy20l0W+f5uXuL72di9lsIDERAgMdf470SnT1QxcRERHJmd1J9NWrV2e7ffny5bc7FrlBwvxFeBlTSPX0dvZQRERERG5b5kr0S5eURL9dmpe7v/QkOqS1dMn8s6NkJNEdfmgRcaL49xZBUhL4+jp7KCIibsHuC4vmpG/fvnz22WeOOpwAKQ80J7Xtg6Q80NzZQxERERG5bSVLqp1LftC83H2kt3MBSEjIm3NktHNRJbqIO0l5oDkpbdopnyAi4iAOS6InJydz5coVRx1ORERERNyMkuj5Q/Ny9xEUlPGeiY93/HsmNRWSk9XORURERORm7G7n8vbbb2fZlpKSwq5du6hYsaJDBiUiIiIi7idzO5eoKCXRb5fm5e4vr5Po6VXooHYuIiIiIrmxO4m+cePGLNv8/PyoUqUKL774okMGJWm8d+7Ay5gCnt4kN9VXsERERKRgy5xEVyX67dO83P1lvpBo3iTRM46pSnQR9+K9c4elJ7pauoiI3D67k+hbt27Ni3FINgKfexbPc2cxli3HlX1HnD0cERERkdtSpEhaou7qVYOS6A6gebn7s76wqOOPr0p0EfcVNET5BBERR7K7J3pKSorVz7t27WLTpk3ExMQ4akwiIiIi4qbS+6IriX77NC93f5nbuSQkOP49k5ioSnQRERERW9icRI+Li6NHjx5WFS+jRo2if//+vPTSSzzyyCOcPXs2TwYpIiIiIu4hvaXL5csGjEYnD6aA0ry88Mjfdi4OP7yIiIiI27A5iT5r1iyuXbtG1apVATh48CAbN25k8uTJ7N69m4YNG7J06dI8G6iIiIiIFHzpSXSz2UB0tKrRb4Xm5YWH9YVFHX9863YuqkQXERERyYnNSfQdO3bw5ptvctdddwGwbds27rnnHh577DGKFSvG8OHD2bFjR16NU0RERETcQHo7F1BLl1uleXnhkdftXFSJLiIiImIbm5Poly9fplq1apaf9+7dS6NGjSw/33nnnURFRTl2dCIiIiLiVtIr0QGiopREvxWalxce1pXoeZFEz7gdEKBKdBEREZGc2JxE9/b2xvi/xpUmk4l9+/ZRp04dy/2pqamOH52IiIiIuJUSJUyW26pEvzWalxceAQEZtxMSHH9860p0JdFFREREcmJzEr18+fL89ttvQNpXSK9fv07Dhg0t9x86dIjQ0FDHj1BERERE3Ebmdi6qRL81mpcXHnnfziXjttq5iIiIiOTMy9Yd/+///o+XXnqJ1q1bs3XrVlq1akWpUqUAOHXqFJMnT6ZVq1Z5NU4RERERcQPqiX77NC8vPPz9wdPTjNFoyKN2LqpEFxEREbGFzUn03r17c+bMGXbt2kXDhg0ZN26c5b5ly5YRFxfH4MGD82SQIiIiIuIe1BP99mleXngYDBAUBDExed8TXZXoIiIiIjmzOYnu5eVlNUHP7JlnnuGVV17B29vbYQMTiN5/BG9vT1JSjKDCEBEREXEDmZPoqkS/NZqXFy6BgWZiYgzExzv+2ImJqkQXcVdX9h1x9hBERNyKzUn03JQrV84RhxERERERN6cket7SvNz9pPdFz5zwdhTrdi4OP7yIiIiI27D5wqIiIiIiIrcrICCj4lXtXERuLjAw7b9XrxpITXXssa3buagSXURERCQnSqKLiIiISL5Kr0ZXJbrIzQUGZiS3ExIce2xVoouIiIjYxiHtXCRv+L81Fa+EeLwCg7g66hVnD0dERETEIUqUMHPqFFy5YsBkAg+VdYjkKL2dC6RdXLRYMcdVjKsSXcR9FXlrKoa4OMxFi3J1tPIJIiK3y6lLltOnTzNgwABq165NkyZNeOuttzCZTFn2S05OZubMmbRu3ZratWvTp08fTp06Zbn/ypUrjBo1iiZNmlC/fn169+7NwYMHrc7z3HPP0bBhQ5o0acKYMWOIjY3Nl+d4O/w+fB/f9+bh9+H7zh6KiIiIiMOULJmWrDOZDERHqxpdJDeZk+gJCY59v6RXohsMZvz8HHpoEXEyvxXvU2Thu/itUD5BRMQRnJZEN5vNDB06lJCQELZv387KlSvZvHkzy5cvz7JvREQEmzZtYvHixezevZt7772XIUOGWBLuEyZMIDo6ms2bN7Nr1y7q1q3LwIEDMRqNADz33HMUK1aMbdu2sWHDBk6cOMH06dPz8+mKiIiIyP9kvrio+qKL5C69JzpAfLxjj51eiV6kCBj0VhQRERHJkdOS6AcOHCAyMpJx48YRHBxMpUqVGDhwIKtXr86y77Zt2+jevTuVK1fGz8+P0aNHc+LECfbt2wfA4cOHadOmDcWKFcPHx4dOnToRFRXFpUuXiI+Pp2bNmowaNYqAgABKlSpFly5d2LNnT34/ZRERERHBOomuvugiubPuiZ43legBAWrlIiIiIpIbpyXRDx06RPny5SlWrJhlW40aNfjnn39IuOGKOWazGbM5Y2Ln7e2Nr68vhw8fBqBVq1Zs3ryZS5cucf36dT777DNq1KhBaGgoQUFBTJ06lRIlSlgef+bMGcqWLZu3T1BEREREslWyZEb7PiXRRXJ3Y090R8pciS4iIiIiOXPahUWjo6MJDg622pb+c3R0NIGZvrfYsmVL1qxZQ7t27ShXrhzLli0jJSXF0td89OjRPPfcczRr1gyAcuXKsXjxYgzZfCfxwIEDrFy5kjlz5tg9Zmd+xVFfr7y59BgpVjenWNlOsbKP4mU7xco+ipftCkKs0nuiQ1o7F2eNtSDESiQoKON2XlWi66KiIiIiIrlzWhLdHoMGDSImJoa+ffvi5eVF7969ueOOO/DyShv+hAkTAPjhhx8oWrQoy5YtY8CAAWzatImAgADLcfbu3ctzzz3HmDFjaNmypV1j8PHxdNjzsVXmhZ23d/6fv6AxGMDT0xODAcxaB+RKsbKdYmUfxct2ipV9FC/bFYRYhYZmJAKjoz2cNs8pCLESsa5Ed9xxjUa4fj09ie6444qIiIi4I6cl0UuUKEFMTIzVtujoaACKFy9utd3Pz4/w8HDCw8OBtPYuixcvJjQ0lMTERNatW8dHH31EaGgoAEOGDGHp0qXs2LGDDh06AGl91UePHs3rr7/OI488Yvd4k5ON+V6llL6YM5shJcWYvycvgNIXwKmpRi2Eb0Kxsp1iZR/Fy3aKlX0UL9sVhFgVK5YxsIsXnTfPKQixEsncE92R7VyuXcu4rUp0ERERkdw5LYkeFhbG2bNniY6OJiQkBID9+/dTpUoVq+pxgIMHDxIXF0eTJk2AtJYsMTEx1K1b17KPyZTRW9NsNmM0GvHwSGv5/ttvv/Hyyy8zZ84cmjZtestjdubiSgs725nNipetFCvbKVb2Ubxsp1jZR/GynSvH6sYLizp7nK4cK5FMXS4d2s4lMTHjWEqii4iIiOTOaRcWrV69OrVq1WLSpEnExcURGRlJREQETz31FAAdOnRgz549AERGRjJq1ChOnjxJbGwsM2bMoH379lSoUIGAgAAaNGjAggULuHLlCsnJySxatAgvLy/q1atHamoq48aNY/jw4beVQBcRERERx8icRI+KUkNykdxkbueSkOC446ZfVBTUzkVERETkZpzaE3327NmMHz+e5s2bExAQQK9evejVqxcAJ06c4Or/ZnZdu3blr7/+onv37qSmptK2bVvGjx9vOc7MmTOZNm0ajz76KElJSVStWpWFCxdSokQJ9uzZw99//82bb77Jm2++aXX+r776ivLly+ffE7ZTStMHMEVfwRhS/OY7i4iIiBQQAQHg72/m2jUDly8riS6SG+ue6I57v6RfVBQgIECV6CLuJqXJAxivXMZUvISzhyIi4hYMZrO+vGqLS5cceBUfG6VfUDQlRX06baF42U6xsp1iZR/Fy3aKlX0UL9sVlFjVqxfAqVMelCxp4tChRKeMwdmxKlUqKP9P6ibsmZs7+//z7bpyBapVS3uttG2byqpV127yCNv8+qsHjzyS1kZz0KBkJk5MAgp+vPKTYmU7xco+ipftFCv7KF62U6xs5w6xsmVe7rR2LiIiIiJSeKW3dLlyxUCmS9uIyA2se6I77riZK9HVE11EREQkd0qii4iIiEi+S0+im0wGoqPV0kUkJz4+4OeX9n7Jq3Yu6okuIiIikjsl0UVEREQk32W+uKj6oovkLjAw7f2SkODIJHrGbVWii4iIiOTOqRcWldwV7foonlGXMJYsRey6jc4ejoiIiIjDlCypJLqIrQIDISpKlegiYrvgbo/icekiplKllU8QEXEAJdFdmOffx/A8dxbKxjp7KCIiIiIOlbkSPSpKSXSR3AQFpVeig9mcdgGv26VKdBH3lp5PMMbFOXsoIiJuQe1cRERERCTflSyZcTVRJdFFcpeeRE9JMZCU5Jhj6sKiIiIiIrZTEl1ERERE8p16oovYLjAw47ajWrpYV6I75JAiIiIibktJdBERERHJd5l7oqsSXSR36RcWhbSWLo6gSnQRERER2ymJLiIiIiL5TpXoIrZLb+cCkJCgSnQRERGR/KYkuoiIiIjkOyXRRWyXOYnuuHYuqkQXERERsZWS6CIiIiKS7wICwN8/LXGnJLpI7qx7ojvmmNZJdMccU0RERMRdKYkuIiIiIvnOYMioRr90SUl0kdzkfTsXVaKLiIiI5EZJdBERERFxivQkenS0AZPJyYMRcWF53c7F398hhxQRERFxW17OHoDk7Oqol/G6dpVUf32/UkRERNxPehLdaDQQEwPFizt3PCKuyrqdi2Mr0YsUMeOh0ioRt3P1pZcxJCZiDghw9lBERNyCkuguLKlPP0zenqSkGEHfsBQRERE3U7Jk5ouLelC8uMrRnen06dOEh4ezd+9e/P396datGy+99BIeN2RYk5OTmTt3Lhs3biQ6OppatWoxefJkKlasCEBYWFiWY6ekpDB16lS6du1q83kkQ2Bg5nYujjlmYmJaMl6tXETc0/U+/Zw9BBERt6KZqoiIiIg4RXolOkBUlPqiO5PZbGbo0KGEhISwfft2Vq5cyebNm1m+fHmWfSMiIti0aROLFy9m9+7d3HvvvQwZMgTT/3ryHDhwwOrfmjVrCAkJoUWLFnadRzLkZU90XVRURERE5OaURBcRERERp1AS3XUcOHCAyMhIxo0bR3BwMJUqVWLgwIGsXr06y77btm2je/fuVK5cGT8/P0aPHs2JEyfYt29fln3NZjPh4eEMHTqUEiVK2HUeyRAUlHHb0T3RVYkuIiIicnNKorsww/nzGM6cwXD+vLOHIiIiIuJwpUpltG+5fFlJdGc6dOgQ5cuXp1ixYpZtNWrU4J9//iHhhv4hZrMZszkj8ert7Y2vry+HDx/OctxNmzaRkJBAz5497T6PZMjcziU+/vaPZzbDtWtpt1WJLuKePC6cx+PsGTwuKJ8gIuII6onuwoo91ArPc2cxli3HlX1HnD0cEREREYdSJbrriI6OJjg42Gpb+s/R0dEEZrqyZcuWLVmzZg3t2rWjXLlyLFu2jJSUFGJjY60ebzKZmDt3LiNHjrT0O7fnPDdjsPElk76frfu7oqJFrdu53O5zuXYNzOaMSvTMx3OHeOUXxcp2ipV9HBGvzPmE6P3um0/Qa8s+ipftFCvbFZZYKYkuIiIiIk6ROYmuSvSCY9CgQcTExNC3b1+8vLzo3bs3d9xxB15e1kuL7du3c+3aNR588EGHj8HHx9PmfQ0G8PT0xGBIq8AuiDIV7pOYaMDb2/bnn52UlIzbAQHWx3OHeOUXxcp2ipV9HBGvzEmt2/3McGV6bdlH8bKdYmW7whIrJdFFRERExCmURHcdJUqUICYmxmpbdHQ0AMWLF7fa7ufnR3h4OOHh4UBae5fFixcTGhpqtd+XX35Jhw4d8PTMSN7Yc57cJCcb7apEN5shNdVYoBd2gYFmEhIMxMVBSorxto4VG5sRPH9/k9Xx3CVe+UGxsp1iZR9HxCv9cWbz7X9muDK9tuyjeNlOsbJdYYmVkugiIiIi4hQlSyqJ7irCwsI4e/Ys0dHRhISEALB//36qVKlCQECA1b4HDx4kLi6OJk2aAGkXJY2JiaFu3bqWfcxmMzt37mTy5Mm3fJ6bsXeRZjYX7Oqo9CR6fLzhtp9HYmLG+61IEXO2xyvo8cpPipXtFCv7OCpehSHmem3ZR/GynWJlO3ePlS4sKiIiIiJOERAAfn5pM231RHeu6tWrU6tWLSZNmkRcXByRkZFERETw1FNPAdChQwf27NkDQGRkJKNGjeLkyZPExsYyY8YM2rdvT4UKFSzHu3jxIpcvX6ZKlSp2nUdyFhSU9l5JSLj998rVqxm3dWFRERERkZtTEl1EREREnMJgyGjpoiS6882ePZv4+HiaN29Ov3796NGjB7169QLgxIkTXP1f5rVr16506tSJ7t2706ZNG8qUKcOUKVOsjnXx4kUAS7W5reeRnAUFpf03IQFMpts71tWr1pXoIiIiIpI7tXMREREREacpWdLMmTNw5YoBkwk8VOLhNGXKlCEiIiLb+yIjIy23DQYDY8eOZezYsTkeKywszOoxtp5HchYYmJbsNpsNXL0KgYG3fixVoouIiIjYR8sUEREREXGa9Ep0o9HADdebFJFM0pPoAPHxt/fNDVWii4iIiNhHSXQRERERcZr0JDrA5cuamorkJL2dC9x+X3RVoouIiIjYRysVEREREXEa6yS6+qKL5CT9wqIA8fG3dyxVoouIiIjYRz3RXVjcui/wwkSq/tYhIiIibqpkyYwEni4uKpKzvGvncluHEhEXFfvpF5CaCl5K+4iIOII+TV2Ysco9eHh7YkwxggpERERExA2VLGmy3FYSXSRnmS8kevtJ9IzbqkQXcU/GKvc4ewgiIm5FJc4iIiIi4jRq5yJim8ztXBISbu9YiYlq5yIiIiJiDyXRRURERMRpMrdzURJdJGfWSXRdWFREREQkP6mdiwvz/fQTPJOT8PDx5Xq3J5w9HBERERGHy1yJrnYuIjnLu57oqkQXcUe+n36C4do1zP7+JD2mfIKIyO1SEt2FFXl9PJ7nzmIsW05JdBEREXFLqkQXsU1QUMbt+PjbO1bmSvSAgNs7loi4poA3MvIJSqKLiNw+tXMREREREacJCABf37REuirRRXLmyHYu6okuIiIiYh8l0UVERETEaQyGjGp0VaKL5Czv2rnc1qFERERECgUl0UVERETEqdL7ol++bMBkcvJgRFxUYGDGbUddWNTX14yn520dSkRERKRQUBJdRERERJwqPYluNBqIjXXyYERclHU7l9s7VnolekCAWrmIiIiI2EJJdBERERFxKl1cVOTm/PzAyyvtvXL77VzS/qtWLiIiIiK2URJdRERERJwqvRIdICpK01OR7BgMEBSUdttRPdF1UVERERER22iVIiIiIiJOlbkSPSpKlegiOUm/uGh8/K0fw2xWJbqIiIiIvZREFxERERGnylyJrnYuIjlLT6InJt76+yQpCUwmVaKLiIiI2MPL2QOQnJlKh2IwgKlUqLOHIiIiIpJnSpY0WW4riS6Ss/SLi167ZiAlBby97T9GehU6qBJdxJ2ZSoda/VdERG6PkuguLHbLdry9PUlJMYKKRERERMRNWfdEVxJdJCeBgRm3ExIgJMT+Y6T3QwdVoou4s5hvtzt7CCIibkXtXERERETEqdTORcQ26ZXocOsXF7VOot/2kEREREQKBSXRRURERMSpSpVSJbqILTIn0RMSbjWJnnFblegiIiIitlESXUREREScKiAAfH3TknlKoovkLHM7F8dUoiuJLiIiImIL9UR3YQEvDccrNobU4GIkzJjt7OGIiIiI5AmDIa2ly9mzBrVzEclFYGDmSvRbO4YuLCpSOAS+NByPmGhMxUJImKl8gojI7VIlugvz+fZrvD//DJ9vv3b2UERECrRmzeqze/euW3rskiULGTiwb473jxz5PIsWzQdg8uQJhIe/AsDy5YsZOnTgLZ1TpDBK74t+5YoBs4pjRbLlmHYuqkQXKQx8tnyN7xfr8dmifIKIOEZhX1erEl1EpABJSUnh/feXsGXLN1y+fAkvL28qV65Cv37PUq9eA2cP77YtWbKQ5csX4+3t/b8tBkqVKsXDDz9K79798PT0dOr4sjNr1rvZbu/b9xn69n3G8vPHH6/g8cd74OWlX70i2SlZMi2Zl5pqIDYWihVz7nhEXFFQUMbtW2/nknFblegiIlIYaV2tdfWt0EpeRKQAWbBgLnv2/MKUKdO5665KXL16ldWrVzJq1DBWrVpHmTJlnT3E21a9+n1ERCwHwGQy8ddfR3j11dF4eHjQp09/q31NJhMAHh6u/cWq6Oho3n13Nl27dlcSXSQH6ZXokNYXvVgxVciK3ChzJXp8/K0dIzFRlegiIlK4aV2tdfWtcO3oiIiIlT17fqFduw5UqlQFDw8PAgMDGTBgEGPH/tfyS+Txxzuxfv1ay2N2795Fs2b1LT+fOXOa4cOH0LbtA3Tr9ghr135s033nz59nzJiRPPhgc7p27cj06ZO5+r9ytmvXrjFx4ngeeaQdrVo9wKBB/fnrryMAXL4cxdixL/Lww21o374lI0c+z/nz52x6vh4eHlSrVoPOnbuxY8f3AHz55Rf07v0E7703m3btmnHhwnlMJhPLly/mySe70L59S557rj+HDx+0OtaZM6d49tk+tGvXjGef7cOJE8ct9/3880/069eLBx9sTpcuD7Ns2aIsY1m+fDGPPtqOTp0eYuHCdzH/r9/E0KEDmT9/bpb907+uduXKZbp2fRiz2czDD7dm6dIImjdvwN9/H7Pa/4knOvPZZ2uzHEeksLBOomuKKpKdzD3RHXNh0dsekoiISIFjy7r6scc68emnayyPyc919aOPtuPBB1sweLDW1a60rtYKRUSkAKlQoSKbN39h+UWarn37jpQsWcqmY0yY8Cp3312JL7/8jrfems2iRfP5+eefbnrf66+/Srly5fnii29ZsuRDTp06yXvvpV2k6JNPPuLvv4+xatWnbNmynVat2jB9+hQAFi9eQGpqKp9+upENG77h7rsrMW/eO3Y9b5PJhKdnxl+ao6Ki8PHx5ZtvfqBMmbJ8+uknbNjwGZMnv8WGDd/wwAMtGD58CDExMZbHfPbZWl55ZTxffPEtFSveyX//OxZIm6iMGzeGRx/tzNdfb2f69Fl8+OFyfvllt+WxJ04cJykpibVrNzJ16gzWrFnFtm3f2TT24sVL8Pbb8wDYvHkb/fsPpHbtunzzzWbLPkePRnLx4gXatGlnV1xE3El6OxdAFxcVyUFgYMbtW++JnnFblegiIlIYufq6+qOPPmXz5q20bFmw1tWvvebe62p9p1xEJJMNG7yYNs3HamFqMJBnF7kLDDQzdmwynTql2rT/8OGjeOON/9K//38oXTqUOnXq0rjxA7Rs2QYfH5+bPv7YsaMcPnyImTPn4uvrR+XKVZg6dSbFi5e46X2HDh3knXfm4+vri5+fHwMGDGL06OGMGvUKV69excPDAx8fX7y8vOjR4ymefPIpAK5eTcTDwxMfHx+8vLx44YUXMRhsW/gbjUaOHDnM55+vo1evPpbtCQnx9Oz5H0uVwKZNG+jatTtVqtwDQM+evfn44xXs2rWDjh07AWkTokqVqlju79evF+fPn6NMmbJ89tlm/P398fDw4N57q3HXXXcRGXmYhg0bA2AwGOjX71l8fHyoWbMWDRo0Zvfunbf8y7lDh0dYunQhL7wwHIDt27fRqFETgoOL3dLxRNyBkugiN2d9YdFbO4Yq0UVEJC9lt6bOa+68ru7Ro2CsqytWrMD69Zvx88vfdfWSJQsZPHgoBoMhz9fVSqKLiGTy7rs+HD2avxfZePddH5t/2ZcuHcq8eRGcOHGcvXt/4fff9zJt2iQWL17AvHkRlCpVOtfHnzlzmoCAAIoWDbZsq1s37Stp27dvy/U+o9HIww+3tjqe0WgkJiaGrl27s2PH93Tp8jBNmzbjgQda0KpVWwwGA//5T19GjRpGt26P0LRpM1q1akvjxk1zHOPhwwdp0yb9fgOhoaE88URPunfvYdknMDCIgICMcrxz585QsWJFy8+enp6UKVOOc+fOWrZVrHiH5XZoaBkALl26SJkyZfn66018+uknXLx4AZPJREpKCg880MKyf5kyZawmU6GhZfj33xM5Poebad26LbNmTef3338jLKw233+/lX79nr3l44m4gxIlTJbbUVFKootkx7on+u1XogcEqBJdREQcyxlr6vTzutO6ukmTB2jWrCWtWxeMdXXFihX46ivnrKv37fud2rXr5vm6Wkl0EZFMhg5N5s0387cS/fnnk+1+3N13V+Luuyvx+OM9uHw5imeffZpPP/2EwYOHZtnXfMPgc3suOd3n4WHA39+fb7/dkeNjP/hgNb/99is//7yLt96awtat3zJp0nTuuacqn3yygd27d7F7905ee200Xbt2Z+jQEdkeJ/MFUHKScZXxDDf+Ff7G520wZO5glnafj48vv/22h9mzZzJx4ps88EALvLy8ePbZPrk8Nu3xtlQo5KRIkQCaN2/F119vJjg4hEuXLtCsWYubP1DEjWXuia5KdJHsOb4nupLoIiLiWNmtqfOau62r9+79lZ9+2slbb01h27aCsa7es+dXp62rv/lmM8WLl8jzdbVTk+inT58mPDycvXv34u/vT7du3XjppZeyXA02OTmZuXPnsnHjRqKjo6lVqxaTJ0+2/HXkypUrTJkyhZ07d5KSkkL16tUZO3Ys9913n13nERHp1CnV6q/XBgN4e3uSkmLMs0S6rS5evMAHHyxl4MDnKVq0qGV7iRIlueeee4mOvgKAr68vKSmpVo9LV65cea5eTeTy5ShKlCgJpP01PDAwMNf7ypevwLVr1zh79gzlypUH0r5OlpKSQnBwMa5du4aHh4FGjZrQrFkzWrZsy5AhzxAXF4fBkDZRaNmyNS1btqZ27bq8/fa0HH/Z34ry5Stw8uS/lp+NRiPnz5+lfPkKlm1nzpy23L5w4TwApUqVYu/eX7jjjjtp2bINAElJ1zl16hSNG2O1f2pqquVrbhcunL9p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iIh9CsuFRUVE8ouS6C7Ma/8+PM+dxVC2nLOHIiIiIiIiIiIFRHo+wah8goiIQ6idi4iIiIiIiIiIiIhIDpREFxERERERERERERHJgZLoIiIiIiIiIiIiIiI5UBJdRERERERERERERCQHSqKLiIiIiIiIiIiIiORASXQRERERERERERERkRwoiS4iIiIiIiIiIiIikgMl0UVEREREREREREREcmAwm81mZw9CRERERERERERERMQVqRJdRERERERERERERCQHSqKLiIiIiIiIiIiIiORASXQRERERERERERERkRwoiS4iIiIiIiIiIiIikgMl0UVEREREREREREREcqAkuoiIiIiIiIiIiIhIDpREFxERERERERERERHJgZLoIiIiIiIiIiIiIiI5UBJdRERERERERERERCQHSqKLiIiIiIiIiIiIiORASXQXcfr0aZ577jkaNmxIkyZNGDNmDLGxsQD89NNP/N///R9hYWE8+OCDbNiwwcmjdb7c4vXzzz/TvXt36tSpQ+vWrXn33XedPFrnyi1W6UwmE926daN3795OGqVryC1WCQkJjBkzhrp169KwYUPGjRvH9evXnTxi58otXl9++SWdOnWidu3atG7dmtmzZ2MymZw8Yuc5fPgwffv2pX79+jRu3Jjhw4dz8eJFQJ/xN8otVvp8zyq3eKXTZ3ya3GKlz/jCYcqUKVStWtXZw3BZrVu3pmbNmoSFhVn+TZw40dnDcmnvvfcezZo1o06dOvTt25dTp045e0gu59dff7V6TaX/q1q1KmfOnHH28FzOwYMH6dOnD/Xq1aNJkyaMGjWKK1euOHtYLmv//v089dRT1KtXj+bNm7NkyRJnD8ml7Nixg6ZNmzJy5Mgs92kNklVO8TKbzSxZsoSaNWuyatUqJ43OteT22nLnNZuS6C7iueeeo1ixYmzbto0NGzZw4sQJpk+fzoULF3juued4/PHH+eWXX3jttdf473//y/79+509ZKfKKV7nzp1j8ODBPP744/z666+8++67LF++nM8//9zZQ3aanGKV2cqVK/nnn3+cM0AXklusXnvtNfz9/dm+fTvr1q3j77//5quvvnLyiJ0rp3gdOXKE0aNH8+KLL/Lbb7+xdOlS1q5dy0cffeTsITtFUlISAwYMoEGDBuzatYuNGzcSFRXFhAkT9Bl/g9xipc/3rHKLV2b6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REREpLDr0KEDBw4c4MCBA/z555+sXr0ao9FIv379SEhIyLK/rfPhG3377bc3TaIPGDCAP//8k/fee489e/awe/duhg8fzqJFi5g5c+YtnTc/Xbt2jRkzZvDss88SEhKSr+c+deoUp0+fpmnTpjfd96mnniI1NZUPPvggH0YmIoWFkugiIrmYMGECgYGBLFiwgBo1auDh4UGRIkV45JFHmDdvHqVLl7YkpGNiYhg3bhytW7embt26dO3alS+//NJyrFmzZtG1a1e+/fZbS1V7ly5d2L9/v2WfqKgoRowYQf369alfvz6DBw/m1KlTlvt//fVXevTowf3330+zZs0YNmwYZ8+etdzfvHlzlixZwhtvvEGjRo2oX78+L774IklJSRw5coSmTZtiNBrp3LkzY8aM4fTp05aK6TZt2jBkyBAA/vnnHwYNGkSjRo2oU6cO3bp148cffwRg69atdOvWDYD69evzzjvv8PPPP1O1alX+/vtvAIxGIxERETz88MPUqVOH9u3bM2fOHEtrlJ07d1K1alX++OMPevToQe3atWndujXr1q3L8f+F2Wxm6dKlPPnkk/j5+Vkd56effqJz586EhYXx2GOPcerUKWbNmkXjxo1p2LAh06ZNsxwnOTmZiRMn0qxZMxo2bMikSZOYN28ezZs3t+yTkJDAH3/8YUmiN2/enIULFzJmzBjq1KlDs2bN+PTTT/nll1949NFHuf/+++nVqxfnz5+3+bVVvHhxOnXqxNKlS21+jIiIiIjcnnLlyvHqq69y7tw59u7dC0CbNm2YNWsWPXr0oGHDhkBaFfvIkSOBm89dp02bxpQpU9i/fz9hYWHs3Lkzy3mjoqI4cuQIPXr0oEqVKnh6euLj40O7du2YN2+eZd75zz//ULVqVTZu3EivXr2oXbs27du3Z/369ZZjmUwm3nvvPdq0acP9999Phw4dWLhwoeVbopBW9NK/f3/q1KlD48aNGTBgAH/99Zfl/oSEBF566SUaNWpEkyZNeOutt276DckNGzYQHx9Pjx49LNt69OjBpEmTePPNN6lfvz4NGzZkwYIF/PXXXzz++OPcf//9dO7cmcjISKvn9+2339KzZ09q1arFww8/zJ9//smHH35I8+bNqVevHqNHj7Yq2Pnhhx8ICwsjODgYs9nM3LlzLc//gQce4NVXX7X8UcTHx4eePXuybNmy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//7Hq6++yogRI447I8Rdf/FE6xr269ePlStX8ttvv1FWVsZLL73E7t27PcvNFy2qbu5tOfXUU2tkfSIiIiJSPSIjI9m3bx+ZmZmeYLZZUlISy5cv56677mLbtm04HA5sNhs///wzzzzzDL169SI5Odlz/2XLlrF582ZsNhsvvfQSWVlZnH/++QBce+21vP7666xZs4aysjIyMjK47rrrePjhhwFXvfG4uDjuvfdeMjMzsdlsvPzyy1x44YXs3r0bq9XKgAEDeO2119ixYwdFRUUsWLCgXND9cKeddlqN1xg/ePAgmzZt8rpY63333cf48eM9PwJkZ2fz+++/eyUvpaen43A4aNeuXY32V0TqLgXRRUSOolGjRrzzzjv06tWLadOm0aNHD/r378+SJUuYOnWqVx3COXPm0KxZM4YOHUpKSgqvvvoqzz33HH379q30+ubMmUO9evU4//zz6du3L1lZWSxYsICQkBCaNGlCWloaP//8M2eddRZ9+/Zl8+bNLF682HNRnWNp3749KSkpTJo0iWnTph3xPp06dWLixIncf//99OzZk9dff52HHnqI1NRUHnroIV577TXOOussOnTowFVXXcUTTzxR7jmuu+46hg8fzi233EK3bt246667+Pe//80tt9xS6dfiSH1v1KgRa9euPe7nAJg0aRJnnXUW1157LX379iUzM5PU1FRCQkKw2Wx89913Pguiz5o1i06dOnn9u/766z3Lv/zySzp06OD1hUlEREREap+hQ4dit9s577zz+Oyzz8otP+WUU3j99dex2+2MHj2abt260adPH2bMmMEFF1zA888/73X/UaNGce+993rm4/PmzfOcuTp69GiuvfZaZsyYQZcuXbj66qs588wzufPOOwGIiYlh4cKFZGdnc84553DmmWeyevVqXnjhBc81fe677z7atWvHpZdeyoABA8jOzub888+ntLS0wm3s168f33//faWuk+Qra9eupVWrVl7XIpo6dSonnXQSV1xxBZ07d+aiiy4iNjaWGTNmeO6zZs0aEhMTOeOMM2qsryJSt1mchxfaEhERCVCLFi1i0aJFfPzxx0e8wGhllZSUeD1+ypQp7N27l1deecUX3ayUzMxMzjnnHO69914uvvjiGluviIiIiASunTt3cs455/Dyyy9z5pln+rs7XgoLCxk0aBDjxo1j1KhR/u5OhWw2G4MHD+byyy9n/Pjx/u6OiNQRykQXEZFa46qrriImJoZFixYd93MsXryYs88+m40bN+JwOPjuu+/4+OOPGTRokA97emzPPPMM7dq146KLLqrR9YqIiIiIHI/o6GgmTZrE/PnzycrK8nd3KvTf//6X0NBQrrnmGn93RUTqEKu/OyAiIlJZ4eHhPPPMM1x55ZX06NGDHj16VPk5/vWvf/H3338zfvx4cnJySE5O5vrrr2fkyJHV0OMj++ijj/joo4948803CQnR79kiIiIiUjsMGzaMn3/+mSlTprBgwYLjvt5RdVm/fj3z58/nlVdeITo62t/dEZE6ROVcREREREREREREREQqoPQ3EREREREREREREZEKKIguIiIiIiIiIiIiIlIBBdFFRERERERERERERCqgILqIiIiIiIiIiIiISAUURBcRERERERERERERqYCC6CIiIiIiIiIiIiIiFVAQXURERERERERERESkAgqii4iIiIiIiIiIiIhUQEF0EREREREREREREZEKKIguIiIiIiIiIiIiIlIBBdFFRERERERERERERCqgILqIiIiIiIiIiIiISAUURBcRERERERERERERqYCC6CIiIiIiIiIiIiIiFVAQXUSkEjZs2MBNN93EwIED6dixI71792bEiBG89dZbfutTu3btmDt3boXLp06dyllnnVXp53vqqado164dJSUlvujeMWVkZDB9+nTOOeccOnXqRI8ePUhNTeWxxx4jNze3RvrgVlRUxAsvvMDQoUPp1asXZ5xxBgMHDuSuu+5i165dnvstX76cdu3asWXLlhrtn4iIiIgcm+bsvvHcc8/Rrl07/vrrrwrvM3fuXNq3b8/evXsr9ZzHeh1ERAKdxel0Ov3dCRGRQPbtt9/y73//m/PPP5+rrrqK5ORksrOzWbFiBYsXL2bq1Klce+21Nd6v/fv3Ex0dTUxMDAD//ve/Oe+887jyyisByMvLo7S0lHr16lXq+QoKCigsLKRhw4YA/O9//+PGG2/kq6++8nnfP//8c26++WZ69OjB6NGjad26NUVFRXz//fc8/fTTREVF8frrr9OgQQOfr/tIrr32WtLT07n99tvp3LkzYWFhbN68mblz51JUVMSqVauIjY2luLiYvLw86tWrR2hoaI30TURERESOTXN2383Z//77bwYMGMB//vMfbr311nLLHQ4H/fv357TTTiMtLa1Sz9muXTv+85//MHnyZJ/1U0SkJln93QERkUD32muv0bBhQ+bMmYPFYgGgadOmdOjQgfz8fDZu3OiXfrknzuCayK5fv57zzjvP0xYXF1el54uJifFM7gF+/vnnE+7jkezfv59JkyYxcOBAHn30Uc9rCtC2bVvOPvtsLrvsMt5//31Gjx5dLX0w27JlC19//TWzZs1i6NChnvYWLVrQokUL7rjjDn799Vd69+5NZGQkkZGR1d4nEREREakazdl9Jzk5mb59+7JixQpuvvlmQkK8ixh89dVX/P3339x9990+X7eISKBSORcRkWMoLS3FbrdTWlpabtkDDzxQ7rTE//73v5x//vl07NiRf/zjH9xzzz3k5eV5lk+aNIlLLrmEH374gWHDhtG5c2cGDhzIG2+84bmP0+nk+eefZ/DgwXTu3JnevXszceJEduzY4bmP+5TInTt30r59e/Lz85k5cybt2rUDvE8NHTlyJMOHDy/X/xdeeIH27dvz999/e50aOnXqVB5++GEOHDhAu3btuO++++jUqRNPPfVUuecYM2YMl156KVC5cidLly6luLiYadOmeQXQ3Zo3b85XX31VLoD+8ssvc9FFF9GlSxfOPPNMxowZw+bNmz3L165dS7t27Vi7di0TJ06ka9eudO/enalTp1JUVFRhf9z79UinxJ522mmsWLGC3r17H3H7Bg4cSLt27cr9M79OBw8eZNq0aaSkpNCxY0cuvPBC3nzzzQr7IyIiIiJVpzm7b+fs//znP9mzZw/ffPNNuWVvv/02DRo0YMCAAYArKeX666+nR48edOzYkQsuuIAlS5ZU+Nzffvst7dq148svv/Rqv/zyyxk1ahTg2p/t2rVjyZIlzJ49m169etG1a1fuuOMOSkpKePLJJ0lJSaFnz55MmzYNm83meZ7CwkJmzZpF37596dixI+eddx5paWmoEIOInAgF0UVEjqF///7s27ePkSNH8vHHH3tNrg83f/58HnjgAS655BLee+89HnzwQb788ktuuOEGz33CwsLIzMzkiSeeYPr06bz//vt07dqVmTNneibcb775JvPnz+f222/ngw8+IC0tjfz8fMaOHVtunU2aNGHZsmUATJ48mbVr15a7T2pqKhs2bPCq7w2watUqevfuTXJyslf7XXfdxfnnn0+9evVYu3YtkyZNYtCgQbz99ttek8+srCzWrVvHP//5TwAuuOAC1q5dy8knn1zha/T999/Trl07GjVqVOF9rFbvE6VWrFjBAw88wBVXXMH777/Pyy+/DMC4ceMoLi72esysWbMYPHgw7777LtOnT2flypU88sgjFa6rTZs2NG/enIceeojHH3+c9PT0Sk+w33zzTdauXev5d/nll2O1WunXrx8ANpuN0aNHs27dOh566CHee+89UlNTueuuu3jnnXcqtQ4REREROTbN2X07Z+/fvz8NGzZk+fLlXu15eXl88sknDB06FKvVysGDB7nqqqvIyspi/vz5rFixgtTUVO6///6jBtKPJSwsDHD92NGgQQOWLVvGTTfdxDvvvMO1115LaWkpr732GjfeeCPLly9n1apVnsfeeOONvPfee0ydOpWVK1cyZswYnnrqKZ555pnj7o+IiILoIiLHMHz4cK6//np+//13Jk6cSK9evRg6dChz5swhIyPDc7/S0lIWLFjABRdcwPXXX0+rVq3o168fd911F99++y3r16/33Hffvn3cfffddO/enZYtW3Lttddit9v59ddfAdi4cSNNmjThnHPOoWnTpnTu3JnHHnuMOXPm4HA4vPoXGhrqqaEYGxvrdcqo25AhQwgLC+PDDz/0tO3YsYNff/2VSy65pNz94+LiiIyMJCQkhIYNGxITE8OVV17Jrl27WLduned+H330EVarldTUVAAiIyNp2LDhUeuF79u3j6ZNmx71NT/cueeey8cff8yoUaNo3rw57dq14+qrr2bv3r38/vvvAJ6s9n79+pGamkqLFi0YNmwYqampvPfeexUGxsPCwnj22Wdp27Ytzz33HBdffLEni+jdd9+lrKyswn7Vq1ePhg0b0rBhQ7Zu3cqbb77JTTfdROfOnQH49NNP+f3337n//vvp168frVq14vrrr2fgwIHMnz+/Sq+BiIiIiFRMc3bfztmtVitDhw7lk08+8fpBYtWqVZSUlHgC8m+99RY5OTnMnTuX7t2706ZNG8aPH89ZZ53F4sWLK3z+ymrYsCFjx47lpJNOYvTo0URHR5OZmcmkSZM4+eSTGTVqFNHR0fz222+A6+Kya9eu5bbbbuOCCy7gpJNOYsSIEVxxxRUsXrzYK2NdRKQqFEQXEamEW2+9lbVr1/LYY48xfPhwioqKWLhwIRdeeCGvvvoqAFu3biUvL48zzzzT67EpKSmA66I/btHR0Zx66qme/yckJACQk5MDwIABA/jrr7+45pprePPNN9m9ezdJSUl07NixXE3CykhKSqJPnz5eE/JVq1YRHR3NoEGDKvUcPXr04JRTTuGtt97yeo4hQ4ZUqZaj1WrFbreXa7/00kvp2rWr1z+3sLAwli9fzoUXXkjPnj3p2rUrN954IwDZ2dlez9OtWzev/7dv3568vDz2799fYZ/atWvHO++8w1tvvcWkSZPo2LEja9euZcqUKVx22WVkZmYedZtycnK4/fbb6dmzJ//5z3887evXr8disdCrVy+v+6ekpLB169ZyfRcRERGR46c5u+/m7OAq6VJcXMzKlSs9bW+//TY9e/akVatWgCto3aRJE1q0aOH12K5du7J9+3by8/OrtM7DdezY0XPbYrGQmJjIaaedVq7NHeh3/whypP2bl5fHH3/8cUL9EZHgpQuLiohUUnx8PBdccAEXXHABAL/++iu33347s2fPZvDgwZ6J26xZs3jwwQfLPd4cxI2OjvZa5s6idmdL9+vXj5dffplXXnmFhx56iLy8PE4//XTuvvtur+ByVaSmpjJp0iT27NlDkyZNWLVqFYMGDSrXl6O54oormDt3Lnl5eZSUlPD99997SqtUVuPGjdm5c2e59qefftpTw/Kjjz7yqlv56KOP8tJLLzF+/HjOO+88oqOjWb9+PVOmTCn3PId/OYiKigI4al10t44dO9KxY0fGjh1LQUEBixcv5sknn+S5557jrrvuqvBxd999N8XFxcyZM8frC1NeXh5Op7PcJN6d3X7gwAESExOP2S8RERERqRzN2X0zZwc4+eST6dmzJ2+//TYjRoxg27Zt/PTTTzz88MOe++Tn53t+XDCLj48HoKCggNjY2Cqv2809l3ezWCxERkaWa3PvE/f+HTZsmNd93GcGHDhw4Lj7IiLBTUF0EZFjKCwsBMpPojt27Mhtt93GDTfcwNatWz3B0MmTJ3tqYptVNfOje/fudO/enbKyMn766SeefPJJ/vOf//DZZ59V+bnAdRHMqKgoPvzwQ/r27cvmzZu54447qvQcQ4cOZd68eXz66afk5+fTsmVLevToUaXn6Nu3Lw888AAZGRmeDBbAq8RL/fr1vR6zatUqzj//fG666SZPm/uUzcO5M4Pc3PsvJiamwj4dPHiw3DpjYmK44YYb+Pjjj0lPT6/wscuWLePDDz/kmWeeKVenMj4+noiIiArrnzdp0qTC5xURERGRytOc3eCLObvbP//5T+644w4yMjJ49913iYuLY8iQIZ7lcXFxbN++vdzj3GdcHimA7v4x4nDFxcVEREQcVz/d3AH9xYsXHzFZ5UhldEREKkPlXEREjmLfvn306tXriFe4BzwX/UlOTqZVq1bEx8eza9cuTjrpJM+/5s2bU1ZW5qmBWBlr1qzxnGpotVrp2bMnd911F3l5eUecpLod7YKYUVFRnHvuufzf//0fH374IY0bN6Z3795H7cfhzxcXF8f555/P+++/z/vvv8/w4cMrvU1uF198MQ0bNuTee++tsCbhn3/+6fX//Pz8cq+fOzB9eB9/+uknr///9ttvJCUl0aBBgyOu6/777+ecc845YlaKzWZj//795YLjbhkZGcyePZsRI0Zw7rnnllvepUsXSkpKKCoq8hoTkZGRxMfHEx4efsTnFREREZHK05zd93N2N3cZmI8++ogPPviA1NRUr0zwM844g927d5c70/T777+nTZs2R0xkcWepm0u95Ofney7YeiLOOOMMwJVxbt6/8fHxREVFVSmjX0TETJnoIiJH0ahRI6688koWLVqE3W7nwgsvpGHDhuTm5vLNN9/w5JNPcskll9CyZUsArrvuOp5++mmaN29O3759KSoq4sUXX+TTTz9l9erVNGrUqFLrXb58Ob/99ht33303rVu3Jj8/n0WLFlG/fn3atGlT7v7uiegPP/xAly5daN269RGfNzU1lQkTJpCVlUVqaupRazXGx8eTnZ3NunXraNKkCSeddBIAV155JVdeeSUWi6XcFe6Li4vJy8ujXr16FV6oKCEhgccee4wbbriBq666irFjx3LaaafhcDhIT09n2bJlrFmzxqu2eJcuXfjoo49ITU0lOjqaxYsX06xZM8BVt9I9WQb45JNPOPXUU+nZsyc//PAD77//PqNGjapwO//1r3+xevVqrr76am644Qbat29PWFgYf/31Fy+88AKFhYWMGTOm3ONKS0uZNGkSSUlJXHfddV6n/oaFhZGYmMiAAQM49dRTmTJlCtOmTeOkk07izz//ZNasWbRv354nn3yywn6JiIiISOVozu77ObtbZGQkF110Ea+88gr79+9n3rx5XssvvfRSFi1axOTJk7njjjuIj4/n3Xff5bvvvvMq+2LmDmq/99579O/fn9LSUh588MHjytw/XMeOHenTpw/3338/TqeT0047jd27d/Pwww8TEhLCG2+8UWEmvIjI0SiILiJyDHfddRcdO3Zk+fLlrFq1iqysLCIjIznllFO4/fbbufzyyz33HTduHDExMSxZsoSHH36Y6OhounbtypIlSyo9GQdXjca5c+dy9913s3//fuLi4jjjjDN48cUXy9UABNfk+ZprruGNN97g+++/54033jji8/7jH/8gPj6e33//nccee+yofRg+fDifffYZ48ePZ8SIEZ7TSDt37kxycjIdO3Y8YtmVadOmsWrVqiN+cXDr0aMH77zzDi+//DKPPvooe/bswWKx0KRJE3r37s17773HKaec4rn/jBkzmD59Otdccw0JCQmMHDmSsWPHcvDgQV5++WXCwsLo2bMnAJMmTWLlypU88MADhIaGMmzYMG699dYK+9KqVSveeOMNT/3zffv2UVpaSqNGjTjzzDN56623jvgF5++//2bjxo0A5bLQe/XqxSuvvEJ4eDiLFy9m7ty5TJo0iby8PBo1asTgwYO5+eabj/r6i4iIiEjlac7u+zm7eR2vvfYap59+Oh06dPBaVq9ePV555RUeeeQRrrvuOkpKSmjdujUPP/wwQ4cOPeLzRUVF8cgjjzBnzhx69+5N48aNmTBhArm5uWRlZR2zP8fy1FNP8dhjj3HfffeRmZlJUlIS/fr1Y9KkSQqgi8hxsziPdh6RiIjIYTZu3Mill17Kq6++Svfu3f3dHY9vv/2Wq6++mpdffrnchTxFRERERIJJoM7ZRURqK2Wii4hIpRw8eJCMjAzuvPNOBg0apMm4iIiIiEiA0ZxdRKR6KIguIiKVcuedd7Ju3ToGDhzI/fff7+/uiIiIiIjIYTRnFxGpHirnIiIiIiIiIiIiIiJSgYov8SwiIiIiIiIiIiIiEuQURBcRERERERERERERqYCC6CIiIiIiIiIiIiIiFVAQXURERERERERERESkAlZ/d6C22L8/z99dCCjh4aHYbHZ/d0NMtE8Ci/ZH4NE+CSzaH4FH+6TmNWwY5+8u1FqBMjfXcSOVoXEix6IxIseiMSLHojFyYiozL1cmulSZxeL9V/xP+ySwaH8EHu2TwKL9EXi0T0SqTseNVIbGiRyLxogci8aIHIvGSM1QEF1ERERERERERERE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iAQNzctFRORwmpdLXeRdE91//ZDgVOULi27ZsoU333yTFStWkJeXx6BBg1i0aBE9expvzD169GD69OkMHTrUl30Vk5wc4xe3xEQFPesK/9VEN26rJrqIiEjtoHm5iIiIBBNloos/VTmIfuGFF3LSSScxZswYLr30UpKSksrdp1+/fuTn5/ukg3Jk5iC6yrnUHeZfVQsLa2693uVcNJ5ERERqA83LRUREJJioJrr4U5WD6C+++CL/+Mc/yrUXFRXx2WefccEFFwDw008/nXjvpELui4oCxMf7sSPiU9HR/qqJbtxWTXQREZHaQfNyERERCSbmeIUy0aWmVbkm+vjx44/YXlxczIwZM064Q1I52dnKRK+L/FcTXZnoIiIitY3m5SIiIhJMzCVwlYkuNa3SmejLli3jzTffxGazMWLEiHLL9+3bR7Q5AijVKifHuK0get0RCDXRlYkuIiIS2DQvFxERkWCkTHTxp0oH0fv27UtxcTG//PILrVq1Kre8ffv2umBRDTKXc1EQve4w10T3XyZ6za1XREREqk7zchEREQlGERHKRBf/qXQQPTk5mVGjRrFnzx5uv/326uyTVILKudRNgVETXeNJREQkkGleLiIiIsHIOxPdf/2Q4FSpIPpff/3FSSedBMDw4cPJyMio8L5HyoYR31Mmet2kTHQRERE5Gs3LRUREJFh510RXORepWZUKol988cWsX78egPPPPx+LpfxAdTqdWCwWNm3a5NseyhHl5JiD6H7siPhUYNRE148yIiIigUrzchEREQlWykQXf6pUEP2FF17w3H7ppZeOOFmXmmW+sGh8vIKedYU5E72wsObWqwuLiq/kvvwa2GwQHu7vroiI1Emal4uISGVoXi51kXdNdM2BpGZVKojeo0cPz+0zzzyz2jojleedia4gel0RE+Ovmujmci4aT3L8ys7o6u8uiIjUaZqXi4hIZWheLnWRMtHFnyoVRB8xYkSln/D1118/7s5I5bmD6JGRTmUO1yHmfem/ci41tloRERGpIs3LRUREJFipJrr4U6WC6CeffLJOFQ0w7iC6SrnULVYrhIc7sdksfrywqMaUiIhIoNK8XERERIKVMtHFnyoVRH/ooYequx9SRe4gemKiAp51TXS0q3RdzZZzMW4rE11ORPhHq10DKjIS26Dz/d0dEZE6R/NyERGpDM3LpS6KiDBu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XE9t4r6wqD8vKio1IzrauF1YWH0723Xak4uC6CIiIrVDdczLRURERGoLc+JhSYkfOyJBpdLlXA4ePMjJJ5/s+X+bNm3Yv3//Ca28cePGpKWlHXFZenq657bFYmHq1KlMnTr1iPdt1KgR8+bNO6711Cbuci666nDd559MdI0rERGR2qA65uUiIiIitYV3TXT/9UOCS6Uz0Q9nsVhwqmhmjbHbzZnofu6MVDvzlaarNxPduK1MdBERkdpJ83IREREJJt410VWqQWrGcQfRpWbl5Rm3lYle94WEGB8Kh6oaVYvDLywqIiIiIiIiIhLIvGui+68fElwqXc6lrKyMxx57zCvLxW638+ijj3rd77bbbvNd78TDXcoFFEQPFtHRToqLLdV6kQxz7TBloouIiNQOmpeLiIhIMPOuia5MdKkZlQ6iJycn895773m1NWrUiPfff9/zf4vFosl6NTEH0ePjFUQPBu6SLtVbE12Z6CIiIrWN5uUiIiISzFQTXfyh0kH0//u//6vOfsgxmIPoiYkKdgYD9y+r1ZmJrproIiIitY/m5SIiIhLMzOVcVBNdaopqotcS3pnofuyI1JjoaNff6qyJrkx0EREREREREalNvC8s6seOSFBREL2WyM01bqsmenBwZ6KXlVkoLa2edSgTXURERERERERqE3f5W1AmutScSpdzEf/KzlY5l2Bj/lAoKoKwMN+vwzsT3ffPL8En+6PPwW6H0FB/d0VEREREJGhpXi51mTLRxR8URK8lcnN1YdFgY77adGGhpVr2u3cmusaVnDhHcmN/d0FEREREJOhpXi51mXdNdP/1Q4KLT8q55Ofn++Jp5CjMNdFVziU4uGuiQ/XVRVc5FxERkbrlROblO3fuZMyYMXTp0oWUlBTmzJmDw+Eodz+bzca8efMYMGAAXbp04eqrr2bHjh2e5ZmZmUyePJmUlBR69OjBqFGj2Lhxo9d6xo8fT69evUhJSeH2228nJyfnuPstIiIiwcU7E13lXKRmVDmInpGRwciRIz3/nz59Oj169ODss89m8+bNPu2cGMzlXBREDw7R0cZ+Liqqng8FXVhURESk9vLlvNzpdDJx4kSSkpL44osvWLJkCatXr2bx4sXl7puWlsbKlStZuHAh69at49RTT2XChAmegPvMmTPJyspi9erVfP3113Tr1o2xY8dit9sBGD9+PImJiXz22We8++67ZGRk8Mgjjxz/CyEiIiJBxbsmuv/6IcGlykH0WbNm0aZNGwC+++47Vq1axUsvvcS//vUv5s2b5/MOiou5nEtCgh87IjXm8Jro1UGZ6OJrkS8vIuq5p4l8eZG/uyIiUuf5cl7+yy+/kJ6ezvTp00lISKB169aMHTuWpUuXlrvvZ599xvDhw2nTpg2RkZFMmTKFjIwM1q9fD8CmTZsYOHAgiYmJhIeHk5qayoEDB9i/fz95eXl07NiRyZMnExMTQ8OGDRk6dCg//PDDib8gIiLioXm51GXKRBd/qHJN9F9//ZUnnngCcE2gBw8ezJlnnkmXLl1YtEhvztXFfIaraqIHB3MmemFh9Waih4Q4q+XCpRJ8ouc9TOie3dibNKX46mv93R0RkTrNl/Py3377jWbNmpGYmOhp69ChA9u2bSM/P5/Y2FhPu9PpxOk05ilhYWFERESwadMmunbtSv/+/Vm9ejWDBg0iLi6Ot99+mw4dOpCcnIzFYuHBBx/0WveuXbto0qTJcbwCIiJSEc3LpS5TTXTxhyoH0UtLSz2T6HXr1nHNNdcAEB4eTklJiW97Jx7umujR0Qp2BouayER3H7KRkWDRj7ciIiK1ii/n5VlZWSQcdrqj+/9ZWVleQfR+/fqxbNkyzj33XJo2bcqiRYsoLS311DWfMmUK48ePp0+fPgA0bdqUhQsXYjnCZOOXX35hyZIlPPnkk1XqL/h/7uJev7/7IYFN40SOpSbGiMZf7ab3kfLM8ZKSEkvQvzYaIzWjykH0li1b8uWXXxIdHc0ff/xB3759AdiwYQONGjXyeQfFxR1ET0xUFnqwiIqq/pro7udVPXQREZHax1/z8nHjxpGdnc3o0aOxWq2MGjWKli1bYrW6vlrMnDkTgC+//JL4+HgWLVrEmDFjWLlyJTExMZ7n+fHHHxk/fjy33347/fr1q1IfwsNDfbY9x8tigdDQUCwWcGoqJRXQOJFjqa4xYg6qhYX5/z1Tjp/eR44sIsJJSYmF4mJL0I9xjZGaUeUg+rhx4xg/fjwOh4OxY8dSr149cnJymDhxIldffXV19FEwaqLroqLBw/zLamFh9azDfdqT6qGLiIjUPr6cl9evX5/s7GyvtqysLADq1avn1R4ZGcmMGTOYMWMG4CrvsnDhQpKTkykoKGD58uW8+uqrJCcnAzBhwgRefPFF1qxZw5AhQwBX+ZkpU6Zw7733cuGFF1Z52202u9+zrdxfVMvK7PrCKhXSOJFjqa4x4n4upxNKS+2+e2KpcXofObLISNfZ9UVFGuMaIzWjykH0888/n+7du5OZmclpp50GQHx8PLfffjupqak+76CAzWbUxFY99OBhroleXZno7proCqKLiIjUPr6cl3fq1Indu3eTlZVFUlIS4Mpob9u2rVf2OMDGjRvJzc0lJSUFcJVkyc7Oplu3bp77OBwOz22n04ndbickJASA//3vf9xxxx08+eST/OMf/6j6hnue97gf6lNOZ+D0RQKXxokcS3WOEY29ukHvI94iIpyAheJivS5uGiPVK+R4HpSXl+eZqO/bt49XXnmlXA1F8R13KRcA07WepI7zzkSvniC6u1yq68NHREREahtfzcvbt29P586dmTVrFrm5uaSnp5OWlsZVV10FwJAhQ/jhhx8ASE9PZ/LkyWzfvp2cnBzmzp3L4MGDad68OTExMfTs2ZPnn3+ezMxMbDYbCxYswGq10r17d8rKypg+fTo333zzCQXQRUREJLi5kwF1eUapKVUOor/11ltceumlABQWFjJixAiWLFnClClTWLp0qc87KJCba9xWJnrw8M5E9/3zO51Ghrs5YC8iIiK1g6/n5U888QR5eXmcffbZXHvttYwYMYKRI0cCkJGRQeGh+nLDhg0jNTWV4cOHM3DgQBo3bszs2bM9zzNv3jwSEhK46KKLSElJ4fPPP2f+/PnUr1+fn3/+mS1btvDQQw/RqVMnr3+7du3ywasiIiIiwcB9HbnqOnNf5HBVLueyePFiHn/8cQA++OADwsLCWLlyJb///jt33HEHV1xxha/7GPTMmeiqiR48oqON29WRiW7+tVYXFhUREal9fD0vb9y4MWlpaUdclp6e7rltsViYOnUqU6dOPeJ9GzVqxLx58464rEePHl7PJSIiInI83Jno7mu9iVS3Kmei79y5kwEDBgDw1VdfMWTIEKxWKx06dFD2SDXJzlYQPRi5f1WF6slEN3/QqCa6iIhI7aN5uYiIiAQrd1lau91CaamfOyNBocpB9IiICIqLi3E6nXz77bf07t0bgKKiIkJDQ33eQYHcXAXRg5G5xEp1nJ5UUmI8p2qii6/Y27SlrN1p2Nu09XdXRETqPM3LRUSkIpqXS11nTgZUXXSpCVUu59K9e3fu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iIiIZEJBdBERERERERERERGRTCiILiIiIiIiIiIiIiKSCQXRRUREREREREREREQyoSC6iIiIiIiIiIiIiEgmFEQXEREREREREREREcmEgugiIiIiIiIiIiIiIplQEF1EREREREREREREJBMGk8lkcnQjRERERERERERERETyImWii4iIiIiIiIiIiIhkQkF0EREREREREREREZFMKIguIiIiIiIiIiIiIpIJBdFFRERERERERERERDKhILqIiIiIiIiIiIiISCYURBcRERERERERERERyYSC6CIiIiIiIiIiIiIimVAQXUREREREREREREQkEwqii4iIiIiIiIiIiIhkQkF0SWfHjh3UqVOHYcOGpZu3e/dunn32WUJCQmjevDlr1qyxmr9w4UIaN25M5cqV6dixI4cPH86tZt/TMusTk8nE3LlzefTRR1m6dGm69dQf9nG7v5Gff/6Zjh07UqVKFRo3bsz06dOt5qtPbO92/bFx40aeffZZHn/8cRo2bMjkyZNJTk42z1d/2Mft+iRNSkoK7du3p3v37lbT1Se2l1l/nDp1ivLlyxMSEmL1b8OGDeZl1B8iGTt8+DA9evSgWrVq1K5dmxEjRnD16lVHN0vykIMHD9KtWzeqVatG/fr1mTt3rqObJHlATr5rS/5wt9/9Jf/ISTxCckZBdLEye/Zsxo8fz/33359u3oULF3j55Zfp0KED//vf/3jzzTcZO3YsBw8eBGDTpk18/PHHTJw4kZ9//pmGDRvSv39/YmNjc/tl3FNu1yf9+/dnz549FCpUKN089Yd93K4/zp07x4ABA+jQoQO//PIL06dPZ8GCBaxevRpQn9jD7frj0KFDvP766wwfPpz9+/fz2Wef8c0337Bo0SJA/WEvt+sTS1988QUnTpywmqY+sb3b9ce1a9dwd3fn0KFDVv9atWoFqD9EMpOcnEy/fv2oUqUKu3fvZsOGDVy9epW33nrL0U2TPCI6Opq+ffvyxBNPsGvXLmbNmsXcuXOtfqSU/Ccn37Ulf7jb7/6Sf+QkHiE5pyC6WPH09OTLL7/M8A9y7dq13H///fTo0QNvb28aNWpE06ZNWblyJQArV66kQ4cO1KpVC29vbwYNGoTBYGDLli25/TLuKbfrk8cff5zw8HC8vLzSzVN/2Mft+uPy5ct07NiRTp064ebmRqVKlahduzZ79+4F1Cf2cLv+8PHx4cMPP6Rhw4a4uLhQoUIFqlevzt9//w2oP+zldn2S5uLFi8ycOZMePXpYTVef2N7t+iM6Oho/P79M11V/iGTs0qVLXL58mdatW+Ph4YG/vz9Nmzbl6NGjjm6a5BG//vorcXFxDBo0CE9PTx555BE6d+7Ml19+6eimiQPl5Lu25A93+91f8o+cxCMk5xREFys9evSgYMGCGc47cuQIjzzyiNW0ihUrmi/tvnW+wWCgQoUKuvQ7h27XJwMHDsRgMGQ4T/1hH7frj5CQEEaPHm017ezZsxQvXhxQn9jD7frjwQcfpFmzZgAkJSWxZ88efvnlF5588klA/WEvt+uTNO+99x5du3aldOnSVtPVJ7Z3u/6IiYkhJSWFfv36UaNGDZ588knmzJmDyWQC1B8imQkKCqJSpUqsWLGCuLg4rl69yqZNm2jUqJGjmyZ5REpKCiaTyTyeAhQoUEA/tORzOfmuLfnD3X73l/wjJ/EIyTkF0SXLIiMj02Ws+fv7m+s/RkZG4u/vbzXfz89P9SEdRP3heIsWLeLUqVM899xzgPrEUVatWkVISAgvv/wyw4cPp0GDBoD6w1F++uknjh49Sr9+/dLNU5/kLnd3d0qXLk3nzp3ZsWMHoaGhzJw505wpqf4QyZjBYOCTTz5h69atPP7449SuXZvk5GRee+01RzdN8oiqVavi5eVFWFgYcXFxHD16lK+//pro6GhHN03yqDt91xYRya5b4xGScwqiS46l/Rqa2a+i+rXUMdQfjrV48WI++eQTZs2aRVBQEKA+cZS2bdty8OBBZs+ezaeffsqyZcsA9YcjJCQk8M477/DWW2/h4eGRbr76JHc1adKE5cuX06RJE7y8vKhbty6dOnXiq6++AtQfIplJTEykf//+tGzZkv3797Nz504KFizIiBEjHN00ySP8/f359NNP+fHHH6lbty7vv/8+rVu3xs3NzdFNEyejz1wRuRsZxSMk5xRElywLDAwkKirKalpkZCSBgYEABAQE3Ha+5C71h+NMnTqV8PBwFi9eTJUqVczT1SeO4+7uTvXq1enRoweLFy8G1B+OMHPmTHPWZkbUJ45XsmRJLl++DKg/RDKza9cuTp8+zdChQ/H19eW+++7jlVdeYfPmzcoaFbMnnniCb775hv3797Nw4UK8vb0VyJBM3em7tohIVmUWj5CcUxBdsiwkJCRdTbaDBw9SuXJl8/zff//dPC85OZkjR46Y50vuUn84xvz58/n2229ZtmwZ5cuXt5qnPslds2bNYujQoVbTkpOTcXFJ/ehTf+S+NWvWsGXLFp544gmeeOIJ3n33Xfbv388TTzzBuXPn1Ce57LvvvuOLL76wmnbs2DFKlSoF6G9EJDMmk4mUlBSraUajEVDWqKRKSEhg1apVXL9+3Tztp59+omrVqg5sleRld/quLSKSFbeLR0jOKYguWda6dWvOnDnDggULiI+PZ+PGjfz444906tQJgM6dO/PVV1+xZ88ebty4wUcffYSXlxdNmjRxcMvzJ/VH7jt16hSffPIJYWFhBAcHp5uvPsldNWvWZPPmzWzYsIGkpCT++usvli1bRtOmTQH1hyMsX76c9evXs3r1alavXs2QIUN49NFHWb16NUWLFlWf5DJ3d3cmT57Mzp07SUpKYteuXXz11Vd069YN0N+ISGYef/xxfH19CQsLIz4+nujoaGbPnk2VKlUICAhwdPMkD3B3dycsLIxZs2aRlJTEpk2b2L17Nz179nR00ySPutN3bRGRO7lTPEJyzmCyvGW45HshISEAJCUlAZjr9h06dAiAvXv38u6773Ls2DGCg4MZMWIEzZs3N6+/dOlSwsPDuXLlCo8++ihvv/02Dz30UC6/intLZn0yb948+vTpA6TW5nRzc8PFxYUaNWowb948QP1hD7f7G5k+fTphYWG4u7tbrRMcHMx3330HqE9s7U5j1saNG5k5cyYnT57E39+fp556iiFDhpjrcas/bO9OfWLp66+/5ptvvmHRokXmaeoT27pTfyxfvpx58+Zx4cIFSpYsSZ8+fWjfvr15ffWHSMYOHjzIBx98wNGjR3F3d6dmzZq88cYbFCtWzNFNkzzi0KFDhIaG8s8//1C8eHFGjBhBs2bNHN0scaCcfteWe19OvvtL/pDTeITkjILoIiIiIiIiIiIiIiKZUDkXEREREREREREREZFMKIguIiIiIiIiIiIiIpIJBdFFRERERERERERERDKhILqIiIiIiIiIiIiISCYURBcRERERERERERERyYSC6CIiIiIiIiIiIiIimVAQXUREREREREREREQkEwqii4iIiIiIiIiIiIhkQkF0ERHJlhYtWvDxxx/bfT/jxo3jxRdfxGQy2X1fp06dolatWuzevdvu+xIRERERSXP69GnKly/Pjz/+6OimZCg8PJw2bdoQHx+f421FRUXRpEkTvvnmGxu0TEQkd7k5ugEiIpLq6tWrzJ49m23btnH+/HkAihYtSoMGDXj55ZcJDAy06/6NRiOLFy9m9erVnDlzBqPRSGBgIA0aNGDYsGH4+fkB8N1339m1HQCrVq1iy5YtrFu3DoPBYPf9lSpVinfeeYehQ4eyZs0agoKC7L5PEREREXFeffr0IS4ujqVLl2Y4/+2332bLli388MMPuLk5Z+hlz549zJw5k1WrVuHl5ZWldUaNGsWxY8dYsWJFunn+/v58/PHH9OjRg4oVK1KhQgVbN1lExG6UiS4ikgecPn2aNm3acOzYMT788EP27dvHzz//zPjx49m3bx9t27blypUrdm3DpEmTmDt3Lq+//jo7d+5k3759hIWFsX//fvr27WvXfVuKi4tjypQp9O3bl4CAgFzb75NPPkmZMmWYPn16ru1TRERERJxT9+7d2b9/P3/88Ue6eTdu3GDNmjV07drVaQPoJpOJiRMn0qFDB+6//36bbbdy5co0btyYDz74wGbbFBHJDQqii4jkAaGhofj5+TF9+nQqVaqEi4sLHh4e1KhRgzlz5tCpUydiY2PNyx88eJDu3btTo0YNqlWrRrdu3Th06JB5/vDhw+nbty/h4eHUq1eP6tWrM2jQIK5evZppG3bu3Enjxo2pXbs2Hh4euLq68sgjj/Dxxx/Tu3dvEhMTAWjSpAlTpkwBYMaMGYSEhJj/Pfroo5QvX958iWZKSgozZsygSZMmPPbYY7Rs2ZLPPvuMpKSkTNuxZs0arl27RufOnc3TOnfuzPjx43n//fepXr06NWvWZNasWfz555906NCBxx57jDZt2hAREQHAiRMnKF++PJs2baJLly5UrlyZVq1a8fvvv7No0SLq169PtWrVGDlyJMnJyeb99OjRg6+++oro6OjsdJ+IiIiI5DONGjXi/vvvZ8mSJenmrV27FqPRyPPPPw/AypUrefbZZ6lSpQrNmjXj3Xff5fr16xlut3v37gwbNsxq2rBhw+jevTuQes5evnx5du/eTZs2bQgJCeG5557j1KlTTJ06lVq1alGzZk0mTZpktY1ly5bRokULKleuTLNmzZg0aRJxcXGZvr6dO3fyxx9/0LNnT/O0UaNGmV9TmqVLl1K+fPnbvFPp9ezZk59++sl87i4i4gwURBcRcbDIyEh27txJ7969M8xUCQwMZNCgQZQqVQqAxMRE+vXrR5kyZdixYwc//fQTpUqVYuDAgaSkpADg7u7O/v37iY2N5YcffmDVqlUcO3aMN954I9N2VKhQgS1btrBt2zarwHLZsmVp1aoVHh4e6dYZOHAghw4dMv979tlnKVasGI0aNQJg/vz5fP3110yfPp39+/fzwQcf8MUXXzB37txM27Ft2zaqV69udcmou7s769atIyQkhN27d9O7d2+mTZvGlClTCAsLY8eOHRgMBqZNmwZgfh/nzp3L5MmT2blzJ56enrzyyitcvHiRzZs3M3PmTNasWcPWrVvN+6lbty4pKSn89NNPmbZPRERERMRgMNC1a1fWrl2bLiC+bNkynn76aQIDA1m1ahUTJkxg2LBh/Pzzz4SHh/O///2PESNG3NV+085zP//8c+bPn8+WLVu4ePEiPXv2pGjRouzYsYPRo0czb948jh49CsDGjRv56KOPGD9+PL/++iufffYZP/30ExMnTsx0P9u2beOBBx6gZMmSd9XO26lcuTL+/v5s377d5tsWEbEXBdFFRBzs1KlTmEwmHnzwwSwt7+HhwebNmxk7dixeXl54e3vzzDPPcPHiRc6ePWteztXVlcGDB+Ph4UHJkiV54YUX2LFjBwkJCRlud+zYsVSuXJkBAwZQvXp1evXqRVhYGL///nuW2rV69WpWr17NRx99ZC7DMm/ePHr37k3FihVxdXUlJCSEnj17smzZsky3ExERQcWKFdNNL1myJE8//TTu7u40b96clJQUWrZsSfHixSlUqBB16tTh+PHjVuu0bt2aUqVKUbBgQerUqcOlS5d45ZVX8PT0pGbNmgQGBnLs2DHz8v7+/hQvXjzDy3JFRERERCx16NABwOpGmb/++itHjx6lR48eACxatIhWrVrRuHFjPDw8KFu2LP369WPbtm23vUr0Tjp37kxgYCBFixalatWqAHTr1g13d3datGgBYD43njdvHs899xw1atTA1dWVBx98kEGDBvHVV19hNBoz3H5m5+S24OLiwsMPP6xzbhFxKs5ZnEtE5B7i6uoKkC7Tu2fPnuzfvx9IrUlYrVo1Fi5cCMCWLVtYuHAhp06dIj4+HpPJBGAVIC9TpoxVZnvJkiVJTk7m4sWL5qx2S4GBgcyaNYsLFy7wv//9j99++42NGzfy6aef8swzzzBlypRMb/J57Ngx3nrrLYYMGUK1atUAuHbtGpcvX2bixIm8//775mXT2pqYmJhhdvvVq1cpXLhwuuklSpQwP07LUi9evLh5mre3d7ofCCwzZ7y9vbnvvvus9pnROgEBAXavPy8iIiIizq9AgQK0a9eOpUuXmsutLF26lOrVq5sD0CdPnuSpp56yWq9MmTKYTCZOnjzJfffdd1f7tjw39vb2TndeDBAfHw+knqsfPnyYxYsXW23DZDJx7tw5SpcunW77V69ezXaZluzQObeIOBsF0UVEHKx06dK4u7tz5MgRKlWqZJ6eFjCH1PqDZ86cAWD//v28/vrrDBs2jBdeeAFfX192795Nr169rLabVtolTVrw2tPT87btCQoKonXr1rRu3RpIreE4ZswY2rVrR7169dItn5CQwNChQ6levbrVDUjTAu6TJ09O98XhTjIK1ru4pL94KqNpt9vOnZbPbN8iIiIiIhl54YUXWLJkCXv27KF8+fJs3LiRyZMnWy1z6/ll2nl5Vs87bz2vz2jd253nuri4MHToUKtz9buV1vbbtS0rdM4tIs5G5VxERBysYMGCNGvWjIULF5pv3nkry5PTX3/9FS8vL1566SV8fX0BrG4qmubUqVNWN/A8deoU7u7uGWa7nDlzhrfffptTp06lm9esWTOATC83nTBhAlFRUUyaNMnqZLhAgQIUKVKEw4cPWy1/+fJlq5uk3iowMDBHl7bmVGaZ8CIiIiIitypbtix169blyy+/ZN26dQQGBtK8eXPz/DJlyvDXX39ZrfPPP//g4uLC/fffn257np6e6UqsWJZsvBsPPPBAunPy6OhooqOjM10nMDCQyMjIdNMvX75s9fzWcopZpXNuEXE2CqKLiOQB48aNw2g00rt3b/bv309SUhLJycn89ddfTJo0ifXr11O9enUg9dLN+Pgkli84AAAENUlEQVR4Dhw4QHJyMuvWrTPfCPPcuXPmbSYkJBAWFkZsbCynTp1iyZIltGrVKsMslSJFirBr1y6GDRvGb7/9RmJiIikpKZw4cYJ33nmHwMDADLPQ169fz5dffslHH31EYGBguvm9e/dm2bJl7Nixg6SkJI4fP85LL73EpEmTMn0vKlSo4LD6iFFRUZw7d86ul66KiIiIyL2lR48ebNmyha+++oquXbuayzVCaonGDRs2sH37doxGI3/++SezZs2iVatW+Pv7p9vWQw89xG+//UZUVBQA33///V0HqtP07t2bTZs2sXbtWhITE7lw4QLDhg1j+PDhma6T2Tn52bNnWblyJUajkb179/Ljjz9muz0pKSn8+eefOucWEaeici4iInlAYGAgX3/9NQsWLODtt9/mzJkzGI1GihQpQrVq1Vi8eDFVqlQB4Mknn+T555+nf//+uLi40Lx5c2bMmMHLL7/Mq6++ypQpUwAICQnB09OTJ598kvj4eGrXrs2bb76Z4f49PDxYvnw5n332GaNGjeLChQtAanC9Xr16rFy5MsMg+bJly0hJSaF3795W019++WUGDhxIr169iIuLIzQ0lIsXLxIQEMBTTz3Fa6+9lul70bBhQ95//30SEhLuWHrG1nbu3InBYMjwBwMRERERkYw0aNCAokWLcuzYMTp27Gg175lnnuHKlSuMHz+e8+fPU6xYMVq0aMHgwYMz3NaLL77IX3/9xVNPPUVAQAANGzakTZs2HDly5K7b17JlS65cucKnn37K6NGjKVCgAI0aNWLUqFGZrtOwYUM+//xzzp07Z1Vv/cEHH+S7777jvffe45FHHqF///6MHj2apKQk8/2YDh48SEhISLptbt26lfvuu4+DBw8SFRVFw4YN7/o1iYjkNoPp1oJWIiLi9NJqqC9atMjRTcm22NhYnnzySfr372++QVNu6dSpEw899BDjx4/P1f2KiIiIiOQlJpOJtm3bUrt2bXOwfdSoURw7dowVK1bkaNtDhw4lOjqa+fPn26KpIiK5QuVcREQkT/Hx8WH48OF89tlnGdZhtJfvvvuOEydO8Morr+TaPkVERERE8iKDwcAbb7zB8uXL+ffff2223YMHD7J161b+7//+z2bbFBHJDQqii4hIntOuXTuaNm3KyJEjyY0Lpk6dOkVoaCgff/wxQUFBdt+fiIiIiEheV6tWLXPJyPj4+BxvLzo6mqFDhxIaGkrFihVt0EIRkdyjci4iIiIiIiIiIiIiIplQJrqIiIiIiIiIiIiISCYURBcRERERERERERERyYSC6CIiIiIiIiIiIiIimVAQXUREREREREREREQkEwqii4iIiIiIiIiIiIhkQkF0EREREREREREREZFMKIguIiIiIiIiIiIiIpIJBdFFRERERERERERERDKhILqIiIiIiIiIiIiISCb+HxEshEPhofBWAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sensitivity analysis completed\n" - ] - } - ], - "source": [ - "# Parameter sensitivity analysis around the optimal point\n", - "print(\"Performing parameter sensitivity analysis...\")\n", - "\n", - "# Create parameter variations around the optimal point\n", - "n_points = 50\n", - "sensitivity_results = {}\n", - "\n", - "fig, axes = plt.subplots(2, 2, figsize=(15, 10))\n", - "fig.suptitle('Parameter Sensitivity Analysis Around Optimal Point', fontsize=14, fontweight='bold')\n", - "\n", - "for param_idx, param_name in enumerate(param_names):\n", - " print(f\"Analyzing sensitivity for {param_name}...\")\n", - " \n", - " # Create range around optimal value\n", - " optimal_value = best_params[param_idx].item()\n", - " param_min = bounds[0, param_idx].item()\n", - " param_max = bounds[1, param_idx].item()\n", - " \n", - " # Create range ±20% around optimal value, bounded by parameter limits\n", - " range_width = (param_max - param_min) * 0.2\n", - " test_min = max(param_min, optimal_value - range_width)\n", - " test_max = min(param_max, optimal_value + range_width)\n", - " test_values = np.linspace(test_min, test_max, n_points)\n", - " \n", - " # Create test parameters (fix other parameters at optimal values)\n", - " test_params_list = []\n", - " for test_val in test_values:\n", - " test_param = best_params.clone()\n", - " test_param[param_idx] = test_val\n", - " test_params_list.append(test_param)\n", - " \n", - " # Stack into batch tensor\n", - " test_batch = torch.stack(test_params_list).float()\n", - " \n", - " # Evaluate objective function\n", - " with torch.no_grad():\n", - " test_scores = objective_function(test_batch).cpu().numpy().flatten()\n", - " \n", - " # Store results\n", - " sensitivity_results[param_name] = {\n", - " 'values': test_values,\n", - " 'scores': test_scores,\n", - " 'optimal_value': optimal_value,\n", - " 'optimal_score': best_score\n", - " }\n", - " \n", - " # Plot results\n", - " ax = axes[param_idx // 2, param_idx % 2]\n", - " ax.plot(test_values, test_scores, 'b-', linewidth=2, label='Success Probability')\n", - " ax.axvline(x=optimal_value, color='red', linestyle='--', linewidth=2, \n", - " label=f'Optimal: {optimal_value:.3f}')\n", - " ax.axhline(y=best_score, color='green', linestyle=':', alpha=0.7, \n", - " label=f'Best Score: {best_score:.4f}')\n", - " \n", - " units = ['mg/mL', 'mm/s', 'mm', 'μL']\n", - " ax.set_xlabel(f'{param_name} ({units[param_idx]})')\n", - " ax.set_ylabel('Success Probability')\n", - " ax.set_title(f'Sensitivity: {param_name}')\n", - " ax.legend()\n", - " ax.grid(True, alpha=0.3)\n", - " \n", - " # Calculate sensitivity metrics\n", - " score_range = test_scores.max() - test_scores.min()\n", - " param_range = test_max - test_min\n", - " sensitivity = score_range / param_range if param_range > 0 else 0\n", - " \n", - " print(f\" Range tested: [{test_min:.3f}, {test_max:.3f}]\")\n", - " print(f\" Score range: [{test_scores.min():.4f}, {test_scores.max():.4f}]\")\n", - " print(f\" Sensitivity: {sensitivity:.6f} (score change per unit parameter change)\")\n", - "\n", - "plt.tight_layout()\n", - "plt.show()\n", - "\n", - "print(\"Sensitivity analysis completed\")\n" - ] - }, - { - "cell_type": "raw", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "## How to Use This Notebook\n", - "\n", - "### Prerequisites\n", - "1. **Trained Model**: You need a trained logistic regression model (.pkl or .joblib file)\n", - "2. **Trained Scaler**: You need a fitted StandardScaler (.pkl or .joblib file)\n", - "3. **Model Requirements**: Your model should be compatible with the feature engineering pipeline\n", - "\n", - "### Quick Start\n", - "1. Run all cells in order\n", - "2. When prompted, provide the paths to your model and scaler files\n", - "3. The notebook will automatically:\n", - " - Load your model\n", - " - Create the objective function with proper feature engineering\n", - " - Run Bayesian optimization\n", - " - Analyze results and provide recommendations\n", - "\n", - "### Customization Options\n", - "- **Parameter Bounds**: Modify the `bounds` tensor to match your experimental ranges\n", - "- **Optimization Settings**: Adjust `n_iterations`, `n_initial_points`, and `batch_size` as needed\n", - "- **Visualization**: Customize plots by modifying the plotting sections\n", - "\n", - "### Expected Output\n", - "- Optimal printing parameters\n", - "- Success probability predictions\n", - "- Sensitivity analysis\n", - "- Detailed visualizations\n", - "- Actionable recommendations\n", - "\n", - "### Troubleshooting\n", - "- **Model Loading Issues**: Check file paths and ensure files are valid joblib/pickle files\n", - "- **Feature Count Mismatch**: The factory function automatically adapts to your model's feature count\n", - "- **Memory Issues**: Reduce `batch_size` or `n_iterations` for faster execution\n", - "\n", - "For more information, see the main package documentation and example scripts.\n" - ] - }, - { - "cell_type": "raw", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "polyprintenv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.2" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/aamp_app/optimizer/notebooks/tutorial_notebook.ipynb b/aamp_app/optimizer/notebooks/tutorial_notebook.ipynb deleted file mode 100644 index 3af6f30..0000000 --- a/aamp_app/optimizer/notebooks/tutorial_notebook.ipynb +++ /dev/null @@ -1,70 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Setup and imports\n", - "import torch\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from bayesian_optimizer import BayesianOptimizer\n", - "from objective_function import MockObjectiveFunction\n", - "import warnings\n", - "warnings.filterwarnings('ignore')\n", - "\n", - "# Configure plots\n", - "plt.style.use('default')\n", - "plt.rcParams['figure.figsize'] = (10, 6)\n", - "plt.rcParams['font.size'] = 12\n", - "\n", - "print(\"Tutorial setup complete!\")\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## Hands-On Example\n", - "\n", - "# Let's implement a simple optimization step-by-step:\n", - "\n", - "# Step 1: Define the problem\n", - "print(\"Step 1: Problem Definition\")\n", - "print(\"=\" * 40)\n", - "\n", - "# Parameter bounds\n", - "bounds = torch.tensor([\n", - " [0.1, 1.0], # concentration\n", - " [10.0, 100.0], # print_speed\n", - " [0.05, 0.5], # gap_size\n", - " [5.0, 25.0] # volume\n", - "]).T\n", - "\n", - "param_names = ['concentration', 'print_speed', 'gap_size', 'volume']\n", - "param_units = ['', 'mm/s', 'mm', 'μL']\n", - "\n", - "print(\"Parameter space:\")\n", - "for i, (name, unit) in enumerate(zip(param_names, param_units)):\n", - " lower, upper = bounds[0, i], bounds[1, i]\n", - " unit_str = f\" ({unit})\" if unit else \"\"\n", - " print(f\" {name}: [{lower:.2f}, {upper:.2f}]{unit_str}\")\n", - "\n", - "print(f\"\\nTotal search space size: {np.prod(bounds[1] - bounds[0]):.2e}\")\n", - "print(\"This is a 4-dimensional continuous optimization problem!\")\n" - ] - } - ], - "metadata": { - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/aamp_app/optimizer/objective_function.py b/aamp_app/optimizer/objective_function.py deleted file mode 100644 index 6ab40c0..0000000 --- a/aamp_app/optimizer/objective_function.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -Mock Objective Function for Bayesian Optimization Demo - -This module contains a realistic simulation of a black-box objective function -for a printing process optimization problem. The function has a single maximum -and includes realistic noise levels. -""" - -import torch -import numpy as np -from typing import Tuple - - -class MockObjectiveFunction: - """ - Mock objective function simulating a printing process optimization. - - This function represents a realistic black-box optimization scenario where: - - The objective is to maximize print quality (score) - - The function has a single global maximum - - Realistic noise is added to simulate measurement uncertainty - - The function depends on 4 continuous parameters - - Parameters: - concentration: [0.1, 1.0] - Material concentration - print_speed: [10.0, 100.0] - Printing speed (mm/s) - gap_size: [0.05, 0.5] - Gap size between layers (mm) - volume: [5.0, 25.0] - Volume per drop (μL) - """ - - def __init__(self, noise_std: float = 0.1, seed: int = 42): - """ - Initialize the mock objective function. - - Args: - noise_std: Standard deviation of Gaussian noise added to observations - seed: Random seed for reproducible results - """ - self.noise_std = noise_std - self.seed = seed - - # Set random seed for reproducible results - torch.manual_seed(seed) - np.random.seed(seed) - - # Define the optimal parameters (global maximum) - # These represent the "true" best settings for our simulated process - self.optimal_params = torch.tensor([ - 0.6, # concentration: medium-high concentration - 45.0, # print_speed: moderate speed - 0.2, # gap_size: medium gap - 15.0 # volume: medium volume - ]) - - # Parameter bounds for normalization - self.bounds = torch.tensor([ - [0.1, 1.0], # concentration - [10.0, 100.0], # print_speed - [0.05, 0.5], # gap_size - [5.0, 25.0] # volume - ]) - - # Maximum possible score (achieved at optimal parameters) - self.max_score = 1.0 - - def __call__(self, X: torch.Tensor) -> torch.Tensor: - """ - Evaluate the objective function on a batch of parameter sets. - - Args: - X: Input tensor of shape (batch_size, 4) containing parameter values - - Returns: - Tensor of shape (batch_size, 1) containing noisy objective values - """ - # Ensure input is the correct shape - if X.dim() == 1: - X = X.unsqueeze(0) - - batch_size = X.shape[0] - - # Normalize parameters to [0, 1] range for easier computation - X_normalized = self._normalize_parameters(X) - optimal_normalized = self._normalize_parameters(self.optimal_params.unsqueeze(0)) - - # Calculate base score using a combination of Gaussian and polynomial terms - scores = self._calculate_base_score(X_normalized, optimal_normalized.squeeze(0)) - - # Add realistic noise to simulate measurement uncertainty - noise = torch.randn(batch_size, 1) * self.noise_std - noisy_scores = scores + noise - - # Ensure scores are non-negative (realistic for quality metrics) - noisy_scores = torch.clamp(noisy_scores, min=0.0) - - return noisy_scores - - def _normalize_parameters(self, X: torch.Tensor) -> torch.Tensor: - """ - Normalize parameters to [0, 1] range based on their bounds. - - Args: - X: Parameter tensor of shape (batch_size, 4) - - Returns: - Normalized parameter tensor - """ - lower_bounds = self.bounds[:, 0] - upper_bounds = self.bounds[:, 1] - - # Normalize to [0, 1] - X_normalized = (X - lower_bounds) / (upper_bounds - lower_bounds) - - return X_normalized - - def _calculate_base_score(self, X_norm: torch.Tensor, optimal_norm: torch.Tensor) -> torch.Tensor: - """ - Calculate the base (noise-free) objective score. - - This function creates a realistic objective landscape with: - - A single global maximum at the optimal parameters - - Smooth transitions between parameter regions - - Realistic parameter interactions - - Args: - X_norm: Normalized parameter tensor (batch_size, 4) - optimal_norm: Normalized optimal parameters (4,) - - Returns: - Base scores tensor (batch_size, 1) - """ - batch_size = X_norm.shape[0] - - # Calculate distance from optimal parameters - distances = torch.norm(X_norm - optimal_norm, dim=1) - - # Primary Gaussian component centered at optimal parameters - gaussian_scores = torch.exp(-8 * distances**2) - - # Add parameter-specific effects to create more realistic landscape - - # Concentration effect: too low or too high concentration reduces quality - conc_effect = 1.0 - 2.0 * (X_norm[:, 0] - 0.5)**2 - - # Print speed effect: very slow or very fast reduces quality - speed_effect = 1.0 - 1.5 * (X_norm[:, 1] - 0.4)**2 - - # Gap size effect: optimal gap size is critical - gap_effect = 1.0 - 3.0 * (X_norm[:, 2] - 0.3)**2 - - # Volume effect: moderate volume is best - volume_effect = 1.0 - 1.2 * (X_norm[:, 3] - 0.5)**2 - - # Combine all effects - combined_effects = (conc_effect + speed_effect + gap_effect + volume_effect) / 4.0 - - # Final score combines Gaussian and parameter effects - base_scores = 0.7 * gaussian_scores + 0.3 * combined_effects - - # Scale to reasonable range and ensure maximum is achievable - base_scores = self.max_score * torch.clamp(base_scores, min=0.0, max=1.0) - - return base_scores.unsqueeze(1) - - def get_optimal_parameters(self) -> Tuple[torch.Tensor, float]: - """ - Get the true optimal parameters and maximum score. - - Returns: - Tuple of (optimal_parameters, max_score) - """ - return self.optimal_params, self.max_score - - def evaluate_at_optimal(self) -> float: - """ - Evaluate the function at the optimal parameters (useful for comparison). - - Returns: - Score at optimal parameters (without noise) - """ - optimal_normalized = self._normalize_parameters(self.optimal_params.unsqueeze(0)) - base_score = self._calculate_base_score(optimal_normalized, optimal_normalized.squeeze(0)) - return base_score.item() - - -# Convenience function for backward compatibility -def mock_objective(X: torch.Tensor, noise_std: float = 0.1) -> torch.Tensor: - """ - Convenience function to evaluate the mock objective function. - - Args: - X: Input tensor of shape (batch_size, 4) - noise_std: Standard deviation of noise to add - - Returns: - Noisy objective values of shape (batch_size, 1) - """ - objective = MockObjectiveFunction(noise_std=noise_std) - return objective(X) \ No newline at end of file diff --git a/aamp_app/optimizer/optimization_progress.png b/aamp_app/optimizer/optimization_progress.png deleted file mode 100644 index 52371bab271e84da569a467493e731e06c3ab4ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 228134 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�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.13.0 -botorch>=0.8.0 -gpytorch>=1.9.0 - -# Scientific computing and utilities -numpy>=1.21.0 -scipy>=1.7.0 -matplotlib>=3.5.0 -seaborn>=0.11.0 - -# Optional: for better numerical stability -scikit-learn>=1.0.0 - -# For interactive notebooks -jupyter>=1.0.0 -pandas>=1.3.0 \ No newline at end of file diff --git a/aamp_app/optimizer/setup.py b/aamp_app/optimizer/setup.py deleted file mode 100644 index 8943a77..0000000 --- a/aamp_app/optimizer/setup.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Setup script for the Bayesian Optimization package. -""" - -from setuptools import setup, find_packages - -with open("README.md", "r", encoding="utf-8") as fh: - long_description = fh.read() - -with open("requirements.txt", "r", encoding="utf-8") as fh: - requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")] - -setup( - name="polyprint-bayesian-optimizer", - version="1.0.0", - author="Polyprint Project", - description="A complete Bayesian optimization package for manufacturing process optimization", - long_description=long_description, - long_description_content_type="text/markdown", - packages=find_packages(), - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Scientific/Engineering :: Mathematics", - ], - python_requires=">=3.8", - install_requires=requirements, - extras_require={ - "dev": [ - "pytest>=6.0", - "black>=22.0", - "flake8>=4.0", - "mypy>=0.900", - ] - }, - entry_points={ - "console_scripts": [ - "run-bo-demo=optimizer.run_demo:main", - ], - }, - include_package_data=True, - package_data={ - "optimizer": ["*.md", "*.txt"], - }, -) \ No newline at end of file diff --git a/aamp_app/pages/sampler.py b/aamp_app/pages/sampler.py index 184b7ef..14d2a09 100644 --- a/aamp_app/pages/sampler.py +++ b/aamp_app/pages/sampler.py @@ -514,7 +514,7 @@ id="sampler-method-dropdown", options=[ {"label": "Sobol Sequence", "value": "sobol"}, - {"label": "Latin Hypercube Sampling", "value": "lhs"}, + {"label": "Random Sampling", "value": "random"}, ], value="sobol", ), From 75b1a2760b45f061e022c85258e7680bf2fcda80 Mon Sep 17 00:00:00 2001 From: Hwang Date: Mon, 23 Mar 2026 06:30:31 -0500 Subject: [PATCH 114/125] Add hardware integrations for ESP301, stages, Z812, and 94043A --- aamp_app/command_sequence.py | 12 +- .../newport_94043a_solar_sim_commands.py | 134 ++++++ aamp_app/commands/z812_commands.py | 76 +++ aamp_app/devices/linear_stage_150.py | 121 +++-- aamp_app/devices/newport_94043a_solar_sim.py | 233 +++++++++ aamp_app/devices/newport_esp301.py | 118 ++++- aamp_app/devices/z812.py | 182 +++++++ aamp_app/util.py | 443 +++++++++++++++++- device_ports.md | 24 + examples/.gitignore | 6 +- examples/example_94043a_solar_sim.py | 73 +++ examples/example_esp301_3n.py | 88 ++++ examples/example_linear_stage_150.py | 54 +++ examples/example_z812.py | 54 +++ implementation_plan.md | 173 +++++++ recipes/recipe_sample.py | 43 +- 16 files changed, 1757 insertions(+), 77 deletions(-) create mode 100644 aamp_app/commands/newport_94043a_solar_sim_commands.py create mode 100644 aamp_app/commands/z812_commands.py create mode 100644 aamp_app/devices/newport_94043a_solar_sim.py create mode 100644 aamp_app/devices/z812.py create mode 100644 device_ports.md create mode 100644 examples/example_94043a_solar_sim.py create mode 100644 examples/example_esp301_3n.py create mode 100644 examples/example_linear_stage_150.py create mode 100644 examples/example_z812.py create mode 100644 implementation_plan.md diff --git a/aamp_app/command_sequence.py b/aamp_app/command_sequence.py index c223d38..aee2a25 100644 --- a/aamp_app/command_sequence.py +++ b/aamp_app/command_sequence.py @@ -9,7 +9,6 @@ from devices.device import Device from commands.utility_commands import LoopStartCommand, LoopEndCommand import inspect -import util from bson.objectid import ObjectId # Representer.add_representer(ABCMeta, Representer.represent_name) @@ -36,6 +35,12 @@ def __init__(self): # self.processed_delays = [] self.device_by_name = {} + @staticmethod + def _get_util_module(): + import util + + return util + def add_device(self, receiver: Device) -> bool: """Add a device to the device list then update the device dict. @@ -582,6 +587,7 @@ def remove_all_loop_commands(self): def get_clean_device_list(self): """Returns a list for use with the dashboard.""" + util = self._get_util_module() device_list = self.get_device_names_classes().copy() device_list_ret = [] for index, device in enumerate(device_list): @@ -594,6 +600,7 @@ def get_clean_device_list(self): def get_recipe(self): """Returns a list for use with the dashboard.""" + util = self._get_util_module() devices = [] commands = [] execution_options = self.execution_options @@ -622,6 +629,7 @@ def get_recipe(self): def load_from_dict(self, recipe_dict): """Loads a recipe from a dictionary.""" + util = self._get_util_module() self.device_list = [] self.command_list = [] for device in recipe_dict["devices"]: @@ -642,6 +650,7 @@ def load_from_dict(self, recipe_dict): self.execution_options = recipe_dict["execution_options"] def add_device_from_dict(self, device_type, device_dict): + util = self._get_util_module() if not hasattr(self, 'document'): self.document = { '_id': ObjectId(), @@ -655,6 +664,7 @@ def add_device_from_dict(self, device_type, device_dict): self.update_device_by_name() def add_command_from_dict(self, device_type, command_type, command_dict): + util = self._get_util_module() if device_type == "UtilityCommands": self.add_command( util.devices_ref_redundancy[device_type]["commands"][command_type]["obj"]( diff --git a/aamp_app/commands/newport_94043a_solar_sim_commands.py b/aamp_app/commands/newport_94043a_solar_sim_commands.py new file mode 100644 index 0000000..2a373b2 --- /dev/null +++ b/aamp_app/commands/newport_94043a_solar_sim_commands.py @@ -0,0 +1,134 @@ +from .command import Command, CommandResult +from devices.newport_94043a_solar_sim import Newport94043ASolarSim + + +class Newport94043ASolarSimParentCommand(Command): + """Parent class for all Newport94043ASolarSim commands controlled through the 69920 power supply.""" + + receiver_cls = Newport94043ASolarSim + + def __init__(self, receiver: Newport94043ASolarSim, **kwargs): + super().__init__(receiver, **kwargs) + + +class Newport94043ASolarSimConnect(Newport94043ASolarSimParentCommand): + """Open a serial port for the 94043A solar simulator via the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.start_serial(delay=0.5)) + + +class Newport94043ASolarSimInitialize(Newport94043ASolarSimParentCommand): + """Initialize the 94043A solar simulator through the 69920 power supply and set power mode.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class Newport94043ASolarSimDeinitialize(Newport94043ASolarSimParentCommand): + """Deinitialize the 94043A solar simulator interface.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize()) + + +class Newport94043ASolarSimIdentify(Newport94043ASolarSimParentCommand): + """Query the 69920 power supply model identifier used by the 94043A solar simulator.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.identify()) + + +class Newport94043ASolarSimStatusByte(Newport94043ASolarSimParentCommand): + """Query the 69920 status byte.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.status_byte()) + + +class Newport94043ASolarSimEventStatus(Newport94043ASolarSimParentCommand): + """Query the 69920 event status register.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.event_status_register()) + + +class Newport94043ASolarSimLampStart(Newport94043ASolarSimParentCommand): + """Start the lamp through the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.lamp_start()) + + +class Newport94043ASolarSimLampStop(Newport94043ASolarSimParentCommand): + """Stop the lamp through the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.lamp_stop()) + + +class Newport94043ASolarSimSetPowerMode(Newport94043ASolarSimParentCommand): + """Set the 94043A solar simulator control path to power mode through the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_power_mode()) + + +class Newport94043ASolarSimGetAmps(Newport94043ASolarSimParentCommand): + """Read the displayed lamp current from the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_amps()) + + +class Newport94043ASolarSimGetVolts(Newport94043ASolarSimParentCommand): + """Read the displayed lamp voltage from the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_volts()) + + +class Newport94043ASolarSimGetWatts(Newport94043ASolarSimParentCommand): + """Read the displayed lamp power from the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_watts()) + + +class Newport94043ASolarSimGetLampHours(Newport94043ASolarSimParentCommand): + """Read accumulated lamp hours from the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_lamp_hours()) + + +class Newport94043ASolarSimGetPowerPreset(Newport94043ASolarSimParentCommand): + """Read the configured power preset from the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_power_preset()) + + +class Newport94043ASolarSimGetCurrentLimit(Newport94043ASolarSimParentCommand): + """Read the configured current limit from the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_current_limit()) + + +class Newport94043ASolarSimGetPowerLimit(Newport94043ASolarSimParentCommand): + """Read the configured power limit from the 69920 power supply.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_power_limit()) + + +class Newport94043ASolarSimSetPowerPreset(Newport94043ASolarSimParentCommand): + """Set the power preset in watts through the 69920 power supply.""" + + def __init__(self, receiver: Newport94043ASolarSim, watts: int, **kwargs): + super().__init__(receiver, **kwargs) + self._params["watts"] = watts + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_power_preset(self._params["watts"])) diff --git a/aamp_app/commands/z812_commands.py b/aamp_app/commands/z812_commands.py new file mode 100644 index 0000000..9f88295 --- /dev/null +++ b/aamp_app/commands/z812_commands.py @@ -0,0 +1,76 @@ +from .command import Command, CommandResult +from devices.z812 import Z812 + + +class Z812ParentCommand(Command): + """Parent class for all Z812 commands.""" + + receiver_cls = Z812 + + def __init__(self, receiver: Z812, **kwargs): + super().__init__(receiver, **kwargs) + + +class Z812Connect(Z812ParentCommand): + """Open a serial port for the Z812 stage.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.start_serial()) + + +class Z812Initialize(Z812ParentCommand): + """Initialize the Z812 stage by homing it.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class Z812Deinitialize(Z812ParentCommand): + """Deinitialize the Z812 stage.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize()) + + +class Z812EnableMotor(Z812ParentCommand): + """Enable the Z812 motor.""" + + def __init__(self, receiver: Z812, **kwargs): + super().__init__(receiver, **kwargs) + self._params["state"] = True + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_enabled_state(self._params["state"])) + + +class Z812DisableMotor(Z812ParentCommand): + """Disable the Z812 motor.""" + + def __init__(self, receiver: Z812, **kwargs): + super().__init__(receiver, **kwargs) + self._params["state"] = False + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_enabled_state(self._params["state"])) + + +class Z812MoveAbsolute(Z812ParentCommand): + """Move the Z812 stage to an absolute position in mm.""" + + def __init__(self, receiver: Z812, position: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params["position"] = position + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_absolute(self._params["position"])) + + +class Z812MoveRelative(Z812ParentCommand): + """Move the Z812 stage by a relative distance in mm.""" + + def __init__(self, receiver: Z812, distance: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params["distance"] = distance + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_relative(self._params["distance"])) diff --git a/aamp_app/devices/linear_stage_150.py b/aamp_app/devices/linear_stage_150.py index 5c4913b..84f4963 100644 --- a/aamp_app/devices/linear_stage_150.py +++ b/aamp_app/devices/linear_stage_150.py @@ -5,10 +5,16 @@ class LinearStage150(SerialDevice): + DEVICE_UNIT_SCALE = 409600 + MIN_POSITION_MM = 0.0 + MAX_POSITION_MM = 150.0 + HOME_TIMEOUT_S = 120.0 + MOVE_TIMEOUT_S = 120.0 + def __init__( self, name: str, - port: str = "'COM6'", + port: str = "COM6", baudrate: int = 115200, timeout: float | None = 0.1, destination: int = 0x50, @@ -19,7 +25,31 @@ def __init__( self._destination = destination self._source = source self._channel = channel - # print("dest: "+str(self._destination)) + self._is_enabled = False + + def _wait_for_message(self, expected_header: bytes, timeout_s: float) -> Tuple[bool, str]: + deadline = time.time() + timeout_s + while time.time() < deadline: + response = self.ser.read(2) + if response == expected_header: + return (True, "") + if response == b"": + continue + return (False, "Response timed out while waiting for stage controller message.") + + def _validate_position(self, position: float) -> Tuple[bool, str]: + if position < self.MIN_POSITION_MM or position > self.MAX_POSITION_MM: + return ( + False, + "Position " + + str(position) + + " is out of range. Expected " + + str(self.MIN_POSITION_MM) + + " to " + + str(self.MAX_POSITION_MM) + + " mm.", + ) + return (True, "") def get_init_args(self) -> dict: args_dict = { @@ -46,20 +76,19 @@ def update_init_args(self, args_dict: dict): def initialize(self) -> Tuple[bool, str]: self._is_initialized = False - # lts150: initialize, home it (if needed) + was_enabled, message = self.set_enabled_state(True) + if not was_enabled: + return (was_enabled, message) # Home Stage; MGMSG_MOT_MOVE_HOME self.ser.write( pack(" Tuple[bool, str]: # return super().initialize() def deinitialize(self) -> Tuple[bool, str]: - # i dont think this is needed: lts150: deinitialize - # if reset_init_flag: //used in other devices + if self.ser.is_open: + self.set_enabled_state(False) self._is_initialized = False return (True, "Successfully deinitialized LTS150.") - # return super().deinitialize() @check_serial - # @check_initialized def get_enabled_state(self) -> bool: - self._is_enabled = False - - # TODO: lts150 get enabled state, MGMSG_MOD_GET_CHANENABLESTATE - # self.ser.write(pack(' Tuple[bool, str]: if state: self.ser.write( - pack(" float: - if not self.ser.is_open: - self.start_serial() self._position = 0.0 - Device_Unit_SF = 409600 # MGMSG_MOT_GET_POSCOUNTER self.ser.write( pack(" float: # Read back position returns by the cube; Rx message MGMSG_MOT_GET_POSCOUNTER header, chan_dent, position_dUnits = unpack("<6sHI", self.ser.read(12)) - self._position = position_dUnits / float(Device_Unit_SF) + self._position = position_dUnits / float(self.DEVICE_UNIT_SCALE) return self._position @check_serial - # @check_initialized + @check_initialized def move_absolute(self, position: float) -> Tuple[bool, str]: - if position > 150: - return (False, "Position " + str(position) + " is out of range.") + is_valid_position, message = self._validate_position(position) + if not is_valid_position: + return (False, message) - Device_Unit_SF = 409600 - dUnitpos = int(Device_Unit_SF * position) + dUnitpos = int(self.DEVICE_UNIT_SCALE * position) self.ser.write( pack( " Tuple[bool, str]: ) # Confirm stage completed move before advancing; MGMSG_MOT_MOVE_COMPLETED - Rx = "" - Moved = pack(" Tuple[bool, str]: ) @check_serial - # @check_initialized + @check_initialized def move_relative(self, distance: float) -> Tuple[bool, str]: - if distance + self.get_position() > 150: - return ( - False, - "Position " + str(distance + self.get_position()) + " is out of range.", - ) + target_position = self.get_position() + distance + is_valid_position, message = self._validate_position(target_position) + if not is_valid_position: + return (False, message) - Device_Unit_SF = 409600 - dUnitpos = int(Device_Unit_SF * distance) + dUnitpos = int(self.DEVICE_UNIT_SCALE * distance) self.ser.write( pack( " Tuple[bool, str]: ) # Confirm stage completed move before advancing; MGMSG_MOT_MOVE_COMPLETED - Rx = "" - Moved = pack(" dict: + return { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "line_terminator": self._line_terminator, + "lamp_hours_warning_threshold": self._lamp_hours_warning_threshold, + "default_power_watts": self._default_power_watts, + "max_power_watts": self._max_power_watts, + } + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._line_terminator = args_dict["line_terminator"] + self._lamp_hours_warning_threshold = args_dict["lamp_hours_warning_threshold"] + self._default_power_watts = args_dict["default_power_watts"] + self._max_power_watts = args_dict["max_power_watts"] + + def _send(self, command: str) -> None: + self.ser.write((command + self._line_terminator).encode("ascii")) + + def _readline(self) -> Tuple[bool, str]: + response = self.ser.readline() + if response == b"": + return (False, "Response timed out.") + return (True, response.decode("ascii", errors="replace").strip()) + + def _query(self, command: str) -> Tuple[bool, str]: + self.ser.reset_input_buffer() + self._send(command) + return self._readline() + + def _query_with_timeout(self, command: str, timeout_s: float) -> Tuple[bool, str]: + original_timeout = self.ser.timeout + self.ser.timeout = timeout_s + try: + return self._query(command) + finally: + self.ser.timeout = original_timeout + + @staticmethod + def _parse_esr(response: str) -> Tuple[bool, str]: + if not response.startswith("ESR"): + return (False, "Unexpected response: " + response) + hex_value = response[3:] + try: + esr_value = int(hex_value, 16) + except ValueError: + return (False, "Could not parse ESR response: " + response) + + has_error = bool(esr_value & 0b00111100) + if has_error: + return (False, "69920 returned error status " + response) + return (True, response) + + @check_serial + def initialize(self) -> Tuple[bool, str]: + was_successful, response = self._set_mode(power_mode=True) + if not was_successful: + self._is_initialized = False + return (was_successful, response) + + was_successful, response = self._set_power_preset(self._default_power_watts) + if not was_successful: + self._is_initialized = False + return (was_successful, response) + + was_successful, lamp_hours_response = self._get_lamp_hours() + if not was_successful: + self._is_initialized = False + return (was_successful, lamp_hours_response) + + self._is_initialized = True + lamp_hours = self.parse_lamp_hours(lamp_hours_response) + message = ( + "Successfully initialized 94043A solar simulator through 69920 power supply in power mode. " + f"Default power preset set to {self._default_power_watts} W. Lamp hours: {lamp_hours:.0f} h." + ) + if lamp_hours >= self._lamp_hours_warning_threshold: + message += " Lamp has exceeded the replacement threshold. Replace the lamp and reset 69920 lamp hours from the front panel." + return (True, message) + + def deinitialize(self) -> Tuple[bool, str]: + self._is_initialized = False + return (True, "Successfully deinitialized 69920.") + + @check_serial + def identify(self) -> Tuple[bool, str]: + return self._query("IDN?") + + @check_serial + def status_byte(self) -> Tuple[bool, str]: + return self._query("STB?") + + @check_serial + def event_status_register(self) -> Tuple[bool, str]: + return self._query("ESR?") + + @check_initialized + @check_serial + def lamp_start(self) -> Tuple[bool, str]: + was_successful, response = self._query_with_timeout("START", self.LAMP_START_TIMEOUT_S) + if not was_successful: + return (was_successful, response) + return self._parse_esr(response) + + @check_initialized + @check_serial + def lamp_stop(self) -> Tuple[bool, str]: + was_successful, response = self._query_with_timeout("STOP", self.LAMP_STOP_TIMEOUT_S) + if not was_successful: + return (was_successful, response) + return self._parse_esr(response) + + @check_initialized + @check_serial + def set_mode(self, power_mode: bool = True) -> Tuple[bool, str]: + return self._set_mode(power_mode) + + @check_serial + def _set_mode(self, power_mode: bool = True) -> Tuple[bool, str]: + mode_value = 0 if power_mode else 1 + was_successful, response = self._query_with_timeout(f"MODE={mode_value}", self.MODE_TIMEOUT_S) + if not was_successful: + return (was_successful, response) + return self._parse_esr(response) + + def set_power_mode(self) -> Tuple[bool, str]: + return self.set_mode(power_mode=True) + + @check_initialized + @check_serial + def get_amps(self) -> Tuple[bool, str]: + return self._query("AMPS?") + + @check_initialized + @check_serial + def get_volts(self) -> Tuple[bool, str]: + return self._query("VOLTS?") + + @check_initialized + @check_serial + def get_watts(self) -> Tuple[bool, str]: + return self._query("WATTS?") + + @check_initialized + @check_serial + def get_lamp_hours(self) -> Tuple[bool, str]: + return self._get_lamp_hours() + + @check_serial + def _get_lamp_hours(self) -> Tuple[bool, str]: + return self._query("LAMP HRS?") + + @staticmethod + def parse_lamp_hours(response: str) -> float: + cleaned = response.strip() + return float(cleaned) + + @check_initialized + @check_serial + def get_current_limit(self) -> Tuple[bool, str]: + return self._query("A-LIM?") + + @check_initialized + @check_serial + def get_power_limit(self) -> Tuple[bool, str]: + return self._query("P-LIM?") + + @check_initialized + @check_serial + def get_power_preset(self) -> Tuple[bool, str]: + return self._query("P-PRESET?") + + @check_initialized + @check_serial + def set_power_preset(self, watts: int) -> Tuple[bool, str]: + return self._set_power_preset(watts) + + @check_serial + def _set_power_preset(self, watts: int) -> Tuple[bool, str]: + if watts < 0: + return (False, "Power preset must be non-negative.") + if watts > self._max_power_watts: + return ( + False, + "Power preset " + + str(watts) + + " W exceeds the configured safety limit of " + + str(self._max_power_watts) + + " W for the 94043A solar simulator.", + ) + was_successful, response = self._query_with_timeout( + f"P-PRESET={int(watts)}", + self.POWER_PRESET_TIMEOUT_S, + ) + if not was_successful: + return (was_successful, response) + return self._parse_esr(response) diff --git a/aamp_app/devices/newport_esp301.py b/aamp_app/devices/newport_esp301.py index db3cb2a..0cf6248 100644 --- a/aamp_app/devices/newport_esp301.py +++ b/aamp_app/devices/newport_esp301.py @@ -1,5 +1,5 @@ import time -from typing import Optional, Tuple, Union +from typing import Dict, Optional, Tuple, Union import functools from .device import SerialDevice, check_initialized, check_serial @@ -20,6 +20,28 @@ def wrapper(self, *args, **kwargs): return wrapper class NewportESP301(SerialDevice): + UNIT_CODE_BY_NAME = { + "encoder_count": 0, + "motor_step": 1, + "mm": 2, + "micrometer": 3, + "inch": 4, + "milli_inch": 5, + "micro_inch": 6, + "deg": 7, + "gradian": 8, + "radian": 9, + "milliradian": 10, + "microradian": 11, + } + + DEFAULT_AXIS_CONFIG = { + "motion_type": "linear", + "units": "mm", + "home_mode": "OR4", + "zero_position": 0.0, + } + def __init__( self, name: str, @@ -28,7 +50,8 @@ def __init__( timeout: Optional[float] = 1.0, axis_list: Tuple[int, ...] = (1,), default_speed: float = 20.0, - poll_interval: float = 0.1): + poll_interval: float = 0.1, + axis_configs: Optional[Dict[int, Dict[str, Union[str, float]]]] = None): super().__init__(name, port, baudrate, timeout) self._axis_list = axis_list @@ -37,6 +60,7 @@ def __init__( self._poll_interval = poll_interval self._max_speed = 200.0 # make list # self._max_speed_list = max_speed_list + self._axis_configs = self._normalize_axis_configs(axis_configs) def get_init_args(self) -> dict: args_dict = { @@ -47,6 +71,7 @@ def get_init_args(self) -> dict: "axis_list": self._axis_list, "default_speed": self._default_speed, "poll_interval": self._poll_interval, + "axis_configs": self._axis_configs, } return args_dict @@ -58,6 +83,7 @@ def update_init_args(self, args_dict: dict): self._axis_list = args_dict["axis_list"] self._default_speed = args_dict["default_speed"] self._poll_interval = args_dict["poll_interval"] + self._axis_configs = self._normalize_axis_configs(args_dict.get("axis_configs")) @property def default_speed(self) -> float: @@ -68,6 +94,51 @@ def default_speed(self, speed: float): if speed > 0.0 and speed < self._max_speed: self._default_speed = speed + def _normalize_axis_configs( + self, + axis_configs: Optional[Dict[int, Dict[str, Union[str, float]]]] + ) -> Dict[int, Dict[str, Union[str, float]]]: + normalized_configs: Dict[int, Dict[str, Union[str, float]]] = {} + + for axis in self._axis_list: + config = dict(NewportESP301.DEFAULT_AXIS_CONFIG) + if axis_configs and axis in axis_configs: + config.update(axis_configs[axis]) + + motion_type = str(config.get("motion_type", "linear")).lower() + units = config.get("units") + if units is None: + units = "deg" if motion_type == "rotary" else "mm" + else: + units = str(units).lower() + + config["motion_type"] = motion_type + config["units"] = units + config["home_mode"] = str(config.get("home_mode", "OR4")).upper() + config["zero_position"] = float(config.get("zero_position", 0.0)) + config["default_speed"] = float(config.get("default_speed", self._default_speed)) + config["max_speed"] = float(config.get("max_speed", self._max_speed)) + normalized_configs[axis] = config + + return normalized_configs + + def _get_axis_config(self, axis_number: int) -> Dict[str, Union[str, float]]: + if axis_number not in self._axis_configs: + self._axis_configs = self._normalize_axis_configs(self._axis_configs) + return self._axis_configs[axis_number] + + def _get_unit_code(self, axis_number: int) -> int: + units = str(self._get_axis_config(axis_number)["units"]).lower() + if units not in NewportESP301.UNIT_CODE_BY_NAME: + raise ValueError(f"Unsupported ESP301 unit '{units}' for axis {axis_number}") + return NewportESP301.UNIT_CODE_BY_NAME[units] + + def _get_axis_default_speed(self, axis_number: int) -> float: + return float(self._get_axis_config(axis_number)["default_speed"]) + + def _get_axis_max_speed(self, axis_number: int) -> float: + return float(self._get_axis_config(axis_number)["max_speed"]) + # check_error already has serial check # easier to just set is_intialized False at the very beginning # do for all receivers @@ -87,8 +158,22 @@ def initialize(self) -> Tuple[bool, str]: if not was_turned_on: self._is_initialized = False return (was_turned_on, message) - # set units to mm, homing value to 0, set max speed, set current speed - command = str(axis) + "SN2;" + str(axis) + "SH0;" + str(axis) + "VU" + str(self._max_speed) + ";" + str(axis) + "VA" + str(self.default_speed) + "\r" + + try: + unit_code = self._get_unit_code(axis) + except ValueError as exc: + self._is_initialized = False + return (False, str(exc)) + + axis_max_speed = self._get_axis_max_speed(axis) + axis_default_speed = self._get_axis_default_speed(axis) + command = ( + str(axis) + "SN" + str(unit_code) + + ";" + str(axis) + "SH0" + + ";" + str(axis) + "VU" + str(axis_max_speed) + + ";" + str(axis) + "VA" + str(axis_default_speed) + + "\r" + ) self.ser.write(command.encode('ascii')) # Make sure initialization of settings was successful @@ -104,7 +189,7 @@ def initialize(self) -> Tuple[bool, str]: return (was_homed, message) self._is_initialized = True - return (True, "Successfully initialized axes by setting units to mm, settings max/current speeds, and homing. Current position set to zero.") + return (True, "Successfully initialized ESP301 axes with axis-specific units, speed settings, and homing.") # move_speed_absolute already has serial check def deinitialize(self, reset_init_flag: bool = True) -> Tuple[bool, str]: @@ -112,7 +197,8 @@ def deinitialize(self, reset_init_flag: bool = True) -> Tuple[bool, str]: # return (False, "Serial port " + self._port + " is not open. ") for axis in self._axis_list: - was_zeroed, message = self.move_speed_absolute(0.0, speed=None, axis_number=axis) + zero_position = float(self._get_axis_config(axis)["zero_position"]) + was_zeroed, message = self.move_speed_absolute(axis, zero_position, speed=None) if not was_zeroed: return (was_zeroed, message) @@ -123,11 +209,13 @@ def deinitialize(self, reset_init_flag: bool = True) -> Tuple[bool, str]: # make a home_all function @check_serial + @check_axis_num def home(self, axis_number: int) -> Tuple[bool, str]: # if not self.ser.is_open: # return (False, "Serial port " + self._port + " is not open. ") - command = str(axis_number) + "OR4\r" + home_mode = str(self._get_axis_config(axis_number)["home_mode"]) + command = str(axis_number) + home_mode + "\r" self.ser.write(command.encode('ascii')) while self.is_any_moving(): @@ -139,7 +227,17 @@ def home(self, axis_number: int) -> Tuple[bool, str]: if not was_successful: return (was_successful, message) else: - return (True, "Successfully homed axes " + str(axis_number)) + axis_config = self._get_axis_config(axis_number) + return ( + True, + "Successfully homed axis " + + str(axis_number) + + " using " + + home_mode + + " in " + + str(axis_config["units"]) + + "." + ) # Consider a decorator for checks? @check_serial @@ -159,7 +257,7 @@ def move_speed_absolute(self, axis_number: int = 1, position: Optional[float] = return (False, "Position was not specified") if speed is None: - speed = self._default_speed + speed = self._get_axis_default_speed(axis_number) command = str(axis_number) + "VA" + str(speed) +"\r" self.ser.write(command.encode('ascii')) @@ -201,7 +299,7 @@ def move_speed_relative(self, axis_number: int = 1, distance: Optional[float] = return (False, "Distance was not specified") if speed is None: - speed = self._default_speed + speed = self._get_axis_default_speed(axis_number) command = str(axis_number) + "VA" + str(speed) +"\r" self.ser.write(command.encode('ascii')) diff --git a/aamp_app/devices/z812.py b/aamp_app/devices/z812.py new file mode 100644 index 0000000..37aecd9 --- /dev/null +++ b/aamp_app/devices/z812.py @@ -0,0 +1,182 @@ +from typing import Optional, Tuple +from struct import pack, unpack +import time + +from .device import SerialDevice, check_serial, check_initialized + + +class Z812(SerialDevice): + DEVICE_UNIT_SCALE = 34555 + MIN_POSITION_MM = 0.0 + MAX_POSITION_MM = 12.0 + HOME_TIMEOUT_S = 120.0 + MOVE_TIMEOUT_S = 120.0 + + def __init__( + self, + name: str, + port: str = "COM6", + baudrate: int = 115200, + timeout: Optional[float] = 0.1, + destination: int = 0x50, + source: int = 0x01, + channel: int = 1, + ): + super().__init__(name, port, baudrate, timeout) + self._destination = destination + self._source = source + self._channel = channel + self._is_enabled = False + + def _wait_for_message(self, expected_header: bytes, timeout_s: float) -> Tuple[bool, str]: + deadline = time.time() + timeout_s + while time.time() < deadline: + response = self.ser.read(2) + if response == expected_header: + return (True, "") + if response == b"": + continue + return (False, "Response timed out while waiting for Z812 controller message.") + + def _validate_position(self, position: float) -> Tuple[bool, str]: + if position < self.MIN_POSITION_MM or position > self.MAX_POSITION_MM: + return ( + False, + "Position " + + str(position) + + " is out of range. Expected " + + str(self.MIN_POSITION_MM) + + " to " + + str(self.MAX_POSITION_MM) + + " mm.", + ) + return (True, "") + + def get_init_args(self) -> dict: + return { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "destination": self._destination, + "source": self._source, + "channel": self._channel, + } + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._destination = args_dict["destination"] + self._source = args_dict["source"] + self._channel = args_dict["channel"] + + @check_serial + def initialize(self) -> Tuple[bool, str]: + self._is_initialized = False + + was_enabled, message = self.set_enabled_state(True) + if not was_enabled: + return (was_enabled, message) + + self.ser.write(pack(" Tuple[bool, str]: + if self.ser.is_open: + self.set_enabled_state(False) + self._is_initialized = False + return (True, "Successfully deinitialized Z812.") + + @check_serial + def get_enabled_state(self) -> bool: + self.ser.write(pack(" Tuple[bool, str]: + if state: + self.ser.write(pack(" float: + self.ser.write(pack(" Tuple[bool, str]: + is_valid_position, message = self._validate_position(position) + if not is_valid_position: + return (False, message) + + dunit_pos = int(self.DEVICE_UNIT_SCALE * position) + self.ser.write( + pack( + " Tuple[bool, str]: + target_position = self.get_position() + distance + is_valid_position, message = self._validate_position(target_position) + if not is_valid_position: + return (False, message) + + dunit_pos = int(self.DEVICE_UNIT_SCALE * distance) + self.ser.write( + pack( + " None: + seq = CommandSequence() + + power_supply = Newport94043ASolarSim( + name="newport_94043a_solar_sim", + port=POWER_SUPPLY_PORT, + baudrate=9600, + timeout=1.0, + default_power_watts=400, + max_power_watts=450, + ) + seq.add_device(power_supply) + + seq.add_command(Newport94043ASolarSimConnect(power_supply)) + seq.add_command(Newport94043ASolarSimIdentify(power_supply)) + seq.add_command(Newport94043ASolarSimInitialize(power_supply)) + seq.add_command(Newport94043ASolarSimStatusByte(power_supply)) + seq.add_command(Newport94043ASolarSimEventStatus(power_supply)) + seq.add_command(Newport94043ASolarSimGetLampHours(power_supply)) + seq.add_command(Newport94043ASolarSimGetWatts(power_supply)) + seq.add_command(Newport94043ASolarSimGetCurrentLimit(power_supply)) + seq.add_command(Newport94043ASolarSimGetPowerLimit(power_supply)) + seq.add_command(Newport94043ASolarSimSetPowerPreset(power_supply, watts=POWER_PRESET_WATTS)) + seq.add_command(Newport94043ASolarSimGetPowerPreset(power_supply)) + seq.add_command(Newport94043ASolarSimLampStart(power_supply)) + seq.add_command(Newport94043ASolarSimGetWatts(power_supply)) + seq.add_command(Newport94043ASolarSimSetPowerPreset(power_supply, watts=420)) + seq.add_command(Newport94043ASolarSimGetPowerPreset(power_supply)) + seq.add_command(Newport94043ASolarSimGetWatts(power_supply)) + seq.add_command(Newport94043ASolarSimSetPowerPreset(power_supply, watts=380)) + seq.add_command(Newport94043ASolarSimGetPowerPreset(power_supply)) + seq.add_command(Newport94043ASolarSimGetWatts(power_supply)) + seq.add_command(Newport94043ASolarSimLampStop(power_supply)) + + seq.add_command(Newport94043ASolarSimDeinitialize(power_supply)) + + log_file = "logs/example_94043a_solar_sim.log" + invoker = CommandInvoker(seq, log_to_file=True, log_filename=log_file, alert_slack=False) + + seq.print_command_names() + print("\nThis smoke test communicates with the 94043A solar simulator through the 69920 power supply in power mode.") + print("Sequence: initialize at 400 W, lamp start, set 420 W, set 380 W, lamp stop.") + print("Software blocks presets above 450 W.") + userinput = input("\ntype 'y' to continue, type anything else to quit: ").strip().lower() + if userinput == "y": + invoker.invoke_commands() + + +if __name__ == "__main__": + main() diff --git a/examples/example_esp301_3n.py b/examples/example_esp301_3n.py new file mode 100644 index 0000000..3ec6544 --- /dev/null +++ b/examples/example_esp301_3n.py @@ -0,0 +1,88 @@ +# ESP301-3N smoke test +# run from root using 'python -m examples.example_esp301_3n' + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_sequence import CommandSequence +from command_invoker import CommandInvoker +from devices.newport_esp301 import NewportESP301 +from commands.newport_esp301_commands import * + + +ESP301_PORT = "COM6" + +AXIS_CONFIGS = { + 1: { + "stage_model": "ILS100CC", + "motion_type": "linear", + "units": "mm", + "home_mode": "OR4", + "zero_position": 0.0, + "default_speed": 10.0, + }, + 2: { + "stage_model": "UTS100PP", + "motion_type": "linear", + "units": "mm", + "home_mode": "OR4", + "zero_position": 0.0, + "default_speed": 10.0, + }, + 3: { + "stage_model": "PR50PP", + "motion_type": "rotary", + "units": "deg", + "home_mode": "OR1", + "zero_position": 0.0, + "default_speed": 10.0, + }, +} + + +def main() -> None: + seq = CommandSequence() + + esp301 = NewportESP301( + name="esp301_3n", + port=ESP301_PORT, + axis_list=(1, 2, 3), + default_speed=10.0, + poll_interval=0.1, + axis_configs=AXIS_CONFIGS, + ) + seq.add_device(esp301) + + seq.add_command(NewportESP301Connect(esp301)) + seq.add_command(NewportESP301Initialize(esp301)) + + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=20.0, speed=5.0)) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=5.0, speed=5.0)) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=20.0, speed=5.0)) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=5.0, speed=5.0)) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=3, position=45.0, speed=10.0)) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=3, position=120.0, speed=10.0)) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=3, position=15.0, speed=10.0)) + + # Re-run initialization at the end to confirm all three axes can home again. + seq.add_command(NewportESP301Initialize(esp301)) + + log_file = "logs/example_esp301_3n.log" + invoker = CommandInvoker(seq, log_to_file=True, log_filename=log_file, alert_slack=False) + + seq.print_command_names() + print("\nBefore running, place axes 1 and 2 away from the negative limit and make sure axis 3 has clearance for a full home search.") + userinput = input("\ntype 'y' to continue, type anything else to quit: ").strip().lower() + if userinput == "y": + invoker.invoke_commands() + + +if __name__ == "__main__": + main() diff --git a/examples/example_linear_stage_150.py b/examples/example_linear_stage_150.py new file mode 100644 index 0000000..26e73e5 --- /dev/null +++ b/examples/example_linear_stage_150.py @@ -0,0 +1,54 @@ +# LinearStage150 smoke test +# run from root using 'python -m examples.example_linear_stage_150' + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_sequence import CommandSequence +from command_invoker import CommandInvoker +from devices.linear_stage_150 import LinearStage150 +from commands.linear_stage_150_commands import * + + +STAGE_PORT = "COM15" + + +def main() -> None: + seq = CommandSequence() + + stage = LinearStage150( + name="linear_stage_150", + port=STAGE_PORT, + baudrate=115200, + timeout=0.1, + destination=0x50, + source=0x01, + channel=1, + ) + seq.add_device(stage) + + seq.add_command(LinearStage150Connect(stage)) + seq.add_command(LinearStage150Initialize(stage)) + seq.add_command(LinearStage150MoveAbsolute(stage, position=100.0)) + seq.add_command(LinearStage150MoveRelative(stage, distance=5.0)) + seq.add_command(LinearStage150MoveAbsolute(stage, position=0.0)) + seq.add_command(LinearStage150Deinitialize(stage)) + + log_file = "logs/example_linear_stage_150.log" + invoker = CommandInvoker(seq, log_to_file=True, log_filename=log_file, alert_slack=False) + + seq.print_command_names() + userinput = input("\ntype 'y' to continue, type anything else to quit: ").strip().lower() + if userinput == "y": + invoker.invoke_commands() + + +if __name__ == "__main__": + main() diff --git a/examples/example_z812.py b/examples/example_z812.py new file mode 100644 index 0000000..534b5fa --- /dev/null +++ b/examples/example_z812.py @@ -0,0 +1,54 @@ +# Z812 smoke test +# run from root using 'python -m examples.example_z812' + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_sequence import CommandSequence +from command_invoker import CommandInvoker +from devices.z812 import Z812 +from commands.z812_commands import * + + +STAGE_PORT = "COM12" + + +def main() -> None: + seq = CommandSequence() + + stage = Z812( + name="z812", + port=STAGE_PORT, + baudrate=115200, + timeout=0.1, + destination=0x50, + source=0x01, + channel=1, + ) + seq.add_device(stage) + + seq.add_command(Z812Connect(stage)) + seq.add_command(Z812Initialize(stage)) + seq.add_command(Z812MoveAbsolute(stage, position=8.0)) + seq.add_command(Z812MoveRelative(stage, distance=3.0)) + seq.add_command(Z812MoveAbsolute(stage, position=0.0)) + seq.add_command(Z812Deinitialize(stage)) + + log_file = "logs/example_z812.log" + invoker = CommandInvoker(seq, log_to_file=True, log_filename=log_file, alert_slack=False) + + seq.print_command_names() + userinput = input("\ntype 'y' to continue, type anything else to quit: ").strip().lower() + if userinput == "y": + invoker.invoke_commands() + + +if __name__ == "__main__": + main() diff --git a/implementation_plan.md b/implementation_plan.md new file mode 100644 index 0000000..5cb1ddc --- /dev/null +++ b/implementation_plan.md @@ -0,0 +1,173 @@ +# Implementation Plan + +This file tracks device and command work for this branch. + +## Workflow + +Use this order for each device: + +1. Define scope and required actions +2. Implement device in `aamp_app/devices/` +3. Implement commands in `aamp_app/commands/` +4. Add or update an example in `examples/` +5. Run the example and confirm behavior +6. Check recipe or YAML compatibility if needed +7. Verify the device or commands appear correctly in the web app +8. Record follow-up issues + +## Status Legend + +- `planned` +- `in progress` +- `blocked` +- `done` + +## Branch Summary + +- Branch goal: add or update devices and commands, verify with examples, then confirm web app integration +- Owner: `TBD` +- Started: `2026-03-23` + +## Device Tracking + +### Device Template + +Copy this section for each device and fill it in as work starts. + +#### Device: `` + +- Status: `planned` +- Device file: `aamp_app/devices/.py` +- Command file: `aamp_app/commands/_commands.py` +- Example file: `examples/.py` +- Similar existing implementation: `TBD` +- Hardware or SDK dependency: `TBD` + +#### Scope + +- Add: +- Update: +- Not in scope: + +#### Required Actions + +- [ ] Device class implemented +- [ ] Initialization path checked +- [ ] Shutdown or cleanup path checked +- [ ] Core commands implemented +- [ ] Command metadata and params reviewed +- [ ] Example added or updated +- [ ] Example executed successfully +- [ ] Logging behavior checked +- [ ] Recipe or YAML compatibility checked +- [ ] Web app visibility checked +- [ ] Manual control page checked if applicable +- [ ] Execute recipe flow checked if applicable +- [ ] Notes recorded + +#### Validation + +- Example run result: +- Web app result: +- Known issues: + +#### Notes + +- + +## Active Work Items + +- [ ] Device 1: `ESP301-3N` +- [ ] Device 2: `Z812` +- [ ] Device 3: `TBD` + +#### Device: `ESP301-3N` + +- Status: `in progress` +- Device file: `aamp_app/devices/newport_esp301.py` +- Command file: `aamp_app/commands/newport_esp301_commands.py` +- Example file: `examples/TBD` +- Similar existing implementation: `NewportESP301`, `LinearStage150`, `MTS50_Z8` +- Hardware or SDK dependency: `Newport ESP301-3N controller` + +#### Scope + +- Add: axis-type-aware initialization and homing behavior for `axis1=LTS150`, `axis2=UTS100PP`, `axis3=PR50PP` +- Update: existing `NewportESP301` device and related commands or metadata as needed +- Not in scope: unrelated non-ESP301 devices + +#### Required Actions + +- [ ] Confirm axis-specific home and initialization requirements +- [x] Device class updated +- [ ] Initialization path checked +- [ ] Shutdown or cleanup path checked +- [x] Core commands implemented or updated +- [x] Command metadata and params reviewed +- [ ] Example added or updated +- [ ] Example executed successfully +- [ ] Logging behavior checked +- [ ] Recipe or YAML compatibility checked +- [ ] Web app visibility checked +- [ ] Manual control page checked if applicable +- [ ] Execute recipe flow checked if applicable +- [ ] Notes recorded + +#### Validation + +- Example run result: +- Web app result: +- Known issues: actual hardware behavior for `LTS150`, `UTS100PP`, and `PR50PP` still needs confirmation on the controller + +#### Notes + +- Target hardware layout: `axis1=LTS150`, `axis2=UTS100PP`, `axis3=PR50PP` +- `NewportESP301` now supports `axis_configs` so each axis can declare `motion_type`, `units`, `home_mode`, `zero_position`, `default_speed`, and `max_speed` +- Current default policy is `linear -> mm`, `rotary -> deg`, and `home_mode -> OR4` +- Next step is to instantiate the controller with explicit `axis_configs` for axes 1 to 3 and test initialization and homing behavior + +#### Device: `Z812` + +- Status: `in progress` +- Device file: `aamp_app/devices/z812.py` +- Command file: `aamp_app/commands/z812_commands.py` +- Example file: `examples/example_z812.py` +- Similar existing implementation: `MTS50_Z8`, `LinearStage150` +- Hardware or SDK dependency: `Thorlabs KDC101 with Z812 actuator` + +#### Scope + +- Add: dedicated `Z812` device, commands, smoke test, and web app metadata +- Update: documentation and port tracking +- Not in scope: refactoring `MTS50_Z8` into a shared base class + +#### Required Actions + +- [x] Device class implemented +- [x] Initialization path checked +- [x] Shutdown or cleanup path checked +- [x] Core commands implemented +- [x] Command metadata and params reviewed +- [x] Example added or updated +- [x] Example executed successfully +- [ ] Logging behavior checked +- [ ] Recipe or YAML compatibility checked +- [ ] Web app visibility checked +- [ ] Manual control page checked if applicable +- [ ] Execute recipe flow checked if applicable +- [x] Notes recorded + +#### Validation + +- Example run result: smoke test passed locally using `COM12` +- Web app result: +- Known issues: none reported yet + +#### Notes + +- Current local port assignment: `COM12` +- Smoke test sequence used `8 mm` absolute move, `3 mm` relative move, and return to `0 mm` + +## Questions or Blockers + +- None yet diff --git a/recipes/recipe_sample.py b/recipes/recipe_sample.py index 8baba53..cd475eb 100644 --- a/recipes/recipe_sample.py +++ b/recipes/recipe_sample.py @@ -44,12 +44,43 @@ def format_speed(speed): gap = $printing_gap # c: um volume = $precursor_volume # d: ul -# configure devices -polarizer = PolarizerServoMotor('polarizer', 'COM22') -printer = NewportESP301('printer', 'COM6') -arm = KinovaArm('kinova') -heating_stage = HeatingStage('heating_stage', 'COM16',115200) -xi = XimeaCamera('xi') +# configure devices +polarizer = PolarizerServoMotor('polarizer', 'COM22') +printer = NewportESP301( + 'printer', + 'COM6', + axis_list=(1, 2, 3), + default_speed=10.0, + axis_configs={ + 1: { + 'stage_model': 'ILS100CC', + 'motion_type': 'linear', + 'units': 'mm', + 'home_mode': 'OR4', + 'zero_position': 0.0, + 'default_speed': 10.0, + }, + 2: { + 'stage_model': 'UTS100PP', + 'motion_type': 'linear', + 'units': 'mm', + 'home_mode': 'OR4', + 'zero_position': 0.0, + 'default_speed': 10.0, + }, + 3: { + 'stage_model': 'PR50PP', + 'motion_type': 'rotary', + 'units': 'deg', + 'home_mode': 'OR1', + 'zero_position': 0.0, + 'default_speed': 10.0, + }, + }, +) +arm = KinovaArm('kinova') +heating_stage = HeatingStage('heating_stage', 'COM16',115200) +xi = XimeaCamera('xi') pump1 = PSD6SyringePump('pump1', 'COM4') pump2 = PSD6SyringePump('pump2', 'COM5') solution_map = SolutionMap() From 3ee3683627ae44c983afa1b8deae79149d402584 Mon Sep 17 00:00:00 2001 From: Hwang Date: Wed, 25 Mar 2026 15:56:48 -0500 Subject: [PATCH 115/125] Add APIS, P4PP, heating stage, and Ximea integrations --- aamp_app/commands/apis_commands.py | 163 ++++++ aamp_app/commands/heating_stage_commands.py | 32 ++ aamp_app/commands/p4pp_commands.py | 152 ++++++ aamp_app/devices/apis.py | 448 ++++++++++++++++ aamp_app/devices/heating_stage.py | 74 ++- aamp_app/devices/p4pp.py | 541 +++++++++++++++++++ aamp_app/devices/ximea_camera.py | 178 +++++++ aamp_app/util.py | 558 ++++++++++++++++++++ device_ports.md | 8 + examples/.gitignore | 3 + examples/example_apis.py | 174 ++++++ examples/example_heating_stage.py | 50 ++ examples/example_p4pp.py | 70 +++ implementation_plan.md | 105 +++- 14 files changed, 2546 insertions(+), 10 deletions(-) create mode 100644 aamp_app/commands/apis_commands.py create mode 100644 aamp_app/commands/p4pp_commands.py create mode 100644 aamp_app/devices/apis.py create mode 100644 aamp_app/devices/p4pp.py create mode 100644 examples/example_apis.py create mode 100644 examples/example_heating_stage.py create mode 100644 examples/example_p4pp.py diff --git a/aamp_app/commands/apis_commands.py b/aamp_app/commands/apis_commands.py new file mode 100644 index 0000000..f6e79ad --- /dev/null +++ b/aamp_app/commands/apis_commands.py @@ -0,0 +1,163 @@ +from .command import Command, CommandResult +from devices.apis import APIS + + +class APISParentCommand(Command): + """Parent class for all APIS commands.""" + + receiver_cls = APIS + + def __init__(self, receiver: APIS, **kwargs): + super().__init__(receiver, **kwargs) + + +class APISConnect(APISParentCommand): + """Open the serial port and run the APIS READY/RESET handshake.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.connect()) + + +class APISInitialize(APISParentCommand): + """Initialize APIS by resetting it into the ARMED state.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class APISDeinitialize(APISParentCommand): + """Deinitialize APIS by sending ESTOP and closing the serial port.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize()) + + +class APISReset(APISParentCommand): + """Send RESET to arm APIS.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.reset()) + + +class APISEmergencyStop(APISParentCommand): + """Send ESTOP to latch and detach the APIS servos.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.emergency_stop()) + + +class APISHome(APISParentCommand): + """Move both APIS stages to 0 degrees.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.home()) + + +class APISRotatePolarizer(APISParentCommand): + """Rotate the APIS polarizer stage to a target stage angle in degrees.""" + + def __init__(self, receiver: APIS, angle_deg: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params["angle_deg"] = angle_deg + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.rotate_polarizer(self._params["angle_deg"])) + + +class APISRotateSample(APISParentCommand): + """Rotate the APIS sample stage to a target stage angle in degrees.""" + + def __init__(self, receiver: APIS, angle_deg: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params["angle_deg"] = angle_deg + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.rotate_sample(self._params["angle_deg"])) + + +class APISGetState(APISParentCommand): + """Return the cached APIS state.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_state()) + + +class APISCaptureRaw16(APISParentCommand): + """Capture a RAW16 TIFF from the APIS Ximea camera.""" + + def __init__( + self, + receiver: APIS, + filename: str = None, + directory: str = None, + exposure_time: int = None, + gain: float = 0.0, + **kwargs + ): + super().__init__(receiver, **kwargs) + self._params["filename"] = filename + self._params["directory"] = directory + self._params["exposure_time"] = exposure_time + self._params["gain"] = gain + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.capture_raw16( + filename=self._params["filename"], + directory=self._params["directory"], + exposure_time=self._params["exposure_time"], + gain=self._params["gain"], + ) + ) + + +class APISCaptureRgb(APISParentCommand): + """Capture an APIS-style RGB preview TIFF from the APIS Ximea camera.""" + + def __init__( + self, + receiver: APIS, + filename: str = None, + directory: str = None, + exposure_time: int = None, + gain: float = 0.0, + **kwargs + ): + super().__init__(receiver, **kwargs) + self._params["filename"] = filename + self._params["directory"] = directory + self._params["exposure_time"] = exposure_time + self._params["gain"] = gain + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.capture_rgb( + filename=self._params["filename"], + directory=self._params["directory"], + exposure_time=self._params["exposure_time"], + gain=self._params["gain"], + ) + ) + + +class APISConvertRaw16ToRgb(APISParentCommand): + """Convert a saved RAW16 TIFF into an APIS-style RGB preview TIFF.""" + + def __init__( + self, + receiver: APIS, + raw16_path: str, + rgb_path: str = None, + **kwargs + ): + super().__init__(receiver, **kwargs) + self._params["raw16_path"] = raw16_path + self._params["rgb_path"] = rgb_path + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.convert_raw16_to_rgb( + raw16_path=self._params["raw16_path"], + rgb_path=self._params["rgb_path"], + ) + ) diff --git a/aamp_app/commands/heating_stage_commands.py b/aamp_app/commands/heating_stage_commands.py index d0b84cc..d9db496 100644 --- a/aamp_app/commands/heating_stage_commands.py +++ b/aamp_app/commands/heating_stage_commands.py @@ -58,3 +58,35 @@ def __init__(self, receiver: HeatingStage, temperature: float, **kwargs): def execute(self) -> None: self._result = CommandResult(*self._receiver.set_settemp(self._params['temperature'])) + + +class HeatingStageWaitForTemperature(HeatingStageParentCommand): + """Wait until the heating stage reaches the target temperature within a tolerance.""" + + def __init__( + self, + receiver: HeatingStage, + target: float, + tolerance: float = 1.0, + timeout: float = 600.0, + poll_interval: float = 1.0, + hold_duration: float = 30.0, + **kwargs + ): + super().__init__(receiver, **kwargs) + self._params['target'] = target + self._params['tolerance'] = tolerance + self._params['timeout'] = timeout + self._params['poll_interval'] = poll_interval + self._params['hold_duration'] = hold_duration + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.wait_for_temperature( + self._params['target'], + self._params['tolerance'], + self._params['timeout'], + self._params['poll_interval'], + self._params['hold_duration'], + ) + ) diff --git a/aamp_app/commands/p4pp_commands.py b/aamp_app/commands/p4pp_commands.py new file mode 100644 index 0000000..0a15029 --- /dev/null +++ b/aamp_app/commands/p4pp_commands.py @@ -0,0 +1,152 @@ +from .command import Command, CommandResult +from devices.p4pp import P4PP + + +class P4PPParentCommand(Command): + """Parent class for all P4PP commands.""" + + receiver_cls = P4PP + + def __init__(self, receiver: P4PP, **kwargs): + super().__init__(receiver, **kwargs) + + +class P4PPConnect(P4PPParentCommand): + """Open the serial port for the P4PP controller.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.start_serial()) + + +class P4PPInitialize(P4PPParentCommand): + """Initialize the P4PP controller by syncing its current position.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class P4PPDeinitialize(P4PPParentCommand): + """Deinitialize the P4PP controller.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize()) + + +class P4PPRefreshPosition(P4PPParentCommand): + """Refresh the cached linear and rotational positions from firmware.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.refresh_position()) + + +class P4PPHomeLinear(P4PPParentCommand): + """Home the P4PP linear axis.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.home_linear()) + + +class P4PPHomeRotational(P4PPParentCommand): + """Home the P4PP rotational axis.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.home_rotational()) + + +class P4PPHomeAll(P4PPParentCommand): + """Home both the P4PP linear and rotational axes.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.home_all()) + + +class P4PPMoveLinearAbsolute(P4PPParentCommand): + """Move the P4PP linear axis to an absolute position in mm.""" + + def __init__(self, receiver: P4PP, position_mm: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params["position_mm"] = position_mm + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_linear_mm(self._params["position_mm"], relative=False)) + + +class P4PPMoveLinearRelative(P4PPParentCommand): + """Move the P4PP linear axis by a relative distance in mm.""" + + def __init__(self, receiver: P4PP, distance_mm: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params["distance_mm"] = distance_mm + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_linear_mm(self._params["distance_mm"], relative=True)) + + +class P4PPMoveRotationalAbsolute(P4PPParentCommand): + """Move the P4PP rotational axis to an absolute position in degrees.""" + + def __init__(self, receiver: P4PP, position_deg: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params["position_deg"] = position_deg + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_rotational_deg(self._params["position_deg"], relative=False)) + + +class P4PPMoveRotationalRelative(P4PPParentCommand): + """Move the P4PP rotational axis by a relative distance in degrees.""" + + def __init__(self, receiver: P4PP, distance_deg: float, **kwargs): + super().__init__(receiver, **kwargs) + self._params["distance_deg"] = distance_deg + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_rotational_deg(self._params["distance_deg"], relative=True)) + + +class P4PPMeasure(P4PPParentCommand): + """Trigger a P4PP measurement. Multi-cycle mode uses firmware averaging.""" + + def __init__(self, receiver: P4PP, cycles: int = 1, **kwargs): + super().__init__(receiver, **kwargs) + self._params["cycles"] = cycles + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.measure(self._params["cycles"])) + + +class P4PPSetMeasurementResistor(P4PPParentCommand): + """Select the P4PP measurement resistor configuration: 681 or 68.1 ohm.""" + + def __init__(self, receiver: P4PP, resistor_ohms: float = 681.0, **kwargs): + super().__init__(receiver, **kwargs) + self._params["resistor_ohms"] = resistor_ohms + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_measurement_resistor(self._params["resistor_ohms"])) + + +class P4PPSaveMeasurementCsv(P4PPParentCommand): + """Append the latest P4PP measurement result to a CSV file.""" + + def __init__( + self, + receiver: P4PP, + sample_id: str = None, + csv_path: str = None, + notes: str = None, + **kwargs + ): + super().__init__(receiver, **kwargs) + self._params["sample_id"] = sample_id + self._params["csv_path"] = csv_path + self._params["notes"] = notes + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.save_measurement_csv( + sample_id=self._params["sample_id"], + csv_path=self._params["csv_path"], + notes=self._params["notes"], + ) + ) diff --git a/aamp_app/devices/apis.py b/aamp_app/devices/apis.py new file mode 100644 index 0000000..dc5fcc3 --- /dev/null +++ b/aamp_app/devices/apis.py @@ -0,0 +1,448 @@ +import os +import time +from decimal import Decimal, getcontext +from typing import Optional, Tuple + +from .device import SerialDevice, check_initialized, check_serial +from .ximea_camera import XimeaCamera + + +class APIS(SerialDevice): + """ + AAMP wrapper for the APIS Arduino controller. + + Python-side behavior is based on the public code in + https://github.com/changhwang/APIS . + Firmware and hardware details should be referenced from + https://github.com/polyprintillinois/APIS . + """ + + SERIAL_BAUDRATE = 9600 + SERIAL_TIMEOUT_S = 0.5 + CONNECTION_WAIT_S = 2.0 + COMMAND_DELAY_S = 0.05 + SETTLING_TIME_S = 1.5 + MAX_RETRIES = 3 + + SERVO_MIN_ANGLE = 0 + SERVO_MAX_ANGLE = 180 + + POLARIZER_STAGE_TO_SERVO_RATIO = 1.059 + SAMPLE_STAGE_TO_SERVO_RATIO = 1.059 + POLARIZER_STAGE_DIRECTION = 1 + SAMPLE_STAGE_DIRECTION = 1 + POLARIZER_SERVO_ZERO_DEG = 0 + SAMPLE_SERVO_ZERO_DEG = 0 + POLARIZER_STAGE_MIN_ANGLE = 0.0 + SAMPLE_STAGE_MIN_ANGLE = 0.0 + POLARIZER_STAGE_MAX_ANGLE = (SERVO_MAX_ANGLE - POLARIZER_SERVO_ZERO_DEG) / POLARIZER_STAGE_TO_SERVO_RATIO + SAMPLE_STAGE_MAX_ANGLE = (SERVO_MAX_ANGLE - SAMPLE_SERVO_ZERO_DEG) / SAMPLE_STAGE_TO_SERVO_RATIO + + CMD_POLARIZER = 10 + CMD_SAMPLE = 11 + CMD_HOME = 96 + CMD_RESET = 98 + CMD_ESTOP = 99 + + STATE_LATCHED = "LATCHED" + STATE_ARMED = "ARMED" + CAMERA_PREVIEW_WB_GAINS = (1.40, 1.00, 1.20) + CAMERA_PREVIEW_GAMMA = 1.0 / 2.2 + + def __init__( + self, + name: str, + port: str, + baudrate: int = SERIAL_BAUDRATE, + timeout: Optional[float] = SERIAL_TIMEOUT_S, + connection_wait_s: float = CONNECTION_WAIT_S, + settling_time_s: float = SETTLING_TIME_S, + command_delay_s: float = COMMAND_DELAY_S, + max_retries: int = MAX_RETRIES, + polarizer_stage_to_servo_ratio: float = POLARIZER_STAGE_TO_SERVO_RATIO, + sample_stage_to_servo_ratio: float = SAMPLE_STAGE_TO_SERVO_RATIO, + polarizer_stage_direction: int = POLARIZER_STAGE_DIRECTION, + sample_stage_direction: int = SAMPLE_STAGE_DIRECTION, + polarizer_servo_zero_deg: int = POLARIZER_SERVO_ZERO_DEG, + sample_servo_zero_deg: int = SAMPLE_SERVO_ZERO_DEG, + use_camera: bool = True, + camera_save_directory: str = "data/imaging/", + camera_bayer_pattern: str = XimeaCamera.DEFAULT_RAW_BAYER_PATTERN, + camera_raw_max_value: float = XimeaCamera.DEFAULT_RAW_MAX_VALUE, + ): + super().__init__(name, port, baudrate, timeout) + self._connection_wait_s = connection_wait_s + self._settling_time_s = settling_time_s + self._command_delay_s = command_delay_s + self._max_retries = max_retries + self._polarizer_stage_to_servo_ratio = polarizer_stage_to_servo_ratio + self._sample_stage_to_servo_ratio = sample_stage_to_servo_ratio + self._polarizer_stage_direction = polarizer_stage_direction + self._sample_stage_direction = sample_stage_direction + self._polarizer_servo_zero_deg = polarizer_servo_zero_deg + self._sample_servo_zero_deg = sample_servo_zero_deg + self._use_camera = use_camera + self._camera_save_directory = camera_save_directory + self._camera_bayer_pattern = camera_bayer_pattern + self._camera_raw_max_value = camera_raw_max_value + + self._state = self.STATE_LATCHED + self._is_connected = False + self._polarizer_angle = 0.0 + self._sample_angle = 0.0 + self.camera = XimeaCamera(name=f"{name}_camera") if use_camera else None + if self.camera is not None: + self.camera.save_directory = camera_save_directory + + def get_init_args(self) -> dict: + return { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "connection_wait_s": self._connection_wait_s, + "settling_time_s": self._settling_time_s, + "command_delay_s": self._command_delay_s, + "max_retries": self._max_retries, + "polarizer_stage_to_servo_ratio": self._polarizer_stage_to_servo_ratio, + "sample_stage_to_servo_ratio": self._sample_stage_to_servo_ratio, + "polarizer_stage_direction": self._polarizer_stage_direction, + "sample_stage_direction": self._sample_stage_direction, + "polarizer_servo_zero_deg": self._polarizer_servo_zero_deg, + "sample_servo_zero_deg": self._sample_servo_zero_deg, + "use_camera": self._use_camera, + "camera_save_directory": self._camera_save_directory, + "camera_bayer_pattern": self._camera_bayer_pattern, + "camera_raw_max_value": self._camera_raw_max_value, + } + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._connection_wait_s = args_dict["connection_wait_s"] + self._settling_time_s = args_dict["settling_time_s"] + self._command_delay_s = args_dict["command_delay_s"] + self._max_retries = args_dict["max_retries"] + self._polarizer_stage_to_servo_ratio = args_dict["polarizer_stage_to_servo_ratio"] + self._sample_stage_to_servo_ratio = args_dict["sample_stage_to_servo_ratio"] + self._polarizer_stage_direction = args_dict["polarizer_stage_direction"] + self._sample_stage_direction = args_dict["sample_stage_direction"] + self._polarizer_servo_zero_deg = args_dict["polarizer_servo_zero_deg"] + self._sample_servo_zero_deg = args_dict["sample_servo_zero_deg"] + self._use_camera = args_dict["use_camera"] + self._camera_save_directory = args_dict["camera_save_directory"] + self._camera_bayer_pattern = args_dict["camera_bayer_pattern"] + self._camera_raw_max_value = args_dict["camera_raw_max_value"] + self.camera = XimeaCamera(name=f"{self._name}_camera") if self._use_camera else None + if self.camera is not None: + self.camera.save_directory = self._camera_save_directory + + @property + def state(self) -> str: + return self._state + + @property + def polarizer_angle(self) -> float: + return self._polarizer_angle + + @property + def sample_angle(self) -> float: + return self._sample_angle + + def connect(self) -> Tuple[bool, str]: + was_successful, response = self.start_serial(delay=2.0) + if not was_successful: + self._is_connected = False + return (was_successful, response) + + self.ser.reset_input_buffer() + self.ser.reset_output_buffer() + + start_t = time.time() + while (time.time() - start_t) < self._connection_wait_s: + if self.ser.in_waiting: + line = self.ser.readline().decode("utf-8", errors="ignore").strip() + if line == "READY": + self._is_connected = True + self._state = self.STATE_LATCHED + return (True, "APIS connected. Arduino boot message READY received; state is LATCHED.") + time.sleep(0.1) + + was_successful, response = self._send_raw_command(self.CMD_RESET, 0) + if was_successful and "OK RESET" in response: + self._is_connected = True + self._state = self.STATE_ARMED + return (True, "APIS connected without READY handshake; force RESET succeeded and system is ARMED.") + + self._is_connected = False + return (False, "APIS connection failed. No READY received and RESET fallback failed: " + response) + + @check_serial + def initialize(self) -> Tuple[bool, str]: + if not self._is_connected: + return (False, "APIS is not connected. Run connect first.") + was_successful, response = self.reset() + if not was_successful: + self._is_initialized = False + return (was_successful, response) + if self.camera is not None: + was_successful, response = self.camera.initialize(set_defaults=True) + if not was_successful: + self._is_initialized = False + return (was_successful, response) + self.camera.save_directory = self._camera_save_directory + self._is_initialized = True + return ( + True, + "Successfully initialized APIS and armed the controller. " + + "For firmware and hardware details, see https://github.com/polyprintillinois/APIS .", + ) + + def deinitialize(self) -> Tuple[bool, str]: + if self.camera is not None and self.camera.is_initialized: + self.camera.deinitialize(reset_init_flag=True) + if self.ser.is_open: + self.emergency_stop() + self.ser.close() + self._is_initialized = False + self._is_connected = False + self._state = self.STATE_LATCHED + return (True, "Successfully deinitialized APIS, sent ESTOP, and closed the serial port.") + + def _format_command(self, pp: int, aaa: int) -> str: + return f"{int(pp):02d}{int(aaa):03d}" + + def _send_raw_command(self, pp: int, aaa: int) -> Tuple[bool, str]: + if not self.ser or not self.ser.is_open: + return (False, "ERR NO_CONNECTION") + + cmd_str = self._format_command(pp, aaa) + attempt = 0 + while attempt < self._max_retries: + try: + self.ser.reset_input_buffer() + self.ser.write((cmd_str + "\n").encode("utf-8")) + self.ser.flush() + response = self.ser.readline().decode("utf-8", errors="ignore").strip() + if not response: + attempt += 1 + time.sleep(0.2 * (2 ** max(0, attempt - 1))) + continue + time.sleep(self._command_delay_s) + return (True, response) + except Exception as exc: + return (False, "ERR EXCEPTION " + str(exc)) + + return (False, "ERR TIMEOUT") + + def _send_command(self, pp: int, aaa: int) -> Tuple[bool, str]: + was_successful, response = self._send_raw_command(pp, aaa) + if not was_successful: + return (False, response) + if not response.startswith("OK"): + return (False, response) + return (True, response) + + @check_initialized + @check_serial + def emergency_stop(self) -> Tuple[bool, str]: + was_successful, response = self._send_command(self.CMD_ESTOP, 0) + if was_successful: + self._state = self.STATE_LATCHED + return (was_successful, response) + + @check_serial + def reset(self) -> Tuple[bool, str]: + was_successful, response = self._send_command(self.CMD_RESET, 0) + if was_successful: + self._state = self.STATE_ARMED + return (was_successful, response) + + def _stage_to_servo_angle( + self, + stage_angle: float, + ratio: float, + direction: int, + zero_offset: int, + axis_name: str, + ) -> Tuple[bool, int]: + servo_angle = zero_offset + (direction * stage_angle * ratio) + servo_cmd = int(round(servo_angle)) + if servo_cmd < self.SERVO_MIN_ANGLE or servo_cmd > self.SERVO_MAX_ANGLE: + return ( + False, + f"{axis_name} angle {stage_angle:.2f} deg maps to servo command {servo_cmd}, outside {self.SERVO_MIN_ANGLE}-{self.SERVO_MAX_ANGLE}.", + ) + return (True, servo_cmd) + + @check_initialized + @check_serial + def home(self) -> Tuple[bool, str]: + was_successful, response = self.rotate_polarizer(0.0) + if not was_successful: + return (was_successful, response) + return self.rotate_sample(0.0) + + @check_initialized + @check_serial + def rotate_polarizer(self, angle_deg: float) -> Tuple[bool, str]: + if angle_deg < self.POLARIZER_STAGE_MIN_ANGLE or angle_deg > self.POLARIZER_STAGE_MAX_ANGLE: + return ( + False, + f"Polarizer angle {angle_deg:.2f} deg is outside the allowed stage range 0-{self.POLARIZER_STAGE_MAX_ANGLE:.2f} deg.", + ) + was_successful, servo_cmd = self._stage_to_servo_angle( + angle_deg, + self._polarizer_stage_to_servo_ratio, + self._polarizer_stage_direction, + self._polarizer_servo_zero_deg, + "Polarizer", + ) + if not was_successful: + return (False, servo_cmd) + was_successful, response = self._send_command(self.CMD_POLARIZER, servo_cmd) + if was_successful: + self._polarizer_angle = angle_deg + time.sleep(self._settling_time_s) + return (was_successful, response) + + @check_initialized + @check_serial + def rotate_sample(self, angle_deg: float) -> Tuple[bool, str]: + if angle_deg < self.SAMPLE_STAGE_MIN_ANGLE or angle_deg > self.SAMPLE_STAGE_MAX_ANGLE: + return ( + False, + f"Sample angle {angle_deg:.2f} deg is outside the allowed stage range 0-{self.SAMPLE_STAGE_MAX_ANGLE:.2f} deg.", + ) + was_successful, servo_cmd = self._stage_to_servo_angle( + angle_deg, + self._sample_stage_to_servo_ratio, + self._sample_stage_direction, + self._sample_servo_zero_deg, + "Sample", + ) + if not was_successful: + return (False, servo_cmd) + was_successful, response = self._send_command(self.CMD_SAMPLE, servo_cmd) + if was_successful: + self._sample_angle = angle_deg + time.sleep(self._settling_time_s) + return (was_successful, response) + + @check_initialized + @check_serial + def get_state(self) -> Tuple[bool, str]: + return (True, self._state) + + @check_initialized + def get_polarizer_angle(self) -> Tuple[bool, float]: + return (True, self._polarizer_angle) + + @check_initialized + def get_sample_angle(self) -> Tuple[bool, float]: + return (True, self._sample_angle) + + @staticmethod + def format_speed(speed: float) -> str: + getcontext().prec = 50 + speed_dec = Decimal(str(speed)) + if speed_dec == 0: + return "0" + if speed_dec >= 1 and speed_dec == speed_dec.to_integral_value(): + return f"{int(speed_dec)}" + integer_part = "".join(map(str, speed_dec.as_tuple().digits)) + exponent = speed_dec.as_tuple().exponent + return f"{integer_part}E{exponent}" + + @classmethod + def build_sample_basename( + cls, + round_num, + sample_num, + polymer: str, + solvent: str, + concentration: int, + speed: float, + temperature: int, + gap: int, + volume: int, + ) -> str: + speed_str = cls.format_speed(speed) + return ( + f"R{round_num}S{sample_num}_{polymer}_{solvent}_{concentration}mgml_" + f"{speed_str}mms_{temperature}C_{gap}um_{volume}ul" + ) + + @staticmethod + def resolve_mode_directory(root_save_dir: str, polymer: str, mode: str) -> str: + mode_dir = os.path.join(root_save_dir, polymer, mode) + os.makedirs(mode_dir, exist_ok=True) + return mode_dir + + @staticmethod + def build_mode_filename(base_sample_name: str, mode: str, angle_deg: float) -> str: + return f"{base_sample_name}_{mode}_{int(angle_deg)}deg" + + def _resolve_capture_directory(self, directory: Optional[str], mode_subdir: Optional[str] = None) -> str: + base_dir = directory or self._camera_save_directory + if mode_subdir: + base_dir = os.path.join(base_dir, mode_subdir) + os.makedirs(base_dir, exist_ok=True) + return base_dir + + @check_initialized + def capture_raw16( + self, + filename: Optional[str] = None, + directory: Optional[str] = None, + exposure_time: Optional[int] = None, + gain: float = 0.0, + ) -> Tuple[bool, str]: + if self.camera is None: + return (False, "APIS camera support is disabled for this device instance.") + return self.camera.capture_raw16( + save_to_file=True, + filename=filename, + directory=self._resolve_capture_directory(directory, "raw16"), + exposure_time=exposure_time, + gain=gain, + ) + + @check_initialized + def capture_rgb( + self, + filename: Optional[str] = None, + directory: Optional[str] = None, + exposure_time: Optional[int] = None, + gain: float = 0.0, + ) -> Tuple[bool, str]: + if self.camera is None: + return (False, "APIS camera support is disabled for this device instance.") + return self.camera.capture_rgb_no_correction( + save_to_file=True, + filename=filename, + directory=self._resolve_capture_directory(directory, "rgb"), + exposure_time=exposure_time, + gain=gain, + bayer_pattern=self._camera_bayer_pattern, + raw_max_value=self._camera_raw_max_value, + wb_gains=self.CAMERA_PREVIEW_WB_GAINS, + gamma=self.CAMERA_PREVIEW_GAMMA, + ) + + @check_initialized + def convert_raw16_to_rgb( + self, + raw16_path: str, + rgb_path: Optional[str] = None, + ) -> Tuple[bool, str]: + return XimeaCamera.convert_saved_raw16_to_rgb( + raw16_path=raw16_path, + rgb_path=rgb_path, + bayer_pattern=self._camera_bayer_pattern, + raw_max_value=self._camera_raw_max_value, + wb_gains=self.CAMERA_PREVIEW_WB_GAINS, + gamma=self.CAMERA_PREVIEW_GAMMA, + ) diff --git a/aamp_app/devices/heating_stage.py b/aamp_app/devices/heating_stage.py index fc59523..cf6daa7 100644 --- a/aamp_app/devices/heating_stage.py +++ b/aamp_app/devices/heating_stage.py @@ -1,5 +1,7 @@ from typing import Optional, Tuple, Union import serial +import time +import re from .device import ArduinoSerialDevice, check_initialized, check_serial @@ -154,8 +156,76 @@ def temperature(self) -> Tuple[bool, Union[float, str]]: if not has_temperature: return (has_temperature, "Message from device did not contain temperature.") - - return (True, float(temperature_str)) + + match = re.search(r"-?\d+(?:\.\d+)?", temperature_str) + if match is None: + return (False, "Could not parse temperature from message: " + str(temperature_str)) + + return (True, float(match.group(0))) + + @check_serial + @check_initialized + def wait_for_temperature( + self, + target: float, + tolerance: float = 1.0, + timeout: Optional[float] = None, + poll_interval: float = 1.0, + hold_duration: float = 30.0, + ) -> Tuple[bool, str]: + if tolerance < 0: + return (False, "Tolerance must be non-negative.") + if poll_interval <= 0: + return (False, "Poll interval must be greater than zero.") + if hold_duration < 0: + return (False, "Hold duration must be non-negative.") + + if timeout is None: + timeout = self._heating_timeout + + deadline = time.time() + timeout + last_temp = None + stable_since = None + + while time.time() <= deadline: + was_successful, current_temp = self.temperature() + if not was_successful: + return (False, str(current_temp)) + + last_temp = current_temp + if abs(current_temp - target) <= tolerance: + if stable_since is None: + stable_since = time.time() + elif time.time() - stable_since >= hold_duration: + return ( + True, + "Heating stage held target temperature " + + str(target) + + " C within ±" + + str(tolerance) + + " C for " + + str(hold_duration) + + " s. Current temperature: " + + str(current_temp) + + " C.", + ) + else: + stable_since = None + + time.sleep(poll_interval) + + return ( + False, + "Heating stage failed to reach target temperature " + + str(target) + + " C and hold it within ±" + + str(tolerance) + + " C for " + + str(hold_duration) + + " s within timeout. Last measured temperature: " + + str(last_temp) + + " C.", + ) diff --git a/aamp_app/devices/p4pp.py b/aamp_app/devices/p4pp.py new file mode 100644 index 0000000..a1f2c93 --- /dev/null +++ b/aamp_app/devices/p4pp.py @@ -0,0 +1,541 @@ +import re +import csv +import os +import time +from collections import deque +from typing import Optional, Tuple + +from .device import SerialDevice, check_initialized, check_serial + + +class P4PP(SerialDevice): + """ + AAMP wrapper for the P4PP controller. + + Python-side behavior is based on the public driver in + https://github.com/changhwang/P4PP . + Firmware and hardware details should be referenced from + https://github.com/polyprintillinois/P4PP . + """ + + POS_PATTERN = re.compile(r"^POS LIN:\s*(-?\d+)\s+ROT:\s*(-?\d+)$") + RS_PATTERN = re.compile(r"Raw R_sheet:\s*(-?\d+(?:\.\d+)?)") + CYCLE_PATTERN = re.compile(r"^CYCLE:(\d+)\s+Rs:(-?\d+(?:\.\d+)?)$") + AVG_STD_PATTERN = re.compile(r"^AVG:(-?\d+(?:\.\d+)?)\s+STD:(-?\d+(?:\.\d+)?)$") + LIN_TARGET_PATTERN = re.compile(r"^OK LIN_TARGET:\s*(-?\d+)$") + ROT_TARGET_PATTERN = re.compile(r"^OK ROT_TARGET:\s*(-?\d+)$") + + LIN_STEPS_PER_MM = 200.0 + ROT_STEPS_PER_DEG = 4.444444 + LIN_MIN_STEPS = 0 + LIN_MAX_STEPS = 10000 + ROT_MIN_STEPS = 0 + ROT_MAX_STEPS = 1250 + + DEFAULT_STARTUP_DELAY_S = 2.0 + DEFAULT_COMMAND_TIMEOUT_S = 30.0 + DEFAULT_MOTION_TIMEOUT_S = 60.0 + DEFAULT_HOME_TIMEOUT_S = 60.0 + DEFAULT_MEASURE_TIMEOUT_S = 30.0 + DEFAULT_POLL_INTERVAL_S = 0.2 + DEFAULT_ROTATION_SAFETY_LINEAR_MM = 45.0 + DEFAULT_MEASUREMENT_RESISTOR_OHMS = 681.0 + DEFAULT_SAVE_DIRECTORY = "data/resistance/" + + RESPONSE_OK_MEASURE_COMPLETE = "OK MEASURE_COMPLETE" + RESPONSE_OK_HOMING_LIN_COMPLETE = "OK HOMING_LIN_COMPLETE" + RESPONSE_OK_HOMING_ROT_COMPLETE = "OK HOMING_ROT_COMPLETE" + RESPONSE_ERR_PREFIX = "ERR " + RESPONSE_ERROR_PREFIX = "ERROR:" + + COMMAND_MEASURE = "MEASURE" + COMMAND_MEASURE_N = "MEASURE_N" + COMMAND_MOVE_LIN = "MOVE_LIN" + COMMAND_MOVE_ROT = "MOVE_ROT" + COMMAND_HOME_LIN = "HOME_LIN" + COMMAND_HOME_ROT = "HOME_ROT" + COMMAND_GET_POS = "GET_POS" + COMMAND_STATUS = "STATUS" + + def __init__( + self, + name: str, + port: str, + baudrate: int = 115200, + timeout: Optional[float] = 0.2, + startup_delay: float = DEFAULT_STARTUP_DELAY_S, + command_timeout: float = DEFAULT_COMMAND_TIMEOUT_S, + motion_timeout: float = DEFAULT_MOTION_TIMEOUT_S, + home_timeout: float = DEFAULT_HOME_TIMEOUT_S, + measure_timeout: float = DEFAULT_MEASURE_TIMEOUT_S, + poll_interval: float = DEFAULT_POLL_INTERVAL_S, + rotation_safety_linear_mm: float = DEFAULT_ROTATION_SAFETY_LINEAR_MM, + measurement_resistor_ohms: float = DEFAULT_MEASUREMENT_RESISTOR_OHMS, + save_directory: str = DEFAULT_SAVE_DIRECTORY, + ): + super().__init__(name, port, baudrate, timeout) + self._startup_delay = startup_delay + self._command_timeout = command_timeout + self._motion_timeout = motion_timeout + self._home_timeout = home_timeout + self._measure_timeout = measure_timeout + self._poll_interval = poll_interval + self._rotation_safety_linear_mm = rotation_safety_linear_mm + self._measurement_resistor_ohms = measurement_resistor_ohms + self._save_directory = save_directory + + self._has_homed_linear = False + self._has_homed_rotational = False + self._linear_steps = 0 + self._rotational_steps = 0 + self._target_linear_steps = None + self._target_rotational_steps = None + self._latest_result = None + self._latest_std = None + self._latest_raw_result = None + self._cycle_results = [] + self._recent_lines = deque(maxlen=200) + + def get_init_args(self) -> dict: + return { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "startup_delay": self._startup_delay, + "command_timeout": self._command_timeout, + "motion_timeout": self._motion_timeout, + "home_timeout": self._home_timeout, + "measure_timeout": self._measure_timeout, + "poll_interval": self._poll_interval, + "rotation_safety_linear_mm": self._rotation_safety_linear_mm, + "measurement_resistor_ohms": self._measurement_resistor_ohms, + "save_directory": self._save_directory, + } + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._startup_delay = args_dict["startup_delay"] + self._command_timeout = args_dict["command_timeout"] + self._motion_timeout = args_dict["motion_timeout"] + self._home_timeout = args_dict["home_timeout"] + self._measure_timeout = args_dict["measure_timeout"] + self._poll_interval = args_dict["poll_interval"] + self._rotation_safety_linear_mm = args_dict["rotation_safety_linear_mm"] + self._measurement_resistor_ohms = args_dict["measurement_resistor_ohms"] + self._save_directory = args_dict["save_directory"] + + @property + def has_homed_linear(self) -> bool: + return self._has_homed_linear + + @property + def has_homed_rotational(self) -> bool: + return self._has_homed_rotational + + @property + def latest_result(self): + return self._latest_result + + @property + def latest_std(self): + return self._latest_std + + @property + def latest_raw_result(self): + return self._latest_raw_result + + @property + def cycle_results(self): + return list(self._cycle_results) + + @staticmethod + def lin_steps_to_mm(steps: int) -> float: + return steps / P4PP.LIN_STEPS_PER_MM + + @staticmethod + def rot_steps_to_deg(steps: int) -> float: + return steps / P4PP.ROT_STEPS_PER_DEG + + @staticmethod + def mm_to_lin_steps(mm: float) -> int: + return int(round(mm * P4PP.LIN_STEPS_PER_MM)) + + @staticmethod + def deg_to_rot_steps(deg: float) -> int: + return int(round(deg * P4PP.ROT_STEPS_PER_DEG)) + + def start_serial(self, delay: Optional[float] = None) -> Tuple[bool, str]: + if delay is None: + delay = self._startup_delay + return super().start_serial(delay=delay) + + def _send(self, command: str) -> None: + self.ser.write((command.strip() + "\n").encode("utf-8")) + + def _readline(self) -> Tuple[bool, str]: + response = self.ser.readline() + if response == b"": + return (False, "") + return (True, response.decode("utf-8", errors="ignore").strip()) + + def _clear_serial_buffer(self) -> None: + if self.ser.is_open: + self.ser.reset_input_buffer() + + def _handle_line(self, line: str) -> Tuple[bool, Optional[str]]: + if not line: + return (True, None) + + self._recent_lines.append(line) + + if line.startswith(self.RESPONSE_ERR_PREFIX) or line.startswith(self.RESPONSE_ERROR_PREFIX): + return (False, line) + + if line == self.RESPONSE_OK_HOMING_LIN_COMPLETE: + self._has_homed_linear = True + self._linear_steps = 0 + self._target_linear_steps = None + return (True, line) + + if line == self.RESPONSE_OK_HOMING_ROT_COMPLETE: + self._has_homed_rotational = True + self._rotational_steps = 0 + self._target_rotational_steps = None + return (True, line) + + rs_match = self.RS_PATTERN.search(line) + if rs_match: + self._latest_raw_result = float(rs_match.group(1)) + self._latest_result = self._latest_raw_result + return (True, line) + + cycle_match = self.CYCLE_PATTERN.match(line) + if cycle_match: + self._cycle_results.append(float(cycle_match.group(2))) + return (True, line) + + avg_std_match = self.AVG_STD_PATTERN.match(line) + if avg_std_match: + self._latest_raw_result = float(avg_std_match.group(1)) + self._latest_result = self._latest_raw_result + self._latest_std = float(avg_std_match.group(2)) + return (True, line) + + pos_match = self.POS_PATTERN.match(line) + if pos_match: + self._linear_steps = int(pos_match.group(1)) + self._rotational_steps = int(pos_match.group(2)) + return (True, line) + + lin_target_match = self.LIN_TARGET_PATTERN.match(line) + if lin_target_match: + self._target_linear_steps = int(lin_target_match.group(1)) + return (True, line) + + rot_target_match = self.ROT_TARGET_PATTERN.match(line) + if rot_target_match: + self._target_rotational_steps = int(rot_target_match.group(1)) + return (True, line) + + return (True, line) + + def _wait_for_condition( + self, + predicate, + timeout_s: float, + poll_position: bool = False, + ) -> Tuple[bool, str]: + deadline = time.monotonic() + timeout_s + last_poll_at = 0.0 + while time.monotonic() < deadline: + now = time.monotonic() + if poll_position and (now - last_poll_at) >= self._poll_interval: + self._send(self.COMMAND_GET_POS) + last_poll_at = now + + was_successful, line = self._readline() + if not was_successful: + continue + + was_successful, error_message = self._handle_line(line) + if not was_successful: + return (False, error_message) + + if predicate(line): + return (True, line) + + return (False, "Timed out waiting for P4PP response.") + + def _rotation_is_safe(self) -> Tuple[bool, str]: + linear_mm = self.lin_steps_to_mm(self._linear_steps) + if linear_mm >= self._rotation_safety_linear_mm: + return ( + False, + f"P4PP rotation blocked: linear axis is at {linear_mm:.3f} mm, which is at or above the safety limit of {self._rotation_safety_linear_mm:.3f} mm. Retract the probe first.", + ) + return (True, "") + + @staticmethod + def _normalize_measurement_resistor_ohms(resistor_ohms: float) -> Tuple[bool, float]: + if abs(float(resistor_ohms) - 681.0) < 1e-6: + return (True, 681.0) + if abs(float(resistor_ohms) - 68.1) < 1e-6: + return (True, 68.1) + return (False, float(resistor_ohms)) + + def get_measurement_resistor_info(self) -> dict: + if abs(self._measurement_resistor_ohms - 68.1) < 1e-6: + return {"R_set": 68.1, "label": "68.1 ohm", "range": "<= 10 kOhm/sq"} + return {"R_set": 681.0, "label": "681 ohm", "range": "1 kOhm/sq - 100 kOhm/sq"} + + @staticmethod + def _ensure_csv_directory(csv_path: str) -> None: + directory = os.path.dirname(csv_path) + if directory: + os.makedirs(directory, exist_ok=True) + + def build_measurement_csv_path(self, filename: str = "p4pp_measurements.csv", directory: Optional[str] = None) -> str: + if directory is None: + directory = self._save_directory + return os.path.join(directory, filename) + + @check_initialized + @check_serial + def set_measurement_resistor(self, resistor_ohms: float) -> Tuple[bool, str]: + was_successful, normalized = self._normalize_measurement_resistor_ohms(resistor_ohms) + if not was_successful: + return (False, "P4PP measurement resistor must be either 681 or 68.1 ohm.") + self._measurement_resistor_ohms = normalized + info = self.get_measurement_resistor_info() + return (True, f"P4PP measurement resistor set to {info['label']} ({info['range']}).") + + @check_serial + def initialize(self) -> Tuple[bool, str]: + was_successful, response = self.refresh_position() + if not was_successful: + self._is_initialized = False + return (was_successful, response) + self._is_initialized = True + return ( + True, + "Successfully initialized P4PP. " + + "For firmware and hardware details, see https://github.com/polyprintillinois/P4PP .", + ) + + def deinitialize(self) -> Tuple[bool, str]: + self._is_initialized = False + return (True, "Successfully deinitialized P4PP.") + + @check_serial + def refresh_position(self) -> Tuple[bool, str]: + self._clear_serial_buffer() + self._send(self.COMMAND_GET_POS) + was_successful, response = self._wait_for_condition( + lambda line: bool(self.POS_PATTERN.match(line)), + self._command_timeout, + poll_position=False, + ) + if not was_successful: + return (was_successful, response) + return ( + True, + "Linear position: " + + f"{self.lin_steps_to_mm(self._linear_steps):.3f} mm, rotational position: {self.rot_steps_to_deg(self._rotational_steps):.3f} deg.", + ) + + @check_initialized + @check_serial + def home_linear(self) -> Tuple[bool, str]: + self._clear_serial_buffer() + self._send(self.COMMAND_HOME_LIN) + was_successful, response = self._wait_for_condition( + lambda line: line == self.RESPONSE_OK_HOMING_LIN_COMPLETE, + self._home_timeout, + poll_position=True, + ) + if not was_successful: + return (was_successful, response) + return (True, "Successfully homed P4PP linear axis.") + + @check_initialized + @check_serial + def home_rotational(self) -> Tuple[bool, str]: + was_successful, message = self._rotation_is_safe() + if not was_successful: + return (was_successful, message) + self._clear_serial_buffer() + self._send(self.COMMAND_HOME_ROT) + was_successful, response = self._wait_for_condition( + lambda line: line == self.RESPONSE_OK_HOMING_ROT_COMPLETE, + self._home_timeout, + poll_position=True, + ) + if not was_successful: + return (was_successful, response) + return (True, "Successfully homed P4PP rotational axis.") + + @check_initialized + @check_serial + def home_all(self) -> Tuple[bool, str]: + was_successful, response = self.home_linear() + if not was_successful: + return (was_successful, response) + return self.home_rotational() + + @check_initialized + @check_serial + def move_linear_mm(self, position_mm: float, relative: bool = False) -> Tuple[bool, str]: + if not self._has_homed_linear: + return (False, "P4PP linear axis must be homed before moving.") + + target_steps = self.mm_to_lin_steps(position_mm) + final_target = self._linear_steps + target_steps if relative else target_steps + if final_target < self.LIN_MIN_STEPS or final_target > self.LIN_MAX_STEPS: + return ( + False, + f"P4PP linear target {self.lin_steps_to_mm(final_target):.3f} mm is outside the allowed range.", + ) + + self._clear_serial_buffer() + self._target_linear_steps = final_target + self._send(f"{self.COMMAND_MOVE_LIN} {final_target}") + was_successful, response = self._wait_for_condition( + lambda line: self._linear_steps == final_target, + self._motion_timeout, + poll_position=True, + ) + if not was_successful: + return (was_successful, response) + return ( + True, + f"Successfully moved P4PP linear axis to {self.lin_steps_to_mm(final_target):.3f} mm.", + ) + + @check_initialized + @check_serial + def move_rotational_deg(self, position_deg: float, relative: bool = False) -> Tuple[bool, str]: + if not self._has_homed_rotational: + return (False, "P4PP rotational axis must be homed before moving.") + was_successful, message = self._rotation_is_safe() + if not was_successful: + return (was_successful, message) + + target_steps = self.deg_to_rot_steps(position_deg) + final_target = self._rotational_steps + target_steps if relative else target_steps + if final_target < self.ROT_MIN_STEPS or final_target > self.ROT_MAX_STEPS: + return ( + False, + f"P4PP rotational target {self.rot_steps_to_deg(final_target):.3f} deg is outside the allowed range.", + ) + + self._clear_serial_buffer() + self._target_rotational_steps = final_target + self._send(f"{self.COMMAND_MOVE_ROT} {final_target}") + was_successful, response = self._wait_for_condition( + lambda line: self._rotational_steps == final_target, + self._motion_timeout, + poll_position=True, + ) + if not was_successful: + return (was_successful, response) + return ( + True, + f"Successfully moved P4PP rotational axis to {self.rot_steps_to_deg(final_target):.3f} deg.", + ) + + @check_initialized + @check_serial + def measure(self, cycles: int = 1) -> Tuple[bool, str]: + if cycles < 1: + return (False, "Measurement cycles must be at least 1.") + + self._latest_result = None + self._latest_std = None + self._latest_raw_result = None + self._cycle_results = [] + + self._clear_serial_buffer() + if cycles == 1: + self._send(self.COMMAND_MEASURE) + else: + self._send(f"{self.COMMAND_MEASURE_N} {int(cycles)}") + + was_successful, response = self._wait_for_condition( + lambda line: line == self.RESPONSE_OK_MEASURE_COMPLETE, + self._measure_timeout, + poll_position=False, + ) + if not was_successful: + return (was_successful, response) + + if self._latest_result is None: + info = self.get_measurement_resistor_info() + return (True, f"P4PP measurement completed with {info['label']}, but no parsed R_sheet value was received.") + + info = self.get_measurement_resistor_info() + message = f"P4PP measurement complete. R_sheet={self._latest_result}" + if self._latest_std is not None: + message += f", std={self._latest_std}" + if cycles > 1: + message += f", cycles={cycles}" + message += f", R_set={info['label']}" + return (True, message) + + @check_initialized + def save_measurement_csv( + self, + sample_id: Optional[str] = None, + csv_path: Optional[str] = None, + notes: Optional[str] = None, + ) -> Tuple[bool, str]: + if self._latest_result is None: + return (False, "No P4PP measurement result is available to save.") + + if csv_path is None: + csv_path = self.build_measurement_csv_path() + + self._ensure_csv_directory(csv_path) + file_exists = os.path.isfile(csv_path) + info = self.get_measurement_resistor_info() + row = { + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + "sample_id": sample_id or "", + "linear_position_mm": self.lin_steps_to_mm(self._linear_steps), + "rotational_position_deg": self.rot_steps_to_deg(self._rotational_steps), + "cycles": len(self._cycle_results) if self._cycle_results else 1, + "r_set_ohms": info["R_set"], + "r_sheet": self._latest_result, + "r_sheet_std": self._latest_std if self._latest_std is not None else "", + "raw_r_sheet": self._latest_raw_result if self._latest_raw_result is not None else "", + "notes": notes or "", + } + fieldnames = list(row.keys()) + try: + with open(csv_path, "a", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + if not file_exists: + writer.writeheader() + writer.writerow(row) + except Exception as exc: + return (False, "Failed to save P4PP measurement CSV: " + str(exc)) + + return (True, "Successfully saved P4PP measurement to " + csv_path) + + @check_initialized + @check_serial + def get_linear_position_mm(self) -> Tuple[bool, float]: + return (True, self.lin_steps_to_mm(self._linear_steps)) + + @check_initialized + @check_serial + def get_rotational_position_deg(self) -> Tuple[bool, float]: + return (True, self.rot_steps_to_deg(self._rotational_steps)) + + def drain_recent_lines(self): + lines = list(self._recent_lines) + self._recent_lines.clear() + return lines diff --git a/aamp_app/devices/ximea_camera.py b/aamp_app/devices/ximea_camera.py index 9231a67..6c57aab 100644 --- a/aamp_app/devices/ximea_camera.py +++ b/aamp_app/devices/ximea_camera.py @@ -1,4 +1,5 @@ from datetime import datetime +import os from typing import Optional, Tuple, List from PIL import Image @@ -14,6 +15,8 @@ # will need to figure out the method resolution order if using multiple inheritance class XimeaCamera(Device): save_directory = 'data/imaging/' + DEFAULT_RAW_BAYER_PATTERN = "GBRG" + DEFAULT_RAW_MAX_VALUE = 1023.0 def __init__(self, name: str): super().__init__(name) @@ -39,6 +42,14 @@ def __init__(self, name: str): self.boundaryx_1 = 1000 # self.set_default_params() # done in initalize because cam is not yet open here + def get_init_args(self) -> dict: + return { + "name": self._name, + } + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + # no setter for imgdataformat at the moment @property def default_imgdataformat(self) -> str: @@ -122,6 +133,173 @@ def deinitialize(self, reset_init_flag: bool = True) -> Tuple[bool, str]: return (True, "Successfully deinitialized camera, communication closed.") + def _normalize_output_path(self, directory: Optional[str], filename: Optional[str], extension: str) -> str: + if filename is None: + filename = datetime.now().strftime('%Y%m%d_%H%M%S') + if directory is None: + directory = self.save_directory + os.makedirs(directory, exist_ok=True) + if not extension.startswith("."): + extension = "." + extension + return os.path.join(directory, filename + extension) + + def _capture_numpy( + self, + imgdataformat: str, + exposure_time: Optional[int] = None, + gain: Optional[float] = None, + invert_rgb_order: bool = False, + ): + if exposure_time is None: + exposure_time = self._default_exposure_time + if gain is None: + gain = self._default_gain + + try: + self.cam.set_imgdataformat(imgdataformat) + self.cam.set_exposure(exposure_time) + self.cam.set_gain(gain) + img = xiapi.Image() + self.cam.start_acquisition() + self.cam.get_image(img) + self.cam.stop_acquisition() + except xiapi.Xi_error as inst: + raise RuntimeError("Error while getting image: " + str(inst)) from inst + + return img.get_image_data_numpy(invert_rgb_order=invert_rgb_order) + + @staticmethod + def raw16_to_rgb_array( + raw16: np.ndarray, + bayer_pattern: str = DEFAULT_RAW_BAYER_PATTERN, + raw_max_value: float = DEFAULT_RAW_MAX_VALUE, + wb_gains: Optional[Tuple[float, float, float]] = None, + gamma: float = 1.0, + ) -> np.ndarray: + # Match the APIS RAW16 preview conversion pipeline: + # demosaic with OpenCV's BGR code, apply fixed WB gains, then gamma. + pattern_map = { + "GBRG": cv2.COLOR_BAYER_GB2BGR, + "RGGB": cv2.COLOR_BAYER_RG2BGR, + "BGGR": cv2.COLOR_BAYER_BG2BGR, + "GRBG": cv2.COLOR_BAYER_GR2BGR, + } + pattern_key = bayer_pattern.upper() + if pattern_key not in pattern_map: + raise ValueError("Unsupported Bayer pattern: " + str(bayer_pattern)) + if raw16.ndim != 2 or raw16.dtype != np.uint16: + raise ValueError("raw16_to_rgb_array expects a 2D uint16 Bayer image.") + + rgb16 = cv2.cvtColor(raw16, pattern_map[pattern_key]) + scale = float(raw_max_value) if raw_max_value and raw_max_value > 0 else float(np.max(rgb16) or 1.0) + rgb = rgb16.astype(np.float32) / scale + + if wb_gains is not None: + rgb[..., 0] *= wb_gains[0] + rgb[..., 1] *= wb_gains[1] + rgb[..., 2] *= wb_gains[2] + + rgb = np.clip(rgb, 0.0, 1.0) + if gamma and gamma > 0: + rgb = np.power(rgb, gamma) + + rgb8 = np.clip(np.round(rgb * 255.0), 0, 255).astype(np.uint8) + return rgb8 + + @check_initialized + def capture_raw16( + self, + save_to_file: bool = True, + filename: str = None, + directory: str = None, + exposure_time: Optional[int] = None, + gain: float = 0.0, + ) -> Tuple[bool, str]: + try: + raw16 = self._capture_numpy("XI_RAW16", exposure_time=exposure_time, gain=gain, invert_rgb_order=False) + except RuntimeError as inst: + return (False, str(inst)) + + if save_to_file: + fullfilename = self._normalize_output_path(directory, filename, ".tif") + try: + if not cv2.imwrite(fullfilename, raw16): + return (False, "Failed to save RAW16 image to " + fullfilename) + except Exception as inst: + return (False, "Failed to save RAW16 image: " + str(inst)) + return (True, "Successfully saved RAW16 image to " + fullfilename) + + return (True, "Successfully captured RAW16 image without saving.") + + @check_initialized + def capture_rgb_no_correction( + self, + save_to_file: bool = True, + filename: str = None, + directory: str = None, + exposure_time: Optional[int] = None, + gain: float = 0.0, + bayer_pattern: str = DEFAULT_RAW_BAYER_PATTERN, + raw_max_value: float = DEFAULT_RAW_MAX_VALUE, + wb_gains: Optional[Tuple[float, float, float]] = None, + gamma: float = 1.0, + ) -> Tuple[bool, str]: + try: + raw16 = self._capture_numpy("XI_RAW16", exposure_time=exposure_time, gain=gain, invert_rgb_order=False) + rgb8 = self.raw16_to_rgb_array( + raw16, + bayer_pattern=bayer_pattern, + raw_max_value=raw_max_value, + wb_gains=wb_gains, + gamma=gamma, + ) + except Exception as inst: + return (False, str(inst)) + + if save_to_file: + fullfilename = self._normalize_output_path(directory, filename, ".tif") + try: + if not cv2.imwrite(fullfilename, cv2.cvtColor(rgb8, cv2.COLOR_RGB2BGR)): + return (False, "Failed to save RGB image to " + fullfilename) + except Exception as inst: + return (False, "Failed to save RGB image: " + str(inst)) + return (True, "Successfully saved RGB image to " + fullfilename) + + return (True, "Successfully captured RGB image without saving.") + + @staticmethod + def convert_saved_raw16_to_rgb( + raw16_path: str, + rgb_path: Optional[str] = None, + bayer_pattern: str = DEFAULT_RAW_BAYER_PATTERN, + raw_max_value: float = DEFAULT_RAW_MAX_VALUE, + wb_gains: Optional[Tuple[float, float, float]] = None, + gamma: float = 1.0, + ) -> Tuple[bool, str]: + if rgb_path is None: + root, ext = os.path.splitext(raw16_path) + rgb_path = root + "_rgb" + (ext if ext else ".tif") + + raw16 = cv2.imread(raw16_path, cv2.IMREAD_UNCHANGED) + if raw16 is None: + return (False, "Failed to read RAW16 image from " + raw16_path) + + try: + rgb8 = XimeaCamera.raw16_to_rgb_array( + raw16, + bayer_pattern=bayer_pattern, + raw_max_value=raw_max_value, + wb_gains=wb_gains, + gamma=gamma, + ) + os.makedirs(os.path.dirname(rgb_path) or ".", exist_ok=True) + if not cv2.imwrite(rgb_path, cv2.cvtColor(rgb8, cv2.COLOR_RGB2BGR)): + return (False, "Failed to save RGB image to " + rgb_path) + except Exception as inst: + return (False, "Failed to convert RAW16 to RGB: " + str(inst)) + + return (True, "Successfully converted RAW16 image to RGB: " + rgb_path) + @check_initialized def update_background( self, diff --git a/aamp_app/util.py b/aamp_app/util.py index 62c6d14..4099c95 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -1,6 +1,7 @@ from commands.command import Command from commands.utility_commands import LoopStartCommand, LoopEndCommand from devices.heating_stage import HeatingStage +from devices.apis import APIS from devices.multi_stepper import MultiStepper from devices.newport_esp301 import NewportESP301 from devices.newport_94043a_solar_sim import Newport94043ASolarSim @@ -10,6 +11,7 @@ from devices.dummy_motor import DummyMotor from devices.linear_stage_150 import LinearStage150 from devices.mts50_z8 import MTS50_Z8 +from devices.p4pp import P4PP from devices.z812 import Z812 from devices.keithley_2450 import Keithley2450 from devices.mfc import MassFlowController @@ -21,7 +23,9 @@ from devices.ximea_camera import XimeaCamera from commands.linear_stage_150_commands import * +from commands.apis_commands import * from commands.mts50_z8_commands import * +from commands.p4pp_commands import * from commands.z812_commands import * from commands.dummy_heater_commands import * from commands.dummy_motor_commands import * @@ -203,6 +207,42 @@ def default(self, obj): }, "obj": HeatingStageSetSetPoint, }, + "HeatingStageWaitForTemperature": { + "default_code": "HeatingStageWaitForTemperature(receiver= '', target= 25.0, tolerance= 1.0, timeout= 600.0, poll_interval= 1.0, hold_duration= 30.0)", + "args": { + "receiver": { + "default": "Stage", + "type": str, + "notes": "Name of the device", + }, + "target": { + "default": 25.0, + "type": float, + "notes": "Target temperature in C", + }, + "tolerance": { + "default": 1.0, + "type": float, + "notes": "Allowed deviation in C", + }, + "timeout": { + "default": 600.0, + "type": float, + "notes": "Maximum wait time in seconds", + }, + "poll_interval": { + "default": 1.0, + "type": float, + "notes": "Polling interval in seconds", + }, + "hold_duration": { + "default": 30.0, + "type": float, + "notes": "Time in seconds that temperature must stay within tolerance", + }, + }, + "obj": HeatingStageWaitForTemperature, + }, }, } @@ -814,6 +854,524 @@ def default(self, obj): }, }, }, + "APIS": { + "obj": APIS, + "serial": True, + "serial_sequence": ["APISConnect", "APISInitialize"], + "import_device": "from devices.apis import APIS", + "import_commands": "from commands.apis_commands import *", + "telemetry": { + "parameters": { + "polarizer_angle": { + "function_name": "get_polarizer_angle", + "data_type": "float", + "units": "deg", + }, + "sample_angle": { + "function_name": "get_sample_angle", + "data_type": "float", + "units": "deg", + }, + }, + "options": {"custom_init_args": ["port"]}, + }, + "init": { + "default_code": "APIS(name='APIS', port='', baudrate=9600, timeout=0.5, connection_wait_s=2.0, settling_time_s=1.5, command_delay_s=0.05, max_retries=3, polarizer_stage_to_servo_ratio=1.059, sample_stage_to_servo_ratio=1.059, polarizer_stage_direction=1, sample_stage_direction=1, polarizer_servo_zero_deg=0, sample_servo_zero_deg=0, use_camera=True, camera_save_directory='data/imaging/', camera_bayer_pattern='GBRG', camera_raw_max_value=1023.0)", + "obj_name": "APIS", + "args": { + "name": { + "default": "APIS", + "type": str, + "notes": "Name of the device.", + }, + "port": { + "default": "COM", + "type": str, + "notes": "Arduino serial port for APIS.", + }, + "baudrate": { + "default": 9600, + "type": int, + "notes": "APIS serial baudrate.", + }, + "timeout": { + "default": 0.5, + "type": float, + "notes": "Serial readline timeout in seconds.", + }, + "connection_wait_s": { + "default": 2.0, + "type": float, + "notes": "Time to wait for READY during connect.", + }, + "settling_time_s": { + "default": 1.5, + "type": float, + "notes": "Post-move settling time before command completion.", + }, + "command_delay_s": { + "default": 0.05, + "type": float, + "notes": "Delay between serial command transactions.", + }, + "max_retries": { + "default": 3, + "type": int, + "notes": "Retry count for serial timeouts.", + }, + "polarizer_stage_to_servo_ratio": { + "default": 1.059, + "type": float, + "notes": "Polarizer stage-to-servo calibration ratio.", + }, + "sample_stage_to_servo_ratio": { + "default": 1.059, + "type": float, + "notes": "Sample stage-to-servo calibration ratio.", + }, + "polarizer_stage_direction": { + "default": 1, + "type": int, + "notes": "Polarizer rotation sign.", + }, + "sample_stage_direction": { + "default": 1, + "type": int, + "notes": "Sample rotation sign.", + }, + "polarizer_servo_zero_deg": { + "default": 0, + "type": int, + "notes": "Polarizer servo zero offset.", + }, + "sample_servo_zero_deg": { + "default": 0, + "type": int, + "notes": "Sample servo zero offset.", + }, + "use_camera": { + "default": True, + "type": bool, + "notes": "Initialize and use the integrated Ximea camera.", + }, + "camera_save_directory": { + "default": "data/imaging/", + "type": str, + "notes": "Default save directory for APIS image outputs.", + }, + "camera_bayer_pattern": { + "default": "GBRG", + "type": str, + "notes": "Bayer pattern used when converting RAW16 data to RGB.", + }, + "camera_raw_max_value": { + "default": 1023.0, + "type": float, + "notes": "Linear scaling maximum used for RAW16 to RGB conversion.", + }, + }, + }, + "commands": { + "APISConnect": { + "default_code": "APISConnect(receiver= '')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""} + }, + "obj": APISConnect, + }, + "APISInitialize": { + "default_code": "APISInitialize(receiver= '')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""} + }, + "obj": APISInitialize, + }, + "APISDeinitialize": { + "default_code": "APISDeinitialize(receiver= '')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""} + }, + "obj": APISDeinitialize, + }, + "APISReset": { + "default_code": "APISReset(receiver= '')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""} + }, + "obj": APISReset, + }, + "APISEmergencyStop": { + "default_code": "APISEmergencyStop(receiver= '')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""} + }, + "obj": APISEmergencyStop, + }, + "APISHome": { + "default_code": "APISHome(receiver= '')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""} + }, + "obj": APISHome, + }, + "APISRotatePolarizer": { + "default_code": "APISRotatePolarizer(receiver= '', angle_deg= 0.0)", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""}, + "angle_deg": { + "default": 0.0, + "type": float, + "notes": "Target polarizer stage angle in degrees.", + }, + }, + "obj": APISRotatePolarizer, + }, + "APISRotateSample": { + "default_code": "APISRotateSample(receiver= '', angle_deg= 0.0)", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""}, + "angle_deg": { + "default": 0.0, + "type": float, + "notes": "Target sample stage angle in degrees.", + }, + }, + "obj": APISRotateSample, + }, + "APISGetState": { + "default_code": "APISGetState(receiver= '')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""} + }, + "obj": APISGetState, + }, + "APISCaptureRaw16": { + "default_code": "APISCaptureRaw16(receiver= '', filename=None, directory=None, exposure_time=None, gain=0.0)", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""}, + "filename": {"default": None, "type": str, "notes": "Output filename without extension."}, + "directory": {"default": None, "type": str, "notes": "Optional override save directory."}, + "exposure_time": {"default": None, "type": int, "notes": "Optional camera exposure in microseconds."}, + "gain": {"default": 0.0, "type": float, "notes": "Camera gain in dB. Default is 0."}, + }, + "obj": APISCaptureRaw16, + }, + "APISCaptureRgb": { + "default_code": "APISCaptureRgb(receiver= '', filename=None, directory=None, exposure_time=None, gain=0.0)", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""}, + "filename": {"default": None, "type": str, "notes": "Output filename without extension."}, + "directory": {"default": None, "type": str, "notes": "Optional override save directory."}, + "exposure_time": {"default": None, "type": int, "notes": "Optional camera exposure in microseconds."}, + "gain": {"default": 0.0, "type": float, "notes": "Camera gain in dB. Default is 0."}, + }, + "obj": APISCaptureRgb, + }, + "APISConvertRaw16ToRgb": { + "default_code": "APISConvertRaw16ToRgb(receiver= '', raw16_path='', rgb_path=None)", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""}, + "raw16_path": {"default": "", "type": str, "notes": "Path to the saved RAW16 TIFF file."}, + "rgb_path": {"default": None, "type": str, "notes": "Optional output path for the converted RGB TIFF."}, + }, + "obj": APISConvertRaw16ToRgb, + }, + }, + }, + "P4PP": { + "obj": P4PP, + "serial": True, + "serial_sequence": ["P4PPConnect", "P4PPInitialize"], + "import_device": "from devices.p4pp import P4PP", + "import_commands": "from commands.p4pp_commands import *", + "telemetry": { + "parameters": { + "linear_position_mm": { + "function_name": "get_linear_position_mm", + "data_type": "float", + "units": "mm", + }, + "rotational_position_deg": { + "function_name": "get_rotational_position_deg", + "data_type": "float", + "units": "deg", + }, + }, + "options": {"custom_init_args": ["port"]}, + }, + "init": { + "default_code": "P4PP(name='P4PP', port='', baudrate=115200, timeout=0.2, startup_delay=2.0, command_timeout=30.0, motion_timeout=60.0, home_timeout=60.0, measure_timeout=30.0, poll_interval=0.2, rotation_safety_linear_mm=45.0, measurement_resistor_ohms=681.0, save_directory='data/resistance/')", + "obj_name": "P4PP", + "args": { + "name": { + "default": "P4PP", + "type": str, + "notes": "Name of the device.", + }, + "port": { + "default": "COM", + "type": str, + "notes": "Arduino serial port for the P4PP controller.", + }, + "baudrate": { + "default": 115200, + "type": int, + "notes": "P4PP serial baudrate.", + }, + "timeout": { + "default": 0.2, + "type": float, + "notes": "Serial readline timeout in seconds.", + }, + "startup_delay": { + "default": 2.0, + "type": float, + "notes": "Delay after opening serial to allow Arduino reset.", + }, + "command_timeout": { + "default": 30.0, + "type": float, + "notes": "Timeout for position refresh and other short commands.", + }, + "motion_timeout": { + "default": 60.0, + "type": float, + "notes": "Timeout for motion commands.", + }, + "home_timeout": { + "default": 60.0, + "type": float, + "notes": "Timeout for homing commands.", + }, + "measure_timeout": { + "default": 30.0, + "type": float, + "notes": "Timeout for MEASURE or MEASURE_N commands.", + }, + "poll_interval": { + "default": 0.2, + "type": float, + "notes": "Position polling interval while motion or homing is active.", + }, + "rotation_safety_linear_mm": { + "default": 45.0, + "type": float, + "notes": "Block rotational homing and moves when linear position is at or above this value in mm.", + }, + "measurement_resistor_ohms": { + "default": 681.0, + "type": float, + "notes": "Measurement resistor selection. Allowed values are 681 and 68.1 ohm.", + }, + "save_directory": { + "default": "data/resistance/", + "type": str, + "notes": "Default directory for saved resistance CSV files.", + }, + }, + }, + "commands": { + "P4PPConnect": { + "default_code": "P4PPConnect(receiver= '')", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + } + }, + "obj": P4PPConnect, + }, + "P4PPInitialize": { + "default_code": "P4PPInitialize(receiver= '')", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + } + }, + "obj": P4PPInitialize, + }, + "P4PPDeinitialize": { + "default_code": "P4PPDeinitialize(receiver= '')", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + } + }, + "obj": P4PPDeinitialize, + }, + "P4PPRefreshPosition": { + "default_code": "P4PPRefreshPosition(receiver= '')", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + } + }, + "obj": P4PPRefreshPosition, + }, + "P4PPHomeLinear": { + "default_code": "P4PPHomeLinear(receiver= '')", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + } + }, + "obj": P4PPHomeLinear, + }, + "P4PPHomeRotational": { + "default_code": "P4PPHomeRotational(receiver= '')", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + } + }, + "obj": P4PPHomeRotational, + }, + "P4PPHomeAll": { + "default_code": "P4PPHomeAll(receiver= '')", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + } + }, + "obj": P4PPHomeAll, + }, + "P4PPMoveLinearAbsolute": { + "default_code": "P4PPMoveLinearAbsolute(receiver= '', position_mm= 0.0)", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + }, + "position_mm": { + "default": 0.0, + "type": float, + "notes": "Absolute linear position in mm.", + }, + }, + "obj": P4PPMoveLinearAbsolute, + }, + "P4PPMoveLinearRelative": { + "default_code": "P4PPMoveLinearRelative(receiver= '', distance_mm= 0.0)", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + }, + "distance_mm": { + "default": 0.0, + "type": float, + "notes": "Relative linear distance in mm.", + }, + }, + "obj": P4PPMoveLinearRelative, + }, + "P4PPMoveRotationalAbsolute": { + "default_code": "P4PPMoveRotationalAbsolute(receiver= '', position_deg= 0.0)", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + }, + "position_deg": { + "default": 0.0, + "type": float, + "notes": "Absolute rotational position in degrees.", + }, + }, + "obj": P4PPMoveRotationalAbsolute, + }, + "P4PPMoveRotationalRelative": { + "default_code": "P4PPMoveRotationalRelative(receiver= '', distance_deg= 0.0)", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + }, + "distance_deg": { + "default": 0.0, + "type": float, + "notes": "Relative rotational distance in degrees.", + }, + }, + "obj": P4PPMoveRotationalRelative, + }, + "P4PPMeasure": { + "default_code": "P4PPMeasure(receiver= '', cycles= 20)", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + }, + "cycles": { + "default": 20, + "type": int, + "notes": "Number of measurement cycles. Uses firmware averaging when greater than 1.", + }, + }, + "obj": P4PPMeasure, + }, + "P4PPSetMeasurementResistor": { + "default_code": "P4PPSetMeasurementResistor(receiver= '', resistor_ohms= 681.0)", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + }, + "resistor_ohms": { + "default": 681.0, + "type": float, + "notes": "Measurement resistor selection. Use 681 or 68.1 ohm.", + }, + }, + "obj": P4PPSetMeasurementResistor, + }, + "P4PPSaveMeasurementCsv": { + "default_code": "P4PPSaveMeasurementCsv(receiver= '', sample_id=None, csv_path=None, notes=None)", + "args": { + "receiver": { + "default": "P4PP", + "type": str, + "notes": "", + }, + "sample_id": { + "default": None, + "type": str, + "notes": "Optional sample identifier for the CSV row.", + }, + "csv_path": { + "default": None, + "type": str, + "notes": "Optional override path. Defaults to data/resistance/p4pp_measurements.csv.", + }, + "notes": { + "default": None, + "type": str, + "notes": "Optional note stored with the measurement row.", + }, + }, + "obj": P4PPSaveMeasurementCsv, + }, + }, + }, "Keithley2450": { "obj": Keithley2450, "serial": True, diff --git a/device_ports.md b/device_ports.md index f6ec277..c130482 100644 --- a/device_ports.md +++ b/device_ports.md @@ -13,12 +13,20 @@ This file tracks the current local serial port assignments and related connectio | Device Name | Model / Controller | Vendor | Port | Baudrate | Notes | Related Files | | --- | --- | --- | --- | --- | --- | --- | | `esp301_3n` | `ESP301-3N` with `axis1=ILS100CC`, `axis2=UTS100PP`, `axis3=PR50PP` | Newport | `COM6` | `921600` | Axis homing config: `OR4 / OR4 / OR1` | `examples/example_esp301_3n.py`, `recipes/recipe_sample.py`, `aamp_app/util.py` | +| `heater` | `HeatingStage` Arduino controller | Custom | `COM16` | `115200` | Smoke test updated locally; supports non-blocking setpoint command and explicit wait-for-temperature hold check | `examples/example_heating_stage.py`, `aamp_app/util.py`, `aamp_app/devices/heating_stage.py` | | `linear_stage_150` | `LTS150` / `LinearStage150` | Thorlabs | `COM15` | `115200` | Smoke test updated locally to use `COM15`; initial absolute move tested at `100 mm` | `examples/example_linear_stage_150.py`, `aamp_app/util.py` | | `z812` | `Z812` with `KDC101` | Thorlabs | `COM12` | `115200` | Smoke test passed locally with absolute move `8 mm` and relative move `3 mm` | `examples/example_z812.py`, `aamp_app/util.py`, `aamp_app/devices/z812.py` | | `newport_94043a_solar_sim` | `94043A` solar simulator via `69920` power supply | Newport | `COM11` | `9600` | RS-232 via USB adapter; default control path is power mode; initialize applies `400 W` preset and software blocks presets above `450 W`; lamp replacement warning starts at `1000 h` | `examples/example_94043a_solar_sim.py`, `aamp_app/util.py`, `aamp_app/devices/newport_94043a_solar_sim.py` | +| `p4pp` | `P4PP` Arduino controller | PolyPrint Illinois | `COM19` | `115200` | Rotation is blocked when linear position is `>= 45.0 mm`; explicit measurement resistor selection supported with default `681 ohm`; measurement default cycles set to `20` | `examples/example_p4pp.py`, `aamp_app/util.py`, `aamp_app/devices/p4pp.py` | +| `apis` | `APIS` Arduino controller + Ximea camera | PolyPrint Illinois | `COM8` | `9600` | Composite device for stage control and imaging; default image root is `data/imaging/`; current example writes mode-organized outputs under `data/imaging/demo_campaign/` | `examples/example_apis.py`, `aamp_app/util.py`, `aamp_app/devices/apis.py` | ## To Fill Later | Device Name | Model / Controller | Vendor | Port | Baudrate | Notes | Related Files | | --- | --- | --- | --- | --- | --- | --- | | | | | | | | | + +## Storage Notes + +- `APIS` image outputs default to `data/imaging/` +- `P4PP` measurement CSV outputs default to `data/resistance/p4pp_measurements.csv` diff --git a/examples/.gitignore b/examples/.gitignore index e5af984..082f61f 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -9,8 +9,11 @@ !example6.py !example_esp301_3n.py !example_linear_stage_150.py +!example_heating_stage.py !example_z812.py !example_94043a_solar_sim.py +!example_p4pp.py +!example_apis.py !Figure_1.png !Figure_2.png !Figure_3.png diff --git a/examples/example_apis.py b/examples/example_apis.py new file mode 100644 index 0000000..1ea0dde --- /dev/null +++ b/examples/example_apis.py @@ -0,0 +1,174 @@ +# APIS imaging workflow example +# run from root using 'python -m examples.example_apis' + +import os +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_invoker import CommandInvoker +from command_sequence import CommandSequence +from devices.apis import APIS +from commands.apis_commands import * + + +APIS_PORT = "COM8" +ROOT_SAVE_DIR = os.path.join("data", "imaging", "demo_campaign") +XPL_EXPOSURE_US = 50000 +PPL_EXPOSURE_US = 18000 +SAMPLE_ANGLES_DEG = [90, 60, 45, 30, 0] + + +def add_mode_capture_commands(seq, apis, mode_name, polarizer_angle, sample_angles, exposure_time, base_name, save_dir): + seq.add_command(APISRotatePolarizer(apis, angle_deg=polarizer_angle)) + for angle in sample_angles: + seq.add_command(APISRotateSample(apis, angle_deg=angle)) + filename = apis.build_mode_filename(base_name, mode_name, angle) + raw16_path = os.path.join(save_dir, "raw16", filename + ".tif") + rgb_path = os.path.join(save_dir, "rgb", filename + "_rgb.tif") + seq.add_command( + APISCaptureRaw16( + apis, + filename=filename, + directory=save_dir, + exposure_time=exposure_time, + gain=0.0, + ) + ) + seq.add_command( + APISConvertRaw16ToRgb( + apis, + raw16_path=raw16_path, + rgb_path=rgb_path, + ) + ) + + +def main() -> None: + print("=== APIS Imaging Workflow ===") + print("Enter sample parameters directly to build an APIS imaging sequence.") + + round_input = input("Enter round number (for example: 1 or a01): ").strip() + sample_input = input("Enter sample number (for example: 450 or a01): ").strip() + + try: + round_num = int(round_input) + except ValueError: + round_num = round_input + + try: + sample_num = int(sample_input) + except ValueError: + sample_num = sample_input + + polymer = input("Enter polymer name (for example: PProDOT): ").strip() + solvent = input("Enter solvent (for example: CB): ").strip() + concentration = int(input("Enter concentration (mg/ml): ").strip()) + speed = float(input("Enter speed (mm/s): ").strip()) + temperature = int(input("Enter temperature (C): ").strip()) + gap = int(input("Enter gap (um): ").strip()) + volume = int(input("Enter volume (ul): ").strip()) + + params = { + "polymer": polymer, + "round_num": round_num, + "sample_num": sample_num, + "temperature": temperature, + "speed": speed, + "gap": gap, + "solvent": solvent, + "concentration": concentration, + "volume": volume, + } + + print("\n=== Sample Parameters ===") + for key, value in params.items(): + print(f"{key}: {value}") + + base_sample_name = APIS.build_sample_basename( + round_num=params["round_num"], + sample_num=params["sample_num"], + polymer=params["polymer"], + solvent=params["solvent"], + concentration=params["concentration"], + speed=params["speed"], + temperature=params["temperature"], + gap=params["gap"], + volume=params["volume"], + ) + print(f"\nGenerated filename prefix: {base_sample_name}") + print("Mode folders will use `xpl/` and `ppl/`.") + print("Each capture will save RAW16 first, then convert that file to RGB.") + + confirm = input("\nProceed with imaging using these parameters? (y/n): ").strip().lower() + if confirm != "y": + print("Imaging cancelled.") + return + + print("\nInitializing hardware...") + apis = APIS( + name="apis", + port=APIS_PORT, + baudrate=9600, + timeout=0.5, + connection_wait_s=2.0, + settling_time_s=1.5, + command_delay_s=0.05, + max_retries=3, + use_camera=True, + camera_save_directory=ROOT_SAVE_DIR, + camera_bayer_pattern="GBRG", + camera_raw_max_value=1023.0, + ) + xpl_dir = apis.resolve_mode_directory(ROOT_SAVE_DIR, params["polymer"], "xpl") + ppl_dir = apis.resolve_mode_directory(ROOT_SAVE_DIR, params["polymer"], "ppl") + + seq = CommandSequence() + seq.add_device(apis) + + seq.add_command(APISConnect(apis)) + seq.add_command(APISInitialize(apis)) + seq.add_command(APISHome(apis)) + + print(f"\nStarting XPL capture ({len(SAMPLE_ANGLES_DEG)} angles)") + add_mode_capture_commands( + seq=seq, + apis=apis, + mode_name="xpl", + polarizer_angle=90.0, + sample_angles=SAMPLE_ANGLES_DEG, + exposure_time=XPL_EXPOSURE_US, + base_name=base_sample_name, + save_dir=xpl_dir, + ) + + print(f"Starting PPL capture ({len(SAMPLE_ANGLES_DEG)} angles)") + add_mode_capture_commands( + seq=seq, + apis=apis, + mode_name="ppl", + polarizer_angle=0.0, + sample_angles=SAMPLE_ANGLES_DEG, + exposure_time=PPL_EXPOSURE_US, + base_name=base_sample_name, + save_dir=ppl_dir, + ) + + seq.add_command(APISRotatePolarizer(apis, angle_deg=0.0)) + seq.add_command(APISRotateSample(apis, angle_deg=0.0)) + seq.add_command(APISDeinitialize(apis)) + + print("\nRunning imaging sequence...") + invoker = CommandInvoker(seq, False) + result = invoker.invoke_commands() + print(f"\nImaging finished. Result: {result}") + + +if __name__ == "__main__": + main() diff --git a/examples/example_heating_stage.py b/examples/example_heating_stage.py new file mode 100644 index 0000000..ca7e36f --- /dev/null +++ b/examples/example_heating_stage.py @@ -0,0 +1,50 @@ +# HeatingStage smoke test +# run from root using 'python -m examples.example_heating_stage' + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_sequence import CommandSequence +from command_invoker import CommandInvoker +from devices.heating_stage import HeatingStage +from commands.heating_stage_commands import * + + +HEATER_PORT = "COM16" +TARGET_TEMPERATURE_C = 40.0 +ROOM_TEMPERATURE_C = 25.0 + + +def main() -> None: + seq = CommandSequence() + + heater = HeatingStage("heater", HEATER_PORT, 115200, timeout=0.1, heating_timeout=1200.0) + seq.add_device(heater) + + seq.add_command(HeatingStageConnect(heater)) + seq.add_command(HeatingStageInitialize(heater)) + seq.add_command(HeatingStageSetSetPoint(heater, TARGET_TEMPERATURE_C)) + # Wait until the target temperature is held within ±1 C for 30 seconds. + seq.add_command(HeatingStageWaitForTemperature(heater, target=TARGET_TEMPERATURE_C, tolerance=1.0, timeout=1200.0, poll_interval=1.0, hold_duration=30.0)) + seq.add_command(HeatingStageSetSetPoint(heater, ROOM_TEMPERATURE_C)) + seq.add_command(HeatingStageDeinitialize(heater)) + + log_file = "logs/example_heating_stage.log" + invoker = CommandInvoker(seq, log_to_file=True, log_filename=log_file, alert_slack=False) + + seq.print_command_names() + print("\nThis smoke test changes the setpoint to the target temperature, then returns the setpoint to room temperature.") + userinput = input("\ntype 'y' to continue, type anything else to quit: ").strip().lower() + if userinput == "y": + invoker.invoke_commands() + + +if __name__ == "__main__": + main() diff --git a/examples/example_p4pp.py b/examples/example_p4pp.py new file mode 100644 index 0000000..fd7d944 --- /dev/null +++ b/examples/example_p4pp.py @@ -0,0 +1,70 @@ +# P4PP smoke test +# run from root using 'python -m examples.example_p4pp' + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_sequence import CommandSequence +from command_invoker import CommandInvoker +from devices.p4pp import P4PP +from commands.p4pp_commands import * + + +P4PP_PORT = "COM19" + + +def main() -> None: + seq = CommandSequence() + + p4pp = P4PP( + name="p4pp", + port=P4PP_PORT, + baudrate=115200, + timeout=0.2, + startup_delay=2.0, + command_timeout=10.0, + motion_timeout=60.0, + home_timeout=60.0, + measure_timeout=30.0, + poll_interval=0.2, + rotation_safety_linear_mm=45.0, + measurement_resistor_ohms=681.0, + save_directory="data/resistance/", + ) + seq.add_device(p4pp) + + seq.add_command(P4PPConnect(p4pp)) + seq.add_command(P4PPInitialize(p4pp)) + seq.add_command(P4PPSetMeasurementResistor(p4pp, resistor_ohms=681.0)) + seq.add_command(P4PPHomeAll(p4pp)) + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=5.0)) + seq.add_command(P4PPMoveRotationalAbsolute(p4pp, position_deg=30.0)) + seq.add_command(P4PPRefreshPosition(p4pp)) + seq.add_command(P4PPMoveRotationalAbsolute(p4pp, position_deg=0.0)) + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=0.0)) + # Uncomment when the probe/sample path is ready for measurement. + # seq.add_command(P4PPMeasure(p4pp, cycles=20)) + # seq.add_command(P4PPSaveMeasurementCsv(p4pp, sample_id="demo_sample")) + seq.add_command(P4PPDeinitialize(p4pp)) + + log_file = "logs/example_p4pp.log" + invoker = CommandInvoker(seq, log_to_file=True, log_filename=log_file, alert_slack=False) + + print("This smoke test uses the Python-side P4PP integration.") + print("Measurement resistor selection is explicit. Default is 681 ohm.") + print("Firmware and hardware details should be referenced from https://github.com/polyprintillinois/P4PP .") + seq.print_command_names() + userinput = input("\ntype 'y' to continue, type anything else to quit: ").strip().lower() + if userinput == "y": + invoker.invoke_commands() + + +if __name__ == "__main__": + main() diff --git a/implementation_plan.md b/implementation_plan.md index 5cb1ddc..a15e89d 100644 --- a/implementation_plan.md +++ b/implementation_plan.md @@ -77,9 +77,11 @@ Copy this section for each device and fill it in as work starts. ## Active Work Items -- [ ] Device 1: `ESP301-3N` -- [ ] Device 2: `Z812` -- [ ] Device 3: `TBD` +- [x] Device 1: `ESP301-3N` +- [x] Device 2: `Z812` +- [x] Device 3: `HeatingStage` +- [x] Device 4: `P4PP` +- [ ] Device 5: `TBD` #### Device: `ESP301-3N` @@ -92,7 +94,7 @@ Copy this section for each device and fill it in as work starts. #### Scope -- Add: axis-type-aware initialization and homing behavior for `axis1=LTS150`, `axis2=UTS100PP`, `axis3=PR50PP` +- Add: axis-type-aware initialization and homing behavior for `axis1=ILS100CC`, `axis2=UTS100PP`, `axis3=PR50PP` - Update: existing `NewportESP301` device and related commands or metadata as needed - Not in scope: unrelated non-ESP301 devices @@ -117,14 +119,14 @@ Copy this section for each device and fill it in as work starts. - Example run result: - Web app result: -- Known issues: actual hardware behavior for `LTS150`, `UTS100PP`, and `PR50PP` still needs confirmation on the controller +- Known issues: web app visibility still not explicitly checked end-to-end #### Notes -- Target hardware layout: `axis1=LTS150`, `axis2=UTS100PP`, `axis3=PR50PP` +- Target hardware layout: `axis1=ILS100CC`, `axis2=UTS100PP`, `axis3=PR50PP` - `NewportESP301` now supports `axis_configs` so each axis can declare `motion_type`, `units`, `home_mode`, `zero_position`, `default_speed`, and `max_speed` -- Current default policy is `linear -> mm`, `rotary -> deg`, and `home_mode -> OR4` -- Next step is to instantiate the controller with explicit `axis_configs` for axes 1 to 3 and test initialization and homing behavior +- Validated homing policy: `OR4 / OR4 / OR1` +- Smoke test completed locally with axis-specific movement and re-home sequence #### Device: `Z812` @@ -168,6 +170,93 @@ Copy this section for each device and fill it in as work starts. - Current local port assignment: `COM12` - Smoke test sequence used `8 mm` absolute move, `3 mm` relative move, and return to `0 mm` +#### Device: `HeatingStage` + +- Status: `done` +- Device file: `aamp_app/devices/heating_stage.py` +- Command file: `aamp_app/commands/heating_stage_commands.py` +- Example file: `examples/example_heating_stage.py` +- Similar existing implementation: `HeatingStage` +- Hardware or SDK dependency: `Heating stage Arduino controller` + +#### Scope + +- Add: explicit wait-for-temperature command with hold-time criterion +- Update: example and documentation with current port assignment +- Not in scope: heater firmware changes + +#### Required Actions + +- [x] Device class implemented +- [x] Initialization path checked +- [x] Shutdown or cleanup path checked +- [x] Core commands implemented +- [x] Command metadata and params reviewed +- [x] Example added or updated +- [x] Example executed successfully +- [x] Logging behavior checked +- [ ] Recipe or YAML compatibility checked +- [ ] Web app visibility checked +- [ ] Manual control page checked if applicable +- [ ] Execute recipe flow checked if applicable +- [x] Notes recorded + +#### Validation + +- Example run result: smoke test executed locally on `COM16` +- Web app result: +- Known issues: none reported + +#### Notes + +- `HeatingStageSetSetPoint` is non-blocking +- `HeatingStageWaitForTemperature` now requires staying within tolerance for a hold duration before succeeding + +#### Device: `P4PP` + +- Status: `done` +- Device file: `aamp_app/devices/p4pp.py` +- Command file: `aamp_app/commands/p4pp_commands.py` +- Example file: `examples/example_p4pp.py` +- Similar existing implementation: external reference `changhwang/P4PP` +- Hardware or SDK dependency: `P4PP Arduino controller` + +#### Scope + +- Add: P4PP device, commands, smoke test, and web app metadata +- Update: safety and measurement configuration defaults +- Not in scope: firmware integration inside this repo + +#### Required Actions + +- [x] Device class implemented +- [x] Initialization path checked +- [x] Shutdown or cleanup path checked +- [x] Core commands implemented +- [x] Command metadata and params reviewed +- [x] Example added or updated +- [x] Example executed successfully +- [x] Logging behavior checked +- [ ] Recipe or YAML compatibility checked +- [ ] Web app visibility checked +- [ ] Manual control page checked if applicable +- [ ] Execute recipe flow checked if applicable +- [x] Notes recorded + +#### Validation + +- Example run result: smoke test executed locally on `COM19` +- Web app result: +- Known issues: none reported + +#### Notes + +- Firmware and hardware details should reference `https://github.com/polyprintillinois/P4PP` +- Python behavior was aligned to the public driver in `https://github.com/changhwang/P4PP` +- Rotation is blocked when linear position is `>= 45.0 mm` +- Measurement resistor selection is explicit: `681 ohm` default, `68.1 ohm` optional +- Default measurement cycles for command metadata set to `20` + ## Questions or Blockers - None yet From e116caef537c43df8848792439d1139d8cd77616 Mon Sep 17 00:00:00 2001 From: Hwang Date: Mon, 30 Mar 2026 08:25:56 -0500 Subject: [PATCH 116/125] Add APIS MongoDB imaging draft and device examples --- .gitignore | 3 +- .../sciencetech_uhe_nl_solar_sim_commands.py | 104 ++ aamp_app/commands/sonicator_commands.py | 65 ++ .../stellarnet_spectrometer_commands.py | 189 +++- .../commands/substrate_dispenser_commands.py | 63 ++ aamp_app/commands/substrate_hotel_commands.py | 63 ++ aamp_app/devices/device.py | 6 +- .../devices/sciencetech_uhe_nl_solar_sim.py | 398 ++++++++ aamp_app/devices/sonicator.py | 244 +++++ aamp_app/devices/stellarnet_spectrometer.py | 947 +++++++++++++++++- aamp_app/devices/substrate_dispenser.py | 28 + aamp_app/devices/substrate_hotel.py | 28 + .../devices/substrate_linear_stage_base.py | 118 +++ .../drafts/ch_apis_imaging_builder_draft.py | 381 +++++++ aamp_app/util.py | 580 ++++++++++- device_ports.md | 16 +- examples/.gitignore | 7 + examples/example_apis_from_mongodb.py | 349 +++++++ .../example_sciencetech_uhe_nl_solar_sim.py | 84 ++ examples/example_sonicator.py | 71 ++ examples/example_stellarnet_spectrometer.py | 153 +++ examples/example_substrate_dispenser.py | 36 + examples/example_substrate_hotel.py | 41 + ...ample_uvvis_film_absorbance_degradation.py | 260 +++++ implementation_plan.md | 230 ++++- recipes/user_recipes/.gitignore | 3 +- .../ch_apis_imaging_from_mongodb.py | 244 +++++ 27 files changed, 4681 insertions(+), 30 deletions(-) create mode 100644 aamp_app/commands/sciencetech_uhe_nl_solar_sim_commands.py create mode 100644 aamp_app/commands/sonicator_commands.py create mode 100644 aamp_app/commands/substrate_dispenser_commands.py create mode 100644 aamp_app/commands/substrate_hotel_commands.py create mode 100644 aamp_app/devices/sciencetech_uhe_nl_solar_sim.py create mode 100644 aamp_app/devices/sonicator.py create mode 100644 aamp_app/devices/substrate_dispenser.py create mode 100644 aamp_app/devices/substrate_hotel.py create mode 100644 aamp_app/devices/substrate_linear_stage_base.py create mode 100644 aamp_app/drafts/ch_apis_imaging_builder_draft.py create mode 100644 examples/example_apis_from_mongodb.py create mode 100644 examples/example_sciencetech_uhe_nl_solar_sim.py create mode 100644 examples/example_sonicator.py create mode 100644 examples/example_stellarnet_spectrometer.py create mode 100644 examples/example_substrate_dispenser.py create mode 100644 examples/example_substrate_hotel.py create mode 100644 examples/example_uvvis_film_absorbance_degradation.py create mode 100644 recipes/user_recipes/ch_apis_imaging_from_mongodb.py diff --git a/.gitignore b/.gitignore index 45a097b..8797cc8 100644 --- a/.gitignore +++ b/.gitignore @@ -167,4 +167,5 @@ pw.py blank.yaml pw.txt ximea_linux_sp_beta.tgz -package \ No newline at end of file +package +firmware/ diff --git a/aamp_app/commands/sciencetech_uhe_nl_solar_sim_commands.py b/aamp_app/commands/sciencetech_uhe_nl_solar_sim_commands.py new file mode 100644 index 0000000..b20328a --- /dev/null +++ b/aamp_app/commands/sciencetech_uhe_nl_solar_sim_commands.py @@ -0,0 +1,104 @@ +from .command import Command, CommandResult +from devices.sciencetech_uhe_nl_solar_sim import SciencetechUHENLSolarSim + + +class SciencetechUHENLSolarSimParentCommand(Command): + receiver_cls = SciencetechUHENLSolarSim + + def __init__(self, receiver: SciencetechUHENLSolarSim, **kwargs): + super().__init__(receiver, **kwargs) + + +class SciencetechUHENLSolarSimConnect(SciencetechUHENLSolarSimParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.connect()) + + +class SciencetechUHENLSolarSimInitialize(SciencetechUHENLSolarSimParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class SciencetechUHENLSolarSimDeinitialize(SciencetechUHENLSolarSimParentCommand): + def __init__(self, receiver: SciencetechUHENLSolarSim, reset_init_flag: bool = True, close_serial: bool = False, **kwargs): + super().__init__(receiver, **kwargs) + self._params["reset_init_flag"] = reset_init_flag + self._params["close_serial"] = close_serial + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.deinitialize( + reset_init_flag=self._params["reset_init_flag"], + close_serial=self._params["close_serial"], + ) + ) + + +class SciencetechUHENLSolarSimCloseShutter(SciencetechUHENLSolarSimParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.close_shutter()) + + +class SciencetechUHENLSolarSimOpenShutter(SciencetechUHENLSolarSimParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.open_shutter()) + + +class SciencetechUHENLSolarSimEnableCooling(SciencetechUHENLSolarSimParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.enable_cooling()) + + +class SciencetechUHENLSolarSimDisableCooling(SciencetechUHENLSolarSimParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.disable_cooling()) + + +class SciencetechUHENLSolarSimEnableArcLamp(SciencetechUHENLSolarSimParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.enable_arc_lamp()) + + +class SciencetechUHENLSolarSimDisableArcLamp(SciencetechUHENLSolarSimParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.disable_arc_lamp()) + + +class SciencetechUHENLSolarSimOpenAttenuator(SciencetechUHENLSolarSimParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.open_attenuator()) + + +class SciencetechUHENLSolarSimSetAttenuator(SciencetechUHENLSolarSimParentCommand): + def __init__(self, receiver: SciencetechUHENLSolarSim, percent: int = 100, **kwargs): + super().__init__(receiver, **kwargs) + self._params["percent"] = percent + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_attenuator(self._params["percent"])) + + +class SciencetechUHENLSolarSimSetCurrent(SciencetechUHENLSolarSimParentCommand): + def __init__(self, receiver: SciencetechUHENLSolarSim, percent: float = 85.0, **kwargs): + super().__init__(receiver, **kwargs) + self._params["percent"] = percent + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.set_current(self._params["percent"])) + + +class SciencetechUHENLSolarSimGetStatus(SciencetechUHENLSolarSimParentCommand): + def execute(self) -> None: + was_successful, message = self._receiver.get_status() + if was_successful and isinstance(message, list): + message = " | ".join(message) + self._result = CommandResult(was_successful, message) + + +class SciencetechUHENLSolarSimGetFeedback(SciencetechUHENLSolarSimParentCommand): + def __init__(self, receiver: SciencetechUHENLSolarSim, feedback_type: str, **kwargs): + super().__init__(receiver, **kwargs) + self._params["feedback_type"] = feedback_type + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_feedback(self._params["feedback_type"])) diff --git a/aamp_app/commands/sonicator_commands.py b/aamp_app/commands/sonicator_commands.py new file mode 100644 index 0000000..6abe48c --- /dev/null +++ b/aamp_app/commands/sonicator_commands.py @@ -0,0 +1,65 @@ +from .command import Command, CommandResult +from devices.sonicator import Sonicator + + +class SonicatorParentCommand(Command): + receiver_cls = Sonicator + + def __init__(self, receiver: Sonicator, **kwargs): + super().__init__(receiver, **kwargs) + + +class SonicatorConnect(SonicatorParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.connect()) + + +class SonicatorInitialize(SonicatorParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class SonicatorDeinitialize(SonicatorParentCommand): + def __init__( + self, + receiver: Sonicator, + reset_init_flag: bool = True, + close_serial: bool = False, + **kwargs, + ): + super().__init__(receiver, **kwargs) + self._params["reset_init_flag"] = reset_init_flag + self._params["close_serial"] = close_serial + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.deinitialize( + reset_init_flag=self._params["reset_init_flag"], + close_serial=self._params["close_serial"], + ) + ) + + +class SonicatorGetStatus(SonicatorParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_status()) + + +class SonicatorProbePowerConnection(SonicatorParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.probe_power_connection()) + + +class SonicatorStartSonicating(SonicatorParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.start_sonicating()) + + +class SonicatorStopSonicating(SonicatorParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.stop_sonicating()) + + +class SonicatorPressButton(SonicatorParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.press_button()) diff --git a/aamp_app/commands/stellarnet_spectrometer_commands.py b/aamp_app/commands/stellarnet_spectrometer_commands.py index 2355b65..0b29ffe 100644 --- a/aamp_app/commands/stellarnet_spectrometer_commands.py +++ b/aamp_app/commands/stellarnet_spectrometer_commands.py @@ -35,17 +35,23 @@ class SpectrometerUpdateDark(SpectrometerParentCommand): def __init__( self, receiver: StellarNetSpectrometer, - integration_times: Tuple[int, ...] = (100, 100), + integration_times: Optional[Tuple[int, ...]] = None, scans_to_avg: Tuple[int, ...] = (3, 3), smoothings: Tuple[int, ...] = (0, 0), + xtimings: Tuple[int, ...] = (1, 1), **kwargs): super().__init__(receiver, **kwargs) self._params['integration_times'] = integration_times self._params['scans_to_avg'] = scans_to_avg self._params['smoothings'] = smoothings + self._params['xtimings'] = xtimings def execute(self) -> None: - self._result = CommandResult(*self._receiver.update_all_dark_spectra(self._params['integration_times'], self._params['scans_to_avg'], self._params['smoothings'])) + self._result = CommandResult(*self._receiver.update_all_dark_spectra( + self._params['integration_times'], + self._params['scans_to_avg'], + self._params['smoothings'], + self._params['xtimings'])) class SpectrometerUpdateBlank(SpectrometerParentCommand): """Update the stored blank spectra for all spectrometers.""" @@ -53,17 +59,48 @@ class SpectrometerUpdateBlank(SpectrometerParentCommand): def __init__( self, receiver: StellarNetSpectrometer, - integration_times: Tuple[int, ...] = (100, 100), + integration_times: Optional[Tuple[int, ...]] = None, scans_to_avg: Tuple[int, ...] = (3, 3), smoothings: Tuple[int, ...] = (0, 0), + xtimings: Tuple[int, ...] = (1, 1), **kwargs): super().__init__(receiver, **kwargs) self._params['integration_times'] = integration_times self._params['scans_to_avg'] = scans_to_avg self._params['smoothings'] = smoothings + self._params['xtimings'] = xtimings def execute(self) -> None: - self._result = CommandResult(*self._receiver.update_all_blank_spectra(self._params['integration_times'], self._params['scans_to_avg'], self._params['smoothings'])) + self._result = CommandResult(*self._receiver.update_all_blank_spectra( + self._params['integration_times'], + self._params['scans_to_avg'], + self._params['smoothings'], + self._params['xtimings'])) + +class SpectrometerAdjDefIntegrationTime(SpectrometerParentCommand): + def __init__( + self, + receiver: StellarNetSpectrometer, + scans_to_avg: Tuple[int, ...] = (3, 3), + smoothings: Tuple[int, ...] = (0, 0), + xtimings: Tuple[int, ...] = (1, 1), + target_max_count: int = 52000, + tolerance: int = 2000, + **kwargs): + super().__init__(receiver, **kwargs) + self._params['scans_to_avg'] = scans_to_avg + self._params['smoothings'] = smoothings + self._params['xtimings'] = xtimings + self._params['target_max_count'] = target_max_count + self._params['tolerance'] = tolerance + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.adjust_default_integration_time( + self._params['scans_to_avg'], + self._params['smoothings'], + self._params['xtimings'], + self._params['target_max_count'], + self._params['tolerance'])) class SpectrometerGetAbsorbance(SpectrometerParentCommand): """Calculate and update the stored absorbance spectra, merge spectra, and optionally save to file. No filename = timestamped filename.""" @@ -73,9 +110,10 @@ def __init__( receiver: StellarNetSpectrometer, save_to_file: bool = True, filename: Optional[str] = None, - integration_times: Tuple[int, ...] = (100, 100), + integration_times: Optional[Tuple[int, ...]] = None, scans_to_avg: Tuple[int, ...] = (3, 3), smoothings: Tuple[int, ...] = (0, 0), + xtimings: Tuple[int, ...] = (1, 1), **kwargs): super().__init__(receiver, **kwargs) self._params['save_to_file'] = save_to_file @@ -83,9 +121,146 @@ def __init__( self._params['integration_times'] = integration_times self._params['scans_to_avg'] = scans_to_avg self._params['smoothings'] = smoothings + self._params['xtimings'] = xtimings + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_all_absorbance( + self._params['save_to_file'], + self._params['filename'], + self._params['integration_times'], + self._params['scans_to_avg'], + self._params['smoothings'], + self._params['xtimings'])) + +class SpectrometerGetAbsorbancebyname(SpectrometerParentCommand): + def __init__( + self, + receiver: StellarNetSpectrometer, + sample_name: Optional[str] = None, + save_to_file: bool = True, + repeat_measure: bool = False, + integration_times: Optional[Tuple[int, ...]] = None, + scans_to_avg: Tuple[int, ...] = (3, 3), + smoothings: Tuple[int, ...] = (0, 0), + xtimings: Tuple[int, ...] = (1, 1), + absorbance_threshold: float = 0.003, + **kwargs): + super().__init__(receiver, **kwargs) + self._params['sample_name'] = sample_name + self._params['save_to_file'] = save_to_file + self._params['repeat_measure'] = repeat_measure + self._params['integration_times'] = integration_times + self._params['scans_to_avg'] = scans_to_avg + self._params['smoothings'] = smoothings + self._params['xtimings'] = xtimings + self._params['absorbance_threshold'] = absorbance_threshold + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_all_absorbance_byname( + self._params['sample_name'], + self._params['save_to_file'], + self._params['repeat_measure'], + self._params['integration_times'], + self._params['scans_to_avg'], + self._params['smoothings'], + self._params['xtimings'], + self._params['absorbance_threshold'])) + +class SpectrometerGetPhotoncountsbyname(SpectrometerParentCommand): + def __init__( + self, + receiver: StellarNetSpectrometer, + sample_name: Optional[str] = None, + save_to_file: bool = True, + repeat_measure: bool = False, + integration_times: Optional[Tuple[int, ...]] = None, + scans_to_avg: Tuple[int, ...] = (3, 3), + smoothings: Tuple[int, ...] = (0, 0), + xtimings: Tuple[int, ...] = (1, 1), + absorbance_threshold: float = 0.003, + **kwargs): + super().__init__(receiver, **kwargs) + self._params['sample_name'] = sample_name + self._params['save_to_file'] = save_to_file + self._params['repeat_measure'] = repeat_measure + self._params['integration_times'] = integration_times + self._params['scans_to_avg'] = scans_to_avg + self._params['smoothings'] = smoothings + self._params['xtimings'] = xtimings + self._params['absorbance_threshold'] = absorbance_threshold + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_all_counts_byname( + self._params['sample_name'], + self._params['save_to_file'], + self._params['repeat_measure'], + self._params['integration_times'], + self._params['scans_to_avg'], + self._params['smoothings'], + self._params['xtimings'], + self._params['absorbance_threshold'])) + +class SpectrometerGetSpecDecay(SpectrometerParentCommand): + def __init__( + self, + receiver: StellarNetSpectrometer, + sample_name: str, + save_to_file: bool = False, + range_start: float = 290.0, + range_end: float = 800.0, + irradiance_file: str = "reference/am15g_spectrum.csv", + Wvlgth_col_name: str = "wavelength_nm", + Irrad_col_name: str = "irradiance_w_m2_nm", + decay_threshold: float = 0.01, + **kwargs): + super().__init__(receiver, **kwargs) + self._params['sample_name'] = sample_name + self._params['save_to_file'] = save_to_file + self._params['range_start'] = range_start + self._params['range_end'] = range_end + self._params['irradiance_file'] = irradiance_file + self._params['Wvlgth_col_name'] = Wvlgth_col_name + self._params['Irrad_col_name'] = Irrad_col_name + self._params['decay_threshold'] = decay_threshold + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.get_spec_decay( + self._params['sample_name'], + self._params['save_to_file'], + self._params['range_start'], + self._params['range_end'], + self._params['irradiance_file'], + self._params['Wvlgth_col_name'], + self._params['Irrad_col_name'], + self._params['decay_threshold'])) + +class SpectrometerPlotSpecDecaySummary(SpectrometerParentCommand): + def __init__( + self, + receiver: StellarNetSpectrometer, + sample_name: str, + range_start: float = 290.0, + range_end: float = 800.0, + save_to_file: bool = True, + output_filename: Optional[str] = None, + figure_dpi: int = 180, + **kwargs): + super().__init__(receiver, **kwargs) + self._params['sample_name'] = sample_name + self._params['range_start'] = range_start + self._params['range_end'] = range_end + self._params['save_to_file'] = save_to_file + self._params['output_filename'] = output_filename + self._params['figure_dpi'] = figure_dpi def execute(self) -> None: - self._result = CommandResult(*self._receiver.get_all_absorbance(self._params['save_to_file'], self._params['filename'], self._params['integration_times'], self._params['scans_to_avg'], self._params['smoothings'])) + self._result = CommandResult(*self._receiver.plot_spec_decay_summary( + self._params['sample_name'], + self._params['range_start'], + self._params['range_end'], + self._params['save_to_file'], + self._params['output_filename'], + self._params['figure_dpi'])) class SpectrometerShutterIn(SpectrometerParentCommand): pass @@ -97,4 +272,4 @@ class SpectrometerLampOn(SpectrometerParentCommand): pass class SpectrometerLampOff(SpectrometerParentCommand): - pass \ No newline at end of file + pass diff --git a/aamp_app/commands/substrate_dispenser_commands.py b/aamp_app/commands/substrate_dispenser_commands.py new file mode 100644 index 0000000..59bde6b --- /dev/null +++ b/aamp_app/commands/substrate_dispenser_commands.py @@ -0,0 +1,63 @@ +from .command import Command, CommandResult +from devices.substrate_dispenser import SubstrateDispenser + + +class SubstrateDispenserParentCommand(Command): + receiver_cls = SubstrateDispenser + + def __init__(self, receiver: SubstrateDispenser, **kwargs): + super().__init__(receiver, **kwargs) + + +class SubstrateDispenserConnect(SubstrateDispenserParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.connect()) + + +class SubstrateDispenserInitialize(SubstrateDispenserParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class SubstrateDispenserDeinitialize(SubstrateDispenserParentCommand): + def __init__( + self, + receiver: SubstrateDispenser, + reset_init_flag: bool = True, + close_serial: bool = False, + **kwargs): + super().__init__(receiver, **kwargs) + self._params["reset_init_flag"] = reset_init_flag + self._params["close_serial"] = close_serial + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize( + self._params["reset_init_flag"], + self._params["close_serial"], + )) + + +class SubstrateDispenserHome(SubstrateDispenserParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.home()) + + +class SubstrateDispenserMoveToPosition(SubstrateDispenserParentCommand): + def __init__( + self, + receiver: SubstrateDispenser, + position_mm: float, + speed_mm_per_s: float = 20.0, + move_timeout: float = None, + **kwargs): + super().__init__(receiver, **kwargs) + self._params["position_mm"] = position_mm + self._params["speed_mm_per_s"] = speed_mm_per_s + self._params["move_timeout"] = move_timeout + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_to_position( + self._params["position_mm"], + self._params["speed_mm_per_s"], + self._params["move_timeout"], + )) diff --git a/aamp_app/commands/substrate_hotel_commands.py b/aamp_app/commands/substrate_hotel_commands.py new file mode 100644 index 0000000..ee59d96 --- /dev/null +++ b/aamp_app/commands/substrate_hotel_commands.py @@ -0,0 +1,63 @@ +from .command import Command, CommandResult +from devices.substrate_hotel import SubstrateHotel + + +class SubstrateHotelParentCommand(Command): + receiver_cls = SubstrateHotel + + def __init__(self, receiver: SubstrateHotel, **kwargs): + super().__init__(receiver, **kwargs) + + +class SubstrateHotelConnect(SubstrateHotelParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.connect()) + + +class SubstrateHotelInitialize(SubstrateHotelParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.initialize()) + + +class SubstrateHotelDeinitialize(SubstrateHotelParentCommand): + def __init__( + self, + receiver: SubstrateHotel, + reset_init_flag: bool = True, + close_serial: bool = False, + **kwargs): + super().__init__(receiver, **kwargs) + self._params["reset_init_flag"] = reset_init_flag + self._params["close_serial"] = close_serial + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.deinitialize( + self._params["reset_init_flag"], + self._params["close_serial"], + )) + + +class SubstrateHotelHome(SubstrateHotelParentCommand): + def execute(self) -> None: + self._result = CommandResult(*self._receiver.home()) + + +class SubstrateHotelMoveToPosition(SubstrateHotelParentCommand): + def __init__( + self, + receiver: SubstrateHotel, + position_mm: float, + speed_mm_per_s: float = 20.0, + move_timeout: float = None, + **kwargs): + super().__init__(receiver, **kwargs) + self._params["position_mm"] = position_mm + self._params["speed_mm_per_s"] = speed_mm_per_s + self._params["move_timeout"] = move_timeout + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.move_to_position( + self._params["position_mm"], + self._params["speed_mm_per_s"], + self._params["move_timeout"], + )) diff --git a/aamp_app/devices/device.py b/aamp_app/devices/device.py index af6dc94..443b071 100644 --- a/aamp_app/devices/device.py +++ b/aamp_app/devices/device.py @@ -288,12 +288,12 @@ def get_response(self, response_timeout: float = 1.0) -> Tuple[bool, str]: response_result = (response_result.decode('ascii') + temp_result.decode('ascii')).encode('ascii') if retry_count == partial_retries and "\\n" not in str(response_result): - return (False, "Timed out. Partial message received for potential control char or str " + control_char + ".") + return (False, "Timed out. Partial message received.") response_result = response_result.strip().decode('ascii') if response_result == "": - return (False, "Timed out. Did not receive any response for control char or str " + control_char + ".") + return (False, "Timed out. Did not receive any response.") return (True, response_result) @@ -320,4 +320,4 @@ def parse_equal_sign(text: str) -> Tuple[bool, str]: class MiscDeviceClass(): def exists(): - return True \ No newline at end of file + return True diff --git a/aamp_app/devices/sciencetech_uhe_nl_solar_sim.py b/aamp_app/devices/sciencetech_uhe_nl_solar_sim.py new file mode 100644 index 0000000..0de21ad --- /dev/null +++ b/aamp_app/devices/sciencetech_uhe_nl_solar_sim.py @@ -0,0 +1,398 @@ +import re +import time +from typing import Optional, Tuple + +from .device import SerialDevice, check_initialized, check_serial + + +class SciencetechUHENLSolarSim(SerialDevice): + """ + RS-232 wrapper for the Sciencetech UHE-NL solar simulator power control path. + + The UHE-NL operating instructions confirm that RS-232 computer control is available, + while the detailed command strings are inferred from the existing lab draft in + `to_implement/sciencetech_lamp.py`. + """ + + STATUS_LINE_MAP = { + "current": 3, + "voltage": 4, + "power": 5, + "po": 6, + "cool": 7, + "lamp": 8, + "starts": 9, + "runtime": 10, + "output": 11, + "hours": 12, + "lamp minutes": 13, + "shutter": 14, + "attenuator": 15, + "stabilization": 16, + } + STATUS_LABEL_MAP = { + "current": ("CURRENT", "I"), + "voltage": ("VOLTAGE", "V"), + "power": ("POWER", "P"), + "po": ("PO",), + "cool": ("COOL", "COOLING", "FANS"), + "lamp": ("LAMP",), + "starts": ("STARTS",), + "runtime": ("RUNTIME", "RUN TIME", "TIME"), + "output": ("OUTPUT",), + "hours": ("HOURS",), + "lamp minutes": ("MINUTES", "LAMP MINUTES"), + "shutter": ("SHUTTER",), + "attenuator": ("ATTENUATOR", "TRANSMISSION"), + "stabilization": ("STABILIZATION",), + } + + def __init__( + self, + name: str, + port: str, + baudrate: int = 9600, + timeout: Optional[float] = 2.0, + line_terminator: str = "\r", + connect_delay_s: float = 3.0, + command_delay_s: float = 8.0, + status_timeout_s: float = 8.0, + default_current_percent: float = 85.0, + default_attenuator_percent: int = 100, + debug_io: bool = False, + ): + super().__init__(name, port, baudrate, timeout) + self._line_terminator = line_terminator + self._connect_delay_s = connect_delay_s + self._command_delay_s = command_delay_s + self._status_timeout_s = status_timeout_s + self._default_current_percent = default_current_percent + self._default_attenuator_percent = default_attenuator_percent + self._debug_io = debug_io + self._requested_output_percent = default_current_percent + + def get_init_args(self) -> dict: + return { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "line_terminator": self._line_terminator, + "connect_delay_s": self._connect_delay_s, + "command_delay_s": self._command_delay_s, + "status_timeout_s": self._status_timeout_s, + "default_current_percent": self._default_current_percent, + "default_attenuator_percent": self._default_attenuator_percent, + "debug_io": self._debug_io, + } + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._line_terminator = args_dict["line_terminator"] + self._connect_delay_s = args_dict["connect_delay_s"] + self._command_delay_s = args_dict["command_delay_s"] + self._status_timeout_s = args_dict["status_timeout_s"] + self._default_current_percent = args_dict["default_current_percent"] + self._default_attenuator_percent = args_dict["default_attenuator_percent"] + self._debug_io = args_dict["debug_io"] + self._requested_output_percent = self._default_current_percent + + def _debug(self, message: str): + if self._debug_io: + print(f"[{self._name} debug] {message}") + + def connect(self) -> Tuple[bool, str]: + return self.start_serial(delay=self._connect_delay_s) + + @check_serial + def initialize(self) -> Tuple[bool, str]: + was_successful, message = self.get_feedback("lamp") + if not was_successful: + self._is_initialized = False + return (False, message) + lamp_state = self._extract_last_number(message) + if lamp_state is not None and int(lamp_state) == 1: + self._is_initialized = False + return (False, "Lamp is currently on. Turn the lamp off before initialize.") + + was_successful, message = self.enable_cooling() + if not was_successful: + self._is_initialized = False + return (False, message) + + if self._default_attenuator_percent >= 100: + was_successful, message = self.open_attenuator() + else: + was_successful, message = self.set_attenuator(self._default_attenuator_percent) + if not was_successful: + self._is_initialized = False + return (False, message) + + was_successful, message = self.set_current(self._default_current_percent) + if not was_successful: + self._is_initialized = False + return (False, message) + + self._is_initialized = True + return ( + True, + "Successfully initialized the UHE-NL solar simulator control path. " + + f"Cooling on, attenuator set to {self._default_attenuator_percent}%, " + + f"output setpoint set to {self._default_current_percent:.1f}%.", + ) + + def deinitialize(self, reset_init_flag: bool = True, close_serial: bool = False) -> Tuple[bool, str]: + lamp_message = None + if self.ser.is_open: + was_successful, message = self.get_feedback("lamp") + if not was_successful: + return (False, message) + lamp_state = self._extract_last_number(message) + if lamp_state is not None and int(lamp_state) == 1: + was_successful, lamp_message = self.disable_arc_lamp() + if not was_successful: + return (False, lamp_message) + if close_serial and self.ser.is_open: + self.ser.close() + if reset_init_flag: + self._is_initialized = False + message = "Successfully deinitialized the UHE-NL solar simulator interface." + if lamp_message is not None: + message += " Arc lamp was turned off. Cooling was left on for post-shutdown cooldown." + return (True, message) + + @check_serial + def _send_command(self, command: str): + self._debug(f"TX -> {command!r}") + self.ser.reset_input_buffer() + self.ser.reset_output_buffer() + self.ser.write((command + self._line_terminator).encode("ascii")) + self.ser.flush() + time.sleep(self._command_delay_s) + + @staticmethod + def _extract_last_number(text: str) -> Optional[float]: + matches = re.findall(r"-?\d+(?:\.\d+)?", text) + if not matches: + return None + return float(matches[-1]) + + @check_serial + def get_status(self) -> Tuple[bool, list]: + self._debug("TX -> 'FS'") + self.ser.reset_input_buffer() + self.ser.reset_output_buffer() + self.ser.write(("FS" + self._line_terminator).encode("ascii")) + self.ser.flush() + + answer = [] + start_t = time.time() + while (time.time() - start_t) < self._status_timeout_s: + line = self.ser.readline().decode("ascii", errors="ignore").strip() + if not line: + continue + self._debug(f"RX <- {line!r}") + if line == "END": + return (True, answer) + answer.append(line) + + return (False, "Timed out while waiting for full status response.") + + @check_serial + def get_feedback(self, feedback_type: str) -> Tuple[bool, str]: + type_lower = feedback_type.lower() + if type_lower not in self.STATUS_LINE_MAP: + return (False, "Invalid feedback type") + + was_successful, response = self.get_status() + if not was_successful: + return (False, response) + + prefixes = self.STATUS_LABEL_MAP.get(type_lower, ()) + for line in response: + line_upper = line.upper() + for prefix in prefixes: + normalized_prefix = prefix.upper() + if line_upper.startswith(normalized_prefix + "=") or line_upper.startswith(normalized_prefix + ":"): + return (True, line) + + line_index = self.STATUS_LINE_MAP[type_lower] + if len(response) <= line_index: + return (False, "Status response did not contain expected feedback lines.") + + return (True, response[line_index]) + + def _verify_binary_feedback(self, feedback_type: str, expected: int, label: str, state_text: str, action_verb: str): + was_successful, message = self.get_feedback(feedback_type) + if not was_successful: + return (False, message) + + value = self._extract_last_number(message) + if value is None: + return (False, f"Failed to parse {label} feedback: {message}") + if int(value) == expected: + return (True, f"Successfully {action_verb} {label.lower()}.") + return (False, f"{label} did not reach the {state_text} state after command. Feedback: {message}") + + @check_serial + def close_shutter(self) -> Tuple[bool, str]: + was_successful, message = self.get_feedback("shutter") + if not was_successful: + return (False, message) + value = self._extract_last_number(message) + if value is not None and int(value) == 1: + return (True, "Shutter is closed.") + + self._send_command("S1") + return self._verify_binary_feedback("shutter", 1, "Shutter", "closed", "close") + + @check_serial + def open_shutter(self) -> Tuple[bool, str]: + was_successful, message = self.get_feedback("shutter") + if not was_successful: + return (False, message) + value = self._extract_last_number(message) + if value is not None and int(value) == 0: + return (True, "Shutter is open.") + + self._send_command("S0") + return self._verify_binary_feedback("shutter", 0, "Shutter", "open", "open") + + @check_serial + def enable_cooling(self) -> Tuple[bool, str]: + was_successful, message = self.get_feedback("cool") + if not was_successful: + return (False, message) + value = self._extract_last_number(message) + if value is not None and int(value) == 1: + return (True, "Cooling is enabled.") + + self._send_command("C1") + return self._verify_binary_feedback("cool", 1, "Cooling", "enabled", "enable") + + @check_serial + def disable_cooling(self) -> Tuple[bool, str]: + was_successful, message = self.get_feedback("cool") + if not was_successful: + return (False, message) + value = self._extract_last_number(message) + if value is not None and int(value) == 0: + return (True, "Cooling is disabled.") + + self._send_command("C0") + return self._verify_binary_feedback("cool", 0, "Cooling", "disabled", "disable") + + @check_serial + def enable_arc_lamp(self) -> Tuple[bool, str]: + was_successful, message = self.get_feedback("cool") + if not was_successful: + return (False, message) + cool_state = self._extract_last_number(message) + if cool_state is None: + return (False, f"Failed to parse cooling feedback: {message}") + if int(cool_state) != 1: + return (False, "Cooling must be on before enabling the arc lamp.") + + was_successful, message = self.get_feedback("lamp") + if not was_successful: + return (False, message) + value = self._extract_last_number(message) + if value is not None and int(value) == 1: + return (True, "Arc lamp is enabled.") + + self._send_command("L1") + return self._verify_binary_feedback("lamp", 1, "Arc lamp", "enabled", "enable") + + @check_serial + def disable_arc_lamp(self) -> Tuple[bool, str]: + was_successful, message = self.get_feedback("lamp") + if not was_successful: + return (False, message) + value = self._extract_last_number(message) + if value is not None and int(value) == 0: + return (True, "Arc lamp is disabled.") + + self._send_command("L0") + return self._verify_binary_feedback("lamp", 0, "Arc lamp", "disabled", "disable") + + @check_serial + def open_attenuator(self) -> Tuple[bool, str]: + self._send_command("A1xxxx") + was_successful, message = self.get_feedback("attenuator") + if not was_successful: + return (False, message) + + percent_read = self._extract_last_number(message) + if percent_read is None: + return (False, f"Failed to parse attenuator feedback: {message}") + if int(round(percent_read)) == 100: + return (True, "Successfully opened attenuator to 100%.") + return (False, f"Failed to fully open attenuator. Feedback: {message}") + + @check_serial + def set_attenuator(self, percent: int) -> Tuple[bool, str]: + if percent < 0 or percent > 100: + return (False, "Invalid attenuator percentage") + + self._send_command(f"A={int(percent):03d}x") + was_successful, message = self.get_feedback("attenuator") + if not was_successful: + return (False, message) + + percent_read = self._extract_last_number(message) + if percent_read is None: + return (False, f"Failed to parse attenuator feedback: {message}") + if int(round(percent_read)) == int(percent): + return (True, f"Successfully set attenuator transmission to {percent}%.") + return (False, f"Failed to set attenuator transmission to {percent}%. Feedback: {message}") + + @check_serial + def set_current(self, percent: float) -> Tuple[bool, str]: + if percent < 0 or percent > 100: + return (False, "Invalid current percentage") + + scaled = int(round(percent * 10)) + self._requested_output_percent = percent + self._send_command(f"P={scaled:04d}") + + was_successful, lamp_feedback = self.get_feedback("lamp") + if not was_successful: + return (False, lamp_feedback) + lamp_state = self._extract_last_number(lamp_feedback) + + was_successful, message = self.get_feedback("output") + if not was_successful: + return (False, message) + + percent_read = self._extract_last_number(message) + if percent_read is None: + return (False, f"Failed to parse output current feedback: {message}") + percent_read = percent_read / 10.0 if percent_read > 100 else percent_read + + if lamp_state is not None and int(lamp_state) == 0: + if abs(percent_read - percent) <= 0.2: + return (True, f"Successfully set output current setpoint to {percent:.1f}%.") + self._debug( + "Lamp is off; controller is reporting OUTPUT while lamp is off as " + + f"{message!r}. Treating the setpoint command as accepted." + ) + return ( + True, + f"Sent output current setpoint command for {percent:.1f}% while lamp was off. " + + f"Controller feedback remained {message}.", + ) + + if abs(percent_read - percent) <= 0.2: + return (True, f"Successfully set output current setpoint to {percent:.1f}%.") + was_successful, status = self.get_status() + if was_successful: + status_text = " | ".join(status) + return ( + False, + f"Failed to set output current setpoint to {percent:.1f}%. " + + f"Feedback: {message}. Full status: {status_text}", + ) + return (False, f"Failed to set output current setpoint to {percent:.1f}%. Feedback: {message}") diff --git a/aamp_app/devices/sonicator.py b/aamp_app/devices/sonicator.py new file mode 100644 index 0000000..e9e2739 --- /dev/null +++ b/aamp_app/devices/sonicator.py @@ -0,0 +1,244 @@ +import time +from typing import Optional, Tuple + +from .device import ArduinoSerialDevice, check_initialized, check_serial + + +class Sonicator(ArduinoSerialDevice): + """ + Arduino Uno R3 wrapper for a sonicator front-panel button and status LED interface. + + The expected wiring follows the older lab draft: + - Uno `5V` to sonicator interface board `5V` + - Uno `GND` to sonicator interface board `GND` + - Uno `D7` as the sonicator button-drive output + - Uno `D8` as the sonication-status input + + Serial protocol: + - `>status` + - `>button` + - `>power` + - `>turnon` + - `>turnoff` + """ + + RESPONSE_MESSAGES = { + "SIW": "Sonicator is sonicating.", + "SNW": "Sonicator is idle.", + "BIP": "Pressed the sonicator button.", + "PIO": "Sonicator power connection is present.", + "PNO": "Sonicator power connection is not present.", + "SAN": "Sonicator is already sonicating.", + "STN": "Successfully started sonication.", + "SAF": "Sonicator is already idle.", + "STF": "Successfully stopped sonication.", + "INV": "Invalid command.", + "ERR": "Controller reported a state-transition error.", + } + + def __init__( + self, + name: str, + port: str, + baudrate: int = 9600, + timeout: Optional[float] = 0.5, + connect_delay_s: float = 3.0, + response_timeout_s: float = 3.0, + power_probe_timeout_s: float = 5.0, + line_terminator: str = "\n", + command_prefix: str = ">", + debug_io: bool = False, + ): + super().__init__(name, port, baudrate, timeout) + self._connect_delay_s = connect_delay_s + self._response_timeout_s = response_timeout_s + self._power_probe_timeout_s = power_probe_timeout_s + self._line_terminator = line_terminator + self._command_prefix = command_prefix + self._debug_io = debug_io + + def get_init_args(self) -> dict: + return { + "name": self._name, + "port": self._port, + "baudrate": self._baudrate, + "timeout": self._timeout, + "connect_delay_s": self._connect_delay_s, + "response_timeout_s": self._response_timeout_s, + "power_probe_timeout_s": self._power_probe_timeout_s, + "line_terminator": self._line_terminator, + "command_prefix": self._command_prefix, + "debug_io": self._debug_io, + } + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._connect_delay_s = args_dict["connect_delay_s"] + self._response_timeout_s = args_dict["response_timeout_s"] + self._power_probe_timeout_s = args_dict["power_probe_timeout_s"] + self._line_terminator = args_dict["line_terminator"] + self._command_prefix = args_dict["command_prefix"] + self._debug_io = args_dict["debug_io"] + + def _debug(self, message: str): + if self._debug_io: + print(f"[{self._name} debug] {message}") + + def connect(self) -> Tuple[bool, str]: + return self.start_serial(delay=self._connect_delay_s) + + @check_serial + def initialize(self) -> Tuple[bool, str]: + was_successful, status_code = self._request_code("status", self._response_timeout_s) + if not was_successful: + self._is_initialized = False + return (False, status_code) + + init_message = "Successfully initialized sonicator control path. Sonicator is idle." + if status_code == "SIW": + was_successful, stop_message = self._stop_sonicating_internal() + if not was_successful: + self._is_initialized = False + return (False, stop_message) + init_message = ( + "Successfully initialized sonicator control path. " + + "Sonication was stopped during initialize." + ) + elif status_code != "SNW": + self._is_initialized = False + return (False, self._unexpected_code_message("status", status_code)) + + self._is_initialized = True + return ( + True, + init_message + " Power connection was not explicitly probed.", + ) + + def deinitialize(self, reset_init_flag: bool = True, close_serial: bool = False) -> Tuple[bool, str]: + stop_message = None + if self.ser.is_open: + was_successful, status_code = self._request_code("status", self._response_timeout_s) + if not was_successful: + return (False, status_code) + if status_code == "SIW": + was_successful, stop_message = self._stop_sonicating_internal() + if not was_successful: + return (False, stop_message) + elif status_code != "SNW": + return (False, self._unexpected_code_message("status", status_code)) + + if close_serial and self.ser.is_open: + self.ser.close() + if reset_init_flag: + self._is_initialized = False + + message = "Successfully deinitialized the sonicator interface." + if stop_message is not None: + message += " Sonication was stopped during deinitialize." + return (True, message) + + @check_serial + def probe_power_connection(self) -> Tuple[bool, str]: + was_successful, status_code = self._request_code("status", self._response_timeout_s) + if not was_successful: + return (False, status_code) + if status_code == "SIW": + return ( + True, + "Sonicator power connection is present. Status indicates sonication is active, so the intrusive power probe was skipped.", + ) + if status_code != "SNW": + return (False, self._unexpected_code_message("status", status_code)) + + was_successful, response_code = self._request_code("power", self._power_probe_timeout_s) + if not was_successful: + return (False, response_code) + if response_code == "PIO": + return (True, self.RESPONSE_MESSAGES[response_code]) + if response_code == "PNO": + return (False, self.RESPONSE_MESSAGES[response_code]) + return (False, self._unexpected_code_message("power", response_code)) + + @check_serial + def get_status(self) -> Tuple[bool, str]: + was_successful, status_code = self._request_code("status", self._response_timeout_s) + if not was_successful: + return (False, status_code) + if status_code in ("SIW", "SNW"): + return (True, self.RESPONSE_MESSAGES[status_code]) + return (False, self._unexpected_code_message("status", status_code)) + + @check_serial + @check_initialized + def start_sonicating(self) -> Tuple[bool, str]: + was_successful, response_code = self._request_code("turnon", self._response_timeout_s) + if not was_successful: + return (False, response_code) + if response_code in ("SAN", "STN"): + return (True, self.RESPONSE_MESSAGES[response_code]) + return (False, self._unexpected_code_message("turnon", response_code)) + + @check_serial + @check_initialized + def stop_sonicating(self) -> Tuple[bool, str]: + return self._stop_sonicating_internal() + + @check_serial + @check_initialized + def press_button(self) -> Tuple[bool, str]: + was_successful, response_code = self._request_code("button", self._response_timeout_s) + if not was_successful: + return (False, response_code) + if response_code == "BIP": + return (True, self.RESPONSE_MESSAGES[response_code]) + return (False, self._unexpected_code_message("button", response_code)) + + def _stop_sonicating_internal(self) -> Tuple[bool, str]: + was_successful, response_code = self._request_code("turnoff", self._response_timeout_s) + if not was_successful: + return (False, response_code) + if response_code in ("SAF", "STF"): + return (True, self.RESPONSE_MESSAGES[response_code]) + return (False, self._unexpected_code_message("turnoff", response_code)) + + def _unexpected_code_message(self, command_keyword: str, response_code: str) -> str: + if response_code in self.RESPONSE_MESSAGES: + return ( + f"Unexpected response for '{command_keyword}': " + + f"{response_code} ({self.RESPONSE_MESSAGES[response_code]})" + ) + return f"Received unknown response code for '{command_keyword}': {response_code}" + + @check_serial + def _request_code(self, command_keyword: str, response_timeout_s: float) -> Tuple[bool, str]: + command = f"{self._command_prefix}{command_keyword}{self._line_terminator}" + self.ser.reset_input_buffer() + self.ser.reset_output_buffer() + self._debug(f"TX -> {command!r}") + self.ser.write(command.encode("ascii")) + self.ser.flush() + return self._read_response_code(response_timeout_s) + + def _read_response_code(self, response_timeout_s: float) -> Tuple[bool, str]: + deadline = time.time() + response_timeout_s + chunks = [] + + while time.time() < deadline: + piece = self.ser.readline() + if not piece: + continue + chunks.append(piece) + if b"\n" in piece or b"\r" in piece: + break + + if not chunks: + return (False, "Timed out waiting for sonicator response.") + + response = b"".join(chunks).decode("ascii", errors="ignore").strip() + self._debug(f"RX <- {response!r}") + if response == "": + return (False, "Received an empty sonicator response.") + return (True, response) diff --git a/aamp_app/devices/stellarnet_spectrometer.py b/aamp_app/devices/stellarnet_spectrometer.py index 3b8e3b0..231cf70 100644 --- a/aamp_app/devices/stellarnet_spectrometer.py +++ b/aamp_app/devices/stellarnet_spectrometer.py @@ -1,11 +1,19 @@ from typing import Union, Tuple, Dict, List, Optional from datetime import datetime +import os +import re +import time import numpy as np import pandas as pd from scipy.interpolate import interp1d from scipy.optimize import minimize -import stellarnet_driver3 as sn +try: + import stellarnet_driver3 as sn + _driver_import_error = None +except Exception as exc: + sn = None + _driver_import_error = str(exc) from .device import Device, check_initialized @@ -19,19 +27,175 @@ class StellarNetSpectrometer(Device): save_directory = 'data/spectroscopy/' - def __init__(self, name: str, spec_keys: List[str] = ['UV-Vis', 'NIR']): + def __init__( + self, + name: str, + spec_keys: List[str] = ['UV-Vis', 'NIR'], + save_directory: str = save_directory, + default_integration_time: Optional[Tuple[int, ...]] = None): super().__init__(name) self.spectrometer_dict = {} self.wavelength_dict = {} self.dark_spectra_dict = {} self.blank_spectra_dict = {} self.absorbance_dict = {} + self.photoncounts_dict = {} self.merged_absorbance = None self.num_spectrometers = 0 self.spec_keys = spec_keys + self.save_directory = save_directory + if default_integration_time is None: + self.default_integration_time = tuple(100 for _ in self.spec_keys) + else: + self.default_integration_time = tuple(default_integration_time) + + def get_init_args(self) -> dict: + return { + "name": self._name, + "spec_keys": self.spec_keys, + "save_directory": self.save_directory, + "default_integration_time": self.default_integration_time, + } + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self.spec_keys = args_dict["spec_keys"] + self.save_directory = args_dict["save_directory"] + self.default_integration_time = tuple(args_dict.get("default_integration_time", tuple(100 for _ in self.spec_keys))) + + def find_file(self, dir_path: str, substr: str, substr2: str) -> Optional[str]: + if not os.path.isdir(dir_path): + return None + + for _root, _dirs, files in os.walk(dir_path): + for fname in files: + if substr in fname and substr2 in fname: + return fname + return None + + def _measurement_qc_log_path(self, sample_name: str) -> str: + return os.path.join(self.save_directory, sample_name + "_measurement_qc_log.csv") + + def _append_measurement_qc_log(self, sample_name: str, qc_rows: List[dict]) -> None: + if not qc_rows: + return + os.makedirs(self.save_directory, exist_ok=True) + fullpath = self._measurement_qc_log_path(sample_name) + df = pd.DataFrame(qc_rows) + write_header = not os.path.exists(fullpath) + df.to_csv(fullpath, mode='a', index=False, header=write_header) + + @staticmethod + def _linear_interpolate( + x_source: np.ndarray, + y_source: np.ndarray, + x_new: np.ndarray, + ) -> np.ndarray: + if x_source.size < 2: + return np.full_like(x_new, np.nan, dtype=float) + source_order = np.argsort(x_source) + xs = x_source[source_order] + ys = y_source[source_order] + result = np.interp(x_new, xs, ys, left=np.nan, right=np.nan) + return result + + @staticmethod + def _trapz(x_values: np.ndarray, y_values: np.ndarray) -> float: + if x_values.size < 2 or y_values.size < 2: + return 0.0 + return float(np.trapezoid(y_values, x_values)) + + @staticmethod + def _parse_elapsed_seconds_from_columns(columns: List[str]) -> np.ndarray: + elapsed_seconds = [] + for column_name in columns: + match = re.search(r'(-?\d+(?:\.\d+)?)', str(column_name)) + if match is None: + elapsed_seconds.append(float('nan')) + else: + elapsed_seconds.append(float(match.group(1))) + return np.asarray(elapsed_seconds, dtype=float) + + @staticmethod + def _interpolate_crossing_time( + times: np.ndarray, + values: np.ndarray, + target: float, + ) -> Optional[float]: + valid = np.isfinite(times) & np.isfinite(values) + if np.count_nonzero(valid) < 2: + return None + times_valid = times[valid] + values_valid = values[valid] + order = np.argsort(times_valid) + times_valid = times_valid[order] + values_valid = values_valid[order] + + for idx in range(len(times_valid) - 1): + t0, t1 = times_valid[idx], times_valid[idx + 1] + v0, v1 = values_valid[idx], values_valid[idx + 1] + if v0 == target: + return float(t0) + if v1 == target: + return float(t1) + if (v0 - target) * (v1 - target) > 0: + continue + if v1 == v0: + return float(t0) + ratio = (target - v0) / (v1 - v0) + return float(t0 + ratio * (t1 - t0)) + return None + + def _load_irradiance_reference( + self, + irradiance_file: str, + wavelength_col_name: str, + irradiance_col_name: str, + ) -> Tuple[bool, Union[Tuple[np.ndarray, np.ndarray], str]]: + irradiance_path = os.path.join(self.save_directory, irradiance_file) + if not os.path.exists(irradiance_path): + return (False, "Irradiance table is missing: " + irradiance_path) + + last_error = None + for read_kwargs in ({}, {"sep": "\t"}): + try: + df_irrad = pd.read_csv(irradiance_path, **read_kwargs) + except Exception as exc: + last_error = str(exc) + continue + + wavelength_candidates = [wavelength_col_name, "wavelength_nm", "Wvlgth nm"] + irradiance_candidates = [ + irradiance_col_name, + "irradiance_w_m2_nm", + "irradiance(trapezoid : W/m2)", + ] + + wavelength_column = next((col for col in wavelength_candidates if col in df_irrad.columns), None) + irradiance_column = next((col for col in irradiance_candidates if col in df_irrad.columns), None) + if wavelength_column is None or irradiance_column is None: + continue + + wavelength = pd.to_numeric(df_irrad[wavelength_column], errors="coerce").to_numpy(dtype=float) + irradiance = pd.to_numeric(df_irrad[irradiance_column], errors="coerce").to_numpy(dtype=float) + valid = np.isfinite(wavelength) & np.isfinite(irradiance) + wavelength = wavelength[valid] + irradiance = irradiance[valid] + if wavelength.size < 2: + continue + + order = np.argsort(wavelength) + wavelength = wavelength[order] + irradiance = np.maximum(irradiance[order], 0.0) + return (True, (wavelength, irradiance)) + + error_message = last_error if last_error is not None else "Unsupported irradiance table format." + return (False, error_message) @staticmethod def num_specs_connected() -> int: + if sn is None: + return 0 num_connected = 0 start_wav = [-1.] while True: @@ -50,6 +214,14 @@ def initialize(self) -> Tuple[bool, str]: #lamp on #shutter in/out #any other Arduino initialization + if sn is None: + self._is_initialized = False + return ( + False, + "stellarnet_driver3 is unavailable. " + + ("Driver import error: " + _driver_import_error + " " if _driver_import_error else "") + + "Add the vendor driver file and Python USB dependency referenced in README before using StellarNetSpectrometer.", + ) self.num_spectrometers = self.num_specs_connected() if self.num_spectrometers == 0: @@ -84,6 +256,130 @@ def deinitialize(self, reset_init_flag: bool = True) -> Tuple[bool, str]: return (True, "Pass, nothing to deinitialize for now.") + @check_initialized + def check_max_count( + self, + spec_key: str, + integration_time: int = 100, + scans_to_avg: int = 3, + smoothing: int = 0, + xtiming: int = 1) -> Tuple[bool, Union[float, str]]: + + if spec_key not in self.spectrometer_dict.keys(): + return (False, spec_key + " spectrometer is not found") + + self.spectrometer_dict[spec_key]['device'].set_config( + int_time=integration_time, + scans_to_avg=scans_to_avg, + x_smooth=smoothing, + x_timing=xtiming) + spectrum_array = sn.array_spectrum(self.spectrometer_dict[spec_key], self.wavelength_dict[spec_key]) + max_count = float(np.amax(spectrum_array[:, 1], axis=0)) + return (True, max_count) + + @check_initialized + def adjust_default_integration_time( + self, + scans_to_avg: Tuple[int, ...] = (3, 3), + smoothings: Tuple[int, ...] = (0, 0), + xtimings: Tuple[int, ...] = (1, 1), + target_max_count: int = 52000, + tolerance: int = 2000, + max_iterations: int = 8) -> Tuple[bool, str]: + + if self.num_spectrometers != len(self.spec_keys): + return (False, "Spectrometers are not all connected") + + integration_time_testing = list(self.default_integration_time) + for ndx, spec_key in enumerate(self.spec_keys): + for _ in range(max_iterations): + result, max_count = self.check_max_count( + spec_key, + integration_time=integration_time_testing[ndx], + scans_to_avg=scans_to_avg[ndx], + smoothing=smoothings[ndx], + xtiming=xtimings[ndx], + ) + if not result: + return (False, max_count) + + if max_count <= 0: + break + if abs(max_count - target_max_count) <= tolerance: + break + + next_integration_time = int(np.ceil(integration_time_testing[ndx] * target_max_count / max_count)) + next_integration_time = max(1, next_integration_time) + if next_integration_time == integration_time_testing[ndx]: + break + + integration_time_testing[ndx] = next_integration_time + time.sleep(0.5) + + self.default_integration_time = tuple(integration_time_testing) + return (True, "Successfully adjusted default integration time to " + str(self.default_integration_time)) + + @staticmethod + def _compute_absorbance(dark_spec, blank_spec, sam_spec): + sam_dark_diff = sam_spec - dark_spec + blank_dark_diff = blank_spec - dark_spec + absorbance = np.zeros_like(sam_dark_diff, dtype=float) + absorbance[(blank_dark_diff > 0) & (sam_dark_diff <= 0)] = 5.0 + valid_index = (blank_dark_diff > 0) & (sam_dark_diff > 0) + absorbance[valid_index] = -np.log10(sam_dark_diff[valid_index] / blank_dark_diff[valid_index]) + absorbance = np.nan_to_num(absorbance, nan=0.0, posinf=5.0, neginf=0.0) + absorbance[absorbance < 0] = 0 + absorbance[absorbance > 5] = 5 + return absorbance + + def _merge_absorbance_byname_onedetector( + self, + save_to_file: bool, + filename: str, + sample_name: str, + comment_list: List[str]) -> Tuple[bool, str]: + + uv_array = self.absorbance_dict[self.spec_keys[0]].copy() + uv_array = self.truncate_ends_by_wavelength(uv_array, 210.0, 1700.0) + merged_array = uv_array[uv_array[:, 0].argsort()] + self.merged_absorbance = merged_array + + if save_to_file: + os.makedirs(self.save_directory, exist_ok=True) + fname = self.find_file(self.save_directory, sample_name, 'merged.csv') + if fname is None: + data = pd.DataFrame() + data['Wavelength'] = np.squeeze(self.merged_absorbance[:, 0].copy()) + copy_abs = self.merged_absorbance[:, 1].copy() + copy_abs[copy_abs < 0] = 0 + data['0'] = np.squeeze(copy_abs) + fullfilename = os.path.join(self.save_directory, filename + '_merged.csv') + else: + fullfilename = os.path.join(self.save_directory, fname) + timestamp_match = re.search(r'_(\d+)_', fname) + time_interval = 0 + if timestamp_match is not None: + time_zero = datetime.strptime(timestamp_match.group(1), '%Y%m%d%H%M%S') + time_interval = int((datetime.now() - time_zero).total_seconds()) + df_old = pd.read_csv(fullfilename, comment='#') + df_new = pd.DataFrame() + df_new['Wavelength'] = np.squeeze(self.merged_absorbance[:, 0].copy()) + copy_abs = self.merged_absorbance[:, 1].copy() + copy_abs[copy_abs < 0] = 0 + df_new[str(time_interval)] = np.squeeze(copy_abs) + data = df_old.drop(columns='Index', errors='ignore').merge(df_new, how='inner', on='Wavelength') + + comment_list = list(comment_list) + comment_list.append("# To merge, NIR data is scaled first then shifted\n") + comment_list.append("# scale = none\n") + comment_list.append("# shift = none\n") + comment_list.append("# First column is Wavelength, later columns are absorbance at elapsed time in seconds\n") + with open(fullfilename, 'w') as file: + file.writelines(comment_list) + data.to_csv(fullfilename, mode='a', index_label='Index') + + return (True, "Successfully merged absorbance spectra for single detector") + @check_initialized def get_spectrum_counts( self, @@ -189,11 +485,14 @@ def get_all_absorbance( self, save_to_file: bool = False, filename: Optional[str] = None, - integration_times: Tuple[int, ...] = (100, 100), + integration_times: Optional[Tuple[int, ...]] = None, scans_to_avg: Tuple[int, ...] = (3, 3), smoothings: Tuple[int, ...] = (0, 0), xtimings: Tuple[int, ...] = (1, 1)) -> Tuple[bool, str]: + if integration_times is None: + integration_times = self.default_integration_time + # get the sample spectra result, spectrum_array_dict = self.get_all_spectra_counts( integration_times, @@ -224,13 +523,14 @@ def get_all_absorbance( blank_spec = self.blank_spectra_dict[spec_key][:,1].copy() sam_spec = spectrum_array_dict[spec_key][:,1].copy() - absorbance = np.expand_dims(-np.log10((sam_spec - dark_spec) / (blank_spec - dark_spec)), axis=1) + absorbance = np.expand_dims(self._compute_absorbance(dark_spec, blank_spec, sam_spec), axis=1) absorbance_array = np.hstack((wavelength, absorbance)) self.absorbance_dict[spec_key] = absorbance_array # save all data related to absorbance calculation to a file per spectrometer if save_to_file: + os.makedirs(self.save_directory, exist_ok=True) data = pd.DataFrame() data[spec_key + ' Wavelength'] = np.squeeze(wavelength) data[spec_key + ' Dark Counts'] = np.squeeze(dark_spec) @@ -258,18 +558,646 @@ def get_all_absorbance( else: all_comments = ['#\n',] # Merge the absorbance spectra and optionally save to file - # What happens if only 1 spectrometer is connected/being used? - if len(self.spec_keys) > 1: + if len(self.spec_keys) == 1: + result, message = self._merge_absorbance_byname_onedetector(save_to_file, filename, filename, all_comments) + elif len(self.spec_keys) > 1: result, message = self.merge_absorbance(save_to_file, filename, all_comments) - if not result: - return result, message + if not result: + return result, message if save_to_file: return (True, "All absorbance spectra stored to instance and saved to file: " + self.save_directory + filename) else: return (True, "All absorbance spectra stored to instance but not saved to file") + def get_all_absorbance_byname( + self, + sample_name: str, + save_to_file: bool = False, + repeat_measure: bool = False, + integration_times: Optional[Tuple[int, ...]] = None, + scans_to_avg: Tuple[int, ...] = (3, 3), + smoothings: Tuple[int, ...] = (0, 0), + xtimings: Tuple[int, ...] = (1, 1), + absorbance_threshold: float = 0.003) -> Tuple[bool, str]: + + if integration_times is None: + integration_times = self.default_integration_time + + filename = sample_name + '_' + datetime.now().strftime('%Y%m%d%H%M%S') + for attempt in range(3): + warning_by_spec = {} + result, spectrum_array_dict = self.get_all_spectra_counts( + integration_times, + scans_to_avg, + smoothings, + xtimings) + if not result: + return result, spectrum_array_dict + + existing_files = {} + if save_to_file and repeat_measure: + for spec_key in self.spec_keys: + existing_files[spec_key] = self.find_file(self.save_directory, sample_name, spec_key) + + for key in self.spectrometer_dict.keys(): + if key not in self.blank_spectra_dict: + return (False, "Blank spectra for " + key + " is missing") + if key not in self.dark_spectra_dict: + return (False, "Dark spectra for " + key + " is missing") + + retry_needed = False + prepared_rows = {} + + for ndx, spec_key in enumerate(self.spec_keys): + wavelength = self.wavelength_dict[spec_key].copy() + dark_spec = self.dark_spectra_dict[spec_key][:, 1].copy() + blank_spec = self.blank_spectra_dict[spec_key][:, 1].copy() + sam_spec = spectrum_array_dict[spec_key][:, 1].copy() + absorbance = self._compute_absorbance(dark_spec, blank_spec, sam_spec) + absorbance_array = np.hstack((wavelength, np.expand_dims(absorbance, axis=1))) + self.absorbance_dict[spec_key] = absorbance_array + + fname = existing_files.get(spec_key) if repeat_measure else None + qc_record = { + "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + "spec_key": spec_key, + "measurement_valid": 1, + "relative_diff": "", + "previous_mean": "", + "current_mean": "", + "previous_sum": "", + "current_sum": "", + "valid_points": "", + "threshold": absorbance_threshold, + "attempt_used": attempt + 1, + "elapsed_seconds": 0, + "reason": "first_measurement" if fname is None else "comparison_not_needed", + "data_file": "", + } + if fname is not None: + fullfilename = os.path.join(self.save_directory, fname) + df_old = pd.read_csv(fullfilename, comment='#') + last_column_name = df_old.columns[-1] + df_cur = pd.DataFrame() + df_cur[spec_key + ' Wavelength'] = np.squeeze(wavelength) + df_cur[spec_key + ' current_absorbance'] = np.squeeze(absorbance) + data = df_old.drop(columns='Index', errors='ignore').merge( + df_cur, + how='inner', + on=spec_key + ' Wavelength') + + current_absorbance = data[spec_key + ' current_absorbance'].to_numpy() + last_absorbance = data[last_column_name].to_numpy() + wavelength_list = data.iloc[:, 0].to_numpy() + + if last_absorbance.shape == current_absorbance.shape: + valid_index = ( + (last_absorbance > 0) + & (last_absorbance < 5) + & (current_absorbance > 0) + & (current_absorbance < 5) + & (wavelength_list > 285) + & (wavelength_list < 800) + ) + if np.any(valid_index): + previous_sum = float(np.sum(np.abs(last_absorbance[valid_index]))) + current_sum = float(np.sum(np.abs(current_absorbance[valid_index]))) + previous_mean = float(np.mean(last_absorbance[valid_index])) + current_mean = float(np.mean(current_absorbance[valid_index])) + absorbance_diff = ( + np.sum(np.abs(last_absorbance[valid_index] - current_absorbance[valid_index])) + / previous_sum + ) + diff_message = ( + f"{spec_key}: relative_diff={absorbance_diff:.6f}, " + f"previous_mean={previous_mean:.6f}, current_mean={current_mean:.6f}, " + f"previous_sum={previous_sum:.6f}, current_sum={current_sum:.6f}, " + f"valid_points={int(np.count_nonzero(valid_index))}, attempt={attempt + 1}" + ) + timestamp_match = re.search(r'_(\d+)_', fname) + time_interval = 0 + if timestamp_match is not None: + time_zero = datetime.strptime(timestamp_match.group(1), '%Y%m%d%H%M%S') + time_interval = int((datetime.now() - time_zero).total_seconds()) + qc_record.update({ + "measurement_valid": int(absorbance_diff <= absorbance_threshold), + "relative_diff": absorbance_diff, + "previous_mean": previous_mean, + "current_mean": current_mean, + "previous_sum": previous_sum, + "current_sum": current_sum, + "valid_points": int(np.count_nonzero(valid_index)), + "elapsed_seconds": time_interval, + "reason": "within_threshold" if absorbance_diff <= absorbance_threshold else "difference_exceeded_threshold", + }) + if absorbance_diff > absorbance_threshold: + warning_by_spec[spec_key] = diff_message + if attempt < 2: + retry_needed = True + break + else: + warning_by_spec.pop(spec_key, None) + else: + qc_record.update({ + "measurement_valid": 1, + "reason": "no_overlap_for_comparison", + }) + + prepared_rows[spec_key] = { + "wavelength": wavelength, + "dark_spec": dark_spec, + "blank_spec": blank_spec, + "sam_spec": sam_spec, + "absorbance": absorbance, + "existing_file": fname, + "qc_record": qc_record, + } + + if retry_needed: + continue + + if save_to_file: + os.makedirs(self.save_directory, exist_ok=True) + qc_rows = [] + for ndx, spec_key in enumerate(self.spec_keys): + row = prepared_rows[spec_key] + wavelength = row["wavelength"] + dark_spec = row["dark_spec"] + blank_spec = row["blank_spec"] + sam_spec = row["sam_spec"] + absorbance = row["absorbance"] + fname = row["existing_file"] + qc_record = row["qc_record"] + + comment = [ + "# spec_key = " + spec_key + "\n", + "# integration_time = " + str(integration_times[ndx]) + "\n", + "# scans_to_avg = " + str(scans_to_avg[ndx]) + "\n", + "# smoothing = " + str(smoothings[ndx]) + "\n", + "# xtiming = " + str(xtimings[ndx]) + "\n", + ] + + if fname is None: + data = pd.DataFrame() + data[spec_key + ' Wavelength'] = np.squeeze(wavelength) + data[spec_key + ' Dark Counts'] = np.squeeze(dark_spec) + data[spec_key + ' Blank Counts'] = np.squeeze(blank_spec) + data[spec_key + ' Sample Counts at time 0'] = np.squeeze(sam_spec) + data[spec_key + ' Absorbance at time 0'] = np.squeeze(absorbance) + fullfilename = os.path.join(self.save_directory, filename + '_' + spec_key + '.csv') + qc_record["elapsed_seconds"] = 0 + else: + fullfilename = os.path.join(self.save_directory, fname) + timestamp_match = re.search(r'_(\d+)_', fname) + time_interval = 0 + if timestamp_match is not None: + time_zero = datetime.strptime(timestamp_match.group(1), '%Y%m%d%H%M%S') + time_interval = int((datetime.now() - time_zero).total_seconds()) + + df_old = pd.read_csv(fullfilename, comment='#') + df_new = pd.DataFrame() + df_new[spec_key + ' Wavelength'] = np.squeeze(wavelength) + df_new[spec_key + ' Sample Counts at time ' + str(time_interval)] = np.squeeze(sam_spec) + df_new[spec_key + ' Absorbance at time ' + str(time_interval)] = np.squeeze(absorbance) + data = df_old.drop(columns='Index', errors='ignore').merge( + df_new, + how='inner', + on=spec_key + ' Wavelength') + qc_record["elapsed_seconds"] = time_interval + + with open(fullfilename, 'w') as file: + file.writelines(comment) + data.to_csv(fullfilename, mode='a', index_label='Index') + qc_record["data_file"] = os.path.basename(fullfilename) + qc_rows.append(qc_record) + + self._append_measurement_qc_log(sample_name, qc_rows) + + break + + if save_to_file: + all_comments = [ + "# spec_keys = " + str(self.spec_keys) + "\n", + "# integration_times = " + str(integration_times) + "\n", + "# scans_to_avg = " + str(scans_to_avg) + "\n", + "# smoothings = " + str(smoothings) + "\n", + "# xtimings = " + str(xtimings) + "\n", + ] + else: + all_comments = ['#\n'] + + if len(self.spec_keys) == 1: + result, message = self._merge_absorbance_byname_onedetector(save_to_file, filename, sample_name, all_comments) + else: + result, message = self.merge_absorbance(save_to_file, filename, all_comments) + + if not result: + return result, message + + if save_to_file: + if warning_by_spec: + warning_summary = " | ".join( + "WARNING " + warning_message for warning_message in warning_by_spec.values() + ) + return ( + True, + "All absorbance spectra stored to instance and saved to file: " + + self.save_directory + + filename + + ". QC log updated: " + + self._measurement_qc_log_path(sample_name) + + ". " + + warning_summary + ) + return ( + True, + "All absorbance spectra stored to instance and saved to file: " + + self.save_directory + + filename + + ". QC log updated: " + + self._measurement_qc_log_path(sample_name) + ) + return (True, "All absorbance spectra stored to instance but not saved to file") + + def get_all_counts_byname( + self, + sample_name: str, + save_to_file: bool = False, + repeat_measure: bool = False, + integration_times: Optional[Tuple[int, ...]] = None, + scans_to_avg: Tuple[int, ...] = (3, 3), + smoothings: Tuple[int, ...] = (0, 0), + xtimings: Tuple[int, ...] = (1, 1), + absorbance_threshold: float = 0.003) -> Tuple[bool, str]: + + del absorbance_threshold + if integration_times is None: + integration_times = self.default_integration_time + + result, spectrum_array_dict = self.get_all_spectra_counts( + integration_times, + scans_to_avg, + smoothings, + xtimings) + if not result: + return result, spectrum_array_dict + + existing_files = {} + if save_to_file and repeat_measure: + for spec_key in self.spec_keys: + existing_files[spec_key] = self.find_file( + self.save_directory, + sample_name, + spec_key + '_photoncounts') + + filename = sample_name + '_' + datetime.now().strftime('%Y%m%d%H%M%S') + + for ndx, spec_key in enumerate(self.spec_keys): + wavelength = self.wavelength_dict[spec_key].copy() + photoncounts = spectrum_array_dict[spec_key][:, 1].copy() + photoncounts = np.nan_to_num(photoncounts, nan=0.0, posinf=0.0, neginf=0.0) + photoncounts[photoncounts < 0] = 0 + self.photoncounts_dict[spec_key] = np.hstack((wavelength, np.expand_dims(photoncounts, axis=1))) + + if save_to_file: + os.makedirs(self.save_directory, exist_ok=True) + comment = [ + "# spec_key = " + spec_key + "\n", + "# integration_time = " + str(integration_times[ndx]) + "\n", + "# scans_to_avg = " + str(scans_to_avg[ndx]) + "\n", + "# smoothing = " + str(smoothings[ndx]) + "\n", + "# xtiming = " + str(xtimings[ndx]) + "\n", + ] + fname = existing_files.get(spec_key) if repeat_measure else None + if fname is None: + data = pd.DataFrame() + data[spec_key + ' Wavelength'] = np.squeeze(wavelength) + data[spec_key + ' Photon Counts at time 0'] = np.squeeze(photoncounts) + fullfilename = os.path.join(self.save_directory, filename + '_' + spec_key + '_photoncounts.csv') + else: + fullfilename = os.path.join(self.save_directory, fname) + timestamp_match = re.search(r'_(\d+)_', fname) + time_interval = 0 + if timestamp_match is not None: + time_zero = datetime.strptime(timestamp_match.group(1), '%Y%m%d%H%M%S') + time_interval = int((datetime.now() - time_zero).total_seconds()) + df_old = pd.read_csv(fullfilename, comment='#') + df_new = pd.DataFrame() + df_new[spec_key + ' Wavelength'] = np.squeeze(wavelength) + df_new[spec_key + ' Photon Counts at time ' + str(time_interval)] = np.squeeze(photoncounts) + data = df_old.drop(columns='Index', errors='ignore').merge( + df_new, + how='inner', + on=spec_key + ' Wavelength') + + with open(fullfilename, 'w') as file: + file.writelines(comment) + data.to_csv(fullfilename, mode='a', index_label='Index') + + if save_to_file: + return (True, "Photon counts saved to file: " + self.save_directory + filename) + return (True, "Photon counts stored to instance but not saved to file") + + def get_spec_decay( + self, + sample_name: str, + save_to_file: bool = False, + range_start: float = 290.0, + range_end: float = 800.0, + irradiance_file: str = "reference/am15g_spectrum.csv", + Wvlgth_col_name: str = "wavelength_nm", + Irrad_col_name: str = "irradiance_w_m2_nm", + decay_threshold: float = 0.01) -> Tuple[bool, str]: + + fname = self.find_file(self.save_directory, sample_name, 'merged.csv') + if fname is None: + return (False, "Absorbance of " + sample_name + " is missing") + + fullfilename = os.path.join(self.save_directory, fname) + df_old = pd.read_csv(fullfilename, comment='#') + if len(df_old.columns) <= 3: + return (False, "Only original absorbance recorded") + + wavelength_list = pd.to_numeric(df_old.iloc[:, 1], errors='coerce').to_numpy(dtype=float) + data_mask = np.logical_and(wavelength_list >= range_start, wavelength_list <= range_end) + if np.count_nonzero(data_mask) < 2: + return (False, "Insufficient wavelength points in the selected spectral decay range.") + + absorbance_df = df_old.iloc[:, 2:].apply(pd.to_numeric, errors='coerce') + decayed_absorbance = absorbance_df.to_numpy(dtype=float)[data_mask] + original_absorbance = absorbance_df.iloc[:, 0].to_numpy(dtype=float)[data_mask] + wavelength_list_inrange = wavelength_list[data_mask] + time_columns = [str(column_name) for column_name in df_old.columns[2:]] + elapsed_seconds = self._parse_elapsed_seconds_from_columns(time_columns) + elapsed_hours = elapsed_seconds / 3600.0 + + result, irradiance_data = self._load_irradiance_reference( + irradiance_file, + Wvlgth_col_name, + Irrad_col_name) + if not result: + return (False, str(irradiance_data)) + wavelength_list_irr, irradiance_reference = irradiance_data + if range_start < np.min(wavelength_list_irr) or range_end > np.max(wavelength_list_irr): + return (False, "Incompatible range with irradiance table") + + irradiance_interp = self._linear_interpolate(wavelength_list_irr, irradiance_reference, wavelength_list_inrange) + irradiance_interp = np.nan_to_num(irradiance_interp, nan=0.0, posinf=0.0, neginf=0.0) + total_irradiance = self._trapz(wavelength_list_inrange, irradiance_interp) + + overlap_percent = np.zeros(decayed_absorbance.shape[1], dtype=float) + for idx in range(decayed_absorbance.shape[1]): + absorbed_fraction = 1.0 - np.power(10.0, -decayed_absorbance[:, idx]) + absorbed_fraction = np.clip(np.nan_to_num(absorbed_fraction, nan=0.0, posinf=1.0, neginf=0.0), 0.0, 1.0) + absorbed_weighted = irradiance_interp * absorbed_fraction + absorbed_irradiance = self._trapz(wavelength_list_inrange, absorbed_weighted) + overlap_percent[idx] = ( + absorbed_irradiance / total_irradiance * 100.0 if total_irradiance > 0 else 0.0 + ) + + baseline_overlap = overlap_percent[0] + if baseline_overlap > 0: + retention_percent = overlap_percent / baseline_overlap * 100.0 + else: + retention_percent = np.zeros_like(overlap_percent) + overlap_delta_vs_t0 = overlap_percent - baseline_overlap + overlap_abs_change_vs_t0 = np.abs(retention_percent - 100.0) + + reference = original_absorbance.copy() + positive_reference = np.where(reference > decay_threshold, np.maximum(reference, 0.0), 0.0) + norm = float(np.sum(positive_reference)) + if norm <= 0: + norm = 1.0 + + decay_mag = np.zeros(decayed_absorbance.shape[1], dtype=float) + decay_signed = np.zeros(decayed_absorbance.shape[1], dtype=float) + decay_positive = np.zeros(decayed_absorbance.shape[1], dtype=float) + decay_negative_abs = np.zeros(decayed_absorbance.shape[1], dtype=float) + + for idx in range(decayed_absorbance.shape[1]): + current = decayed_absorbance[:, idx] + valid_reference = np.where(reference > decay_threshold, reference, 0.0) + current_valid = np.where(reference > decay_threshold, np.nan_to_num(current, nan=0.0), 0.0) + signed = (current_valid - valid_reference) / norm + magnitude = np.abs(valid_reference - current_valid) / norm + + decay_mag[idx] = float(np.sum(magnitude)) + decay_signed[idx] = float(np.sum(signed)) + decay_positive[idx] = float(np.sum(np.maximum(signed, 0.0))) + decay_negative_abs[idx] = float(np.sum(np.maximum(-signed, 0.0))) + + t80_h = self._interpolate_crossing_time(elapsed_hours, decay_mag, 0.20) + + df_specdecay = pd.DataFrame() + df_specdecay['time_s'] = elapsed_seconds + df_specdecay['time_h'] = elapsed_hours + df_specdecay['spectral_overlap_percent'] = overlap_percent + df_specdecay['spectral_overlap_delta_vs_t0_percent'] = overlap_delta_vs_t0 + df_specdecay['retention_vs_t0_percent'] = retention_percent + df_specdecay['spectral_overlap_abs_change_vs_t0_percent'] = overlap_abs_change_vs_t0 + df_specdecay['decay_index_mag'] = decay_mag + df_specdecay['decay_index_signed'] = decay_signed + df_specdecay['decay_index_positive'] = decay_positive + df_specdecay['decay_index_negative_abs'] = decay_negative_abs + df_specdecay['t80_h'] = t80_h + + if save_to_file: + new_filename = os.path.join(self.save_directory, sample_name + "_specdecay.csv") + comment = [ + "# range start from (wavelength) " + str(range_start) + " (nm)\n", + "# end at = " + str(range_end) + "\n", + "# irradiance_file = " + str(irradiance_file) + "\n", + "# decay_threshold = " + str(decay_threshold) + "\n", + "# methodology = UVVis_Converter style overlap/interpolate/trapz and decay index summary\n", + ] + with open(new_filename, 'w') as file: + file.writelines(comment) + df_specdecay.to_csv(new_filename, mode='a', index_label='Index') + return (True, "Spectral decay saved to file: " + new_filename) + return (True, "Spectral decay calculated but not saved to file") + + def plot_spec_decay_summary( + self, + sample_name: str, + range_start: float = 290.0, + range_end: float = 800.0, + save_to_file: bool = True, + output_filename: Optional[str] = None, + figure_dpi: int = 180) -> Tuple[bool, str]: + try: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + except Exception as exc: + return (False, "matplotlib is required to plot spectral decay summaries: " + str(exc)) + + specdecay_path = os.path.join(self.save_directory, sample_name + "_specdecay.csv") + if not os.path.exists(specdecay_path): + fname = self.find_file(self.save_directory, sample_name, 'specdecay.csv') + if fname is None: + return (False, "Spectral decay of " + sample_name + " is missing") + specdecay_path = os.path.join(self.save_directory, fname) + + df_specdecay = pd.read_csv(specdecay_path, comment='#') + if df_specdecay.empty: + return (False, "Spectral decay file is empty: " + specdecay_path) + + required_columns = [ + "time_h", + "spectral_overlap_percent", + "retention_vs_t0_percent", + "decay_index_mag", + "decay_index_signed", + "decay_index_positive", + "decay_index_negative_abs", + ] + missing_columns = [column for column in required_columns if column not in df_specdecay.columns] + if missing_columns: + return (False, "Spectral decay file is missing columns: " + ", ".join(missing_columns)) + + time_h = pd.to_numeric(df_specdecay["time_h"], errors="coerce").to_numpy(dtype=float) + overlap_percent = pd.to_numeric( + df_specdecay["spectral_overlap_percent"], errors="coerce").to_numpy(dtype=float) + retention_percent = pd.to_numeric( + df_specdecay["retention_vs_t0_percent"], errors="coerce").to_numpy(dtype=float) + decay_mag = pd.to_numeric(df_specdecay["decay_index_mag"], errors="coerce").to_numpy(dtype=float) + decay_signed = pd.to_numeric(df_specdecay["decay_index_signed"], errors="coerce").to_numpy(dtype=float) + decay_positive = pd.to_numeric(df_specdecay["decay_index_positive"], errors="coerce").to_numpy(dtype=float) + decay_negative_abs = pd.to_numeric( + df_specdecay["decay_index_negative_abs"], errors="coerce").to_numpy(dtype=float) + + t80_h = None + if "t80_h" in df_specdecay.columns: + t80_values = pd.to_numeric(df_specdecay["t80_h"], errors="coerce").to_numpy(dtype=float) + finite_t80 = t80_values[np.isfinite(t80_values)] + if finite_t80.size > 0: + t80_h = float(finite_t80[0]) + + merged_path = None + merged_name = self.find_file(self.save_directory, sample_name, 'merged.csv') + if merged_name is not None: + merged_path = os.path.join(self.save_directory, merged_name) + + qc_path = self._measurement_qc_log_path(sample_name) + df_qc = pd.read_csv(qc_path) if os.path.exists(qc_path) else None + + fig, axes = plt.subplots(2, 2, figsize=(13, 9), constrained_layout=True) + fig.suptitle(sample_name) + + ax_overlap = axes[0, 0] + ax_overlap.plot(time_h, overlap_percent, color="#1f77b4", linewidth=1.8, label="Overlap [%]") + ax_overlap.set_xlabel("Time [h]") + ax_overlap.set_ylabel("Spectral Overlap [%]", color="#1f77b4") + ax_overlap.tick_params(axis='y', labelcolor="#1f77b4") + ax_overlap.grid(alpha=0.25) + ax_retention = ax_overlap.twinx() + ax_retention.plot(time_h, retention_percent, color="#ff7f0e", linewidth=1.5, label="Retention [%]") + ax_retention.set_ylabel("Retention vs t0 [%]", color="#ff7f0e") + ax_retention.tick_params(axis='y', labelcolor="#ff7f0e") + if t80_h is not None and np.isfinite(t80_h): + ax_overlap.axvline(t80_h, color="black", linestyle="--", linewidth=1.0, alpha=0.7) + ax_overlap.text( + t80_h, + np.nanmax(overlap_percent) if np.any(np.isfinite(overlap_percent)) else 0.0, + f"t80={t80_h:.2f} h", + fontsize=8, + ha="left", + va="bottom", + ) + ax_overlap.set_title("Overlap / Retention") + + ax_decay = axes[0, 1] + ax_decay.plot(time_h, decay_mag, color="#d62728", linewidth=1.8, label="Decay magnitude") + ax_decay.plot(time_h, decay_positive, color="#2ca02c", linewidth=1.2, label="Positive") + ax_decay.plot(time_h, decay_negative_abs, color="#9467bd", linewidth=1.2, label="Negative abs") + ax_decay.plot(time_h, decay_signed, color="#7f7f7f", linewidth=1.0, linestyle="--", label="Signed") + if t80_h is not None and np.isfinite(t80_h): + ax_decay.axvline(t80_h, color="black", linestyle="--", linewidth=1.0, alpha=0.7) + ax_decay.set_xlabel("Time [h]") + ax_decay.set_ylabel("Decay Index") + ax_decay.set_title("Decay Indices") + ax_decay.grid(alpha=0.25) + ax_decay.legend(loc="best", fontsize=8) + + ax_abs = axes[1, 0] + if merged_path is not None and os.path.exists(merged_path): + df_merged = pd.read_csv(merged_path, comment='#') + wavelength = pd.to_numeric(df_merged.iloc[:, 1], errors="coerce").to_numpy(dtype=float) + absorbance_df = df_merged.iloc[:, 2:].apply(pd.to_numeric, errors='coerce') + mask = np.logical_and(wavelength >= range_start, wavelength <= range_end) + if np.count_nonzero(mask) >= 2 and absorbance_df.shape[1] >= 1: + selected_indices = sorted(set([0, absorbance_df.shape[1] // 2, absorbance_df.shape[1] - 1])) + colors = ["#1f77b4", "#ff7f0e", "#d62728"] + for color, idx in zip(colors, selected_indices): + label = f"t={absorbance_df.columns[idx]} s" + ax_abs.plot( + wavelength[mask], + absorbance_df.iloc[:, idx].to_numpy(dtype=float)[mask], + linewidth=1.1, + color=color, + label=label, + ) + ax_abs.legend(loc="best", fontsize=8) + else: + ax_abs.text(0.5, 0.5, "Merged absorbance range unavailable", ha="center", va="center") + else: + ax_abs.text(0.5, 0.5, "Merged absorbance file not found", ha="center", va="center") + ax_abs.set_xlabel("Wavelength [nm]") + ax_abs.set_ylabel("Absorbance") + ax_abs.set_title(f"Absorbance Snapshots ({range_start:.0f}-{range_end:.0f} nm)") + ax_abs.grid(alpha=0.25) + + ax_qc = axes[1, 1] + if df_qc is not None and not df_qc.empty: + qc_time_h = pd.to_numeric(df_qc.get("elapsed_seconds", np.nan), errors="coerce").to_numpy(dtype=float) / 3600.0 + measurement_valid = pd.to_numeric(df_qc.get("measurement_valid", np.nan), errors="coerce").to_numpy(dtype=float) + colors = np.where(measurement_valid > 0.5, "#2ca02c", "#d62728") + ax_qc.scatter(qc_time_h, measurement_valid, c=colors, s=22, label="measurement_valid") + ax_qc.set_ylim(-0.1, 1.1) + ax_qc.set_yticks([0, 1]) + ax_qc.set_xlabel("Time [h]") + ax_qc.set_ylabel("Measurement Valid") + ax_qc.grid(alpha=0.25) + valid_count = int(np.nansum(measurement_valid > 0.5)) + ax_qc.set_title(f"QC Summary ({valid_count}/{len(measurement_valid)} valid)") + + if "relative_diff" in df_qc.columns: + relative_diff = pd.to_numeric(df_qc["relative_diff"], errors="coerce").to_numpy(dtype=float) + qc_threshold = pd.to_numeric(df_qc.get("threshold", np.nan), errors="coerce").to_numpy(dtype=float) + ax_qc_diff = ax_qc.twinx() + ax_qc_diff.plot(qc_time_h, relative_diff, color="#ff7f0e", linewidth=1.3, alpha=0.9, label="relative_diff") + finite_threshold = qc_threshold[np.isfinite(qc_threshold)] + if finite_threshold.size > 0: + ax_qc_diff.axhline( + float(finite_threshold[0]), + color="#ff7f0e", + linestyle="--", + linewidth=1.0, + alpha=0.7, + ) + ax_qc_diff.set_ylabel("Relative Diff", color="#ff7f0e") + ax_qc_diff.tick_params(axis='y', labelcolor="#ff7f0e") + else: + ax_qc.text(0.5, 0.5, "QC log not found", ha="center", va="center") + ax_qc.set_title("QC Summary") + ax_qc.set_xticks([]) + ax_qc.set_yticks([]) + + if output_filename is None: + figure_path = os.path.join(self.save_directory, sample_name + "_specdecay_summary.png") + else: + figure_path = output_filename if os.path.isabs(output_filename) else os.path.join(self.save_directory, output_filename) + + if save_to_file: + fig.savefig(figure_path, dpi=figure_dpi, bbox_inches='tight') + plt.close(fig) + return (True, "Spectral decay summary figure saved to file: " + figure_path) + + plt.close(fig) + return (True, "Spectral decay summary figure created but not saved to file") + # hard coded for UV-Vis and NIR # what happens if only 1 spectrometer is connected/being used? def merge_absorbance(self, save_to_file: bool, filename: str, comment_list: List[str]) -> Tuple[bool, str]: @@ -310,6 +1238,7 @@ def merge_absorbance(self, save_to_file: bool, filename: str, comment_list: List self.merged_absorbance = merged_array if save_to_file: + os.makedirs(self.save_directory, exist_ok=True) data = pd.DataFrame() data['Wavelength'] = np.squeeze(self.merged_absorbance[:,0].copy()) data['Absorbance'] = np.squeeze(self.merged_absorbance[:,1].copy()) @@ -388,4 +1317,4 @@ def lamp_relay_off(self): # The data is then incorrect for those spectrometers since they use the old dark/blank but the user has no idea this just happened. # Another option is to null all spectrometer dark/blanks when even only retaking for 1 spectrometer, but requires implementing checks and error messages # It is easier at the moment to just enforce all spectrometers are used all the time by writing methods that always use all spectrometers -# However, The "get_all_..." methods can generalize to 1 spectrometer based on the spec_keys arg passed during construction \ No newline at end of file +# However, The "get_all_..." methods can generalize to 1 spectrometer based on the spec_keys arg passed during construction diff --git a/aamp_app/devices/substrate_dispenser.py b/aamp_app/devices/substrate_dispenser.py new file mode 100644 index 0000000..87af5f4 --- /dev/null +++ b/aamp_app/devices/substrate_dispenser.py @@ -0,0 +1,28 @@ +from .substrate_linear_stage_base import SubstrateLinearStageBase + + +class SubstrateDispenser(SubstrateLinearStageBase): + """Arduino-controlled linear stage for the substrate dispenser.""" + + def __init__( + self, + name: str, + port: str, + max_position_mm: float = 45.0, + baudrate: int = 9600, + timeout: float = 1.0, + connect_delay_s: float = 5.0, + ready_timeout_s: float = 5.0, + home_timeout_s: float = 30.0, + move_timeout_s: float = 30.0): + super().__init__( + name=name, + port=port, + max_position_mm=max_position_mm, + baudrate=baudrate, + timeout=timeout, + connect_delay_s=connect_delay_s, + ready_timeout_s=ready_timeout_s, + home_timeout_s=home_timeout_s, + move_timeout_s=move_timeout_s, + ) diff --git a/aamp_app/devices/substrate_hotel.py b/aamp_app/devices/substrate_hotel.py new file mode 100644 index 0000000..4675a18 --- /dev/null +++ b/aamp_app/devices/substrate_hotel.py @@ -0,0 +1,28 @@ +from .substrate_linear_stage_base import SubstrateLinearStageBase + + +class SubstrateHotel(SubstrateLinearStageBase): + """Arduino-controlled linear stage for the substrate hotel.""" + + def __init__( + self, + name: str, + port: str, + max_position_mm: float = 430.0, + baudrate: int = 9600, + timeout: float = 1.0, + connect_delay_s: float = 5.0, + ready_timeout_s: float = 5.0, + home_timeout_s: float = 300.0, + move_timeout_s: float = 300.0): + super().__init__( + name=name, + port=port, + max_position_mm=max_position_mm, + baudrate=baudrate, + timeout=timeout, + connect_delay_s=connect_delay_s, + ready_timeout_s=ready_timeout_s, + home_timeout_s=home_timeout_s, + move_timeout_s=move_timeout_s, + ) diff --git a/aamp_app/devices/substrate_linear_stage_base.py b/aamp_app/devices/substrate_linear_stage_base.py new file mode 100644 index 0000000..a04f9f5 --- /dev/null +++ b/aamp_app/devices/substrate_linear_stage_base.py @@ -0,0 +1,118 @@ +from typing import Optional, Tuple + +from .device import ArduinoSerialDevice, check_initialized, check_serial + + +class SubstrateLinearStageBase(ArduinoSerialDevice): + """Shared Arduino serial wrapper for the substrate hotel/dispenser linear stages.""" + + def __init__( + self, + name: str, + port: str, + max_position_mm: float, + baudrate: int = 9600, + timeout: float = 1.0, + connect_delay_s: float = 5.0, + ready_timeout_s: float = 5.0, + home_timeout_s: float = 30.0, + move_timeout_s: float = 30.0): + super().__init__(name, port, baudrate, timeout) + self._max_position_mm = float(max_position_mm) + self._connect_delay_s = float(connect_delay_s) + self._ready_timeout_s = float(ready_timeout_s) + self._home_timeout_s = float(home_timeout_s) + self._move_timeout_s = float(move_timeout_s) + + def get_init_args(self) -> dict: + return { + "name": self._name, + "port": self._port, + "max_position_mm": self._max_position_mm, + "baudrate": self._baudrate, + "timeout": self._timeout, + "connect_delay_s": self._connect_delay_s, + "ready_timeout_s": self._ready_timeout_s, + "home_timeout_s": self._home_timeout_s, + "move_timeout_s": self._move_timeout_s, + } + + def update_init_args(self, args_dict: dict): + self._name = args_dict["name"] + self._port = args_dict["port"] + self._max_position_mm = float(args_dict["max_position_mm"]) + self._baudrate = args_dict["baudrate"] + self._timeout = args_dict["timeout"] + self._connect_delay_s = float(args_dict.get("connect_delay_s", 5.0)) + self._ready_timeout_s = float(args_dict.get("ready_timeout_s", 5.0)) + self._home_timeout_s = float(args_dict.get("home_timeout_s", 30.0)) + self._move_timeout_s = float(args_dict.get("move_timeout_s", 30.0)) + + def connect(self) -> Tuple[bool, str]: + return self.start_serial(delay=self._connect_delay_s) + + @check_serial + def initialize(self) -> Tuple[bool, str]: + ready_success, ready_message = self.get_response(response_timeout=self._ready_timeout_s) + if not ready_success or "Ready" not in ready_message: + self._is_initialized = False + return (False, "Arduino did not send ready signal. Response: " + str(ready_message)) + + success, message = self.home() + if not success: + self._is_initialized = False + return (False, "Homing failed: " + str(message)) + + self._is_initialized = True + return (True, "Device initialized and homed.") + + @check_serial + def deinitialize( + self, + reset_init_flag: bool = True, + close_serial: bool = False) -> Tuple[bool, str]: + if reset_init_flag: + self._is_initialized = False + if close_serial and self.ser.is_open: + self.ser.close() + return (True, "Device deinitialized.") + + @check_serial + def home(self) -> Tuple[bool, str]: + self.ser.write(b'H\n') + return self.check_ack_succ(succ_timeout=self._home_timeout_s) + + @check_serial + @check_initialized + def move_to_position( + self, + position_mm: float, + speed_mm_per_s: float, + move_timeout: Optional[float] = None) -> Tuple[bool, str]: + if not (0.0 <= float(position_mm) <= self._max_position_mm): + return ( + False, + f"Command error: position {position_mm} mm is out of range (0-{self._max_position_mm} mm).", + ) + + command = f"M{float(position_mm)},{float(speed_mm_per_s)}\n".encode("ascii") + self.ser.write(command) + + ack_success, ack_message = self.check_response( + self.char_ACK, + self.char_delimiter, + response_timeout=2.0, + ) + if not ack_success: + return (False, "Did not receive ACK for move command: " + str(ack_message)) + + timeout_s = self._move_timeout_s if move_timeout is None else float(move_timeout) + succ_success, succ_message = self.check_response( + self.char_SUCC, + self.char_delimiter, + response_timeout=timeout_s, + ) + if not succ_success: + return (False, "Move did not complete successfully: " + str(succ_message)) + + return (True, succ_message) diff --git a/aamp_app/drafts/ch_apis_imaging_builder_draft.py b/aamp_app/drafts/ch_apis_imaging_builder_draft.py new file mode 100644 index 0000000..49b4d4d --- /dev/null +++ b/aamp_app/drafts/ch_apis_imaging_builder_draft.py @@ -0,0 +1,381 @@ +from pathlib import Path +from string import Template +import os +import sys + +import dash +from dash import html, dcc, dash_table, callback, Input, Output, State +import dash_bootstrap_components as dbc + +ROOT_DIR = Path(__file__).resolve().parents[2] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from mongodb_helper import MongoDBHelper +from console_interceptor import ConsoleInterceptor + + +TEMPLATE_PATH = ROOT_DIR / "recipes" / "user_recipes" / "ch_apis_imaging_from_mongodb.py" + + +def load_env(file_path: Path = ROOT_DIR / ".env") -> None: + if file_path.exists(): + with open(file_path, "r", encoding="utf-8") as file: + for line in file: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + + +def get_mongo() -> MongoDBHelper: + load_env() + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") + if not mongo_uri or not mongo_db_name: + raise RuntimeError("MONGO_URI and MONGO_DB_NAME must be set in .env.") + return MongoDBHelper(mongo_uri, mongo_db_name) + + +with open(TEMPLATE_PATH, "r", encoding="utf-8") as file: + CH_APIS_RECIPE_TEMPLATE = Template(file.read()) + + +app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) + +app.layout = html.Div( + [ + html.Div( + [ + html.H1( + [ + "CH APIS Imaging Builder Draft", + html.Span( + " ?", + id="ch-apis-builder-help", + style={ + "cursor": "pointer", + "color": "gray", + "fontWeight": "bold", + "fontSize": "0.7em", + "marginLeft": "10px", + }, + ), + ], + style={"display": "inline-block"}, + ), + dbc.Tooltip( + "Standalone draft for APIS imaging recipe generation. " + "Select a campaign and it will list all sets that do not yet have APIS imaging records in the images collection. " + "Generated recipes perform MongoDB lookup at execution time using campaign_name, batch_no, and sample_no.", + target="ch-apis-builder-help", + placement="right", + style={"maxWidth": "420px"}, + ), + ] + ), + dbc.Alert( + id="ch-apis-alert", + color="success", + is_open=False, + fade=True, + className="mb-3", + ), + html.Div( + [ + dbc.Row( + [ + dbc.Col( + [ + html.H5("Select Campaign"), + dcc.Dropdown( + id="ch-apis-campaign-dropdown", + options=[], + placeholder="Select a campaign...", + ), + ], + width=8, + ), + dbc.Col( + [ + dbc.Button( + "Generate Pending APIS Recipes", + id="ch-apis-generate-button", + color="primary", + className="mt-4", + ), + ], + width=2, + ), + dbc.Col( + [ + dbc.Button( + "Run 0 recipes", + id="ch-apis-run-button", + n_clicks=0, + className="btn btn-success mt-4", + ), + ], + width=2, + ), + ], + className="mb-3", + ), + html.Div( + [ + html.H4("Pending Parameter Sets"), + html.Div(id="ch-apis-sets"), + ], + className="mb-3", + ), + html.Div(id="ch-apis-output", className="mb-3"), + html.Div(id="ch-apis-run-log", className="mb-3"), + dcc.Store(id="ch-apis-parameter-sets"), + dcc.Store(id="ch-apis-generated-scripts"), + ], + className="container", + ), + ], + className="container", +) + + +@callback( + Output("ch-apis-campaign-dropdown", "options"), + Input("ch-apis-campaign-dropdown", "search_value"), +) +def update_campaign_options(search_value): + mongo = get_mongo() + try: + campaigns = list(mongo.db.campaigns.find({}, {"campaign_name": 1})) + options = [{"label": doc["campaign_name"], "value": doc["campaign_name"]} for doc in campaigns] + if search_value: + options = [opt for opt in options if search_value.lower() in opt["label"].lower()] + return options + finally: + mongo.close_connection() + + +@callback( + Output("ch-apis-sets", "children"), + Output("ch-apis-parameter-sets", "data"), + Output("ch-apis-alert", "children"), + Output("ch-apis-alert", "is_open"), + Output("ch-apis-alert", "color"), + Input("ch-apis-campaign-dropdown", "value"), +) +def display_pending_campaign_sets(selected_campaign): + if not selected_campaign: + return html.Div("Select a campaign to view pending APIS imaging sets."), [], "", False, "success" + + mongo = get_mongo() + try: + campaign = mongo.db.campaigns.find_one({"campaign_name": selected_campaign}) + if not campaign: + return html.Div(f"Campaign '{selected_campaign}' was not found."), [], "Campaign not found.", True, "danger" + + sets = list( + mongo.db.sets.find({"campaign_id": campaign["_id"]}).sort( + [("batch_no", 1), ("sample_no", 1)] + ) + ) + if not sets: + return html.Div(f"No parameter sets found for campaign '{selected_campaign}'."), [], "No parameter sets found.", True, "warning" + + executed_set_ids = set( + mongo.db.images.distinct( + "set_id", + { + "measurement_type": "apis_imaging", + "campaign_id": campaign["_id"], + }, + ) + ) + pending_sets = [doc for doc in sets if doc["_id"] not in executed_set_ids] + + if not pending_sets: + return ( + html.Div(f"All parameter sets for campaign '{selected_campaign}' already have APIS imaging records."), + [], + "No pending APIS imaging sets remain for this campaign.", + True, + "info", + ) + + table_rows = [] + for doc in pending_sets: + table_rows.append( + { + "set_id": str(doc["_id"]), + "batch_no": doc.get("batch_no"), + "sample_no": doc.get("sample_no"), + "polymer_name": doc.get("polymer_name"), + "solvent": doc.get("solvent"), + "concentration": doc.get("concentration"), + "motor_speed": doc.get("motor_speed"), + "temperature": doc.get("temperature"), + "printing_gap": doc.get("printing_gap"), + "precursor_volume": doc.get("precursor_volume"), + } + ) + + table = dash_table.DataTable( + id="ch-apis-sets-table", + data=table_rows, + columns=[ + {"name": "Batch No", "id": "batch_no"}, + {"name": "Sample No", "id": "sample_no"}, + {"name": "Polymer", "id": "polymer_name"}, + {"name": "Solvent", "id": "solvent"}, + {"name": "Conc.", "id": "concentration"}, + {"name": "Speed", "id": "motor_speed"}, + {"name": "Temp", "id": "temperature"}, + {"name": "Gap", "id": "printing_gap"}, + {"name": "Volume", "id": "precursor_volume"}, + ], + selected_rows=list(range(len(table_rows))), + row_selectable="multi", + page_size=12, + style_table={"overflowX": "auto"}, + style_cell={"textAlign": "left"}, + ) + msg = f"Loaded {len(table_rows)} pending APIS imaging sets for campaign '{selected_campaign}'." + return table, table_rows, msg, True, "success" + finally: + mongo.close_connection() + + +@callback( + Output("ch-apis-run-button", "children"), + Input("ch-apis-parameter-sets", "data"), + Input("ch-apis-sets-table", "selected_rows"), + prevent_initial_call=False, +) +def update_run_button_label(parameter_sets, selected_rows): + if not parameter_sets: + return "Run 0 recipes" + num_recipes = len(selected_rows) if selected_rows else 0 + return f"Run {num_recipes} recipe{'s' if num_recipes != 1 else ''}" + + +@callback( + Output("ch-apis-output", "children"), + Output("ch-apis-generated-scripts", "data"), + Input("ch-apis-generate-button", "n_clicks"), + State("ch-apis-sets-table", "selected_rows"), + State("ch-apis-parameter-sets", "data"), + State("ch-apis-campaign-dropdown", "value"), + prevent_initial_call=True, +) +def generate_pending_apis_recipes(n_clicks, selected_rows, parameter_sets, selected_campaign): + if not selected_campaign: + return html.Div("Select a campaign first.", style={"color": "red"}), [] + if not parameter_sets: + return html.Div("No pending parameter sets are available.", style={"color": "red"}), [] + if not selected_rows: + return html.Div("Select at least one parameter set.", style={"color": "red"}), [] + + generated_scripts = [] + for idx in selected_rows: + params = parameter_sets[idx] + sub_dict = { + "campaign_name": selected_campaign, + "batch_no": params["batch_no"], + "sample_no": params["sample_no"], + } + script = CH_APIS_RECIPE_TEMPLATE.safe_substitute(sub_dict) + generated_scripts.append( + { + "batch_no": params["batch_no"], + "sample_no": params["sample_no"], + "script": script, + } + ) + + return ( + html.Div( + [ + html.H4("Generated APIS Imaging Recipes"), + html.Ul( + [ + html.Li( + [ + html.P(f"Batch {entry['batch_no']} Sample {entry['sample_no']}"), + html.Pre( + entry["script"], + style={ + "height": "420px", + "overflowY": "auto", + "backgroundColor": "#f8f9fa", + "padding": "10px", + "border": "1px solid #dee2e6", + }, + ), + ] + ) + for entry in generated_scripts + ] + ), + ] + ), + generated_scripts, + ) + + +@callback( + Output("ch-apis-run-log", "children"), + Input("ch-apis-run-button", "n_clicks"), + State("ch-apis-generated-scripts", "data"), + prevent_initial_call=True, +) +def run_generated_apis_recipes(n_clicks, generated_scripts): + if not generated_scripts: + return html.Div("No generated recipes to run.", style={"color": "red"}) + + log_components = [] + interceptor = ConsoleInterceptor() + + try: + for entry in generated_scripts: + code = entry["script"] + log_components.append(html.H5(f"Running Batch {entry['batch_no']} Sample {entry['sample_no']}")) + + interceptor.start_interception() + try: + exec(code) + status = "Success" + alert_color = "success" + except Exception as exc: + status = f"Error: {str(exc)}" + alert_color = "danger" + finally: + interceptor.stop_interception() + + output = interceptor.get_intercepted_messages() + log_components.extend( + [ + dbc.Alert(status, color=alert_color), + html.Pre( + f"Output:\n{''.join(output)}", + style={ + "backgroundColor": "#f8f9fa", + "padding": "10px", + "border": "1px solid #dee2e6", + "maxHeight": "300px", + "overflowY": "auto", + }, + ), + html.Hr(), + ] + ) + interceptor.intercepted_messages = [] + + return log_components + finally: + del interceptor + + +if __name__ == "__main__": + app.run(debug=True, port=8051) diff --git a/aamp_app/util.py b/aamp_app/util.py index 4099c95..fc15258 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -2,6 +2,8 @@ from commands.utility_commands import LoopStartCommand, LoopEndCommand from devices.heating_stage import HeatingStage from devices.apis import APIS +from devices.sciencetech_uhe_nl_solar_sim import SciencetechUHENLSolarSim +from devices.stellarnet_spectrometer import StellarNetSpectrometer from devices.multi_stepper import MultiStepper from devices.newport_esp301 import NewportESP301 from devices.newport_94043a_solar_sim import Newport94043ASolarSim @@ -12,6 +14,9 @@ from devices.linear_stage_150 import LinearStage150 from devices.mts50_z8 import MTS50_Z8 from devices.p4pp import P4PP +from devices.sonicator import Sonicator +from devices.substrate_hotel import SubstrateHotel +from devices.substrate_dispenser import SubstrateDispenser from devices.z812 import Z812 from devices.keithley_2450 import Keithley2450 from devices.mfc import MassFlowController @@ -24,6 +29,8 @@ from commands.linear_stage_150_commands import * from commands.apis_commands import * +from commands.sciencetech_uhe_nl_solar_sim_commands import * +from commands.stellarnet_spectrometer_commands import * from commands.mts50_z8_commands import * from commands.p4pp_commands import * from commands.z812_commands import * @@ -37,6 +44,9 @@ from commands.heating_stage_commands import * from commands.multi_stepper_commands import * from commands.mfc_commands import * +from commands.sonicator_commands import * +from commands.substrate_hotel_commands import * +from commands.substrate_dispenser_commands import * from commands.sht85_sensor_commands import * from commands.newport_esp301_commands import * from commands.newport_94043a_solar_sim_commands import * @@ -54,7 +64,10 @@ "AnnealingStage": HeatingStage, "MultiStepper1": MultiStepper, "PrinterMotorX": NewportESP301, - # "Spectrometer": StellarNetSpectrometer, + "StellarNetSpectrometer": StellarNetSpectrometer, + "Sonicator": Sonicator, + "SubstrateHotel": SubstrateHotel, + "SubstrateDispenser": SubstrateDispenser, "SampleCamera": XimeaCamera, "DummyHeater1": DummyHeater, "DummyHeater2": DummyHeater, @@ -1789,6 +1802,243 @@ def default(self, obj): }, }, }, + "Sonicator": { + "obj": Sonicator, + "serial": True, + "serial_sequence": ["SonicatorConnect", "SonicatorInitialize"], + "import_device": "from devices.sonicator import Sonicator", + "import_commands": "from commands.sonicator_commands import *", + "init": { + "default_code": "Sonicator(name='Sonicator', port='', baudrate=9600, timeout=0.5, connect_delay_s=3.0, response_timeout_s=3.0, power_probe_timeout_s=5.0, line_terminator='\\n', command_prefix='>', debug_io=False)", + "obj_name": "Sonicator", + "args": { + "name": { + "default": "Sonicator", + "type": str, + "notes": "Name of the device", + }, + "port": { + "default": "COM", + "type": str, + "notes": "Port of the Arduino Uno R3 wrapper", + }, + "baudrate": { + "default": 9600, + "type": int, + "notes": "Baudrate of the device", + }, + "timeout": { + "default": 0.5, + "type": float, + "notes": "Serial readline timeout in seconds.", + }, + "connect_delay_s": { + "default": 3.0, + "type": float, + "notes": "Wait time after opening the serial port.", + }, + "response_timeout_s": { + "default": 3.0, + "type": float, + "notes": "Timeout for status, start, stop, and button commands.", + }, + "power_probe_timeout_s": { + "default": 5.0, + "type": float, + "notes": "Timeout for the intrusive power-probe command.", + }, + "line_terminator": { + "default": "\\n", + "type": str, + "notes": "Line terminator sent after each command keyword.", + }, + "command_prefix": { + "default": ">", + "type": str, + "notes": "Command header character expected by the Arduino sketch.", + }, + "debug_io": { + "default": False, + "type": bool, + "notes": "Print raw TX/RX serial traffic for debugging.", + }, + }, + }, + "commands": { + "SonicatorConnect": { + "default_code": "SonicatorConnect(receiver= '')", + "args": { + "receiver": {"default": "Sonicator", "type": str, "notes": "Name of the device"} + }, + "obj": SonicatorConnect, + }, + "SonicatorInitialize": { + "default_code": "SonicatorInitialize(receiver= '')", + "args": { + "receiver": {"default": "Sonicator", "type": str, "notes": "Name of the device"} + }, + "obj": SonicatorInitialize, + }, + "SonicatorDeinitialize": { + "default_code": "SonicatorDeinitialize(receiver= '', reset_init_flag=True, close_serial=False)", + "args": { + "receiver": {"default": "Sonicator", "type": str, "notes": "Name of the device"}, + "reset_init_flag": {"default": True, "type": bool, "notes": "Reset the initialized flag."}, + "close_serial": {"default": False, "type": bool, "notes": "Close the serial port after deinitialize."}, + }, + "obj": SonicatorDeinitialize, + }, + "SonicatorGetStatus": { + "default_code": "SonicatorGetStatus(receiver= '')", + "args": { + "receiver": {"default": "Sonicator", "type": str, "notes": "Name of the device"} + }, + "obj": SonicatorGetStatus, + }, + "SonicatorStartSonicating": { + "default_code": "SonicatorStartSonicating(receiver= '')", + "args": { + "receiver": {"default": "Sonicator", "type": str, "notes": "Name of the device"} + }, + "obj": SonicatorStartSonicating, + }, + "SonicatorStopSonicating": { + "default_code": "SonicatorStopSonicating(receiver= '')", + "args": { + "receiver": {"default": "Sonicator", "type": str, "notes": "Name of the device"} + }, + "obj": SonicatorStopSonicating, + }, + "SonicatorPressButton": { + "default_code": "SonicatorPressButton(receiver= '')", + "args": { + "receiver": {"default": "Sonicator", "type": str, "notes": "Name of the device"} + }, + "obj": SonicatorPressButton, + }, + "SonicatorProbePowerConnection": { + "default_code": "SonicatorProbePowerConnection(receiver= '')", + "args": { + "receiver": {"default": "Sonicator", "type": str, "notes": "Name of the device"} + }, + "obj": SonicatorProbePowerConnection, + }, + }, + }, + "SubstrateHotel": { + "obj": SubstrateHotel, + "serial": True, + "serial_sequence": ["SubstrateHotelConnect", "SubstrateHotelInitialize"], + "import_device": "from devices.substrate_hotel import SubstrateHotel", + "import_commands": "from commands.substrate_hotel_commands import *", + "init": { + "default_code": "SubstrateHotel(name='SubstrateHotel', port='', baudrate=9600, timeout=1.0, connect_delay_s=5.0, ready_timeout_s=5.0, home_timeout_s=300.0, move_timeout_s=300.0)", + "obj_name": "SubstrateHotel", + "args": { + "name": {"default": "SubstrateHotel", "type": str, "notes": "Name of the device"}, + "port": {"default": "COM", "type": str, "notes": "Arduino serial port"}, + "baudrate": {"default": 9600, "type": int, "notes": "Baudrate of the Arduino controller"}, + "timeout": {"default": 1.0, "type": float, "notes": "Serial readline timeout in seconds"}, + "connect_delay_s": {"default": 5.0, "type": float, "notes": "Delay after opening the serial port to allow Arduino reset"}, + "ready_timeout_s": {"default": 5.0, "type": float, "notes": "Timeout while waiting for the Arduino Ready banner"}, + "home_timeout_s": {"default": 300.0, "type": float, "notes": "Timeout for the homing operation"}, + "move_timeout_s": {"default": 300.0, "type": float, "notes": "Default timeout for absolute moves"}, + }, + }, + "commands": { + "SubstrateHotelConnect": { + "default_code": "SubstrateHotelConnect(receiver= '')", + "args": {"receiver": {"default": "SubstrateHotel", "type": str, "notes": "Name of the device"}}, + "obj": SubstrateHotelConnect, + }, + "SubstrateHotelInitialize": { + "default_code": "SubstrateHotelInitialize(receiver= '')", + "args": {"receiver": {"default": "SubstrateHotel", "type": str, "notes": "Name of the device"}}, + "obj": SubstrateHotelInitialize, + }, + "SubstrateHotelDeinitialize": { + "default_code": "SubstrateHotelDeinitialize(receiver= '', reset_init_flag=True, close_serial=False)", + "args": { + "receiver": {"default": "SubstrateHotel", "type": str, "notes": "Name of the device"}, + "reset_init_flag": {"default": True, "type": bool, "notes": "Reset the initialized flag."}, + "close_serial": {"default": False, "type": bool, "notes": "Close the serial port after deinitialize."}, + }, + "obj": SubstrateHotelDeinitialize, + }, + "SubstrateHotelHome": { + "default_code": "SubstrateHotelHome(receiver= '')", + "args": {"receiver": {"default": "SubstrateHotel", "type": str, "notes": "Name of the device"}}, + "obj": SubstrateHotelHome, + }, + "SubstrateHotelMoveToPosition": { + "default_code": "SubstrateHotelMoveToPosition(receiver= '', position_mm=0.0, speed_mm_per_s=20.0, move_timeout=None)", + "args": { + "receiver": {"default": "SubstrateHotel", "type": str, "notes": "Name of the device"}, + "position_mm": {"default": 0.0, "type": float, "notes": "Absolute target position in mm (0-430)."}, + "speed_mm_per_s": {"default": 20.0, "type": float, "notes": "Requested move speed in mm/s."}, + "move_timeout": {"default": None, "type": float, "notes": "Optional override timeout for this move."}, + }, + "obj": SubstrateHotelMoveToPosition, + }, + }, + }, + "SubstrateDispenser": { + "obj": SubstrateDispenser, + "serial": True, + "serial_sequence": ["SubstrateDispenserConnect", "SubstrateDispenserInitialize"], + "import_device": "from devices.substrate_dispenser import SubstrateDispenser", + "import_commands": "from commands.substrate_dispenser_commands import *", + "init": { + "default_code": "SubstrateDispenser(name='SubstrateDispenser', port='', baudrate=9600, timeout=1.0, connect_delay_s=5.0, ready_timeout_s=5.0, home_timeout_s=30.0, move_timeout_s=30.0)", + "obj_name": "SubstrateDispenser", + "args": { + "name": {"default": "SubstrateDispenser", "type": str, "notes": "Name of the device"}, + "port": {"default": "COM", "type": str, "notes": "Arduino serial port"}, + "baudrate": {"default": 9600, "type": int, "notes": "Baudrate of the Arduino controller"}, + "timeout": {"default": 1.0, "type": float, "notes": "Serial readline timeout in seconds"}, + "connect_delay_s": {"default": 5.0, "type": float, "notes": "Delay after opening the serial port to allow Arduino reset"}, + "ready_timeout_s": {"default": 5.0, "type": float, "notes": "Timeout while waiting for the Arduino Ready banner"}, + "home_timeout_s": {"default": 30.0, "type": float, "notes": "Timeout for the homing operation"}, + "move_timeout_s": {"default": 30.0, "type": float, "notes": "Default timeout for absolute moves"}, + }, + }, + "commands": { + "SubstrateDispenserConnect": { + "default_code": "SubstrateDispenserConnect(receiver= '')", + "args": {"receiver": {"default": "SubstrateDispenser", "type": str, "notes": "Name of the device"}}, + "obj": SubstrateDispenserConnect, + }, + "SubstrateDispenserInitialize": { + "default_code": "SubstrateDispenserInitialize(receiver= '')", + "args": {"receiver": {"default": "SubstrateDispenser", "type": str, "notes": "Name of the device"}}, + "obj": SubstrateDispenserInitialize, + }, + "SubstrateDispenserDeinitialize": { + "default_code": "SubstrateDispenserDeinitialize(receiver= '', reset_init_flag=True, close_serial=False)", + "args": { + "receiver": {"default": "SubstrateDispenser", "type": str, "notes": "Name of the device"}, + "reset_init_flag": {"default": True, "type": bool, "notes": "Reset the initialized flag."}, + "close_serial": {"default": False, "type": bool, "notes": "Close the serial port after deinitialize."}, + }, + "obj": SubstrateDispenserDeinitialize, + }, + "SubstrateDispenserHome": { + "default_code": "SubstrateDispenserHome(receiver= '')", + "args": {"receiver": {"default": "SubstrateDispenser", "type": str, "notes": "Name of the device"}}, + "obj": SubstrateDispenserHome, + }, + "SubstrateDispenserMoveToPosition": { + "default_code": "SubstrateDispenserMoveToPosition(receiver= '', position_mm=0.0, speed_mm_per_s=20.0, move_timeout=None)", + "args": { + "receiver": {"default": "SubstrateDispenser", "type": str, "notes": "Name of the device"}, + "position_mm": {"default": 0.0, "type": float, "notes": "Absolute target position in mm (0-45)."}, + "speed_mm_per_s": {"default": 20.0, "type": float, "notes": "Requested move speed in mm/s."}, + "move_timeout": {"default": None, "type": float, "notes": "Optional override timeout for this move."}, + }, + "obj": SubstrateDispenserMoveToPosition, + }, + }, + }, "MassFlowController": { "obj": MassFlowController, "serial": True, @@ -2301,6 +2551,334 @@ def default(self, obj): }, }, }, + "SciencetechUHENLSolarSim": { + "obj": SciencetechUHENLSolarSim, + "serial": True, + "serial_sequence": ["SciencetechUHENLSolarSimConnect", "SciencetechUHENLSolarSimInitialize"], + "import_device": "from devices.sciencetech_uhe_nl_solar_sim import SciencetechUHENLSolarSim", + "import_commands": "from commands.sciencetech_uhe_nl_solar_sim_commands import *", + "init": { + "default_code": "SciencetechUHENLSolarSim(name='SciencetechUHENLSolarSim', port='', baudrate=9600, timeout=2.0, line_terminator='\\r', connect_delay_s=3.0, command_delay_s=8.0, status_timeout_s=8.0, default_current_percent=85.0, default_attenuator_percent=100, debug_io=False)", + "obj_name": "SciencetechUHENLSolarSim", + "args": { + "name": { + "default": "SciencetechUHENLSolarSim", + "type": str, + "notes": "Name of the device", + }, + "port": { + "default": "COM", + "type": str, + "notes": "COM port via the UHE-NL RS-232 to USB cable", + }, + "baudrate": { + "default": 9600, + "type": int, + "notes": "Serial baudrate", + }, + "timeout": { + "default": 2.0, + "type": float, + "notes": "Serial timeout in seconds", + }, + "line_terminator": { + "default": "\r", + "type": str, + "notes": "Command line terminator", + }, + "connect_delay_s": { + "default": 3.0, + "type": float, + "notes": "Delay after opening the serial port", + }, + "command_delay_s": { + "default": 8.0, + "type": float, + "notes": "Delay after each control command before polling feedback", + }, + "status_timeout_s": { + "default": 8.0, + "type": float, + "notes": "Timeout while reading the multiline status response", + }, + "default_current_percent": { + "default": 85.0, + "type": float, + "notes": "Output percentage setpoint applied during initialize after cooling is confirmed on", + }, + "default_attenuator_percent": { + "default": 100, + "type": int, + "notes": "Attenuator transmission percentage applied during initialize", + }, + "debug_io": { + "default": False, + "type": bool, + "notes": "Print raw TX/RX serial traffic for debugging", + }, + }, + }, + "commands": { + "SciencetechUHENLSolarSimConnect": { + "default_code": "SciencetechUHENLSolarSimConnect(receiver= '')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"} + }, + "obj": SciencetechUHENLSolarSimConnect, + }, + "SciencetechUHENLSolarSimInitialize": { + "default_code": "SciencetechUHENLSolarSimInitialize(receiver= '')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"} + }, + "obj": SciencetechUHENLSolarSimInitialize, + }, + "SciencetechUHENLSolarSimDeinitialize": { + "default_code": "SciencetechUHENLSolarSimDeinitialize(receiver= '', reset_init_flag=True, close_serial=False)", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"}, + "reset_init_flag": {"default": True, "type": bool, "notes": "Reset initialized flag"}, + "close_serial": {"default": False, "type": bool, "notes": "Close the serial port"}, + }, + "obj": SciencetechUHENLSolarSimDeinitialize, + }, + "SciencetechUHENLSolarSimCloseShutter": { + "default_code": "SciencetechUHENLSolarSimCloseShutter(receiver= '')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"} + }, + "obj": SciencetechUHENLSolarSimCloseShutter, + }, + "SciencetechUHENLSolarSimOpenShutter": { + "default_code": "SciencetechUHENLSolarSimOpenShutter(receiver= '')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"} + }, + "obj": SciencetechUHENLSolarSimOpenShutter, + }, + "SciencetechUHENLSolarSimEnableCooling": { + "default_code": "SciencetechUHENLSolarSimEnableCooling(receiver= '')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"} + }, + "obj": SciencetechUHENLSolarSimEnableCooling, + }, + "SciencetechUHENLSolarSimDisableCooling": { + "default_code": "SciencetechUHENLSolarSimDisableCooling(receiver= '')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"} + }, + "obj": SciencetechUHENLSolarSimDisableCooling, + }, + "SciencetechUHENLSolarSimEnableArcLamp": { + "default_code": "SciencetechUHENLSolarSimEnableArcLamp(receiver= '')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"} + }, + "obj": SciencetechUHENLSolarSimEnableArcLamp, + }, + "SciencetechUHENLSolarSimDisableArcLamp": { + "default_code": "SciencetechUHENLSolarSimDisableArcLamp(receiver= '')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"} + }, + "obj": SciencetechUHENLSolarSimDisableArcLamp, + }, + "SciencetechUHENLSolarSimOpenAttenuator": { + "default_code": "SciencetechUHENLSolarSimOpenAttenuator(receiver= '')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"} + }, + "obj": SciencetechUHENLSolarSimOpenAttenuator, + }, + "SciencetechUHENLSolarSimSetAttenuator": { + "default_code": "SciencetechUHENLSolarSimSetAttenuator(receiver= '', percent=100)", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"}, + "percent": {"default": 100, "type": int, "notes": "Attenuator transmission percentage"}, + }, + "obj": SciencetechUHENLSolarSimSetAttenuator, + }, + "SciencetechUHENLSolarSimSetCurrent": { + "default_code": "SciencetechUHENLSolarSimSetCurrent(receiver= '', percent=85.0)", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"}, + "percent": {"default": 85.0, "type": float, "notes": "Lamp output percentage setpoint"}, + }, + "obj": SciencetechUHENLSolarSimSetCurrent, + }, + "SciencetechUHENLSolarSimGetStatus": { + "default_code": "SciencetechUHENLSolarSimGetStatus(receiver= '')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"} + }, + "obj": SciencetechUHENLSolarSimGetStatus, + }, + "SciencetechUHENLSolarSimGetFeedback": { + "default_code": "SciencetechUHENLSolarSimGetFeedback(receiver= '', feedback_type='lamp')", + "args": { + "receiver": {"default": "SciencetechUHENLSolarSim", "type": str, "notes": "Name of the device"}, + "feedback_type": {"default": "lamp", "type": str, "notes": "Feedback key such as lamp, cool, shutter, attenuator, output, current, voltage, power, hours"}, + }, + "obj": SciencetechUHENLSolarSimGetFeedback, + }, + }, + }, + "StellarNetSpectrometer": { + "obj": StellarNetSpectrometer, + "serial": False, + "serial_sequence": ["SpectrometerInitialize"], + "import_device": "from devices.stellarnet_spectrometer import StellarNetSpectrometer", + "import_commands": "from commands.stellarnet_spectrometer_commands import *", + "init": { + "default_code": "StellarNetSpectrometer(name='StellarNetSpectrometer', spec_keys=['UV-Vis'], save_directory='data/spectroscopy/', default_integration_time=(100,))", + "obj_name": "StellarNetSpectrometer", + "args": { + "name": { + "default": "StellarNetSpectrometer", + "type": str, + "notes": "Name of the device", + }, + "spec_keys": { + "default": ["UV-Vis"], + "type": list, + "notes": "Declared spectrometer keys. Common values are ['UV-Vis'] or ['UV-Vis', 'NIR'].", + }, + "save_directory": { + "default": "data/spectroscopy/", + "type": str, + "notes": "Directory used when absorbance CSV files are saved.", + }, + "default_integration_time": { + "default": (100,), + "type": tuple, + "notes": "Default integration time in ms for each declared spectrometer.", + }, + }, + }, + "commands": { + "SpectrometerInitialize": { + "default_code": "SpectrometerInitialize(receiver= '')", + "args": { + "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"} + }, + "obj": SpectrometerInitialize, + }, + "SpectrometerDeinitialize": { + "default_code": "SpectrometerDeinitialize(receiver= '', reset_init_flag=True)", + "args": { + "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, + "reset_init_flag": {"default": True, "type": bool, "notes": "Reset the initialized flag."}, + }, + "obj": SpectrometerDeinitialize, + }, + "SpectrometerUpdateDark": { + "default_code": "SpectrometerUpdateDark(receiver= '', integration_times=(100,), scans_to_avg=(3,), smoothings=(0,), xtimings=(1,))", + "args": { + "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, + "integration_times": {"default": (100,), "type": tuple, "notes": "Integration times in ms for each declared spectrometer."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + }, + "obj": SpectrometerUpdateDark, + }, + "SpectrometerUpdateBlank": { + "default_code": "SpectrometerUpdateBlank(receiver= '', integration_times=(100,), scans_to_avg=(3,), smoothings=(0,), xtimings=(1,))", + "args": { + "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, + "integration_times": {"default": (100,), "type": tuple, "notes": "Integration times in ms for each declared spectrometer."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + }, + "obj": SpectrometerUpdateBlank, + }, + "SpectrometerAdjDefIntegrationTime": { + "default_code": "SpectrometerAdjDefIntegrationTime(receiver= '', scans_to_avg=(3,), smoothings=(0,), xtimings=(1,), target_max_count=52000, tolerance=2000)", + "args": { + "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + "target_max_count": {"default": 52000, "type": int, "notes": "Target detector max count used to tune the default integration time."}, + "tolerance": {"default": 2000, "type": int, "notes": "Allowed deviation from the target max count."}, + }, + "obj": SpectrometerAdjDefIntegrationTime, + }, + "SpectrometerGetAbsorbance": { + "default_code": "SpectrometerGetAbsorbance(receiver= '', save_to_file=True, filename=None, integration_times=(100,), scans_to_avg=(3,), smoothings=(0,), xtimings=(1,))", + "args": { + "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, + "save_to_file": {"default": True, "type": bool, "notes": "Save absorbance CSV output to the spectrometer save directory."}, + "filename": {"default": None, "type": str, "notes": "Optional output filename prefix. Uses a timestamp when omitted."}, + "integration_times": {"default": (100,), "type": tuple, "notes": "Integration times in ms for each declared spectrometer."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + }, + "obj": SpectrometerGetAbsorbance, + }, + "SpectrometerGetAbsorbancebyname": { + "default_code": "SpectrometerGetAbsorbancebyname(receiver= '', sample_name='sample', save_to_file=True, repeat_measure=False, integration_times=None, scans_to_avg=(3,), smoothings=(0,), xtimings=(1,), absorbance_threshold=0.003)", + "args": { + "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, + "sample_name": {"default": "sample", "type": str, "notes": "Sample name used to build absorbance filenames."}, + "save_to_file": {"default": True, "type": bool, "notes": "Save absorbance CSV output to the spectrometer save directory."}, + "repeat_measure": {"default": False, "type": bool, "notes": "Retained for compatibility with the older workflow."}, + "integration_times": {"default": None, "type": tuple, "notes": "Optional integration times in ms. Uses the device default integration time when omitted."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + "absorbance_threshold": {"default": 0.003, "type": float, "notes": "Compatibility placeholder for the older repeat-measure workflow."}, + }, + "obj": SpectrometerGetAbsorbancebyname, + }, + "SpectrometerGetPhotoncountsbyname": { + "default_code": "SpectrometerGetPhotoncountsbyname(receiver= '', sample_name='sample', save_to_file=True, repeat_measure=False, integration_times=None, scans_to_avg=(3,), smoothings=(0,), xtimings=(1,), absorbance_threshold=0.003)", + "args": { + "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, + "sample_name": {"default": "sample", "type": str, "notes": "Sample name used to build photon count filenames."}, + "save_to_file": {"default": True, "type": bool, "notes": "Save photon count CSV output to the spectrometer save directory."}, + "repeat_measure": {"default": False, "type": bool, "notes": "Retained for compatibility with the older workflow."}, + "integration_times": {"default": None, "type": tuple, "notes": "Optional integration times in ms. Uses the device default integration time when omitted."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + "absorbance_threshold": {"default": 0.003, "type": float, "notes": "Compatibility placeholder for the older repeat-measure workflow."}, + }, + "obj": SpectrometerGetPhotoncountsbyname, + }, + "SpectrometerGetSpecDecay": { + "default_code": "SpectrometerGetSpecDecay(receiver= '', sample_name='sample', save_to_file=True, range_start=290.0, range_end=800.0, irradiance_file='reference/am15g_spectrum.csv', Wvlgth_col_name='wavelength_nm', Irrad_col_name='irradiance_w_m2_nm', decay_threshold=0.01)", + "args": { + "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, + "sample_name": {"default": "sample", "type": str, "notes": "Sample name used to find the saved merged absorbance file."}, + "save_to_file": {"default": True, "type": bool, "notes": "Save the spectral decay CSV to the spectrometer save directory."}, + "range_start": {"default": 290.0, "type": float, "notes": "Start wavelength for spectral decay calculation."}, + "range_end": {"default": 800.0, "type": float, "notes": "End wavelength for spectral decay calculation."}, + "irradiance_file": {"default": "reference/am15g_spectrum.csv", "type": str, "notes": "Reference irradiance CSV stored relative to the spectrometer save directory."}, + "Wvlgth_col_name": {"default": "wavelength_nm", "type": str, "notes": "Irradiance-table wavelength column name."}, + "Irrad_col_name": {"default": "irradiance_w_m2_nm", "type": str, "notes": "Irradiance-table irradiance column name."}, + "decay_threshold": {"default": 0.01, "type": float, "notes": "Reference absorbance threshold used when computing decay indices."}, + }, + "obj": SpectrometerGetSpecDecay, + }, + "SpectrometerPlotSpecDecaySummary": { + "default_code": "SpectrometerPlotSpecDecaySummary(receiver= '', sample_name='sample', range_start=290.0, range_end=800.0, save_to_file=True, output_filename=None, figure_dpi=180)", + "args": { + "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, + "sample_name": {"default": "sample", "type": str, "notes": "Sample name used to find the saved specdecay and QC files."}, + "range_start": {"default": 290.0, "type": float, "notes": "Start wavelength used for the absorbance snapshot panel."}, + "range_end": {"default": 800.0, "type": float, "notes": "End wavelength used for the absorbance snapshot panel."}, + "save_to_file": {"default": True, "type": bool, "notes": "Save the summary figure as PNG in the spectrometer save directory."}, + "output_filename": {"default": None, "type": str, "notes": "Optional output PNG filename. Relative paths are resolved against the spectrometer save directory."}, + "figure_dpi": {"default": 180, "type": int, "notes": "PNG export resolution."}, + }, + "obj": SpectrometerPlotSpecDecaySummary, + }, + }, + }, "DummyMotor": { "obj": DummyMotor, "default_obj": DummyMotor(name="DummyMotor", speed=20.0), diff --git a/device_ports.md b/device_ports.md index c130482..92d96ef 100644 --- a/device_ports.md +++ b/device_ports.md @@ -12,21 +12,23 @@ This file tracks the current local serial port assignments and related connectio | Device Name | Model / Controller | Vendor | Port | Baudrate | Notes | Related Files | | --- | --- | --- | --- | --- | --- | --- | -| `esp301_3n` | `ESP301-3N` with `axis1=ILS100CC`, `axis2=UTS100PP`, `axis3=PR50PP` | Newport | `COM6` | `921600` | Axis homing config: `OR4 / OR4 / OR1` | `examples/example_esp301_3n.py`, `recipes/recipe_sample.py`, `aamp_app/util.py` | +| `esp301_3n` | `ESP301-3N` with `axis1=ILS100CC`, `axis2=UTS100PP`, `axis3=PR50PP` | Newport | `COM6` | `921600` | Axis homing config: `OR4 / OR4 / OR1`; axis 3 is also used for the UV-Vis film degradation chamber workflow | `examples/example_esp301_3n.py`, `examples/example_uvvis_film_absorbance_degradation.py`, `recipes/recipe_sample.py`, `aamp_app/util.py` | | `heater` | `HeatingStage` Arduino controller | Custom | `COM16` | `115200` | Smoke test updated locally; supports non-blocking setpoint command and explicit wait-for-temperature hold check | `examples/example_heating_stage.py`, `aamp_app/util.py`, `aamp_app/devices/heating_stage.py` | | `linear_stage_150` | `LTS150` / `LinearStage150` | Thorlabs | `COM15` | `115200` | Smoke test updated locally to use `COM15`; initial absolute move tested at `100 mm` | `examples/example_linear_stage_150.py`, `aamp_app/util.py` | | `z812` | `Z812` with `KDC101` | Thorlabs | `COM12` | `115200` | Smoke test passed locally with absolute move `8 mm` and relative move `3 mm` | `examples/example_z812.py`, `aamp_app/util.py`, `aamp_app/devices/z812.py` | | `newport_94043a_solar_sim` | `94043A` solar simulator via `69920` power supply | Newport | `COM11` | `9600` | RS-232 via USB adapter; default control path is power mode; initialize applies `400 W` preset and software blocks presets above `450 W`; lamp replacement warning starts at `1000 h` | `examples/example_94043a_solar_sim.py`, `aamp_app/util.py`, `aamp_app/devices/newport_94043a_solar_sim.py` | | `p4pp` | `P4PP` Arduino controller | PolyPrint Illinois | `COM19` | `115200` | Rotation is blocked when linear position is `>= 45.0 mm`; explicit measurement resistor selection supported with default `681 ohm`; measurement default cycles set to `20` | `examples/example_p4pp.py`, `aamp_app/util.py`, `aamp_app/devices/p4pp.py` | | `apis` | `APIS` Arduino controller + Ximea camera | PolyPrint Illinois | `COM8` | `9600` | Composite device for stage control and imaging; default image root is `data/imaging/`; current example writes mode-organized outputs under `data/imaging/demo_campaign/` | `examples/example_apis.py`, `aamp_app/util.py`, `aamp_app/devices/apis.py` | - -## To Fill Later - -| Device Name | Model / Controller | Vendor | Port | Baudrate | Notes | Related Files | -| --- | --- | --- | --- | --- | --- | --- | -| | | | | | | | +| `uhe_nl` | `UHE-NL 7 Sun` solar simulator power control | Sciencetech | `COM4` | `9600` | `initialize` requires lamp OFF and enables cooling first; lamp enable is blocked unless cooling feedback is on; `deinitialize` turns lamp off but leaves cooling on for cooldown | `examples/example_sciencetech_uhe_nl_solar_sim.py`, `aamp_app/util.py`, `aamp_app/devices/sciencetech_uhe_nl_solar_sim.py` | +| `stellarnet_uv_vis` | `StellarNet UV-Vis spectrometer` | StellarNet | `USB` | `n/a` | Vendor Python driver required in the environment; current local examples cover manual dark/blank/sample acquisition and ESP301-driven film degradation loops; spectral decay now uses `reference/am15g_spectrum.csv` | `examples/example_stellarnet_spectrometer.py`, `examples/example_uvvis_film_absorbance_degradation.py`, `aamp_app/devices/stellarnet_spectrometer.py` | +| `sonicator` | `Arduino Uno R3` wrapper for sonicator front-panel button/status lines | Custom | `COM13` | `9600` | Expected wiring is `5V`, `GND`, `D7` button drive, and `D8` status sense; Python smoke test passed locally; explicit power probe is intrusive and not used during initialize | `examples/example_sonicator.py`, `aamp_app/devices/sonicator.py`, `aamp_app/util.py`, `firmware/sonicator/sonicator_uno_r3/sonicator_uno_r3.ino` | +| `substrate_hotel` | `Arduino linear stage` for substrate hotel | Custom | `COM10` | `9600` | Current local assignment; protocol expects `Ready`, `H`, and `M{position},{speed}`; current Python guard range is `0-430 mm`; default homing and move timeouts are `300 s` | `examples/example_substrate_hotel.py`, `aamp_app/devices/substrate_hotel.py`, `aamp_app/util.py` | +| `substrate_dispenser` | `Arduino linear stage` for substrate dispenser | Custom | `COM7` | `9600` | Current local assignment; protocol expects `Ready`, `H`, and `M{position},{speed}`; current Python guard range is `0-45 mm` | `examples/example_substrate_dispenser.py`, `aamp_app/devices/substrate_dispenser.py`, `aamp_app/util.py` | ## Storage Notes - `APIS` image outputs default to `data/imaging/` - `P4PP` measurement CSV outputs default to `data/resistance/p4pp_measurements.csv` +- `StellarNetSpectrometer` outputs default to `data/spectroscopy/` +- StellarNet spectral-decay reference data is stored in `data/spectroscopy/reference/am15g_spectrum.csv` +- StellarNet repeated absorbance QC is appended to `sample_name_measurement_qc_log.csv` in `data/spectroscopy/` diff --git a/examples/.gitignore b/examples/.gitignore index 082f61f..d8416f3 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -14,6 +14,13 @@ !example_94043a_solar_sim.py !example_p4pp.py !example_apis.py +!example_apis_from_mongodb.py +!example_stellarnet_spectrometer.py +!example_uvvis_film_absorbance_degradation.py +!example_sciencetech_uhe_nl_solar_sim.py +!example_sonicator.py +!example_substrate_hotel.py +!example_substrate_dispenser.py !Figure_1.png !Figure_2.png !Figure_3.png diff --git a/examples/example_apis_from_mongodb.py b/examples/example_apis_from_mongodb.py new file mode 100644 index 0000000..a74ac84 --- /dev/null +++ b/examples/example_apis_from_mongodb.py @@ -0,0 +1,349 @@ +# APIS imaging workflow example backed by MongoDB parameter sets. +# Run from repo root using: +# python -m examples.example_apis_from_mongodb + +import os +import re +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, List, Tuple + +from gridfs import GridFS + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_invoker import CommandInvoker +from command_sequence import CommandSequence +from commands.apis_commands import * +from devices.apis import APIS +from mongodb_helper import MongoDBHelper + + +APIS_PORT = "COM8" +ROOT_SAVE_DIR = os.path.join("data", "imaging") +XPL_EXPOSURE_US = 50000 +PPL_EXPOSURE_US = 18000 +SAMPLE_ANGLES_DEG = [90, 60, 45, 30, 0] + + +def load_env(file_path: Path = ROOT_DIR / ".env") -> None: + if file_path.exists(): + with open(file_path, "r", encoding="utf-8") as file: + for line in file: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + + +def sanitize_path_component(value: str) -> str: + sanitized = re.sub(r'[<>:"/\\|?*]+', "_", value.strip()) + return sanitized or "unnamed_campaign" + + +def format_display_value(value): + if isinstance(value, float) and value.is_integer(): + return int(value) + return value + + +def get_mongo_helper() -> MongoDBHelper: + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") + if not mongo_uri or not mongo_db_name: + raise RuntimeError("MONGO_URI and MONGO_DB_NAME must be set in .env to use this example.") + return MongoDBHelper(mongo_uri, mongo_db_name) + + +def fetch_parameter_set( + mongo: MongoDBHelper, + campaign_name: str, + round_num: int, + sample_num: int, +) -> Tuple[dict, dict]: + campaign_doc = mongo.db["campaigns"].find_one({"campaign_name": campaign_name}) + if campaign_doc is None: + raise LookupError(f"Campaign '{campaign_name}' was not found in the campaigns collection.") + + set_doc = mongo.db["sets"].find_one( + { + "campaign_id": campaign_doc["_id"], + "batch_no": round_num, + "sample_no": sample_num, + } + ) + if set_doc is None: + available_samples = sorted( + mongo.db["sets"].distinct( + "sample_no", + { + "campaign_id": campaign_doc["_id"], + "batch_no": round_num, + }, + ) + ) + if available_samples: + raise LookupError( + f"No parameter set found for campaign='{campaign_name}', round(batch_no)={round_num}, " + f"sample_no={sample_num}. Available sample_no values for that round: {available_samples}" + ) + available_rounds = sorted( + mongo.db["sets"].distinct( + "batch_no", + {"campaign_id": campaign_doc["_id"]}, + ) + ) + raise LookupError( + f"No parameter set found for campaign='{campaign_name}', round(batch_no)={round_num}, " + f"sample_no={sample_num}. Available rounds(batch_no): {available_rounds}" + ) + return campaign_doc, set_doc + + +def build_sample_params(round_num: int, sample_num: int, set_doc: dict) -> Dict[str, object]: + return { + "polymer": format_display_value(set_doc["polymer_name"]), + "round_num": round_num, + "sample_num": sample_num, + "temperature": format_display_value(set_doc["temperature"]), + "speed": float(set_doc["motor_speed"]), + "gap": format_display_value(set_doc["printing_gap"]), + "solvent": format_display_value(set_doc["solvent"]), + "concentration": format_display_value(set_doc["concentration"]), + "volume": format_display_value(set_doc["precursor_volume"]), + } + + +def add_mode_capture_commands( + seq: CommandSequence, + apis: APIS, + mode_name: str, + polarizer_angle: float, + sample_angles: List[float], + exposure_time: int, + base_name: str, + save_dir: str, + capture_records: List[dict], +) -> None: + seq.add_command(APISRotatePolarizer(apis, angle_deg=polarizer_angle)) + for angle in sample_angles: + seq.add_command(APISRotateSample(apis, angle_deg=angle)) + filename = apis.build_mode_filename(base_name, mode_name, angle) + raw16_path = os.path.join(save_dir, "raw16", filename + ".tif") + rgb_path = os.path.join(save_dir, "rgb", filename + "_rgb.tif") + capture_records.append( + { + "mode": mode_name, + "image_kind": "raw16", + "sample_angle_deg": angle, + "local_path": raw16_path, + } + ) + capture_records.append( + { + "mode": mode_name, + "image_kind": "rgb", + "sample_angle_deg": angle, + "local_path": rgb_path, + } + ) + seq.add_command( + APISCaptureRaw16( + apis, + filename=filename, + directory=save_dir, + exposure_time=exposure_time, + gain=0.0, + ) + ) + seq.add_command( + APISConvertRaw16ToRgb( + apis, + raw16_path=raw16_path, + rgb_path=rgb_path, + ) + ) + + +def upload_capture_records_to_mongodb( + mongo: MongoDBHelper, + campaign_doc: dict, + set_doc: dict, + capture_records: List[dict], +) -> Tuple[int, int]: + fs = GridFS(mongo.db) + uploaded_count = 0 + skipped_count = 0 + for record in capture_records: + local_path = record["local_path"] + if not os.path.isfile(local_path): + skipped_count += 1 + continue + + with open(local_path, "rb") as file_obj: + file_id = fs.put(file_obj, filename=os.path.basename(local_path)) + + metadata = { + "campaign_id": campaign_doc["_id"], + "campaign_name": campaign_doc["campaign_name"], + "set_id": set_doc["_id"], + "batch_no": set_doc.get("batch_no"), + "sample_no": set_doc.get("sample_no"), + "polymer_name": set_doc.get("polymer_name"), + "solvent": set_doc.get("solvent"), + "concentration": set_doc.get("concentration"), + "motor_speed": set_doc.get("motor_speed"), + "temperature": set_doc.get("temperature"), + "printing_gap": set_doc.get("printing_gap"), + "precursor_volume": set_doc.get("precursor_volume"), + "mode": record["mode"], + "image_kind": record["image_kind"], + "sample_angle_deg": record["sample_angle_deg"], + "file_id": file_id, + "filename": os.path.basename(local_path), + "relative_local_path": os.path.relpath(local_path, ROOT_DIR), + "measurement_type": "apis_imaging", + "uploaded_at": datetime.now(timezone.utc), + "source": "example_apis_from_mongodb", + } + mongo.db["images"].insert_one(metadata) + uploaded_count += 1 + + return uploaded_count, skipped_count + + +def main() -> None: + print("=== APIS Imaging From MongoDB Parameter Sets ===") + print("This example resolves sample parameters from MongoDB using campaign name, round number, and sample number.") + print("Round number is matched to MongoDB field `batch_no`.") + print("Sample number is matched to MongoDB field `sample_no`.") + + load_env() + mongo = get_mongo_helper() + + campaign_name = input("Enter campaign name: ").strip() + round_num = int(input("Enter round number (MongoDB batch_no): ").strip()) + sample_num = int(input("Enter sample number (MongoDB sample_no): ").strip()) + + try: + campaign_doc, set_doc = fetch_parameter_set(mongo, campaign_name, round_num, sample_num) + except LookupError as exc: + print(f"\nParameter lookup failed: {exc}") + mongo.close_connection() + return + + params = build_sample_params(round_num, sample_num, set_doc) + + print("\n=== Resolved Sample Parameters ===") + for key, value in params.items(): + print(f"{key}: {value}") + + base_sample_name = APIS.build_sample_basename( + round_num=params["round_num"], + sample_num=params["sample_num"], + polymer=params["polymer"], + solvent=params["solvent"], + concentration=params["concentration"], + speed=params["speed"], + temperature=params["temperature"], + gap=params["gap"], + volume=params["volume"], + ) + root_save_dir = os.path.join(ROOT_SAVE_DIR, sanitize_path_component(campaign_name)) + print(f"\nGenerated filename prefix: {base_sample_name}") + print(f"Local save root: {root_save_dir}") + print("Mode folders will use `xpl/` and `ppl/`.") + print("Each capture will save RAW16 first, then convert that file to RGB.") + + upload_after_capture = ( + input("\nUpload captured files to MongoDB GridFS after local imaging? (y/n): ").strip().lower() == "y" + ) + confirm = input("Proceed with imaging using these parameters? (y/n): ").strip().lower() + if confirm != "y": + print("Imaging cancelled.") + mongo.close_connection() + return + + print("\nInitializing hardware...") + apis = APIS( + name="apis", + port=APIS_PORT, + baudrate=9600, + timeout=0.5, + connection_wait_s=2.0, + settling_time_s=1.5, + command_delay_s=0.05, + max_retries=3, + use_camera=True, + camera_save_directory=root_save_dir, + camera_bayer_pattern="GBRG", + camera_raw_max_value=1023.0, + ) + xpl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer"], "xpl") + ppl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer"], "ppl") + + capture_records: List[dict] = [] + seq = CommandSequence() + seq.add_device(apis) + + seq.add_command(APISConnect(apis)) + seq.add_command(APISInitialize(apis)) + seq.add_command(APISHome(apis)) + + add_mode_capture_commands( + seq=seq, + apis=apis, + mode_name="xpl", + polarizer_angle=90.0, + sample_angles=SAMPLE_ANGLES_DEG, + exposure_time=XPL_EXPOSURE_US, + base_name=base_sample_name, + save_dir=xpl_dir, + capture_records=capture_records, + ) + add_mode_capture_commands( + seq=seq, + apis=apis, + mode_name="ppl", + polarizer_angle=0.0, + sample_angles=SAMPLE_ANGLES_DEG, + exposure_time=PPL_EXPOSURE_US, + base_name=base_sample_name, + save_dir=ppl_dir, + capture_records=capture_records, + ) + + seq.add_command(APISRotatePolarizer(apis, angle_deg=0.0)) + seq.add_command(APISRotateSample(apis, angle_deg=0.0)) + seq.add_command(APISDeinitialize(apis)) + + print("\nRunning imaging sequence...") + invoker = CommandInvoker(seq, False) + result = invoker.invoke_commands() + print(f"\nImaging finished. Result: {result}") + + if result and upload_after_capture: + uploaded_count, skipped_count = upload_capture_records_to_mongodb( + mongo=mongo, + campaign_doc=campaign_doc, + set_doc=set_doc, + capture_records=capture_records, + ) + print( + f"Uploaded {uploaded_count} files to GridFS and inserted matching metadata into `images`." + ) + if skipped_count: + print(f"Skipped {skipped_count} expected files because they were not found on disk.") + elif upload_after_capture: + print("Skipping MongoDB upload because imaging did not complete successfully.") + + mongo.close_connection() + + +if __name__ == "__main__": + main() diff --git a/examples/example_sciencetech_uhe_nl_solar_sim.py b/examples/example_sciencetech_uhe_nl_solar_sim.py new file mode 100644 index 0000000..9fce551 --- /dev/null +++ b/examples/example_sciencetech_uhe_nl_solar_sim.py @@ -0,0 +1,84 @@ +# Sciencetech UHE-NL solar simulator smoke test +# run from root using 'python -m examples.example_sciencetech_uhe_nl_solar_sim' + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_invoker import CommandInvoker +from command_sequence import CommandSequence +from devices.sciencetech_uhe_nl_solar_sim import SciencetechUHENLSolarSim +from commands.sciencetech_uhe_nl_solar_sim_commands import * + + +def main() -> None: + port = input("Enter the UHE-NL COM port (for example COM7): ").strip() + if not port: + print("No COM port provided. Exiting.") + return + + confirm = input( + "This test will ignite the lamp, move the shutter, and change the attenuator. Type 'y' to continue: " + ).strip().lower() + if confirm != "y": + print("Smoke test cancelled.") + return + + simulator = SciencetechUHENLSolarSim( + name="uhe_nl", + port=port, + baudrate=9600, + timeout=2.0, + connect_delay_s=3.0, + command_delay_s=8.0, + status_timeout_s=8.0, + default_current_percent=85.0, + default_attenuator_percent=100, + debug_io=False, + ) + + seq = CommandSequence() + seq.add_device(simulator) + seq.add_command(SciencetechUHENLSolarSimConnect(simulator)) + seq.add_command(SciencetechUHENLSolarSimInitialize(simulator)) + seq.add_command(SciencetechUHENLSolarSimGetStatus(simulator)) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="cool")) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="lamp")) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="shutter")) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="attenuator")) + seq.add_command(SciencetechUHENLSolarSimCloseShutter(simulator)) + seq.add_command(SciencetechUHENLSolarSimSetAttenuator(simulator, percent=75)) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="attenuator")) + seq.add_command(SciencetechUHENLSolarSimEnableArcLamp(simulator)) + seq.add_command(SciencetechUHENLSolarSimGetStatus(simulator)) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="lamp")) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="output")) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="current")) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="voltage")) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="power")) + seq.add_command(SciencetechUHENLSolarSimOpenShutter(simulator)) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="shutter")) + seq.add_command(SciencetechUHENLSolarSimCloseShutter(simulator)) + seq.add_command(SciencetechUHENLSolarSimOpenAttenuator(simulator)) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="attenuator")) + seq.add_command(SciencetechUHENLSolarSimDisableArcLamp(simulator)) + seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="lamp")) + seq.add_command(SciencetechUHENLSolarSimDeinitialize(simulator, close_serial=True)) + + print("\nThis smoke test verifies the full UHE-NL control path.") + print("Sequence: initialize with lamp OFF, close shutter, set attenuator to 75%, ignite lamp,") + print("check output/current/voltage/power, open and close shutter, reopen attenuator to 100%,") + print("turn lamp OFF, then deinitialize while leaving cooling on for cooldown.") + + invoker = CommandInvoker(seq, False) + invoker.invoke_commands() + + +if __name__ == "__main__": + main() diff --git a/examples/example_sonicator.py b/examples/example_sonicator.py new file mode 100644 index 0000000..061c1c4 --- /dev/null +++ b/examples/example_sonicator.py @@ -0,0 +1,71 @@ +# Sonicator smoke test +# run from root using 'python -m examples.example_sonicator' + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_invoker import CommandInvoker +from command_sequence import CommandSequence +from devices.sonicator import Sonicator +from commands.sonicator_commands import * + + +def main() -> None: + port = input("Enter the Sonicator Arduino COM port (for example COM13): ").strip() + if not port: + print("No COM port provided. Exiting.") + return + + confirm = input( + "This test will stop sonication if it is already running, then start and stop it once. Type 'y' to continue: " + ).strip().lower() + if confirm != "y": + print("Smoke test cancelled.") + return + + sonicator = Sonicator( + name="sonicator", + port=port, + baudrate=9600, + timeout=0.5, + connect_delay_s=3.0, + response_timeout_s=3.0, + power_probe_timeout_s=5.0, + line_terminator="\n", + command_prefix=">", + debug_io=False, + ) + + seq = CommandSequence() + seq.add_device(sonicator) + seq.add_command(SonicatorConnect(sonicator)) + seq.add_command(SonicatorInitialize(sonicator)) + seq.add_command(SonicatorGetStatus(sonicator)) + seq.add_command(SonicatorStartSonicating(sonicator)) + seq.add_command(SonicatorGetStatus(sonicator, delay=2.0)) + seq.add_command(SonicatorStopSonicating(sonicator, delay=5.0)) + seq.add_command(SonicatorGetStatus(sonicator)) + seq.add_command(SonicatorDeinitialize(sonicator, close_serial=True)) + + print("\nThis smoke test assumes an Arduino Uno R3 wrapper wired to 5V, GND, D7, and D8.") + print("The explicit power-probe command is not part of this example because it is intrusive.") + print("Sequence: connect, initialize to idle, read status, start sonication, wait briefly, stop, read status, deinitialize.") + + invoker = CommandInvoker( + seq, + log_to_file=True, + log_filename="logs/example_sonicator.log", + alert_slack=False, + ) + invoker.invoke_commands() + + +if __name__ == "__main__": + main() diff --git a/examples/example_stellarnet_spectrometer.py b/examples/example_stellarnet_spectrometer.py new file mode 100644 index 0000000..6e72250 --- /dev/null +++ b/examples/example_stellarnet_spectrometer.py @@ -0,0 +1,153 @@ +# StellarNet UV-Vis calibration and absorbance example +# run from root using 'python -m examples.example_stellarnet_spectrometer' + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from devices.stellarnet_spectrometer import StellarNetSpectrometer + + +SAVE_DIRECTORY = "data/spectroscopy/" +SPEC_KEYS = ["UV-Vis"] +DEFAULT_INTEGRATION_TIMES = (100,) +SCANS_TO_AVG = (100,) +SMOOTHINGS = (3,) +XTIMINGS = (3,) +TARGET_MAX_COUNT = 52000 +TARGET_TOLERANCE = 2000 + + +def prompt_continue(message: str) -> bool: + response = input(f"{message} Press ENTER to continue or type anything else to cancel: ").strip() + return response == "" + + +def main() -> None: + print("=== StellarNet UV-Vis Calibration Example ===") + print("This example initializes the UV-Vis spectrometer, adjusts the default integration time at the blank position,") + print("records dark and blank references, and then captures absorbance and photon counts by sample name.") + + spec = StellarNetSpectrometer( + name="spec", + spec_keys=SPEC_KEYS, + save_directory=SAVE_DIRECTORY, + default_integration_time=DEFAULT_INTEGRATION_TIMES, + ) + + ok, message = spec.initialize() + print(f"initialize -> {ok}, {message}") + if not ok: + return + + print(f"Connected spectrometers: {list(spec.spectrometer_dict.keys())}") + print(f"Save directory: {spec.save_directory}") + print(f"Initial default integration time: {spec.default_integration_time}") + + if not prompt_continue("Set the optical path to BLANK/reference for integration-time adjustment."): + spec.deinitialize() + print("Cancelled before integration-time adjustment.") + return + + ok, message = spec.adjust_default_integration_time( + scans_to_avg=SCANS_TO_AVG, + smoothings=SMOOTHINGS, + xtimings=XTIMINGS, + target_max_count=TARGET_MAX_COUNT, + tolerance=TARGET_TOLERANCE, + ) + print(f"adjust_default_integration_time -> {ok}, {message}") + print(f"Updated default integration time: {spec.default_integration_time}") + if not ok: + spec.deinitialize() + return + + if not prompt_continue("Set the optical path to DARK."): + spec.deinitialize() + print("Cancelled before dark capture.") + return + + ok, message = spec.update_all_dark_spectra( + integration_times=spec.default_integration_time, + scans_to_avg=SCANS_TO_AVG, + smoothings=SMOOTHINGS, + xtimings=XTIMINGS, + ) + print(f"update_dark -> {ok}, {message}") + if not ok: + spec.deinitialize() + return + + if not prompt_continue("Set the optical path to BLANK/reference."): + spec.deinitialize() + print("Cancelled before blank capture.") + return + + ok, message = spec.update_all_blank_spectra( + integration_times=spec.default_integration_time, + scans_to_avg=SCANS_TO_AVG, + smoothings=SMOOTHINGS, + xtimings=XTIMINGS, + ) + print(f"update_blank -> {ok}, {message}") + if not ok: + spec.deinitialize() + return + + if not prompt_continue("Load the SAMPLE for absorbance acquisition."): + spec.deinitialize() + print("Cancelled before sample acquisition.") + return + + sample_name = input("Enter sample name for the saved absorbance file prefix: ").strip() + if not sample_name: + sample_name = "uvvis_sample" + + repeat_measure = input( + "Append this measurement to an existing sample time series if matching files are found? [Y/N]: " + ).strip().lower() == "y" + + ok, message = spec.get_all_absorbance_byname( + sample_name=sample_name, + save_to_file=True, + repeat_measure=repeat_measure, + integration_times=spec.default_integration_time, + scans_to_avg=SCANS_TO_AVG, + smoothings=SMOOTHINGS, + xtimings=XTIMINGS, + ) + print(f"get_absorbance_byname -> {ok}, {message}") + + if ok: + save_counts = input("Save photon counts for the same sample name as well? [Y/N]: ").strip().lower() + if save_counts != "n": + ok_counts, message_counts = spec.get_all_counts_byname( + sample_name=sample_name, + save_to_file=True, + repeat_measure=repeat_measure, + integration_times=spec.default_integration_time, + scans_to_avg=SCANS_TO_AVG, + smoothings=SMOOTHINGS, + xtimings=XTIMINGS, + ) + print(f"get_photoncounts_byname -> {ok_counts}, {message_counts}") + + calc_decay = input( + "Compute spectral decay now if repeated absorbance data and reference/am15g_spectrum.csv are available? [Y/N]: " + ).strip().lower() == "y" + if calc_decay: + ok_decay, message_decay = spec.get_spec_decay(sample_name=sample_name, save_to_file=True) + print(f"get_spec_decay -> {ok_decay}, {message_decay}") + + ok, message = spec.deinitialize() + print(f"deinitialize -> {ok}, {message}") + + +if __name__ == "__main__": + main() diff --git a/examples/example_substrate_dispenser.py b/examples/example_substrate_dispenser.py new file mode 100644 index 0000000..40ff64c --- /dev/null +++ b/examples/example_substrate_dispenser.py @@ -0,0 +1,36 @@ +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_invoker import CommandInvoker +from command_sequence import CommandSequence + +from devices.substrate_dispenser import SubstrateDispenser +from commands.substrate_dispenser_commands import * + + +def main(): + port = input("Enter substrate dispenser COM port [example: COM7]: ").strip() or "COM7" + + dispenser = SubstrateDispenser("dispenser", port) + + seq = CommandSequence() + seq.add_device(dispenser) + seq.add_command(SubstrateDispenserConnect(dispenser)) + seq.add_command(SubstrateDispenserInitialize(dispenser)) + seq.add_command(SubstrateDispenserMoveToPosition(dispenser, position_mm=25.0, speed_mm_per_s=10.0, delay=2.0)) + seq.add_command(SubstrateDispenserHome(dispenser, delay=2.0)) + seq.add_command(SubstrateDispenserDeinitialize(dispenser, close_serial=True)) + + invoker = CommandInvoker(seq, log_to_file=False) + print(invoker.invoke_commands()) + + +if __name__ == "__main__": + main() diff --git a/examples/example_substrate_hotel.py b/examples/example_substrate_hotel.py new file mode 100644 index 0000000..0497263 --- /dev/null +++ b/examples/example_substrate_hotel.py @@ -0,0 +1,41 @@ +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_invoker import CommandInvoker +from command_sequence import CommandSequence + +from devices.substrate_hotel import SubstrateHotel +from commands.substrate_hotel_commands import * + + +def main(): + port = input("Enter substrate hotel COM port [example: COM10]: ").strip() or "COM10" + + hotel = SubstrateHotel( + "hotel", + port, + home_timeout_s=300.0, + move_timeout_s=300.0, + ) + + seq = CommandSequence() + seq.add_device(hotel) + seq.add_command(SubstrateHotelConnect(hotel)) + seq.add_command(SubstrateHotelInitialize(hotel)) + seq.add_command(SubstrateHotelMoveToPosition(hotel, position_mm=405.0, speed_mm_per_s=10.0, delay=2.0)) + # seq.add_command(SubstrateHotelHome(hotel, delay=2.0)) + seq.add_command(SubstrateHotelDeinitialize(hotel, close_serial=True)) + + invoker = CommandInvoker(seq, log_to_file=False) + print(invoker.invoke_commands()) + + +if __name__ == "__main__": + main() diff --git a/examples/example_uvvis_film_absorbance_degradation.py b/examples/example_uvvis_film_absorbance_degradation.py new file mode 100644 index 0000000..f997861 --- /dev/null +++ b/examples/example_uvvis_film_absorbance_degradation.py @@ -0,0 +1,260 @@ +# UV-Vis film absorbance degradation example +# run from root using 'python -m examples.example_uvvis_film_absorbance_degradation' + +import sys +import time +from pathlib import Path +from typing import Dict, Iterable, Tuple + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from devices.newport_esp301 import NewportESP301 +from devices.stellarnet_spectrometer import StellarNetSpectrometer + + +ESP301_PORT = "COM6" +ESP301_AXIS_NUMBER = 3 +ESP301_SPEED = 20.0 + +ESP301_AXIS_CONFIGS = { + 3: { + "stage_model": "PR50PP", + "motion_type": "rotary", + "units": "deg", + "home_mode": "OR1", + "zero_position": 0.0, + "default_speed": ESP301_SPEED, + }, +} + +SAVE_DIRECTORY = "data/spectroscopy/" +SPEC_KEYS = ["UV-Vis"] +DEFAULT_INTEGRATION_TIMES = (100,) +SCANS_TO_AVG = (100,) +SMOOTHINGS = (3,) +XTIMINGS = (3,) +TARGET_MAX_COUNT = 52000 +TARGET_TOLERANCE = 2000 + +ACTIVE_SLOTS = (1, 2, 3, 4, 5, 6, 7, 8) +SAMPLE_NAMES: Dict[int, str] = { + 1: "sample_1", + 2: "sample_2", + 3: "sample_3", + 4: "sample_4", + 5: "sample_5", + 6: "sample_6", + 7: "sample_7", + 8: "sample_8", +} + +NUM_CYCLES = 1 +DWELL_BETWEEN_CYCLES_S = 0.0 +SAVE_PHOTON_COUNTS = True + + +def sample_angle_deg(slot: int) -> float: + return float((slot - 1) * 45.0) + + +def dark_angle_deg() -> float: + return 15.0 + + +def blank_angle_deg(slot: int) -> float: + return float(sample_angle_deg(slot) + 22.5) + + +def validate_slots(active_slots: Iterable[int], sample_names: Dict[int, str]) -> Tuple[bool, str]: + active_slots = tuple(active_slots) + if not active_slots: + return False, "ACTIVE_SLOTS is empty." + for slot in active_slots: + if slot < 1 or slot > 8: + return False, f"Slot {slot} is invalid. Valid slots are 1 through 8." + if slot not in sample_names or not str(sample_names[slot]).strip(): + return False, f"Slot {slot} is active but has no sample name." + return True, "Slot configuration is valid." + + +def move_axis3(esp301: NewportESP301, angle_deg: float) -> bool: + ok, message = esp301.move_speed_absolute( + axis_number=ESP301_AXIS_NUMBER, + position=angle_deg, + speed=ESP301_SPEED, + ) + print(f"move axis3 -> {ok}, {message}") + return ok + + +def prompt_continue(message: str) -> bool: + response = input(f"{message} Press ENTER to continue or type anything else to cancel: ").strip() + return response == "" + + +def main() -> None: + ok, message = validate_slots(ACTIVE_SLOTS, SAMPLE_NAMES) + if not ok: + print(message) + return + + print("=== UV-Vis Film Absorbance Degradation Example ===") + print("This example uses ESP301 axis 3 for rotary positioning and StellarNet UV-Vis for repeated absorbance acquisition.") + print("Workflow:") + print("1. Move to 22.5 deg for initial blank-based integration-time adjustment.") + print("2. Move to 15.0 deg for a one-time dark measurement.") + print("3. For each loop, measure blank before every sample and append absorbance by sample name.") + print("4. Photon counts can be saved alongside absorbance.") + print("") + print(f"Active slots: {ACTIVE_SLOTS}") + for slot in ACTIVE_SLOTS: + print( + f" slot {slot}: sample={SAMPLE_NAMES[slot]}, " + f"sample_angle={sample_angle_deg(slot):.1f} deg, " + f"blank_angle={blank_angle_deg(slot):.1f} deg" + ) + print(f"Dark angle: {dark_angle_deg():.1f} deg") + print(f"Configured loop count: {NUM_CYCLES}") + print(f"Dwell between cycles: {DWELL_BETWEEN_CYCLES_S} s") + print(f"Photon counts enabled: {SAVE_PHOTON_COUNTS}") + + if not prompt_continue("Confirm the chamber is clear and the spectrometer optical path is ready."): + print("Cancelled before initialization.") + return + + esp301 = NewportESP301( + name="uvvis_stage", + port=ESP301_PORT, + axis_list=(ESP301_AXIS_NUMBER,), + default_speed=ESP301_SPEED, + poll_interval=0.1, + axis_configs=ESP301_AXIS_CONFIGS, + ) + spec = StellarNetSpectrometer( + name="spec", + spec_keys=SPEC_KEYS, + save_directory=SAVE_DIRECTORY, + default_integration_time=DEFAULT_INTEGRATION_TIMES, + ) + + esp_initialized = False + spec_initialized = False + + try: + ok, message = esp301.connect() + print(f"esp301 connect -> {ok}, {message}") + if not ok: + return + + ok, message = esp301.initialize() + print(f"esp301 initialize -> {ok}, {message}") + if not ok: + return + esp_initialized = True + + ok, message = spec.initialize() + print(f"spectrometer initialize -> {ok}, {message}") + if not ok: + return + spec_initialized = True + + if not move_axis3(esp301, 22.5): + return + ok, message = spec.adjust_default_integration_time( + scans_to_avg=SCANS_TO_AVG, + smoothings=SMOOTHINGS, + xtimings=XTIMINGS, + target_max_count=TARGET_MAX_COUNT, + tolerance=TARGET_TOLERANCE, + ) + print(f"adjust_default_integration_time -> {ok}, {message}") + print(f"default_integration_time -> {spec.default_integration_time}") + if not ok: + return + + if not move_axis3(esp301, dark_angle_deg()): + return + ok, message = spec.update_all_dark_spectra( + integration_times=spec.default_integration_time, + scans_to_avg=SCANS_TO_AVG, + smoothings=SMOOTHINGS, + xtimings=XTIMINGS, + ) + print(f"update_dark -> {ok}, {message}") + if not ok: + return + + if NUM_CYCLES < 1: + print("NUM_CYCLES must be at least 1.") + return + + for cycle_index in range(NUM_CYCLES): + print("") + print(f"=== Begin cycle {cycle_index + 1} / {NUM_CYCLES} ===") + for slot in ACTIVE_SLOTS: + sample_name = SAMPLE_NAMES[slot] + current_blank_angle = blank_angle_deg(slot) + current_sample_angle = sample_angle_deg(slot) + + if not move_axis3(esp301, current_blank_angle): + return + ok, message = spec.update_all_blank_spectra( + integration_times=spec.default_integration_time, + scans_to_avg=SCANS_TO_AVG, + smoothings=SMOOTHINGS, + xtimings=XTIMINGS, + ) + print(f"update_blank slot {slot} -> {ok}, {message}") + if not ok: + return + + if not move_axis3(esp301, current_sample_angle): + return + ok, message = spec.get_all_absorbance_byname( + sample_name=sample_name, + save_to_file=True, + repeat_measure=True, + integration_times=spec.default_integration_time, + scans_to_avg=SCANS_TO_AVG, + smoothings=SMOOTHINGS, + xtimings=XTIMINGS, + ) + print(f"get_absorbance_byname slot {slot} -> {ok}, {message}") + if not ok: + return + + if SAVE_PHOTON_COUNTS: + ok, message = spec.get_all_counts_byname( + sample_name=sample_name, + save_to_file=True, + repeat_measure=True, + integration_times=spec.default_integration_time, + scans_to_avg=SCANS_TO_AVG, + smoothings=SMOOTHINGS, + xtimings=XTIMINGS, + ) + print(f"get_photoncounts_byname slot {slot} -> {ok}, {message}") + if not ok: + return + + print(f"=== End cycle {cycle_index + 1} / {NUM_CYCLES} ===") + if cycle_index < NUM_CYCLES - 1 and DWELL_BETWEEN_CYCLES_S > 0: + print(f"Sleeping for {DWELL_BETWEEN_CYCLES_S} s before next cycle.") + time.sleep(DWELL_BETWEEN_CYCLES_S) + + finally: + if spec_initialized: + ok, message = spec.deinitialize() + print(f"spectrometer deinitialize -> {ok}, {message}") + if esp_initialized: + ok, message = esp301.deinitialize() + print(f"esp301 deinitialize -> {ok}, {message}") + + +if __name__ == "__main__": + main() diff --git a/implementation_plan.md b/implementation_plan.md index a15e89d..3d6a673 100644 --- a/implementation_plan.md +++ b/implementation_plan.md @@ -81,7 +81,11 @@ Copy this section for each device and fill it in as work starts. - [x] Device 2: `Z812` - [x] Device 3: `HeatingStage` - [x] Device 4: `P4PP` -- [ ] Device 5: `TBD` +- [x] Device 5: `Sciencetech UHE-NL` +- [x] Device 6: `StellarNetSpectrometer` +- [x] Device 7: `Sonicator` +- [ ] Device 8: `SubstrateHotel` +- [ ] Device 9: `SubstrateDispenser` #### Device: `ESP301-3N` @@ -257,6 +261,230 @@ Copy this section for each device and fill it in as work starts. - Measurement resistor selection is explicit: `681 ohm` default, `68.1 ohm` optional - Default measurement cycles for command metadata set to `20` +#### Device: `Sciencetech UHE-NL` + +- Status: `done` +- Device file: `aamp_app/devices/sciencetech_uhe_nl_solar_sim.py` +- Command file: `aamp_app/commands/sciencetech_uhe_nl_solar_sim_commands.py` +- Example file: `examples/example_sciencetech_uhe_nl_solar_sim.py` +- Similar existing implementation: `to_implement/sciencetech_lamp.py` +- Hardware or SDK dependency: `Sciencetech UHE-NL / LPC controller over RS-232` + +#### Scope + +- Add: UHE-NL power control device, commands, smoke test, and web app metadata +- Update: safety handling for cooling and lamp sequencing +- Not in scope: optical calibration workflow + +#### Required Actions + +- [x] Device class implemented +- [x] Initialization path checked +- [x] Shutdown or cleanup path checked +- [x] Core commands implemented +- [x] Command metadata and params reviewed +- [x] Example added or updated +- [x] Example executed successfully +- [x] Logging behavior checked +- [ ] Recipe or YAML compatibility checked +- [ ] Web app visibility checked +- [ ] Manual control page checked if applicable +- [ ] Execute recipe flow checked if applicable +- [x] Notes recorded + +#### Validation + +- Example run result: full smoke test executed locally on `COM4` +- Web app result: +- Known issues: controller reports `OUTPUT=0000` while the lamp is off even after a setpoint command; live output feedback becomes meaningful only after lamp ignition + +#### Notes + +- `initialize` requires the lamp to be off and enables cooling first +- `enable_arc_lamp` refuses to run unless cooling feedback is on +- `deinitialize` turns the lamp off when needed and intentionally leaves cooling on for cooldown + +#### Device: `StellarNetSpectrometer` + +- Status: `done` +- Device file: `aamp_app/devices/stellarnet_spectrometer.py` +- Command file: `aamp_app/commands/stellarnet_spectrometer_commands.py` +- Example file: `examples/example_stellarnet_spectrometer.py`, `examples/example_uvvis_film_absorbance_degradation.py` +- Similar existing implementation: existing StellarNet driver wrapper +- Hardware or SDK dependency: `stellarnet_driver3` and compatible `pyusb` + +#### Scope + +- Add: calibration/smoke test example for the UV-Vis spectrometer, by-name absorbance and photon-count acquisition, spectral-decay calculation, and an ESP301-driven UV-Vis film degradation example +- Update: driver import handling, initialization metadata, repeated-measure QC logging, and AM1.5 reference handling +- Not in scope: automated analysis filtering based on QC validity inside the decay calculation itself + +#### Required Actions + +- [x] Device class implemented +- [x] Initialization path checked +- [x] Shutdown or cleanup path checked +- [x] Core commands implemented +- [x] Command metadata and params reviewed +- [x] Example added or updated +- [x] Example executed successfully +- [x] Logging behavior checked +- [ ] Recipe or YAML compatibility checked +- [ ] Web app visibility checked +- [ ] Manual control page checked if applicable +- [ ] Execute recipe flow checked if applicable +- [x] Notes recorded + +#### Validation + +- Example run result: local initialization succeeded with spectrometer key `UV-Vis`; manual dark/blank/sample absorbance example executed locally; spectral decay recalculation was validated against legacy `specdecay_sample` data with very high trend agreement +- Web app result: +- Known issues: vendor driver must be installed in the active Python environment + +#### Notes + +- `adjust_default_integration_time()` is used at the blank position and keeps the default target near `52000` counts +- Negative absorbance is clamped to `0` and high absorbance is capped at `5`, matching the older UV-Vis workflow +- Repeated absorbance measurements now append even when QC differences exceed threshold; validity and comparison statistics are logged separately in `sample_name_measurement_qc_log.csv` +- Spectral decay now follows the `UVVis_Converter` method more closely: + - default spectral range `290-800 nm` + - default irradiance reference `data/spectroscopy/reference/am15g_spectrum.csv` + - spectral overlap computed by interpolating AM1.5 irradiance onto the measured wavelength grid and integrating with `trapz` +- Added `example_uvvis_film_absorbance_degradation.py` for ESP301 axis-3 sample rotation with: + - dark measured once at startup + - blank measured before every sample + - loop count and active slots configured at the top of the example + +#### Device: `Sonicator` + +- Status: `done` +- Device file: `aamp_app/devices/sonicator.py` +- Command file: `aamp_app/commands/sonicator_commands.py` +- Example file: `examples/example_sonicator.py` +- Similar existing implementation: `to_implement/sonicator/test6/test6.ino` +- Hardware or SDK dependency: `Arduino Uno R3 wrapper for sonicator front-panel button/status wiring` + +#### Scope + +- Add: Sonicator device, commands, example, and cleaned-up Uno firmware sketch +- Update: web-app metadata and documentation stubs +- Not in scope: redesigning the sonicator hardware interface board + +#### Required Actions + +- [x] Device class implemented +- [x] Initialization path checked +- [x] Shutdown or cleanup path checked +- [x] Core commands implemented +- [x] Command metadata and params reviewed +- [x] Example added or updated +- [x] Example executed successfully +- [ ] Logging behavior checked +- [ ] Recipe or YAML compatibility checked +- [ ] Web app visibility checked +- [ ] Manual control page checked if applicable +- [ ] Execute recipe flow checked if applicable +- [x] Notes recorded + +#### Validation + +- Example run result: Python smoke test executed locally on `COM13` +- Web app result: +- Known issues: `power` probing is intentionally treated as intrusive because it may toggle the front-panel button when the sonicator is idle + +#### Notes + +- Host-side protocol follows the final draft family in `to_implement/sonicator/test6/test6.ino` +- Command set: `>status`, `>button`, `>power`, `>turnon`, `>turnoff` +- Expected Uno R3 wiring is `5V`, `GND`, `D7` button drive, and `D8` status sense +- Added a cleaned-up firmware sketch at `firmware/sonicator/sonicator_uno_r3/sonicator_uno_r3.ino` + +#### Device: `SubstrateHotel` + +- Status: `in progress` +- Device file: `aamp_app/devices/substrate_hotel.py` +- Command file: `aamp_app/commands/substrate_hotel_commands.py` +- Example file: `examples/example_substrate_hotel.py` +- Similar existing implementation: `to_implement/substratehotel/substrate_hotel.py` +- Hardware or SDK dependency: `Arduino-based linear stage controller` + +#### Scope + +- Add: dedicated device, commands, example, and web-app metadata +- Update: current repo to use `Connect -> Initialize -> Move/Home -> Deinitialize` style +- Not in scope: Arduino firmware changes + +#### Required Actions + +- [x] Device class implemented +- [ ] Initialization path checked +- [ ] Shutdown or cleanup path checked +- [x] Core commands implemented +- [x] Command metadata and params reviewed +- [x] Example added or updated +- [ ] Example executed successfully +- [ ] Logging behavior checked +- [ ] Recipe or YAML compatibility checked +- [ ] Web app visibility checked +- [ ] Manual control page checked if applicable +- [ ] Execute recipe flow checked if applicable +- [x] Notes recorded + +#### Validation + +- Example run result: +- Web app result: +- Known issues: current repo path not hardware-smoke-tested yet + +#### Notes + +- Serial protocol inferred from old draft: Arduino emits `Ready`, homing uses `H`, and absolute motion uses `M{position},{speed}` +- Current Python-side position guard is `0-430 mm` +- Default homing and move timeouts are `300 s` + +#### Device: `SubstrateDispenser` + +- Status: `in progress` +- Device file: `aamp_app/devices/substrate_dispenser.py` +- Command file: `aamp_app/commands/substrate_dispenser_commands.py` +- Example file: `examples/example_substrate_dispenser.py` +- Similar existing implementation: `to_implement/substratehotel/substrate_dispenser.py` +- Hardware or SDK dependency: `Arduino-based linear stage controller` + +#### Scope + +- Add: dedicated device, commands, example, and web-app metadata +- Update: current repo to use `Connect -> Initialize -> Move/Home -> Deinitialize` style +- Not in scope: Arduino firmware changes + +#### Required Actions + +- [x] Device class implemented +- [ ] Initialization path checked +- [ ] Shutdown or cleanup path checked +- [x] Core commands implemented +- [x] Command metadata and params reviewed +- [x] Example added or updated +- [ ] Example executed successfully +- [ ] Logging behavior checked +- [ ] Recipe or YAML compatibility checked +- [ ] Web app visibility checked +- [ ] Manual control page checked if applicable +- [ ] Execute recipe flow checked if applicable +- [x] Notes recorded + +#### Validation + +- Example run result: +- Web app result: +- Known issues: current repo path not hardware-smoke-tested yet + +#### Notes + +- Serial protocol inferred from old draft: Arduino emits `Ready`, homing uses `H`, and absolute motion uses `M{position},{speed}` +- Current Python-side position guard is `0-45 mm` +- Default homing and move timeouts are `30 s` + ## Questions or Blockers - None yet diff --git a/recipes/user_recipes/.gitignore b/recipes/user_recipes/.gitignore index c96a04f..db372c4 100644 --- a/recipes/user_recipes/.gitignore +++ b/recipes/user_recipes/.gitignore @@ -1,2 +1,3 @@ * -!.gitignore \ No newline at end of file +!.gitignore +!ch_apis_imaging_from_mongodb.py diff --git a/recipes/user_recipes/ch_apis_imaging_from_mongodb.py b/recipes/user_recipes/ch_apis_imaging_from_mongodb.py new file mode 100644 index 0000000..56695e4 --- /dev/null +++ b/recipes/user_recipes/ch_apis_imaging_from_mongodb.py @@ -0,0 +1,244 @@ +import os +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + +from gridfs import GridFS + +CWD = Path.cwd().resolve() +ROOT_DIR = CWD.parent if CWD.name == "aamp_app" else CWD +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_invoker import CommandInvoker +from command_sequence import CommandSequence +from commands.apis_commands import * +from devices.apis import APIS +from mongodb_helper import MongoDBHelper + + +CAMPAIGN_NAME = "${campaign_name}" +BATCH_NO = ${batch_no} +SAMPLE_NO = ${sample_no} +APIS_PORT = "COM8" +ROOT_SAVE_DIR = ROOT_DIR / "data" / "imaging" +XPL_EXPOSURE_US = 50000 +PPL_EXPOSURE_US = 18000 +SAMPLE_ANGLES_DEG = [90, 60, 45, 30, 0] + + +def load_env(file_path=ROOT_DIR / ".env"): + if file_path.exists(): + with open(file_path, "r", encoding="utf-8") as file: + for line in file: + if "=" in line and not line.strip().startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key] = value + + +def sanitize_path_component(value: str) -> str: + sanitized = re.sub(r'[<>:"/\\\\|?*]+', "_", value.strip()) + return sanitized or "unnamed_campaign" + + +def get_mongo(): + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") + if not mongo_uri or not mongo_db_name: + raise RuntimeError("MONGO_URI and MONGO_DB_NAME must be set in .env.") + return MongoDBHelper(mongo_uri, mongo_db_name) + + +def fetch_parameter_set(mongo, campaign_name, batch_no, sample_no): + campaign_doc = mongo.db["campaigns"].find_one({"campaign_name": campaign_name}) + if campaign_doc is None: + raise LookupError(f"Campaign '{campaign_name}' was not found.") + set_doc = mongo.db["sets"].find_one( + { + "campaign_id": campaign_doc["_id"], + "batch_no": batch_no, + "sample_no": sample_no, + } + ) + if set_doc is None: + raise LookupError( + f"No parameter set found for campaign='{campaign_name}', batch_no={batch_no}, sample_no={sample_no}." + ) + return campaign_doc, set_doc + + +def build_sample_params(batch_no, sample_no, set_doc): + return { + "polymer": set_doc["polymer_name"], + "round_num": batch_no, + "sample_num": sample_no, + "temperature": set_doc["temperature"], + "speed": float(set_doc["motor_speed"]), + "gap": set_doc["printing_gap"], + "solvent": set_doc["solvent"], + "concentration": set_doc["concentration"], + "volume": set_doc["precursor_volume"], + } + + +def add_mode_capture_commands(seq, apis, mode_name, polarizer_angle, sample_angles, exposure_time, base_name, save_dir, capture_records): + seq.add_command(APISRotatePolarizer(apis, angle_deg=polarizer_angle)) + for angle in sample_angles: + seq.add_command(APISRotateSample(apis, angle_deg=angle)) + filename = apis.build_mode_filename(base_name, mode_name, angle) + raw16_path = os.path.join(save_dir, "raw16", filename + ".tif") + rgb_path = os.path.join(save_dir, "rgb", filename + "_rgb.tif") + capture_records.append( + { + "mode": mode_name, + "image_kind": "raw16", + "sample_angle_deg": angle, + "local_path": raw16_path, + } + ) + capture_records.append( + { + "mode": mode_name, + "image_kind": "rgb", + "sample_angle_deg": angle, + "local_path": rgb_path, + } + ) + seq.add_command( + APISCaptureRaw16( + apis, + filename=filename, + directory=save_dir, + exposure_time=exposure_time, + gain=0.0, + ) + ) + seq.add_command( + APISConvertRaw16ToRgb( + apis, + raw16_path=raw16_path, + rgb_path=rgb_path, + ) + ) + + +def upload_capture_records_to_mongodb(mongo, campaign_doc, set_doc, capture_records): + fs = GridFS(mongo.db) + uploaded_count = 0 + for record in capture_records: + local_path = record["local_path"] + if not os.path.isfile(local_path): + continue + with open(local_path, "rb") as file_obj: + file_id = fs.put(file_obj, filename=os.path.basename(local_path)) + metadata = { + "campaign_id": campaign_doc["_id"], + "campaign_name": campaign_doc["campaign_name"], + "set_id": set_doc["_id"], + "batch_no": set_doc["batch_no"], + "sample_no": set_doc["sample_no"], + "polymer_name": set_doc["polymer_name"], + "solvent": set_doc["solvent"], + "concentration": set_doc["concentration"], + "motor_speed": set_doc["motor_speed"], + "temperature": set_doc["temperature"], + "printing_gap": set_doc["printing_gap"], + "precursor_volume": set_doc["precursor_volume"], + "mode": record["mode"], + "image_kind": record["image_kind"], + "sample_angle_deg": record["sample_angle_deg"], + "file_id": file_id, + "filename": os.path.basename(local_path), + "relative_local_path": os.path.relpath(local_path, ROOT_DIR), + "measurement_type": "apis_imaging", + "source": "ch_apis_imaging_recipe", + "uploaded_at": datetime.now(timezone.utc), + } + mongo.db["images"].insert_one(metadata) + uploaded_count += 1 + return uploaded_count + + +load_env() +mongo = get_mongo() +campaign_doc, set_doc = fetch_parameter_set(mongo, CAMPAIGN_NAME, BATCH_NO, SAMPLE_NO) +params = build_sample_params(BATCH_NO, SAMPLE_NO, set_doc) + +base_sample_name = APIS.build_sample_basename( + round_num=params["round_num"], + sample_num=params["sample_num"], + polymer=params["polymer"], + solvent=params["solvent"], + concentration=params["concentration"], + speed=params["speed"], + temperature=params["temperature"], + gap=params["gap"], + volume=params["volume"], +) + +root_save_dir = os.path.join(str(ROOT_SAVE_DIR), sanitize_path_component(CAMPAIGN_NAME)) +apis = APIS( + name="apis", + port=APIS_PORT, + baudrate=9600, + timeout=0.5, + connection_wait_s=2.0, + settling_time_s=1.5, + command_delay_s=0.05, + max_retries=3, + use_camera=True, + camera_save_directory=root_save_dir, + camera_bayer_pattern="GBRG", + camera_raw_max_value=1023.0, +) + +xpl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer"], "xpl") +ppl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer"], "ppl") +capture_records = [] + +seq = CommandSequence() +seq.add_device(apis) +seq.add_command(APISConnect(apis)) +seq.add_command(APISInitialize(apis)) +seq.add_command(APISHome(apis)) + +add_mode_capture_commands( + seq=seq, + apis=apis, + mode_name="xpl", + polarizer_angle=90.0, + sample_angles=SAMPLE_ANGLES_DEG, + exposure_time=XPL_EXPOSURE_US, + base_name=base_sample_name, + save_dir=xpl_dir, + capture_records=capture_records, +) +add_mode_capture_commands( + seq=seq, + apis=apis, + mode_name="ppl", + polarizer_angle=0.0, + sample_angles=SAMPLE_ANGLES_DEG, + exposure_time=PPL_EXPOSURE_US, + base_name=base_sample_name, + save_dir=ppl_dir, + capture_records=capture_records, +) + +seq.add_command(APISRotatePolarizer(apis, angle_deg=0.0)) +seq.add_command(APISRotateSample(apis, angle_deg=0.0)) +seq.add_command(APISDeinitialize(apis)) + +invoker = CommandInvoker(seq, False) +result = invoker.invoke_commands() +print(f"APIS imaging finished for campaign={CAMPAIGN_NAME}, batch_no={BATCH_NO}, sample_no={SAMPLE_NO}. Result={result}") + +if result: + uploaded_count = upload_capture_records_to_mongodb(mongo, campaign_doc, set_doc, capture_records) + print(f"Uploaded {uploaded_count} APIS image files to GridFS and inserted metadata into images collection.") + +mongo.close_connection() From c46e4aa5d6c3ba345aad842d8ad9542f929ceb7c Mon Sep 17 00:00:00 2001 From: Hwang Date: Mon, 30 Mar 2026 08:31:45 -0500 Subject: [PATCH 117/125] Align APIS imaging params with MongoDB set fields --- examples/example_apis.py | 51 +++++++++-------- examples/example_apis_from_mongodb.py | 57 ++++++++++--------- .../ch_apis_imaging_from_mongodb.py | 33 +++++------ 3 files changed, 72 insertions(+), 69 deletions(-) diff --git a/examples/example_apis.py b/examples/example_apis.py index 1ea0dde..ab0214b 100644 --- a/examples/example_apis.py +++ b/examples/example_apis.py @@ -52,39 +52,40 @@ def add_mode_capture_commands(seq, apis, mode_name, polarizer_angle, sample_angl def main() -> None: print("=== APIS Imaging Workflow ===") - print("Enter sample parameters directly to build an APIS imaging sequence.") + print("Enter parameter values using the MongoDB `sets` field names.") - round_input = input("Enter round number (for example: 1 or a01): ").strip() - sample_input = input("Enter sample number (for example: 450 or a01): ").strip() + batch_input = input("Enter batch_no (for example: 1): ").strip() + sample_input = input("Enter sample_no (for example: 1): ").strip() try: - round_num = int(round_input) + batch_no = int(batch_input) except ValueError: - round_num = round_input + batch_no = batch_input try: - sample_num = int(sample_input) + sample_no = int(sample_input) except ValueError: - sample_num = sample_input + sample_no = sample_input - polymer = input("Enter polymer name (for example: PProDOT): ").strip() + polymer_name = input("Enter polymer_name (for example: PProDOT): ").strip() solvent = input("Enter solvent (for example: CB): ").strip() concentration = int(input("Enter concentration (mg/ml): ").strip()) - speed = float(input("Enter speed (mm/s): ").strip()) + motor_speed = float(input("Enter motor_speed (mm/s): ").strip()) temperature = int(input("Enter temperature (C): ").strip()) - gap = int(input("Enter gap (um): ").strip()) - volume = int(input("Enter volume (ul): ").strip()) + printing_gap = int(input("Enter printing_gap (um): ").strip()) + precursor_volume = int(input("Enter precursor_volume (ul): ").strip()) params = { - "polymer": polymer, - "round_num": round_num, - "sample_num": sample_num, + "campaign_name": "demo_campaign", + "batch_no": batch_no, + "sample_no": sample_no, + "polymer_name": polymer_name, "temperature": temperature, - "speed": speed, - "gap": gap, + "motor_speed": motor_speed, + "printing_gap": printing_gap, "solvent": solvent, "concentration": concentration, - "volume": volume, + "precursor_volume": precursor_volume, } print("\n=== Sample Parameters ===") @@ -92,15 +93,15 @@ def main() -> None: print(f"{key}: {value}") base_sample_name = APIS.build_sample_basename( - round_num=params["round_num"], - sample_num=params["sample_num"], - polymer=params["polymer"], + round_num=params["batch_no"], + sample_num=params["sample_no"], + polymer=params["polymer_name"], solvent=params["solvent"], concentration=params["concentration"], - speed=params["speed"], + speed=params["motor_speed"], temperature=params["temperature"], - gap=params["gap"], - volume=params["volume"], + gap=params["printing_gap"], + volume=params["precursor_volume"], ) print(f"\nGenerated filename prefix: {base_sample_name}") print("Mode folders will use `xpl/` and `ppl/`.") @@ -126,8 +127,8 @@ def main() -> None: camera_bayer_pattern="GBRG", camera_raw_max_value=1023.0, ) - xpl_dir = apis.resolve_mode_directory(ROOT_SAVE_DIR, params["polymer"], "xpl") - ppl_dir = apis.resolve_mode_directory(ROOT_SAVE_DIR, params["polymer"], "ppl") + xpl_dir = apis.resolve_mode_directory(ROOT_SAVE_DIR, params["polymer_name"], "xpl") + ppl_dir = apis.resolve_mode_directory(ROOT_SAVE_DIR, params["polymer_name"], "ppl") seq = CommandSequence() seq.add_device(apis) diff --git a/examples/example_apis_from_mongodb.py b/examples/example_apis_from_mongodb.py index a74ac84..108978e 100644 --- a/examples/example_apis_from_mongodb.py +++ b/examples/example_apis_from_mongodb.py @@ -63,8 +63,8 @@ def get_mongo_helper() -> MongoDBHelper: def fetch_parameter_set( mongo: MongoDBHelper, campaign_name: str, - round_num: int, - sample_num: int, + batch_no: int, + sample_no: int, ) -> Tuple[dict, dict]: campaign_doc = mongo.db["campaigns"].find_one({"campaign_name": campaign_name}) if campaign_doc is None: @@ -73,8 +73,8 @@ def fetch_parameter_set( set_doc = mongo.db["sets"].find_one( { "campaign_id": campaign_doc["_id"], - "batch_no": round_num, - "sample_no": sample_num, + "batch_no": batch_no, + "sample_no": sample_no, } ) if set_doc is None: @@ -83,14 +83,14 @@ def fetch_parameter_set( "sample_no", { "campaign_id": campaign_doc["_id"], - "batch_no": round_num, + "batch_no": batch_no, }, ) ) if available_samples: raise LookupError( - f"No parameter set found for campaign='{campaign_name}', round(batch_no)={round_num}, " - f"sample_no={sample_num}. Available sample_no values for that round: {available_samples}" + f"No parameter set found for campaign='{campaign_name}', batch_no={batch_no}, " + f"sample_no={sample_no}. Available sample_no values for that round: {available_samples}" ) available_rounds = sorted( mongo.db["sets"].distinct( @@ -99,23 +99,24 @@ def fetch_parameter_set( ) ) raise LookupError( - f"No parameter set found for campaign='{campaign_name}', round(batch_no)={round_num}, " - f"sample_no={sample_num}. Available rounds(batch_no): {available_rounds}" + f"No parameter set found for campaign='{campaign_name}', batch_no={batch_no}, " + f"sample_no={sample_no}. Available rounds(batch_no): {available_rounds}" ) return campaign_doc, set_doc -def build_sample_params(round_num: int, sample_num: int, set_doc: dict) -> Dict[str, object]: +def build_set_params(batch_no: int, sample_no: int, set_doc: dict) -> Dict[str, object]: return { - "polymer": format_display_value(set_doc["polymer_name"]), - "round_num": round_num, - "sample_num": sample_num, + "campaign_name": format_display_value(set_doc.get("campaign_name", "")), + "batch_no": batch_no, + "sample_no": sample_no, + "polymer_name": format_display_value(set_doc["polymer_name"]), "temperature": format_display_value(set_doc["temperature"]), - "speed": float(set_doc["motor_speed"]), - "gap": format_display_value(set_doc["printing_gap"]), + "motor_speed": float(set_doc["motor_speed"]), + "printing_gap": format_display_value(set_doc["printing_gap"]), "solvent": format_display_value(set_doc["solvent"]), "concentration": format_display_value(set_doc["concentration"]), - "volume": format_display_value(set_doc["precursor_volume"]), + "precursor_volume": format_display_value(set_doc["precursor_volume"]), } @@ -227,32 +228,32 @@ def main() -> None: mongo = get_mongo_helper() campaign_name = input("Enter campaign name: ").strip() - round_num = int(input("Enter round number (MongoDB batch_no): ").strip()) - sample_num = int(input("Enter sample number (MongoDB sample_no): ").strip()) + batch_no = int(input("Enter batch_no: ").strip()) + sample_no = int(input("Enter sample_no: ").strip()) try: - campaign_doc, set_doc = fetch_parameter_set(mongo, campaign_name, round_num, sample_num) + campaign_doc, set_doc = fetch_parameter_set(mongo, campaign_name, batch_no, sample_no) except LookupError as exc: print(f"\nParameter lookup failed: {exc}") mongo.close_connection() return - params = build_sample_params(round_num, sample_num, set_doc) + params = build_set_params(batch_no, sample_no, set_doc) print("\n=== Resolved Sample Parameters ===") for key, value in params.items(): print(f"{key}: {value}") base_sample_name = APIS.build_sample_basename( - round_num=params["round_num"], - sample_num=params["sample_num"], - polymer=params["polymer"], + round_num=params["batch_no"], + sample_num=params["sample_no"], + polymer=params["polymer_name"], solvent=params["solvent"], concentration=params["concentration"], - speed=params["speed"], + speed=params["motor_speed"], temperature=params["temperature"], - gap=params["gap"], - volume=params["volume"], + gap=params["printing_gap"], + volume=params["precursor_volume"], ) root_save_dir = os.path.join(ROOT_SAVE_DIR, sanitize_path_component(campaign_name)) print(f"\nGenerated filename prefix: {base_sample_name}") @@ -284,8 +285,8 @@ def main() -> None: camera_bayer_pattern="GBRG", camera_raw_max_value=1023.0, ) - xpl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer"], "xpl") - ppl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer"], "ppl") + xpl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer_name"], "xpl") + ppl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer_name"], "ppl") capture_records: List[dict] = [] seq = CommandSequence() diff --git a/recipes/user_recipes/ch_apis_imaging_from_mongodb.py b/recipes/user_recipes/ch_apis_imaging_from_mongodb.py index 56695e4..4f97ea1 100644 --- a/recipes/user_recipes/ch_apis_imaging_from_mongodb.py +++ b/recipes/user_recipes/ch_apis_imaging_from_mongodb.py @@ -71,17 +71,18 @@ def fetch_parameter_set(mongo, campaign_name, batch_no, sample_no): return campaign_doc, set_doc -def build_sample_params(batch_no, sample_no, set_doc): +def build_set_params(batch_no, sample_no, set_doc): return { - "polymer": set_doc["polymer_name"], - "round_num": batch_no, - "sample_num": sample_no, + "campaign_name": CAMPAIGN_NAME, + "batch_no": batch_no, + "sample_no": sample_no, + "polymer_name": set_doc["polymer_name"], "temperature": set_doc["temperature"], - "speed": float(set_doc["motor_speed"]), - "gap": set_doc["printing_gap"], + "motor_speed": float(set_doc["motor_speed"]), + "printing_gap": set_doc["printing_gap"], "solvent": set_doc["solvent"], "concentration": set_doc["concentration"], - "volume": set_doc["precursor_volume"], + "precursor_volume": set_doc["precursor_volume"], } @@ -166,18 +167,18 @@ def upload_capture_records_to_mongodb(mongo, campaign_doc, set_doc, capture_reco load_env() mongo = get_mongo() campaign_doc, set_doc = fetch_parameter_set(mongo, CAMPAIGN_NAME, BATCH_NO, SAMPLE_NO) -params = build_sample_params(BATCH_NO, SAMPLE_NO, set_doc) +params = build_set_params(BATCH_NO, SAMPLE_NO, set_doc) base_sample_name = APIS.build_sample_basename( - round_num=params["round_num"], - sample_num=params["sample_num"], - polymer=params["polymer"], + round_num=params["batch_no"], + sample_num=params["sample_no"], + polymer=params["polymer_name"], solvent=params["solvent"], concentration=params["concentration"], - speed=params["speed"], + speed=params["motor_speed"], temperature=params["temperature"], - gap=params["gap"], - volume=params["volume"], + gap=params["printing_gap"], + volume=params["precursor_volume"], ) root_save_dir = os.path.join(str(ROOT_SAVE_DIR), sanitize_path_component(CAMPAIGN_NAME)) @@ -196,8 +197,8 @@ def upload_capture_records_to_mongodb(mongo, campaign_doc, set_doc, capture_reco camera_raw_max_value=1023.0, ) -xpl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer"], "xpl") -ppl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer"], "ppl") +xpl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer_name"], "xpl") +ppl_dir = apis.resolve_mode_directory(root_save_dir, params["polymer_name"], "ppl") capture_records = [] seq = CommandSequence() From 2dd862efad05d9948e9de7ee3753e7c2ca2a0b86 Mon Sep 17 00:00:00 2001 From: Hwang Date: Fri, 24 Apr 2026 00:28:58 -0500 Subject: [PATCH 118/125] Add Festo valve example and docs --- .../commands/festo_solenoid_valve_commands.py | 2 +- aamp_app/devices/festo_solenoid_valve.py | 12 ++- aamp_app/util.py | 16 +++- device_ports.md | 1 + examples/.gitignore | 1 + examples/example_festo_solenoid_valve.py | 82 +++++++++++++++++++ .../Festo_Multiple.ino | 45 ++++++++++ 7 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 examples/example_festo_solenoid_valve.py create mode 100644 to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino diff --git a/aamp_app/commands/festo_solenoid_valve_commands.py b/aamp_app/commands/festo_solenoid_valve_commands.py index bae8bcc..aeed3c7 100644 --- a/aamp_app/commands/festo_solenoid_valve_commands.py +++ b/aamp_app/commands/festo_solenoid_valve_commands.py @@ -77,4 +77,4 @@ def __init__(self, receiver: FestoSolenoidValve, **kwargs): super().__init__(receiver, **kwargs) def execute(self) -> None: - self._result = CommandResult(*self._receiver.valve) + self._result = CommandResult(*self._receiver.close_all()) diff --git a/aamp_app/devices/festo_solenoid_valve.py b/aamp_app/devices/festo_solenoid_valve.py index e79abfd..9716f04 100644 --- a/aamp_app/devices/festo_solenoid_valve.py +++ b/aamp_app/devices/festo_solenoid_valve.py @@ -8,6 +8,13 @@ class FestoSolenoidValve(ArduinoSerialDevice): + """Simple serial wrapper for an Arduino sketch that toggles Festo valve driver outputs. + + The companion sketch in ``to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino`` + maps valve 1/2/3 to Arduino pins 12/8/4 and expects single-byte commands: + A/B/C=open valve 1/2/3, D/E/F=close valve 1/2/3. + """ + def __init__( self, name: str, port: str, baudrate: int = 9600, timeout: float = 0.1 ): @@ -33,7 +40,8 @@ def initialize(self) -> Tuple[bool, str]: return (True, "Solenoid valve initialized") def deinitialize(self) -> Tuple[bool, str]: - self.ser.close() + if self.ser.is_open: + self.ser.close() self._is_initialized = False return (True, "Solenoid valve deinitialized") @@ -51,6 +59,7 @@ def valve_open(self, valve_num: int) -> Tuple[bool, str]: return (True, "Solenoid valve is open") @check_serial + @check_initialized def valve_closed(self, valve_num: int) -> Tuple[bool, str]: if valve_num == 1: self.ser.write(b"D") @@ -63,6 +72,7 @@ def valve_closed(self, valve_num: int) -> Tuple[bool, str]: return (True, "Solenoid valve is closed") @check_serial + @check_initialized def close_all(self) -> Tuple[bool, str]: self.ser.write(b"D") time.sleep(0.25) diff --git a/aamp_app/util.py b/aamp_app/util.py index fc15258..a28a140 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -324,7 +324,7 @@ def default(self, obj): "import_device": "from devices.festo_solenoid_valve import FestoSolenoidValve", "import_commands": "from commands.festo_solenoid_valve_commands import *", "init": { - "default_code": "FestoSolenoidValve(name='FestoSolenoidValve', numchannel=, port='COM5', baudrate=9600, timeout=0.1)", + "default_code": "FestoSolenoidValve(name='FestoSolenoidValve', port='COM5', baudrate=9600, timeout=0.1)", "obj_name": "FestoSolenoidValve", "args": { "name": { @@ -389,24 +389,34 @@ def default(self, obj): "obj": FestoDeinitialize, }, "FestoValveOpen": { - "default_code": "FestoValveOpen(receiver= '')", + "default_code": "FestoValveOpen(receiver= '', valve_num=1)", "args": { "receiver": { "default": "FestoSolenoidValve", "type": str, "notes": "Name of the device", }, + "valve_num": { + "default": 1, + "type": int, + "notes": "Valve number to open. Arduino sketch maps 1/2/3 to pins 12/8/4.", + }, }, "obj": FestoValveOpen, }, "FestoValveClosed": { - "default_code": "FestoValveClosed(receiver= '')", + "default_code": "FestoValveClosed(receiver= '', valve_num=1)", "args": { "receiver": { "default": "FestoSolenoidValve", "type": str, "notes": "Name of the device", }, + "valve_num": { + "default": 1, + "type": int, + "notes": "Valve number to close. Arduino sketch maps 1/2/3 to pins 12/8/4.", + }, }, "obj": FestoValveClosed, }, diff --git a/device_ports.md b/device_ports.md index 92d96ef..a12fa34 100644 --- a/device_ports.md +++ b/device_ports.md @@ -19,6 +19,7 @@ This file tracks the current local serial port assignments and related connectio | `newport_94043a_solar_sim` | `94043A` solar simulator via `69920` power supply | Newport | `COM11` | `9600` | RS-232 via USB adapter; default control path is power mode; initialize applies `400 W` preset and software blocks presets above `450 W`; lamp replacement warning starts at `1000 h` | `examples/example_94043a_solar_sim.py`, `aamp_app/util.py`, `aamp_app/devices/newport_94043a_solar_sim.py` | | `p4pp` | `P4PP` Arduino controller | PolyPrint Illinois | `COM19` | `115200` | Rotation is blocked when linear position is `>= 45.0 mm`; explicit measurement resistor selection supported with default `681 ohm`; measurement default cycles set to `20` | `examples/example_p4pp.py`, `aamp_app/util.py`, `aamp_app/devices/p4pp.py` | | `apis` | `APIS` Arduino controller + Ximea camera | PolyPrint Illinois | `COM8` | `9600` | Composite device for stage control and imaging; default image root is `data/imaging/`; current example writes mode-organized outputs under `data/imaging/demo_campaign/` | `examples/example_apis.py`, `aamp_app/util.py`, `aamp_app/devices/apis.py` | +| `festo_valve` | `MHJ10-S-2,5-QS-1/4-MF-U` x2 via Arduino relay/driver wrapper | Festo + Custom | `COM9` | `9600` | Current local wiring uses Arduino `D8 -> valve_num=2` and `D4 -> valve_num=3`; companion sketch is `to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino`; valve supply is external `24 V DC`, `3-wire`, default closed/monostable | `examples/example_festo_solenoid_valve.py`, `aamp_app/devices/festo_solenoid_valve.py`, `aamp_app/commands/festo_solenoid_valve_commands.py`, `aamp_app/util.py`, `to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino` | | `uhe_nl` | `UHE-NL 7 Sun` solar simulator power control | Sciencetech | `COM4` | `9600` | `initialize` requires lamp OFF and enables cooling first; lamp enable is blocked unless cooling feedback is on; `deinitialize` turns lamp off but leaves cooling on for cooldown | `examples/example_sciencetech_uhe_nl_solar_sim.py`, `aamp_app/util.py`, `aamp_app/devices/sciencetech_uhe_nl_solar_sim.py` | | `stellarnet_uv_vis` | `StellarNet UV-Vis spectrometer` | StellarNet | `USB` | `n/a` | Vendor Python driver required in the environment; current local examples cover manual dark/blank/sample acquisition and ESP301-driven film degradation loops; spectral decay now uses `reference/am15g_spectrum.csv` | `examples/example_stellarnet_spectrometer.py`, `examples/example_uvvis_film_absorbance_degradation.py`, `aamp_app/devices/stellarnet_spectrometer.py` | | `sonicator` | `Arduino Uno R3` wrapper for sonicator front-panel button/status lines | Custom | `COM13` | `9600` | Expected wiring is `5V`, `GND`, `D7` button drive, and `D8` status sense; Python smoke test passed locally; explicit power probe is intrusive and not used during initialize | `examples/example_sonicator.py`, `aamp_app/devices/sonicator.py`, `aamp_app/util.py`, `firmware/sonicator/sonicator_uno_r3/sonicator_uno_r3.ino` | diff --git a/examples/.gitignore b/examples/.gitignore index d8416f3..8fd1989 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -18,6 +18,7 @@ !example_stellarnet_spectrometer.py !example_uvvis_film_absorbance_degradation.py !example_sciencetech_uhe_nl_solar_sim.py +!example_festo_solenoid_valve.py !example_sonicator.py !example_substrate_hotel.py !example_substrate_dispenser.py diff --git a/examples/example_festo_solenoid_valve.py b/examples/example_festo_solenoid_valve.py new file mode 100644 index 0000000..0bb2705 --- /dev/null +++ b/examples/example_festo_solenoid_valve.py @@ -0,0 +1,82 @@ +# Festo solenoid valve smoke test +# run from repo root with: +# python -m examples.example_festo_solenoid_valve + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_invoker import CommandInvoker +from command_sequence import CommandSequence +from devices.festo_solenoid_valve import FestoSolenoidValve +from commands.festo_solenoid_valve_commands import ( + FestoCloseAll, + FestoConnect, + FestoDeinitialize, + FestoInitialize, + FestoValveClosed, + FestoValveOpen, +) + +DEFAULT_PORT = "COM9" +PIN8_VALVE_NUM = 2 +PIN4_VALVE_NUM = 3 + + +def main() -> None: + port = input( + f"Enter the Arduino COM port for the Festo valve controller [{DEFAULT_PORT}]: " + ).strip() + if not port: + port = DEFAULT_PORT + + confirm = input( + f"This test will use {port}, then close both valves, open valve_num={PIN8_VALVE_NUM} (pin 8), close it, open valve_num={PIN4_VALVE_NUM} (pin 4), close it, then deinitialize. Type 'y' to continue: " + ).strip().lower() + if confirm != "y": + print("Smoke test cancelled.") + return + + valve = FestoSolenoidValve( + name="festo_valve", + port=port, + baudrate=9600, + timeout=0.5, + ) + + seq = CommandSequence() + seq.add_device(valve) + seq.add_command(FestoConnect(valve)) + seq.add_command(FestoInitialize(valve)) + seq.add_command(FestoCloseAll(valve)) + seq.add_command(FestoValveOpen(valve, valve_num=PIN4_VALVE_NUM, delay=1.0)) + seq.add_command(FestoValveClosed(valve, valve_num=PIN4_VALVE_NUM, delay=5)) + seq.add_command(FestoValveOpen(valve, valve_num=PIN8_VALVE_NUM, delay=1.0)) + seq.add_command(FestoValveClosed(valve, valve_num=PIN8_VALVE_NUM, delay=5)) + seq.add_command(FestoCloseAll(valve)) + seq.add_command(FestoDeinitialize(valve)) + + print("\nAssumptions:") + print("- Arduino sketch: to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino") + print("- With the current sketch, valve_num=2 uses Arduino pin D8 and valve_num=3 uses D4.") + print("- valve_num=1 is still mapped to D12 unless you remap the sketch.") + print("- The Arduino pin must drive a MOSFET/relay/driver board, not the valve coil directly.") + print("- The valve coil power must come from an external supply with flyback protection.") + + invoker = CommandInvoker( + seq, + log_to_file=True, + log_filename="logs/example_festo_solenoid_valve.log", + alert_slack=False, + ) + invoker.invoke_commands() + + +if __name__ == "__main__": + main() diff --git a/to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino b/to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino new file mode 100644 index 0000000..e228b0d --- /dev/null +++ b/to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino @@ -0,0 +1,45 @@ +int incomingByte; + +void setup() { + // put your setup code here, to run once: + Serial.begin(9600); + pinMode(12, OUTPUT); + pinMode(8, OUTPUT); + pinMode(4, OUTPUT); + delay(500); + digitalWrite(12, LOW); + digitalWrite(8, LOW); + digitalWrite(4, LOW); + +} + +void loop() { + // put your main code here, to run repeatedly: + if (Serial.available() > 0){ + incomingByte = Serial.read(); + if (incomingByte == 'A'){ + digitalWrite(12, HIGH); + delay(250); + } + if (incomingByte == 'B'){ + digitalWrite(8, HIGH); + delay(250); + } + if (incomingByte == 'C'){ + digitalWrite(4, HIGH); + delay(250); + } + if (incomingByte == 'D'){ + digitalWrite(12, LOW); + delay(250); + } + if (incomingByte == 'E'){ + digitalWrite(8, LOW); + delay(250); + } + if (incomingByte == 'F'){ + digitalWrite(4, LOW); + delay(250); + } + } +} From ee9a4c144e5f3632e4e971916ebf7ad0738f6c76 Mon Sep 17 00:00:00 2001 From: Hwang Date: Fri, 24 Apr 2026 01:47:26 -0500 Subject: [PATCH 119/125] Weight spectral decay by wavelength spacing --- aamp_app/devices/stellarnet_spectrometer.py | 69 ++++++++++++++++----- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/aamp_app/devices/stellarnet_spectrometer.py b/aamp_app/devices/stellarnet_spectrometer.py index 231cf70..aab8995 100644 --- a/aamp_app/devices/stellarnet_spectrometer.py +++ b/aamp_app/devices/stellarnet_spectrometer.py @@ -105,6 +105,46 @@ def _trapz(x_values: np.ndarray, y_values: np.ndarray) -> float: return 0.0 return float(np.trapezoid(y_values, x_values)) + @staticmethod + def _compute_decay_metrics_with_spacing( + wavelength_values: np.ndarray, + reference_absorbance: np.ndarray, + current_absorbance: np.ndarray, + decay_threshold: float, + ) -> Tuple[float, float, float, float]: + valid_mask = ( + np.isfinite(wavelength_values) + & np.isfinite(reference_absorbance) + & np.isfinite(current_absorbance) + & (reference_absorbance > decay_threshold) + ) + if np.count_nonzero(valid_mask) < 2: + return (0.0, 0.0, 0.0, 0.0) + + x_valid = wavelength_values[valid_mask] + reference_valid = np.maximum(reference_absorbance[valid_mask], 0.0) + current_valid = np.nan_to_num(current_absorbance[valid_mask], nan=0.0, posinf=0.0, neginf=0.0) + signed_delta = current_valid - reference_valid + + norm = StellarNetSpectrometer._trapz(x_valid, reference_valid) + if norm <= 0: + return (0.0, 0.0, 0.0, 0.0) + + magnitude = np.abs(signed_delta) + positive = np.maximum(signed_delta, 0.0) + negative_abs = np.maximum(-signed_delta, 0.0) + + decay_mag = StellarNetSpectrometer._trapz(x_valid, magnitude) / norm + decay_signed = StellarNetSpectrometer._trapz(x_valid, signed_delta) / norm + decay_positive = StellarNetSpectrometer._trapz(x_valid, positive) / norm + decay_negative_abs = StellarNetSpectrometer._trapz(x_valid, negative_abs) / norm + return ( + float(decay_mag), + float(decay_signed), + float(decay_positive), + float(decay_negative_abs), + ) + @staticmethod def _parse_elapsed_seconds_from_columns(columns: List[str]) -> np.ndarray: elapsed_seconds = [] @@ -965,28 +1005,23 @@ def get_spec_decay( overlap_delta_vs_t0 = overlap_percent - baseline_overlap overlap_abs_change_vs_t0 = np.abs(retention_percent - 100.0) - reference = original_absorbance.copy() - positive_reference = np.where(reference > decay_threshold, np.maximum(reference, 0.0), 0.0) - norm = float(np.sum(positive_reference)) - if norm <= 0: - norm = 1.0 - decay_mag = np.zeros(decayed_absorbance.shape[1], dtype=float) decay_signed = np.zeros(decayed_absorbance.shape[1], dtype=float) decay_positive = np.zeros(decayed_absorbance.shape[1], dtype=float) decay_negative_abs = np.zeros(decayed_absorbance.shape[1], dtype=float) for idx in range(decayed_absorbance.shape[1]): - current = decayed_absorbance[:, idx] - valid_reference = np.where(reference > decay_threshold, reference, 0.0) - current_valid = np.where(reference > decay_threshold, np.nan_to_num(current, nan=0.0), 0.0) - signed = (current_valid - valid_reference) / norm - magnitude = np.abs(valid_reference - current_valid) / norm - - decay_mag[idx] = float(np.sum(magnitude)) - decay_signed[idx] = float(np.sum(signed)) - decay_positive[idx] = float(np.sum(np.maximum(signed, 0.0))) - decay_negative_abs[idx] = float(np.sum(np.maximum(-signed, 0.0))) + ( + decay_mag[idx], + decay_signed[idx], + decay_positive[idx], + decay_negative_abs[idx], + ) = self._compute_decay_metrics_with_spacing( + wavelength_list_inrange, + original_absorbance, + decayed_absorbance[:, idx], + decay_threshold, + ) t80_h = self._interpolate_crossing_time(elapsed_hours, decay_mag, 0.20) @@ -1010,7 +1045,7 @@ def get_spec_decay( "# end at = " + str(range_end) + "\n", "# irradiance_file = " + str(irradiance_file) + "\n", "# decay_threshold = " + str(decay_threshold) + "\n", - "# methodology = UVVis_Converter style overlap/interpolate/trapz and decay index summary\n", + "# methodology = UVVis_Converter style overlap/interpolate/trapz and decay index summary with wavelength-spacing weighting\n", ] with open(new_filename, 'w') as file: file.writelines(comment) From 197b26600c860472782dff302e311f18f9a99193 Mon Sep 17 00:00:00 2001 From: Hwang Date: Fri, 24 Apr 2026 11:54:54 -0500 Subject: [PATCH 120/125] Update Kinova fallback and Kortex install notes --- aamp_app/devices/kinova_arm.py | 43 ++++++++------------------------- requirements.txt | Bin 600 -> 588 bytes 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/aamp_app/devices/kinova_arm.py b/aamp_app/devices/kinova_arm.py index d2304cc..0b7d5fe 100644 --- a/aamp_app/devices/kinova_arm.py +++ b/aamp_app/devices/kinova_arm.py @@ -26,6 +26,7 @@ class KinovaArm(Device): pose_dict_file = 'robot_arm_poses.yaml' + safe_action_names = ("Home", "Above Fork Pickup") def __init__( self, @@ -118,40 +119,16 @@ def deinitialize(self) -> Tuple[bool, str]: def home(self) -> Tuple[bool, str]: - # Make sure the arm is in Single Level Servoing mode - base_servo_mode = Base_pb2.ServoingModeInformation() - base_servo_mode.servoing_mode = Base_pb2.SINGLE_LEVEL_SERVOING - self._base.SetServoingMode(base_servo_mode) - - # Move arm to ready position - # print("Moving the arm to a safe position") - action_type = Base_pb2.RequestedActionType() - action_type.action_type = Base_pb2.REACH_JOINT_ANGLES - action_list = self._base.ReadAllActions(action_type) - action_handle = None - - for action in action_list.action_list: - # if action.name == "Home": - if action.name == 'Above Fork Pickup': - action_handle = action.handle + for action_name in self.safe_action_names: + was_successful, message = self.execute_action(action_name) + if was_successful: + return (True, message) - if action_handle is None: - return (False, "Can't reach safe position") + if not message.startswith("Did not find action of name "): + return (False, message) - e = threading.Event() - notification_handle = self._base.OnNotificationActionTopic( - self.check_for_end_or_abort(e), - Base_pb2.NotificationOptions() - ) - - self._base.ExecuteActionFromReference(action_handle) - finished = e.wait(self._action_timeout) - self._base.Unsubscribe(notification_handle) - - if finished: - return (True, "Safe position reached") - else: - return (False, "Timeout on action notification wait") + action_name_str = ", ".join(self.safe_action_names) + return (False, "Can't reach safe position. None of these saved actions were found: " + action_name_str) def execute_action(self, action_name: str): # Make sure the arm is in Single Level Servoing mode @@ -390,4 +367,4 @@ def check(notification, e = e): if notification.action_event == Base_pb2.ACTION_END \ or notification.action_event == Base_pb2.ACTION_ABORT: e.set() - return check \ No newline at end of file + return check diff --git a/requirements.txt b/requirements.txt index 0c0e65d8eacd4663b4ec26a63b84049120c29557..8dbf765c6d87943f09be0f08fd49bde84968b8f9 100644 GIT binary patch literal 588 zcmZXR!EPKO42JJKg{5B0D(T)Tb3DOo*8QaQ}9f}+m~UwQq^(*jQ{^L z21i+WV&lY1#(-wTvh>NKqhm9OXwi(xItfW4MJ-20aqxdcINLe%Xu{-7!q_k-jpEFe zy_a{BYLGZfTMK1IZ?5&n%jn*PHpz+Zq9Qnwx8TsttZUUtf5`x z*d{OPtBGr9hJe~*9*iW4seX8)lHJlnx7k#6k$8u3z?sY-hh%Qat~HQ*PC`vF%hHFQ_`Skbz6gSQi!~Pmyo6W-ag$ra#0;5pbU9eFcn@<xitz~UzM+?lx#e}6T4(WIoH zSK?lGYBVSklWKK@v1k6X-gf5-% zqHl2u^k7X#pG`iEX!_EbGb87r Date: Wed, 27 May 2026 15:03:01 -0500 Subject: [PATCH 121/125] Update APIS core defaults and commands --- aamp_app/commands/apis_commands.py | 148 +++++++ aamp_app/devices/apis.py | 634 +++++++++++++++++++++++++++++ aamp_app/util.py | 90 +++- device_ports.md | 4 +- 4 files changed, 873 insertions(+), 3 deletions(-) diff --git a/aamp_app/commands/apis_commands.py b/aamp_app/commands/apis_commands.py index f6e79ad..d583e90 100644 --- a/aamp_app/commands/apis_commands.py +++ b/aamp_app/commands/apis_commands.py @@ -75,6 +75,66 @@ def execute(self) -> None: self._result = CommandResult(*self._receiver.rotate_sample(self._params["angle_deg"])) +class APISSetPolarizerBaseline(APISParentCommand): + """Set APIS XPL angle and derive the reachable orthogonal PPL angle.""" + + def __init__( + self, + receiver: APIS, + xpl_angle_deg: float, + persist: bool = False, + source: str = "manual", + **kwargs + ): + super().__init__(receiver, **kwargs) + self._params["xpl_angle_deg"] = xpl_angle_deg + self._params["persist"] = persist + self._params["source"] = source + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.set_polarizer_baseline( + self._params["xpl_angle_deg"], + persist=self._params["persist"], + source=self._params["source"], + ) + ) + + +class APISLoadPolarizerBaseline(APISParentCommand): + """Load the persisted APIS XPL/PPL polarizer baseline.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.load_polarizer_baseline()) + + +class APISSavePolarizerBaseline(APISParentCommand): + """Persist the current APIS XPL/PPL polarizer baseline.""" + + def __init__(self, receiver: APIS, source: str = "manual", **kwargs): + super().__init__(receiver, **kwargs) + self._params["source"] = source + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.save_polarizer_baseline(source=self._params["source"]) + ) + + +class APISRotateXPL(APISParentCommand): + """Rotate the APIS polarizer to the configured XPL baseline angle.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.rotate_xpl()) + + +class APISRotatePPL(APISParentCommand): + """Rotate the APIS polarizer to the configured PPL baseline angle.""" + + def execute(self) -> None: + self._result = CommandResult(*self._receiver.rotate_ppl()) + + class APISGetState(APISParentCommand): """Return the cached APIS state.""" @@ -161,3 +221,91 @@ def execute(self) -> None: rgb_path=self._params["rgb_path"], ) ) + + +class APISRunImagingSequence(APISParentCommand): + """Run the current APIS XPL/PPL RAW16 imaging sequence with metadata logging.""" + + def __init__( + self, + receiver: APIS, + sample_id: str, + directory: str = None, + sample_angles=None, + xpl_exposure_time: int = APIS.XIMEA_DEFAULT_XPL_EXPOSURE_US, + ppl_exposure_time: int = APIS.XIMEA_DEFAULT_PPL_EXPOSURE_US, + do_xpl: bool = True, + do_ppl: bool = True, + xpl_polarizer_angle: float = None, + ppl_polarizer_angle: float = None, + gain: float = 0.0, + **kwargs + ): + super().__init__(receiver, **kwargs) + self._params["sample_id"] = sample_id + self._params["directory"] = directory + self._params["sample_angles"] = sample_angles + self._params["xpl_exposure_time"] = xpl_exposure_time + self._params["ppl_exposure_time"] = ppl_exposure_time + self._params["do_xpl"] = do_xpl + self._params["do_ppl"] = do_ppl + self._params["xpl_polarizer_angle"] = xpl_polarizer_angle + self._params["ppl_polarizer_angle"] = ppl_polarizer_angle + self._params["gain"] = gain + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.run_imaging_sequence( + sample_id=self._params["sample_id"], + directory=self._params["directory"], + sample_angles=self._params["sample_angles"], + xpl_exposure_time=self._params["xpl_exposure_time"], + ppl_exposure_time=self._params["ppl_exposure_time"], + do_xpl=self._params["do_xpl"], + do_ppl=self._params["do_ppl"], + xpl_polarizer_angle=self._params["xpl_polarizer_angle"], + ppl_polarizer_angle=self._params["ppl_polarizer_angle"], + gain=self._params["gain"], + ) + ) + + +class APISRunPolarizerCalibration(APISParentCommand): + """Run a RAW16 polarizer scan and update APIS XPL/PPL baseline angles.""" + + def __init__( + self, + receiver: APIS, + sample_id: str = "polarizer_calibration", + directory: str = None, + exposure_time: int = APIS.XIMEA_DEFAULT_POLARIZER_CALIBRATION_EXPOSURE_US, + polarizer_angles=None, + sample_angle: float = 0.0, + fine_radius_deg: int = APIS.POLARIZER_CALIBRATION_FINE_RADIUS_DEG, + fine_step_deg: int = APIS.POLARIZER_CALIBRATION_FINE_STEP_DEG, + gain: float = 0.0, + **kwargs + ): + super().__init__(receiver, **kwargs) + self._params["sample_id"] = sample_id + self._params["directory"] = directory + self._params["exposure_time"] = exposure_time + self._params["polarizer_angles"] = polarizer_angles + self._params["sample_angle"] = sample_angle + self._params["fine_radius_deg"] = fine_radius_deg + self._params["fine_step_deg"] = fine_step_deg + self._params["gain"] = gain + + def execute(self) -> None: + self._result = CommandResult( + *self._receiver.run_polarizer_calibration( + sample_id=self._params["sample_id"], + directory=self._params["directory"], + exposure_time=self._params["exposure_time"], + polarizer_angles=self._params["polarizer_angles"], + sample_angle=self._params["sample_angle"], + fine_radius_deg=self._params["fine_radius_deg"], + fine_step_deg=self._params["fine_step_deg"], + gain=self._params["gain"], + ) + ) diff --git a/aamp_app/devices/apis.py b/aamp_app/devices/apis.py index dc5fcc3..b9c1f3f 100644 --- a/aamp_app/devices/apis.py +++ b/aamp_app/devices/apis.py @@ -1,8 +1,13 @@ +import csv +import json import os import time from decimal import Decimal, getcontext from typing import Optional, Tuple +import cv2 +import numpy as np + from .device import SerialDevice, check_initialized, check_serial from .ximea_camera import XimeaCamera @@ -46,6 +51,18 @@ class APIS(SerialDevice): STATE_LATCHED = "LATCHED" STATE_ARMED = "ARMED" + + XIMEA_DEFAULT_PPL_EXPOSURE_US = 18000 + XIMEA_DEFAULT_XPL_EXPOSURE_US = 400000 + XIMEA_DEFAULT_POLARIZER_CALIBRATION_EXPOSURE_US = 200000 + XIMEA_DEFAULT_NORMAL_EXPOSURE_US = XIMEA_DEFAULT_PPL_EXPOSURE_US + XIMEA_DEFAULT_CROSSPOL_EXPOSURE_US = XIMEA_DEFAULT_XPL_EXPOSURE_US + + POLARIZER_PPL_ANGLE_DEG = 30 + POLARIZER_XPL_ANGLE_DEG = 120 + POLARIZER_CALIBRATION_FINE_RADIUS_DEG = 10 + POLARIZER_CALIBRATION_FINE_STEP_DEG = 1 + CAMERA_PREVIEW_WB_GAINS = (1.40, 1.00, 1.20) CAMERA_PREVIEW_GAMMA = 1.0 / 2.2 @@ -69,6 +86,7 @@ def __init__( camera_save_directory: str = "data/imaging/", camera_bayer_pattern: str = XimeaCamera.DEFAULT_RAW_BAYER_PATTERN, camera_raw_max_value: float = XimeaCamera.DEFAULT_RAW_MAX_VALUE, + polarizer_baseline_path: Optional[str] = None, ): super().__init__(name, port, baudrate, timeout) self._connection_wait_s = connection_wait_s @@ -85,11 +103,17 @@ def __init__( self._camera_save_directory = camera_save_directory self._camera_bayer_pattern = camera_bayer_pattern self._camera_raw_max_value = camera_raw_max_value + self._polarizer_baseline_path = polarizer_baseline_path self._state = self.STATE_LATCHED self._is_connected = False self._polarizer_angle = 0.0 self._sample_angle = 0.0 + self._polarizer_xpl_angle_deg = self.POLARIZER_XPL_ANGLE_DEG + self._polarizer_ppl_angle_deg = self.derive_ppl_angle_from_xpl(self._polarizer_xpl_angle_deg) + self.load_polarizer_baseline() + self.last_calibration_info = {} + self.last_run_info = {} self.camera = XimeaCamera(name=f"{name}_camera") if use_camera else None if self.camera is not None: self.camera.save_directory = camera_save_directory @@ -114,6 +138,7 @@ def get_init_args(self) -> dict: "camera_save_directory": self._camera_save_directory, "camera_bayer_pattern": self._camera_bayer_pattern, "camera_raw_max_value": self._camera_raw_max_value, + "polarizer_baseline_path": self._polarizer_baseline_path, } def update_init_args(self, args_dict: dict): @@ -135,6 +160,8 @@ def update_init_args(self, args_dict: dict): self._camera_save_directory = args_dict["camera_save_directory"] self._camera_bayer_pattern = args_dict["camera_bayer_pattern"] self._camera_raw_max_value = args_dict["camera_raw_max_value"] + self._polarizer_baseline_path = args_dict.get("polarizer_baseline_path") + self.load_polarizer_baseline() self.camera = XimeaCamera(name=f"{self._name}_camera") if self._use_camera else None if self.camera is not None: self.camera.save_directory = self._camera_save_directory @@ -151,6 +178,14 @@ def polarizer_angle(self) -> float: def sample_angle(self) -> float: return self._sample_angle + @property + def xpl_angle(self) -> float: + return self._polarizer_xpl_angle_deg + + @property + def ppl_angle(self) -> float: + return self._polarizer_ppl_angle_deg + def connect(self) -> Tuple[bool, str]: was_successful, response = self.start_serial(delay=2.0) if not was_successful: @@ -344,6 +379,183 @@ def get_polarizer_angle(self) -> Tuple[bool, float]: def get_sample_angle(self) -> Tuple[bool, float]: return (True, self._sample_angle) + @staticmethod + def choose_orthogonal_polarizer_angle( + xpl_angle: float, + min_angle: float = POLARIZER_STAGE_MIN_ANGLE, + max_angle: float = POLARIZER_STAGE_MAX_ANGLE, + prefer_positive: bool = True, + ) -> Tuple[int, int]: + xpl_angle = int(round(xpl_angle)) + min_angle = int(min_angle) + max_angle = int(max_angle) + offsets = (90, -90) if prefer_positive else (-90, 90) + for offset in offsets: + candidate = xpl_angle + offset + if min_angle <= candidate <= max_angle: + return (candidate, offset) + raise ValueError( + f"No valid PPL angle for XPL={xpl_angle} deg within range {min_angle}-{max_angle}." + ) + + @classmethod + def derive_ppl_angle_from_xpl(cls, xpl_angle: float, prefer_positive: bool = True) -> int: + ppl_angle, _ = cls.choose_orthogonal_polarizer_angle( + xpl_angle, + cls.POLARIZER_STAGE_MIN_ANGLE, + cls.POLARIZER_STAGE_MAX_ANGLE, + prefer_positive=prefer_positive, + ) + return ppl_angle + + @staticmethod + def build_angle_window( + center_angle: float, + radius: int, + min_angle: float, + max_angle: float, + step: int = 1, + ) -> list: + if step <= 0: + raise ValueError("Angle window step must be positive.") + start = max(int(min_angle), int(round(center_angle)) - int(radius)) + end = min(int(max_angle), int(round(center_angle)) + int(radius)) + return list(range(start, end + 1, int(step))) + + @staticmethod + def parse_angle_spec(angle_spec, min_angle: float, max_angle: float) -> Tuple[list, str]: + if angle_spec is None: + return ([], "Angles are empty.") + if isinstance(angle_spec, str): + raw = angle_spec.strip() + if not raw: + return ([], "Angles are empty.") + parts = [p for p in raw.replace(",", " ").split() if p] + else: + parts = list(angle_spec) + + angles = [] + for part in parts: + token = str(part).strip() + if ":" in token: + nums = token.split(":") + if len(nums) not in (2, 3): + return ([], f"Invalid range token: '{token}'") + try: + start = int(nums[0]) + end = int(nums[1]) + step = int(nums[2]) if len(nums) == 3 else 1 + except ValueError: + return ([], f"Invalid range token: '{token}'") + if step == 0: + return ([], f"Step cannot be 0 in '{token}'") + if start < min_angle or start > max_angle or end < min_angle or end > max_angle: + return ([], f"Range out of bounds ({min_angle}-{max_angle}): '{token}'") + stop = end + 1 if step > 0 else end - 1 + angles.extend(list(range(start, stop, step))) + else: + try: + value = int(float(token)) + except ValueError: + return ([], f"Invalid angle token: '{token}'") + if value < min_angle or value > max_angle: + return ([], f"Angle out of bounds ({min_angle}-{max_angle}): '{token}'") + angles.append(value) + + if not angles: + return ([], "No valid angles found.") + return (angles, "") + + def _get_polarizer_baseline_path(self) -> str: + return self._polarizer_baseline_path or os.path.join("data", "polarizer_baseline.json") + + def load_polarizer_baseline(self) -> Tuple[bool, str]: + baseline_path = self._get_polarizer_baseline_path() + if not os.path.isfile(baseline_path): + return ( + True, + f"Using default APIS polarizer baseline: XPL={self._polarizer_xpl_angle_deg} deg, " + f"PPL={self._polarizer_ppl_angle_deg} deg.", + ) + + try: + with open(baseline_path, "r", encoding="utf-8") as file_obj: + baseline = json.load(file_obj) + xpl_angle_deg = baseline["xpl_angle_deg"] + except Exception as exc: + return (False, f"Failed to load APIS polarizer baseline from {baseline_path}: {exc}") + + was_successful, response = self.set_polarizer_baseline( + xpl_angle_deg, + persist=False, + source="saved baseline", + ) + if not was_successful: + return (False, response) + return (True, f"Loaded APIS polarizer baseline from {baseline_path}. {response}") + + def save_polarizer_baseline(self, source: str = "manual") -> Tuple[bool, str]: + baseline_path = self._get_polarizer_baseline_path() + payload = { + "saved_at": time.strftime("%Y-%m-%dT%H:%M:%S"), + "source": source, + "xpl_angle_deg": int(self._polarizer_xpl_angle_deg), + "ppl_angle_deg": int(self._polarizer_ppl_angle_deg), + "ppl_offset_deg": int(self._polarizer_ppl_angle_deg - self._polarizer_xpl_angle_deg), + } + try: + os.makedirs(os.path.dirname(baseline_path) or ".", exist_ok=True) + with open(baseline_path, "w", encoding="utf-8") as file_obj: + json.dump(payload, file_obj, indent=2) + except Exception as exc: + return (False, f"Failed to save APIS polarizer baseline to {baseline_path}: {exc}") + return (True, f"Saved APIS polarizer baseline to {baseline_path}.") + + def set_polarizer_baseline( + self, + xpl_angle_deg: float, + persist: bool = False, + source: str = "manual", + ) -> Tuple[bool, str]: + if xpl_angle_deg < self.POLARIZER_STAGE_MIN_ANGLE or xpl_angle_deg > self.POLARIZER_STAGE_MAX_ANGLE: + return ( + False, + f"XPL angle {xpl_angle_deg:.2f} deg is outside the allowed polarizer range " + f"{self.POLARIZER_STAGE_MIN_ANGLE}-{self.POLARIZER_STAGE_MAX_ANGLE:.2f} deg.", + ) + try: + ppl_angle, offset = self.choose_orthogonal_polarizer_angle( + xpl_angle_deg, + self.POLARIZER_STAGE_MIN_ANGLE, + self.POLARIZER_STAGE_MAX_ANGLE, + ) + except ValueError as exc: + return (False, str(exc)) + + self._polarizer_xpl_angle_deg = int(round(xpl_angle_deg)) + self._polarizer_ppl_angle_deg = int(ppl_angle) + persist_msg = "" + if persist: + was_successful, persist_msg = self.save_polarizer_baseline(source=source) + if not was_successful: + return (False, persist_msg) + persist_msg = " " + persist_msg + return ( + True, + f"APIS polarizer baseline set: XPL={self._polarizer_xpl_angle_deg} deg, " + f"PPL=XPL{offset:+d} -> {self._polarizer_ppl_angle_deg} deg.{persist_msg}", + ) + + @check_initialized + @check_serial + def rotate_xpl(self) -> Tuple[bool, str]: + return self.rotate_polarizer(self._polarizer_xpl_angle_deg) + + @check_initialized + @check_serial + def rotate_ppl(self) -> Tuple[bool, str]: + return self.rotate_polarizer(self._polarizer_ppl_angle_deg) + @staticmethod def format_speed(speed: float) -> str: getcontext().prec = 50 @@ -385,6 +597,428 @@ def resolve_mode_directory(root_save_dir: str, polymer: str, mode: str) -> str: def build_mode_filename(base_sample_name: str, mode: str, angle_deg: float) -> str: return f"{base_sample_name}_{mode}_{int(angle_deg)}deg" + @staticmethod + def _append_calibration_log(log_path: str, row: dict) -> None: + os.makedirs(os.path.dirname(log_path), exist_ok=True) + fieldnames = [ + "timestamp", + "mode", + "exposure_us", + "gain", + "polarizer_angle", + "sample_angle", + "signal_mean", + "filepath", + "arduino_response", + "attempt_count", + ] + file_exists = os.path.isfile(log_path) + with open(log_path, mode="a", newline="", encoding="utf-8") as file_obj: + writer = csv.DictWriter(file_obj, fieldnames=fieldnames) + if not file_exists: + writer.writeheader() + writer.writerow({key: row.get(key, "") for key in fieldnames}) + + @staticmethod + def _write_json(filepath: str, data: dict) -> None: + os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True) + with open(filepath, "w", encoding="utf-8") as file_obj: + json.dump(data, file_obj, indent=2) + + def _capture_imaging_phase( + self, + *, + mode_name: str, + display_name: str, + sample_angles, + exposure_time: int, + polarizer_angle: float, + output_dir: str, + sample_id: str, + log_path: str, + metadata: dict, + gain: float, + ) -> Tuple[bool, str]: + os.makedirs(output_dir, exist_ok=True) + + was_successful, response = self.rotate_polarizer(polarizer_angle) + if not was_successful: + return (False, f"Failed to move Polarizer to {polarizer_angle}: {response}") + + for angle in sample_angles: + was_successful, response = self.rotate_sample(angle) + if not was_successful and mode_name == "xpl": + was_successful, response = self.rotate_sample(angle) + if not was_successful: + return (False, f"Failed to move Sample to {angle}: {response}") + + filename = f"{sample_id}_{mode_name}_{int(angle):03d}" + image_path = os.path.join(output_dir, filename + ".tif") + was_successful, response = self.camera.capture_raw16( + save_to_file=True, + filename=filename, + directory=output_dir, + exposure_time=exposure_time, + gain=gain, + ) + if not was_successful: + return (False, response) + + timestamp = time.strftime("%Y-%m-%dT%H:%M:%S") + self._append_calibration_log( + log_path, + { + "timestamp": timestamp, + "mode": mode_name, + "exposure_us": exposure_time, + "gain": gain, + "polarizer_angle": polarizer_angle, + "sample_angle": angle, + "filepath": image_path, + "arduino_response": "OK", + "attempt_count": 1, + }, + ) + metadata["images"].append( + { + "filename": os.path.basename(image_path), + "mode": mode_name, + "display_name": display_name, + "polarizer_angle_deg": polarizer_angle, + "sample_angle_deg": angle, + "exposure_us": exposure_time, + "filepath": image_path, + "timestamp": timestamp, + } + ) + + return (True, f"{display_name} capture complete.") + + @check_initialized + @check_serial + def run_imaging_sequence( + self, + sample_id: str, + directory: Optional[str] = None, + sample_angles=None, + xpl_exposure_time: int = XIMEA_DEFAULT_XPL_EXPOSURE_US, + ppl_exposure_time: int = XIMEA_DEFAULT_PPL_EXPOSURE_US, + do_xpl: bool = True, + do_ppl: bool = True, + xpl_polarizer_angle: Optional[float] = None, + ppl_polarizer_angle: Optional[float] = None, + gain: float = 0.0, + ) -> Tuple[bool, str]: + if self.camera is None: + return (False, "APIS camera support is disabled for this device instance.") + if not (do_xpl or do_ppl): + return (False, "No modes enabled for APIS imaging sequence.") + if sample_angles is None: + sample_angles = [90, 60, 45, 30, 0] + sample_angles, angle_error = self.parse_angle_spec( + sample_angles, + self.SAMPLE_STAGE_MIN_ANGLE, + self.SAMPLE_STAGE_MAX_ANGLE, + ) + if not sample_angles: + return (False, angle_error) + + xpl_angle = self._polarizer_xpl_angle_deg if xpl_polarizer_angle is None else xpl_polarizer_angle + ppl_angle = self._polarizer_ppl_angle_deg if ppl_polarizer_angle is None else ppl_polarizer_angle + save_root = directory or self._camera_save_directory + sample_root = os.path.join(save_root, sample_id) + log_path = os.path.join(sample_root, f"{sample_id}_log.csv") + metadata_path = os.path.join(sample_root, f"{sample_id}_metadata.json") + metadata = { + "sample_id": sample_id, + "sequence_started_at": time.strftime("%Y-%m-%dT%H:%M:%S"), + "sequence_completed": False, + "error_message": "", + "save_root": save_root, + "mode_angles_deg": { + "xpl": xpl_angle if do_xpl else None, + "ppl": ppl_angle if do_ppl else None, + }, + "images": [], + } + run_info = { + "saved_frame_count": 0, + "sequence_completed": False, + "body_error": "", + "metadata_path": metadata_path, + } + + try: + was_successful, response = self.reset() + if not was_successful: + raise RuntimeError(response) + + if do_xpl: + was_successful, response = self._capture_imaging_phase( + mode_name="xpl", + display_name="XPL", + sample_angles=sample_angles, + exposure_time=xpl_exposure_time, + polarizer_angle=xpl_angle, + output_dir=os.path.join(sample_root, "xpl"), + sample_id=sample_id, + log_path=log_path, + metadata=metadata, + gain=gain, + ) + if not was_successful: + raise RuntimeError(response) + + if do_ppl: + was_successful, response = self._capture_imaging_phase( + mode_name="ppl", + display_name="PPL", + sample_angles=sample_angles, + exposure_time=ppl_exposure_time, + polarizer_angle=ppl_angle, + output_dir=os.path.join(sample_root, "ppl"), + sample_id=sample_id, + log_path=log_path, + metadata=metadata, + gain=gain, + ) + if not was_successful: + raise RuntimeError(response) + + self.home() + metadata["sequence_completed"] = True + run_info["sequence_completed"] = True + except Exception as exc: + metadata["error_message"] = str(exc) + run_info["body_error"] = str(exc) + try: + self.emergency_stop() + except Exception: + pass + finally: + run_info["saved_frame_count"] = len(metadata["images"]) + metadata["saved_frame_count"] = len(metadata["images"]) + try: + self._write_json(metadata_path, metadata) + except Exception as exc: + run_info["body_error"] = run_info["body_error"] or str(exc) + metadata["error_message"] = metadata["error_message"] or str(exc) + self.last_run_info = run_info + + if run_info["body_error"]: + return (False, "APIS imaging sequence failed: " + run_info["body_error"]) + return ( + True, + f"APIS imaging sequence complete. Saved {run_info['saved_frame_count']} frame(s). " + f"Metadata: {metadata_path}", + ) + + def _capture_polarizer_scan( + self, + *, + sample_id: str, + exposure_time: int, + gain: float, + sample_angle: float, + polarizer_angles, + scan_phase: str, + output_dir: str, + log_path: str, + ) -> Tuple[bool, object]: + results = [] + for angle in polarizer_angles: + was_successful, response = self.rotate_polarizer(float(angle)) + if not was_successful: + return (False, response) + + filename = f"{sample_id}_polarizer_cal_{scan_phase}_{int(angle):03d}" + image_path = os.path.join(output_dir, filename + ".tif") + was_successful, response = self.camera.capture_raw16( + save_to_file=True, + filename=filename, + directory=output_dir, + exposure_time=exposure_time, + gain=gain, + ) + if not was_successful: + return (False, response) + + image_data = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) + if image_data is None: + return (False, f"Failed to read calibration image: {image_path}") + signal_mean = float(np.asarray(image_data, dtype=np.float32).mean()) + row = { + "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), + "mode": "polarizer_calibration", + "exposure_us": exposure_time, + "gain": gain, + "polarizer_angle": angle, + "sample_angle": sample_angle, + "signal_mean": signal_mean, + "filepath": image_path, + "arduino_response": scan_phase.upper(), + "attempt_count": 1, + } + self._append_calibration_log(log_path, row) + results.append( + { + "filename": os.path.basename(image_path), + "scan_phase": scan_phase, + "polarizer_angle_deg": int(angle), + "sample_angle_deg": sample_angle, + "exposure_us": exposure_time, + "mean_signal": signal_mean, + "filepath": image_path, + "timestamp": row["timestamp"], + } + ) + return (True, results) + + @check_initialized + @check_serial + def run_polarizer_calibration( + self, + sample_id: str = "polarizer_calibration", + directory: Optional[str] = None, + exposure_time: int = XIMEA_DEFAULT_POLARIZER_CALIBRATION_EXPOSURE_US, + polarizer_angles=None, + sample_angle: float = 0.0, + fine_radius_deg: int = POLARIZER_CALIBRATION_FINE_RADIUS_DEG, + fine_step_deg: int = POLARIZER_CALIBRATION_FINE_STEP_DEG, + gain: float = 0.0, + ) -> Tuple[bool, str]: + if self.camera is None: + return (False, "APIS camera support is disabled for this device instance.") + + if polarizer_angles is None: + polarizer_angles = f"{int(self.POLARIZER_STAGE_MIN_ANGLE)}:{int(self.POLARIZER_STAGE_MAX_ANGLE)}:5" + polarizer_angles, angle_error = self.parse_angle_spec( + polarizer_angles, + self.POLARIZER_STAGE_MIN_ANGLE, + self.POLARIZER_STAGE_MAX_ANGLE, + ) + if not polarizer_angles: + return (False, angle_error) + + sample_root = os.path.join(directory or self._camera_save_directory, sample_id) + calibration_dir = os.path.join(sample_root, "polarizer_calibration") + log_path = os.path.join(sample_root, f"{sample_id}_log.csv") + metadata_path = os.path.join(sample_root, f"{sample_id}_polarizer_calibration.json") + os.makedirs(calibration_dir, exist_ok=True) + + metadata = { + "sample_id": sample_id, + "capture_type": "polarizer_calibration", + "calibration_sample_angle_deg": sample_angle, + "coarse_scan_angles_deg": list(polarizer_angles), + "fine_radius_deg": fine_radius_deg, + "fine_step_deg": fine_step_deg, + "exposure_us": exposure_time, + "gain": gain, + "images": [], + "scan_results": [], + "sequence_completed": False, + "error_message": "", + } + + try: + was_successful, response = self.reset() + if not was_successful: + raise RuntimeError(response) + + was_successful, response = self.rotate_sample(sample_angle) + if not was_successful: + raise RuntimeError(response) + + was_successful, coarse_results = self._capture_polarizer_scan( + sample_id=sample_id, + exposure_time=exposure_time, + gain=gain, + sample_angle=sample_angle, + polarizer_angles=polarizer_angles, + scan_phase="coarse", + output_dir=calibration_dir, + log_path=log_path, + ) + if not was_successful: + raise RuntimeError(coarse_results) + + metadata["scan_results"].extend(coarse_results) + metadata["images"].extend(coarse_results) + coarse_xpl = min( + coarse_results, + key=lambda item: (item["mean_signal"], item["polarizer_angle_deg"]), + ) + fine_angles = self.build_angle_window( + coarse_xpl["polarizer_angle_deg"], + fine_radius_deg, + self.POLARIZER_STAGE_MIN_ANGLE, + self.POLARIZER_STAGE_MAX_ANGLE, + fine_step_deg, + ) + metadata["coarse_darkest_angle_deg"] = coarse_xpl["polarizer_angle_deg"] + metadata["coarse_darkest_signal_mean"] = coarse_xpl["mean_signal"] + metadata["fine_scan_angles_deg"] = fine_angles + + was_successful, fine_results = self._capture_polarizer_scan( + sample_id=sample_id, + exposure_time=exposure_time, + gain=gain, + sample_angle=sample_angle, + polarizer_angles=fine_angles, + scan_phase="fine", + output_dir=calibration_dir, + log_path=log_path, + ) + if not was_successful: + raise RuntimeError(fine_results) + + metadata["scan_results"].extend(fine_results) + metadata["images"].extend(fine_results) + xpl_candidate = min( + fine_results, + key=lambda item: (item["mean_signal"], item["polarizer_angle_deg"]), + ) + was_successful, response = self.set_polarizer_baseline( + xpl_candidate["polarizer_angle_deg"], + persist=True, + source="calibration baseline", + ) + if not was_successful: + raise RuntimeError(response) + + _, ppl_offset = self.choose_orthogonal_polarizer_angle(self._polarizer_xpl_angle_deg) + metadata["recommended_xpl_angle_deg"] = self._polarizer_xpl_angle_deg + metadata["recommended_ppl_angle_deg"] = self._polarizer_ppl_angle_deg + metadata["recommended_ppl_offset_deg"] = ppl_offset + metadata["xpl_signal_mean"] = xpl_candidate["mean_signal"] + metadata["sequence_completed"] = True + + self.home() + self.last_calibration_info = metadata + with open(metadata_path, "w", encoding="utf-8") as file_obj: + json.dump(metadata, file_obj, indent=2) + + return ( + True, + "Polarizer calibration complete. " + f"XPL={self._polarizer_xpl_angle_deg} deg, " + f"PPL={self._polarizer_ppl_angle_deg} deg. Metadata: {metadata_path}", + ) + except Exception as exc: + metadata["error_message"] = str(exc) + self.last_calibration_info = metadata + try: + with open(metadata_path, "w", encoding="utf-8") as file_obj: + json.dump(metadata, file_obj, indent=2) + except Exception: + pass + try: + self.emergency_stop() + except Exception: + pass + return (False, "Polarizer calibration failed: " + str(exc)) + def _resolve_capture_directory(self, directory: Optional[str], mode_subdir: Optional[str] = None) -> str: base_dir = directory or self._camera_save_directory if mode_subdir: diff --git a/aamp_app/util.py b/aamp_app/util.py index a28a140..fd7de45 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -899,7 +899,7 @@ def default(self, obj): "options": {"custom_init_args": ["port"]}, }, "init": { - "default_code": "APIS(name='APIS', port='', baudrate=9600, timeout=0.5, connection_wait_s=2.0, settling_time_s=1.5, command_delay_s=0.05, max_retries=3, polarizer_stage_to_servo_ratio=1.059, sample_stage_to_servo_ratio=1.059, polarizer_stage_direction=1, sample_stage_direction=1, polarizer_servo_zero_deg=0, sample_servo_zero_deg=0, use_camera=True, camera_save_directory='data/imaging/', camera_bayer_pattern='GBRG', camera_raw_max_value=1023.0)", + "default_code": "APIS(name='APIS', port='', baudrate=9600, timeout=0.5, connection_wait_s=2.0, settling_time_s=1.5, command_delay_s=0.05, max_retries=3, polarizer_stage_to_servo_ratio=1.059, sample_stage_to_servo_ratio=1.059, polarizer_stage_direction=1, sample_stage_direction=1, polarizer_servo_zero_deg=0, sample_servo_zero_deg=0, use_camera=True, camera_save_directory='data/imaging/', camera_bayer_pattern='GBRG', camera_raw_max_value=1023.0, polarizer_baseline_path=None)", "obj_name": "APIS", "args": { "name": { @@ -992,6 +992,11 @@ def default(self, obj): "type": float, "notes": "Linear scaling maximum used for RAW16 to RGB conversion.", }, + "polarizer_baseline_path": { + "default": None, + "type": str, + "notes": "Optional path for persisted XPL/PPL polarizer baseline JSON.", + }, }, }, "commands": { @@ -1061,6 +1066,57 @@ def default(self, obj): }, "obj": APISRotateSample, }, + "APISSetPolarizerBaseline": { + "default_code": "APISSetPolarizerBaseline(receiver= '', xpl_angle_deg= 120.0, persist=False, source='manual')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""}, + "xpl_angle_deg": { + "default": 120.0, + "type": float, + "notes": "XPL polarizer angle; PPL is derived as the reachable orthogonal angle.", + }, + "persist": { + "default": False, + "type": bool, + "notes": "Save the resulting XPL/PPL baseline to JSON.", + }, + "source": { + "default": "manual", + "type": str, + "notes": "Source label stored in the baseline JSON when persist is enabled.", + }, + }, + "obj": APISSetPolarizerBaseline, + }, + "APISLoadPolarizerBaseline": { + "default_code": "APISLoadPolarizerBaseline(receiver= '')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""} + }, + "obj": APISLoadPolarizerBaseline, + }, + "APISSavePolarizerBaseline": { + "default_code": "APISSavePolarizerBaseline(receiver= '', source='manual')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""}, + "source": {"default": "manual", "type": str, "notes": "Source label stored in baseline JSON."}, + }, + "obj": APISSavePolarizerBaseline, + }, + "APISRotateXPL": { + "default_code": "APISRotateXPL(receiver= '')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""} + }, + "obj": APISRotateXPL, + }, + "APISRotatePPL": { + "default_code": "APISRotatePPL(receiver= '')", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""} + }, + "obj": APISRotatePPL, + }, "APISGetState": { "default_code": "APISGetState(receiver= '')", "args": { @@ -1099,6 +1155,38 @@ def default(self, obj): }, "obj": APISConvertRaw16ToRgb, }, + "APISRunImagingSequence": { + "default_code": "APISRunImagingSequence(receiver= '', sample_id='sample', directory=None, sample_angles=None, xpl_exposure_time=400000, ppl_exposure_time=18000, do_xpl=True, do_ppl=True, xpl_polarizer_angle=None, ppl_polarizer_angle=None, gain=0.0)", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""}, + "sample_id": {"default": "sample", "type": str, "notes": "Sample ID used for output folder and filenames."}, + "directory": {"default": None, "type": str, "notes": "Optional output root directory."}, + "sample_angles": {"default": None, "type": list, "notes": "Sample angles, or None for APIS default sequence."}, + "xpl_exposure_time": {"default": 400000, "type": int, "notes": "XPL exposure in microseconds."}, + "ppl_exposure_time": {"default": 18000, "type": int, "notes": "PPL exposure in microseconds."}, + "do_xpl": {"default": True, "type": bool, "notes": "Capture XPL mode."}, + "do_ppl": {"default": True, "type": bool, "notes": "Capture PPL mode."}, + "xpl_polarizer_angle": {"default": None, "type": float, "notes": "Optional XPL polarizer angle override."}, + "ppl_polarizer_angle": {"default": None, "type": float, "notes": "Optional PPL polarizer angle override."}, + "gain": {"default": 0.0, "type": float, "notes": "Camera gain in dB."}, + }, + "obj": APISRunImagingSequence, + }, + "APISRunPolarizerCalibration": { + "default_code": "APISRunPolarizerCalibration(receiver= '', sample_id='polarizer_calibration', directory=None, exposure_time=200000, polarizer_angles=None, sample_angle=0.0, fine_radius_deg=10, fine_step_deg=1, gain=0.0)", + "args": { + "receiver": {"default": "APIS", "type": str, "notes": ""}, + "sample_id": {"default": "polarizer_calibration", "type": str, "notes": "Calibration sample ID used in output filenames."}, + "directory": {"default": None, "type": str, "notes": "Optional calibration output root."}, + "exposure_time": {"default": 200000, "type": int, "notes": "Calibration exposure in microseconds."}, + "polarizer_angles": {"default": None, "type": list, "notes": "Coarse scan angles, or None for 0:max:5."}, + "sample_angle": {"default": 0.0, "type": float, "notes": "Sample stage angle during calibration."}, + "fine_radius_deg": {"default": 10, "type": int, "notes": "Fine scan radius around the darkest coarse angle."}, + "fine_step_deg": {"default": 1, "type": int, "notes": "Fine scan angle step."}, + "gain": {"default": 0.0, "type": float, "notes": "Camera gain in dB."}, + }, + "obj": APISRunPolarizerCalibration, + }, }, }, "P4PP": { diff --git a/device_ports.md b/device_ports.md index a12fa34..dfaf117 100644 --- a/device_ports.md +++ b/device_ports.md @@ -18,9 +18,9 @@ This file tracks the current local serial port assignments and related connectio | `z812` | `Z812` with `KDC101` | Thorlabs | `COM12` | `115200` | Smoke test passed locally with absolute move `8 mm` and relative move `3 mm` | `examples/example_z812.py`, `aamp_app/util.py`, `aamp_app/devices/z812.py` | | `newport_94043a_solar_sim` | `94043A` solar simulator via `69920` power supply | Newport | `COM11` | `9600` | RS-232 via USB adapter; default control path is power mode; initialize applies `400 W` preset and software blocks presets above `450 W`; lamp replacement warning starts at `1000 h` | `examples/example_94043a_solar_sim.py`, `aamp_app/util.py`, `aamp_app/devices/newport_94043a_solar_sim.py` | | `p4pp` | `P4PP` Arduino controller | PolyPrint Illinois | `COM19` | `115200` | Rotation is blocked when linear position is `>= 45.0 mm`; explicit measurement resistor selection supported with default `681 ohm`; measurement default cycles set to `20` | `examples/example_p4pp.py`, `aamp_app/util.py`, `aamp_app/devices/p4pp.py` | -| `apis` | `APIS` Arduino controller + Ximea camera | PolyPrint Illinois | `COM8` | `9600` | Composite device for stage control and imaging; default image root is `data/imaging/`; current example writes mode-organized outputs under `data/imaging/demo_campaign/` | `examples/example_apis.py`, `aamp_app/util.py`, `aamp_app/devices/apis.py` | +| `apis` | `APIS` Arduino controller + Ximea camera | PolyPrint Illinois | `COM8` | `9600` | Composite device for stage control and imaging; current XPL/PPL defaults are `120 deg` / `30 deg`, XPL exposure is `400000 us`, persisted baseline defaults to `data/polarizer_baseline.json`, and core APIS capture is exposed through `APISRunImagingSequence` / `APISRunPolarizerCalibration`; default image root is `data/imaging/` | `examples/example_apis.py`, `aamp_app/util.py`, `aamp_app/devices/apis.py`, `aamp_app/commands/apis_commands.py` | | `festo_valve` | `MHJ10-S-2,5-QS-1/4-MF-U` x2 via Arduino relay/driver wrapper | Festo + Custom | `COM9` | `9600` | Current local wiring uses Arduino `D8 -> valve_num=2` and `D4 -> valve_num=3`; companion sketch is `to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino`; valve supply is external `24 V DC`, `3-wire`, default closed/monostable | `examples/example_festo_solenoid_valve.py`, `aamp_app/devices/festo_solenoid_valve.py`, `aamp_app/commands/festo_solenoid_valve_commands.py`, `aamp_app/util.py`, `to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino` | -| `uhe_nl` | `UHE-NL 7 Sun` solar simulator power control | Sciencetech | `COM4` | `9600` | `initialize` requires lamp OFF and enables cooling first; lamp enable is blocked unless cooling feedback is on; `deinitialize` turns lamp off but leaves cooling on for cooldown | `examples/example_sciencetech_uhe_nl_solar_sim.py`, `aamp_app/util.py`, `aamp_app/devices/sciencetech_uhe_nl_solar_sim.py` | +| `uhe_nl` | `UHE-NL 7 Sun` solar simulator power control | Sciencetech | `COM20` | `9600` | `initialize` requires lamp OFF and enables cooling first; lamp enable is blocked unless cooling feedback is on; `deinitialize` turns lamp off but leaves cooling on for cooldown | `examples/example_sciencetech_uhe_nl_solar_sim.py`, `aamp_app/util.py`, `aamp_app/devices/sciencetech_uhe_nl_solar_sim.py` | | `stellarnet_uv_vis` | `StellarNet UV-Vis spectrometer` | StellarNet | `USB` | `n/a` | Vendor Python driver required in the environment; current local examples cover manual dark/blank/sample acquisition and ESP301-driven film degradation loops; spectral decay now uses `reference/am15g_spectrum.csv` | `examples/example_stellarnet_spectrometer.py`, `examples/example_uvvis_film_absorbance_degradation.py`, `aamp_app/devices/stellarnet_spectrometer.py` | | `sonicator` | `Arduino Uno R3` wrapper for sonicator front-panel button/status lines | Custom | `COM13` | `9600` | Expected wiring is `5V`, `GND`, `D7` button drive, and `D8` status sense; Python smoke test passed locally; explicit power probe is intrusive and not used during initialize | `examples/example_sonicator.py`, `aamp_app/devices/sonicator.py`, `aamp_app/util.py`, `firmware/sonicator/sonicator_uno_r3/sonicator_uno_r3.ino` | | `substrate_hotel` | `Arduino linear stage` for substrate hotel | Custom | `COM10` | `9600` | Current local assignment; protocol expects `Ready`, `H`, and `M{position},{speed}`; current Python guard range is `0-430 mm`; default homing and move timeouts are `300 s` | `examples/example_substrate_hotel.py`, `aamp_app/devices/substrate_hotel.py`, `aamp_app/util.py` | From 967e8ccfe74bbfb5c763fef91cef4e44e97136d8 Mon Sep 17 00:00:00 2001 From: Hwang Date: Wed, 27 May 2026 15:04:42 -0500 Subject: [PATCH 122/125] Improve StellarNet detector selection --- aamp_app/devices/stellarnet_spectrometer.py | 247 +++++++++++++++----- 1 file changed, 188 insertions(+), 59 deletions(-) diff --git a/aamp_app/devices/stellarnet_spectrometer.py b/aamp_app/devices/stellarnet_spectrometer.py index aab8995..0f05218 100644 --- a/aamp_app/devices/stellarnet_spectrometer.py +++ b/aamp_app/devices/stellarnet_spectrometer.py @@ -25,14 +25,15 @@ # type hint the static methods? class StellarNetSpectrometer(Device): + SUPPORTED_SPEC_KEYS = ("UV-Vis", "NIR") save_directory = 'data/spectroscopy/' def __init__( self, name: str, - spec_keys: List[str] = ['UV-Vis', 'NIR'], + spec_keys: Union[str, List[str], Tuple[str, ...]] = ("UV-Vis",), save_directory: str = save_directory, - default_integration_time: Optional[Tuple[int, ...]] = None): + default_integration_time: Optional[Union[int, List[int], Tuple[int, ...]]] = None): super().__init__(name) self.spectrometer_dict = {} self.wavelength_dict = {} @@ -42,12 +43,13 @@ def __init__( self.photoncounts_dict = {} self.merged_absorbance = None self.num_spectrometers = 0 - self.spec_keys = spec_keys + self.spec_keys = self._normalize_spec_keys(spec_keys) self.save_directory = save_directory - if default_integration_time is None: - self.default_integration_time = tuple(100 for _ in self.spec_keys) - else: - self.default_integration_time = tuple(default_integration_time) + self.default_integration_time = self._normalize_detector_setting( + default_integration_time, + "default_integration_time", + default_value=100, + ) def get_init_args(self) -> dict: return { @@ -59,9 +61,89 @@ def get_init_args(self) -> dict: def update_init_args(self, args_dict: dict): self._name = args_dict["name"] - self.spec_keys = args_dict["spec_keys"] + self.spec_keys = self._normalize_spec_keys(args_dict["spec_keys"]) self.save_directory = args_dict["save_directory"] - self.default_integration_time = tuple(args_dict.get("default_integration_time", tuple(100 for _ in self.spec_keys))) + self.default_integration_time = self._normalize_detector_setting( + args_dict.get("default_integration_time"), + "default_integration_time", + default_value=100, + ) + + @classmethod + def _normalize_spec_keys( + cls, + spec_keys: Union[str, List[str], Tuple[str, ...], None], + ) -> Tuple[str, ...]: + if spec_keys is None: + spec_keys = ("UV-Vis",) + elif isinstance(spec_keys, str): + spec_keys = (spec_keys,) + + normalized = [] + for spec_key in spec_keys: + cleaned_key = str(spec_key).strip().rstrip(",") + if cleaned_key not in cls.SUPPORTED_SPEC_KEYS: + raise ValueError( + "Unsupported spectrometer key: " + + cleaned_key + + ". Supported keys: " + + ", ".join(cls.SUPPORTED_SPEC_KEYS) + ) + if cleaned_key not in normalized: + normalized.append(cleaned_key) + + if not normalized: + raise ValueError("At least one spectrometer key must be selected.") + return tuple(normalized) + + def _normalize_detector_setting( + self, + values: Optional[Union[int, float, List[Union[int, float]], Tuple[Union[int, float], ...]]], + value_name: str, + default_value: int, + ) -> Tuple[int, ...]: + if values is None: + return tuple(default_value for _ in self.spec_keys) + + if isinstance(values, (int, float, np.integer, np.floating)): + normalized_values = [values] * len(self.spec_keys) + elif isinstance(values, str): + normalized_values = [values] * len(self.spec_keys) + else: + normalized_values = list(values) + if not normalized_values: + raise ValueError(value_name + " cannot be empty.") + if len(normalized_values) == 1: + normalized_values = normalized_values * len(self.spec_keys) + elif len(normalized_values) < len(self.spec_keys): + raise ValueError( + value_name + + " must provide at least " + + str(len(self.spec_keys)) + + " values for spec_keys " + + str(list(self.spec_keys)) + + "." + ) + elif len(normalized_values) > len(self.spec_keys): + normalized_values = normalized_values[:len(self.spec_keys)] + + return tuple(int(value) for value in normalized_values) + + def _normalize_measurement_settings( + self, + integration_times: Optional[Union[int, List[int], Tuple[int, ...]]] = None, + scans_to_avg: Union[int, List[int], Tuple[int, ...]] = (3, 3), + smoothings: Union[int, List[int], Tuple[int, ...]] = (0, 0), + xtimings: Union[int, List[int], Tuple[int, ...]] = (1, 1), + ) -> Tuple[Tuple[int, ...], Tuple[int, ...], Tuple[int, ...], Tuple[int, ...]]: + if integration_times is None: + integration_times = self.default_integration_time + return ( + self._normalize_detector_setting(integration_times, "integration_times", default_value=100), + self._normalize_detector_setting(scans_to_avg, "scans_to_avg", default_value=3), + self._normalize_detector_setting(smoothings, "smoothings", default_value=0), + self._normalize_detector_setting(xtimings, "xtimings", default_value=1), + ) def find_file(self, dir_path: str, substr: str, substr2: str) -> Optional[str]: if not os.path.isdir(dir_path): @@ -263,30 +345,56 @@ def initialize(self) -> Tuple[bool, str]: + "Add the vendor driver file and Python USB dependency referenced in README before using StellarNetSpectrometer.", ) - self.num_spectrometers = self.num_specs_connected() - if self.num_spectrometers == 0: + connected_spectrometers = self.num_specs_connected() + if connected_spectrometers == 0: self._is_initialized = False return (False, "There are no spectrometers connected") - for ndx in range(self.num_spectrometers): + detected_spectrometer_dict = {} + detected_wavelength_dict = {} + for ndx in range(connected_spectrometers): spectrometer, wavelengths = sn.array_get_spec(ndx) if wavelengths[0] < 200.0: # UV-VIS - self.spectrometer_dict['UV-Vis'] = spectrometer - self.wavelength_dict['UV-Vis'] = wavelengths + detected_spectrometer_dict['UV-Vis'] = spectrometer + detected_wavelength_dict['UV-Vis'] = wavelengths else: # NIR - self.spectrometer_dict['NIR'] = spectrometer - self.wavelength_dict['NIR'] = wavelengths + detected_spectrometer_dict['NIR'] = spectrometer + detected_wavelength_dict['NIR'] = wavelengths # For additional spectrometers add to the if else ladder - # Check that we were able to initialize all spectrometers that were declared during construction - if set(self.spec_keys) == set(self.spectrometer_dict.keys()): + missing_spec_keys = [ + spec_key for spec_key in self.spec_keys if spec_key not in detected_spectrometer_dict + ] + if not missing_spec_keys: + self.spectrometer_dict = { + spec_key: detected_spectrometer_dict[spec_key] for spec_key in self.spec_keys + } + self.wavelength_dict = { + spec_key: detected_wavelength_dict[spec_key] for spec_key in self.spec_keys + } + self.num_spectrometers = len(self.spec_keys) self._is_initialized = True - return (True, "Successfully initialized " + str(self.num_spectrometers) + " spectrometers: " + str(list(self.spectrometer_dict.keys()))) - else: - self._is_initialized = False - return (False, "Not all declared spectrometers were initialized. Only initialized: " + str(list(self.spectrometer_dict.keys()))) + return ( + True, + "Successfully initialized requested spectrometers: " + + str(list(self.spec_keys)) + + ". Detected connected spectrometers: " + + str(list(detected_spectrometer_dict.keys())) + ) + + self._is_initialized = False + self.spectrometer_dict = detected_spectrometer_dict + self.wavelength_dict = detected_wavelength_dict + self.num_spectrometers = len(detected_spectrometer_dict) + return ( + False, + "Not all declared spectrometers were initialized. Missing: " + + str(missing_spec_keys) + + ". Detected connected spectrometers: " + + str(list(detected_spectrometer_dict.keys())) + ) def deinitialize(self, reset_init_flag: bool = True) -> Tuple[bool, str]: # turn off lamp? @@ -320,9 +428,9 @@ def check_max_count( @check_initialized def adjust_default_integration_time( self, - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1), + scans_to_avg: Union[int, List[int], Tuple[int, ...]] = (3, 3), + smoothings: Union[int, List[int], Tuple[int, ...]] = (0, 0), + xtimings: Union[int, List[int], Tuple[int, ...]] = (1, 1), target_max_count: int = 52000, tolerance: int = 2000, max_iterations: int = 8) -> Tuple[bool, str]: @@ -330,6 +438,9 @@ def adjust_default_integration_time( if self.num_spectrometers != len(self.spec_keys): return (False, "Spectrometers are not all connected") + scans_to_avg = self._normalize_detector_setting(scans_to_avg, "scans_to_avg", default_value=3) + smoothings = self._normalize_detector_setting(smoothings, "smoothings", default_value=0) + xtimings = self._normalize_detector_setting(xtimings, "xtimings", default_value=1) integration_time_testing = list(self.default_integration_time) for ndx, spec_key in enumerate(self.spec_keys): for _ in range(max_iterations): @@ -450,16 +561,22 @@ def get_spectrum_counts( return (False, spec_key + " spectrometer is not found" ) def get_all_spectra_counts( - self, - integration_times: Tuple[int, ...] = (100, 100), - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1)) -> Tuple[bool, str]: + self, + integration_times: Optional[Union[int, List[int], Tuple[int, ...]]] = None, + scans_to_avg: Union[int, List[int], Tuple[int, ...]] = (3, 3), + smoothings: Union[int, List[int], Tuple[int, ...]] = (0, 0), + xtimings: Union[int, List[int], Tuple[int, ...]] = (1, 1)) -> Tuple[bool, str]: # Modify the variable 'spec_keys' if you don't intend to use all spectrometers if self.num_spectrometers != len(self.spec_keys): return (False, "Spectrometers are not all connected") + integration_times, scans_to_avg, smoothings, xtimings = self._normalize_measurement_settings( + integration_times, + scans_to_avg, + smoothings, + xtimings, + ) spectrum_array_dict = {} # print(spec_keys) # using self.spec_keys to ensure that the order within the parameter tuple matches the order of the declared spec_keys @@ -480,11 +597,11 @@ def get_all_spectra_counts( def update_all_dark_spectra( - self, - integration_times: Tuple[int, ...] = (100, 100), - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1)) -> Tuple[bool, str]: + self, + integration_times: Optional[Union[int, List[int], Tuple[int, ...]]] = None, + scans_to_avg: Union[int, List[int], Tuple[int, ...]] = (3, 3), + smoothings: Union[int, List[int], Tuple[int, ...]] = (0, 0), + xtimings: Union[int, List[int], Tuple[int, ...]] = (1, 1)) -> Tuple[bool, str]: result, spectrum_array_dict = self.get_all_spectra_counts( integration_times, @@ -501,11 +618,11 @@ def update_all_dark_spectra( return (True, "All dark spectra stored") def update_all_blank_spectra( - self, - integration_times: Tuple[int, ...] = (100, 100), - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1)) -> Tuple[bool, str]: + self, + integration_times: Optional[Union[int, List[int], Tuple[int, ...]]] = None, + scans_to_avg: Union[int, List[int], Tuple[int, ...]] = (3, 3), + smoothings: Union[int, List[int], Tuple[int, ...]] = (0, 0), + xtimings: Union[int, List[int], Tuple[int, ...]] = (1, 1)) -> Tuple[bool, str]: result, spectrum_array_dict = self.get_all_spectra_counts( integration_times, @@ -523,15 +640,19 @@ def update_all_blank_spectra( def get_all_absorbance( self, - save_to_file: bool = False, + save_to_file: bool = False, filename: Optional[str] = None, - integration_times: Optional[Tuple[int, ...]] = None, - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1)) -> Tuple[bool, str]: + integration_times: Optional[Union[int, List[int], Tuple[int, ...]]] = None, + scans_to_avg: Union[int, List[int], Tuple[int, ...]] = (3, 3), + smoothings: Union[int, List[int], Tuple[int, ...]] = (0, 0), + xtimings: Union[int, List[int], Tuple[int, ...]] = (1, 1)) -> Tuple[bool, str]: - if integration_times is None: - integration_times = self.default_integration_time + integration_times, scans_to_avg, smoothings, xtimings = self._normalize_measurement_settings( + integration_times, + scans_to_avg, + smoothings, + xtimings, + ) # get the sample spectra result, spectrum_array_dict = self.get_all_spectra_counts( @@ -616,14 +737,18 @@ def get_all_absorbance_byname( sample_name: str, save_to_file: bool = False, repeat_measure: bool = False, - integration_times: Optional[Tuple[int, ...]] = None, - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1), + integration_times: Optional[Union[int, List[int], Tuple[int, ...]]] = None, + scans_to_avg: Union[int, List[int], Tuple[int, ...]] = (3, 3), + smoothings: Union[int, List[int], Tuple[int, ...]] = (0, 0), + xtimings: Union[int, List[int], Tuple[int, ...]] = (1, 1), absorbance_threshold: float = 0.003) -> Tuple[bool, str]: - if integration_times is None: - integration_times = self.default_integration_time + integration_times, scans_to_avg, smoothings, xtimings = self._normalize_measurement_settings( + integration_times, + scans_to_avg, + smoothings, + xtimings, + ) filename = sample_name + '_' + datetime.now().strftime('%Y%m%d%H%M%S') for attempt in range(3): @@ -866,15 +991,19 @@ def get_all_counts_byname( sample_name: str, save_to_file: bool = False, repeat_measure: bool = False, - integration_times: Optional[Tuple[int, ...]] = None, - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1), + integration_times: Optional[Union[int, List[int], Tuple[int, ...]]] = None, + scans_to_avg: Union[int, List[int], Tuple[int, ...]] = (3, 3), + smoothings: Union[int, List[int], Tuple[int, ...]] = (0, 0), + xtimings: Union[int, List[int], Tuple[int, ...]] = (1, 1), absorbance_threshold: float = 0.003) -> Tuple[bool, str]: del absorbance_threshold - if integration_times is None: - integration_times = self.default_integration_time + integration_times, scans_to_avg, smoothings, xtimings = self._normalize_measurement_settings( + integration_times, + scans_to_avg, + smoothings, + xtimings, + ) result, spectrum_array_dict = self.get_all_spectra_counts( integration_times, From e8c49a6dd2147f444513ae8450ada398a0691707 Mon Sep 17 00:00:00 2001 From: Hwang Date: Mon, 29 Jun 2026 13:22:19 -0500 Subject: [PATCH 123/125] Add Mongo-backed printing recipe and PSD6 tests --- .gitignore | 1 + aamp_app/app.py | 28 +- device_ports.md | 2 + examples/.gitignore | 6 + examples/example_printing_recipe.py | 314 ++++++++++++++++++ examples/example_psd6_1ml_suction_test.py | 255 ++++++++++++++ .../example_psd6_cleaning_solvent_test.py | 176 ++++++++++ examples/example_psd6_syringe_pump.py | 190 +++++++++++ recipes/user_recipes/.gitignore | 1 + .../example_mongodb_recipe_lookup.py | 226 +++++++++++++ 10 files changed, 1186 insertions(+), 13 deletions(-) create mode 100644 examples/example_printing_recipe.py create mode 100644 examples/example_psd6_1ml_suction_test.py create mode 100644 examples/example_psd6_cleaning_solvent_test.py create mode 100644 examples/example_psd6_syringe_pump.py create mode 100644 recipes/user_recipes/example_mongodb_recipe_lookup.py diff --git a/.gitignore b/.gitignore index 8797cc8..a9cbafb 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,4 @@ pw.txt ximea_linux_sp_beta.tgz package firmware/ +to_implement/ diff --git a/aamp_app/app.py b/aamp_app/app.py index f2503aa..8faf31e 100644 --- a/aamp_app/app.py +++ b/aamp_app/app.py @@ -2950,7 +2950,7 @@ def generate_parameter_sets(n_clicks, campaign_name, polymer_name, smiles_string else: log_speed = log_min + random.random() * (log_max - log_min) - motor_speed = round(10 ** log_speed, 2) + motor_speed = round(10 ** log_speed, 3) else: motor_speed = random.choice(motor_speeds) if motor_speeds else random.choice(MOTOR_SPEEDS_D) @@ -3108,20 +3108,21 @@ def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_na image_id = fs.put(decoded, filename=image_filename) existing_campaign = mongo.db.campaigns.find_one({"campaign_name": campaign_name}) - pipeline = [ - {"$group": {"_id": None, "max_value": {"$max": "$batch_no"}}} - ] - - try: - result = list(mongo.db.sets.aggregate(pipeline)) - max_value = result[0]["max_value"] if result else None - except (IndexError, KeyError): - max_value = None - - batch_no = max_value + 1 if max_value is not None else 1 if existing_campaign: campaign_id = existing_campaign["_id"] + pipeline = [ + {"$match": {"campaign_id": campaign_id}}, + {"$group": {"_id": None, "max_value": {"$max": "$batch_no"}}} + ] + + try: + result = list(mongo.db.sets.aggregate(pipeline)) + max_value = result[0]["max_value"] if result else None + except (IndexError, KeyError): + max_value = None + + batch_no = max_value + 1 if max_value is not None else 1 update_data = {} if gpc_data: @@ -3175,6 +3176,7 @@ def save_parameter_sets_to_mongo(n_clicks, parameter_sets, gpc_data, campaign_na campaign_result = mongo.db.campaigns.insert_one(campaign_doc) campaign_id = campaign_result.inserted_id + batch_no = 1 sets_to_insert = [{ "campaign_id": campaign_id, @@ -3670,4 +3672,4 @@ def start_optimization(n_clicks, function_str): 'showlegend': True } } - return f"Optimization completed. Best value: {best_value:.2f} at parameters {best_params}.", fig, results \ No newline at end of file + return f"Optimization completed. Best value: {best_value:.2f} at parameters {best_params}.", fig, results diff --git a/device_ports.md b/device_ports.md index dfaf117..3897853 100644 --- a/device_ports.md +++ b/device_ports.md @@ -18,6 +18,8 @@ This file tracks the current local serial port assignments and related connectio | `z812` | `Z812` with `KDC101` | Thorlabs | `COM12` | `115200` | Smoke test passed locally with absolute move `8 mm` and relative move `3 mm` | `examples/example_z812.py`, `aamp_app/util.py`, `aamp_app/devices/z812.py` | | `newport_94043a_solar_sim` | `94043A` solar simulator via `69920` power supply | Newport | `COM11` | `9600` | RS-232 via USB adapter; default control path is power mode; initialize applies `400 W` preset and software blocks presets above `450 W`; lamp replacement warning starts at `1000 h` | `examples/example_94043a_solar_sim.py`, `aamp_app/util.py`, `aamp_app/devices/newport_94043a_solar_sim.py` | | `p4pp` | `P4PP` Arduino controller | PolyPrint Illinois | `COM19` | `115200` | Rotation is blocked when linear position is `>= 45.0 mm`; explicit measurement resistor selection supported with default `681 ohm`; measurement default cycles set to `20` | `examples/example_p4pp.py`, `aamp_app/util.py`, `aamp_app/devices/p4pp.py` | +| `psd6_25ml` | `PSD/6` syringe pump, 25 mL syringe, 6-port distribution valve | Hamilton | `COM17` | `9600` | Port map: `1=sonicator reservoir`, `2=IPA`, `3=acetone`, `4=toluene`, `5=empty/air`, `6=waste`; smoke test uses safe idle valve `5`, air purge `5->2`, and IPA prime `2->6` | `examples/example_psd6_syringe_pump.py`, `examples/example_psd6_cleaning_solvent_test.py`, `aamp_app/devices/psd6_syringe_pump.py`, `aamp_app/commands/psd6_syringe_pump_commands.py` | +| `psd6_1ml_liquid_handler` | `PSD/6` syringe pump, 1 mL syringe, 6-port distribution valve | Hamilton | `COM14` | `9600` | Same valve mapping as the PSD/6 pump except port `1=liquid handler`; port map: `1=liquid handler`, `2=IPA`, `5=empty/air`, `6=waste`; printing recipe registers this pump for liquid-handler dispensing | `examples/example_psd6_1ml_suction_test.py`, `examples/example_printing_recipe.py`, `aamp_app/devices/psd6_syringe_pump.py`, `aamp_app/commands/psd6_syringe_pump_commands.py` | | `apis` | `APIS` Arduino controller + Ximea camera | PolyPrint Illinois | `COM8` | `9600` | Composite device for stage control and imaging; current XPL/PPL defaults are `120 deg` / `30 deg`, XPL exposure is `400000 us`, persisted baseline defaults to `data/polarizer_baseline.json`, and core APIS capture is exposed through `APISRunImagingSequence` / `APISRunPolarizerCalibration`; default image root is `data/imaging/` | `examples/example_apis.py`, `aamp_app/util.py`, `aamp_app/devices/apis.py`, `aamp_app/commands/apis_commands.py` | | `festo_valve` | `MHJ10-S-2,5-QS-1/4-MF-U` x2 via Arduino relay/driver wrapper | Festo + Custom | `COM9` | `9600` | Current local wiring uses Arduino `D8 -> valve_num=2` and `D4 -> valve_num=3`; companion sketch is `to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino`; valve supply is external `24 V DC`, `3-wire`, default closed/monostable | `examples/example_festo_solenoid_valve.py`, `aamp_app/devices/festo_solenoid_valve.py`, `aamp_app/commands/festo_solenoid_valve_commands.py`, `aamp_app/util.py`, `to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino` | | `uhe_nl` | `UHE-NL 7 Sun` solar simulator power control | Sciencetech | `COM20` | `9600` | `initialize` requires lamp OFF and enables cooling first; lamp enable is blocked unless cooling feedback is on; `deinitialize` turns lamp off but leaves cooling on for cooldown | `examples/example_sciencetech_uhe_nl_solar_sim.py`, `aamp_app/util.py`, `aamp_app/devices/sciencetech_uhe_nl_solar_sim.py` | diff --git a/examples/.gitignore b/examples/.gitignore index 8fd1989..bb33d5e 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -15,10 +15,16 @@ !example_p4pp.py !example_apis.py !example_apis_from_mongodb.py +!example_film_characterization.py +!example_psd6_syringe_pump.py +!example_psd6_1ml_suction_test.py +!example_psd6_cleaning_solvent_test.py !example_stellarnet_spectrometer.py !example_uvvis_film_absorbance_degradation.py !example_sciencetech_uhe_nl_solar_sim.py !example_festo_solenoid_valve.py +!example_p4pp_rotation_sweep.py +!example_printing_recipe.py !example_sonicator.py !example_substrate_hotel.py !example_substrate_dispenser.py diff --git a/examples/example_printing_recipe.py b/examples/example_printing_recipe.py new file mode 100644 index 0000000..0f532f5 --- /dev/null +++ b/examples/example_printing_recipe.py @@ -0,0 +1,314 @@ +# Printing recipe example scaffold. +# run from root using 'python -m examples.example_printing_recipe' + +import sys +from pathlib import Path +from typing import Dict + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_invoker import CommandInvoker +from command_sequence import CommandSequence +from commands.newport_esp301_commands import * +from commands.kinova_arm_commands import * +from commands.psd6_syringe_pump_commands import * +from commands.sonicator_commands import * +from devices.kinova_arm import KinovaArm +from devices.newport_esp301 import NewportESP301 +from devices.psd6_syringe_pump import PSD6SyringePump +from devices.sonicator import Sonicator +from recipes.user_recipes.example_mongodb_recipe_lookup import ( + DEFAULT_CAMPAIGN_NAME, + build_recipe_params, + fetch_campaign, + fetch_parameter_set, + get_mongo_helper, + list_batch_numbers, + list_sample_numbers, + load_env, + prompt_choice, + prompt_with_default, +) + + +ESP301_PORT = "COM6" +PSD6_1ML_PORT = "COM14" +PSD6_25ML_PORT = "COM17" +SONICATOR_PORT = "COM13" +PSD6_1ML_STROKE_VOLUME_UL = 1_000.0 +PSD6_25ML_STROKE_VOLUME_UL = 25_000.0 +PSD6_1ML_DEFAULT_FLOWRATE_UL_S = 50.0 +PSD6_25ML_DEFAULT_FLOWRATE_UL_S = 500.0 + +PSD6_PORT_LIQUID_HANDLER = 1 +PSD6_PORT_IPA = 2 +PSD6_PORT_AIR_EMPTY = 5 +PSD6_PORT_WASTE = 6 +PSD6_PORT_SAFE_IDLE = PSD6_PORT_AIR_EMPTY +PSD6_AIR_PURGE_FLOWRATE_UL_S = 5.0 +PSD6_LOAD_FLOWRATE_UL_S = 10.0 +PSD6_PURGE_FLOWRATE_UL_S = 10.0 +PSD6_AIR_PURGE_VOLUME_UL = 100.0 +PSD6_LOAD_VOLUME_UL = 1_000.0 +PSD6_FIRST_PURGE_VOLUME_UL = 700.0 +PSD6_RELOAD_VOLUME_UL = 300.0 +PSD6_SECOND_PURGE_VOLUME_UL = 500.0 +PSD6_CLEANING_FLOWRATE_UL_S = 1_000.0 +PSD6_CLEANING_VOLUME_UL = 15_000.0 +PSD6_CLEANING_RESERVOIR_DRAIN_REPEATS = 2 +PSD6_PORT_SONICATOR_RESERVOIR = 1 +PSD6_PORT_ACETONE = 3 +PSD6_PORT_TOLUENE = 4 +PSD6_CLEANING_SOLVENT_PORTS = ( + (PSD6_PORT_IPA, "IPA"), + (PSD6_PORT_ACETONE, "acetone"), + (PSD6_PORT_TOLUENE, "toluene"), +) +SONICATION_TIME_S = 30.0 + +ESP301_AXIS_CONFIGS = { + 1: { + "stage_model": "ILS100CC", + "motion_type": "linear", + "units": "mm", + "home_mode": "OR4", + "zero_position": 0.0, + "default_speed": 10.0, + }, + 2: { + "stage_model": "UTS100PP", + "motion_type": "linear", + "units": "mm", + "home_mode": "OR4", + "zero_position": 0.0, + "default_speed": 10.0, + }, +} + + +PRINTING_AXIS2_POSITION_AT_0_UM = 36.9 +PRINTING_AXIS2_MM_PER_UM_GAP = 0.001 + + +def printing_gap_to_axis2_position(printing_gap_um: float) -> float: + return PRINTING_AXIS2_POSITION_AT_0_UM - PRINTING_AXIS2_MM_PER_UM_GAP * printing_gap_um + + +def load_recipe_params() -> Dict[str, object]: + load_env() + mongo = get_mongo_helper() + + campaign_name = prompt_with_default("Campaign name", DEFAULT_CAMPAIGN_NAME) + if not campaign_name: + raise ValueError("Campaign name is required.") + + campaign_doc = fetch_campaign(mongo, campaign_name) + batch_no = prompt_choice("batch_no values", list_batch_numbers(mongo, campaign_doc)) + sample_no = prompt_choice("sample_no values", list_sample_numbers(mongo, campaign_doc, batch_no)) + + campaign_doc, set_doc = fetch_parameter_set(mongo, campaign_doc, batch_no, sample_no) + params = build_recipe_params(batch_no, sample_no, set_doc) + params["campaign_name"] = campaign_doc["campaign_name"] + return params + + +def build_sequence(params: Dict[str, object]) -> CommandSequence: + seq = CommandSequence() + printing_speed = float(params["motor_speed"]) + printing_gap_um = float(params["printing_gap"]) + precursor_volume_ul = float(params["precursor_volume"]) + printing_ready_axis2_position = printing_gap_to_axis2_position(printing_gap_um) + + esp301 = NewportESP301( + name="esp301", + port=ESP301_PORT, + axis_list=(1, 2), + default_speed=10.0, + poll_interval=0.1, + axis_configs=ESP301_AXIS_CONFIGS, + ) + arm = KinovaArm("arm") + sonicator = Sonicator( + name="sonicator", + port=SONICATOR_PORT, + baudrate=9600, + timeout=0.5, + connect_delay_s=3.0, + response_timeout_s=3.0, + power_probe_timeout_s=5.0, + line_terminator="\n", + command_prefix=">", + debug_io=False, + ) + liquid_handler_pump = PSD6SyringePump( + name="psd6_1ml_liquid_handler", + port=PSD6_1ML_PORT, + baudrate=9600, + timeout=10.0, + stroke_volume=PSD6_1ML_STROKE_VOLUME_UL, + stroke_steps=6000, + default_flowrate=PSD6_1ML_DEFAULT_FLOWRATE_UL_S, + port_dead_volumes=[0.0] * 6, + poll_interval=0.1, + ) + solvent_pump = PSD6SyringePump( + name="psd6_25ml_solvent", + port=PSD6_25ML_PORT, + baudrate=9600, + timeout=10.0, + stroke_volume=PSD6_25ML_STROKE_VOLUME_UL, + stroke_steps=6000, + default_flowrate=PSD6_25ML_DEFAULT_FLOWRATE_UL_S, + port_dead_volumes=[0.0] * 6, + poll_interval=0.1, + ) + + seq.add_device(esp301) + seq.add_device(arm) + seq.add_device(sonicator) + seq.add_device(liquid_handler_pump) + seq.add_device(solvent_pump) + + seq.add_command(NewportESP301Connect(esp301)) + seq.add_command(KinovaArmConnect(arm)) + seq.add_command(SonicatorConnect(sonicator)) + seq.add_command(PSD6SyringePumpConnect(liquid_handler_pump)) + seq.add_command(PSD6SyringePumpConnect(solvent_pump)) + seq.add_command(NewportESP301Initialize(esp301)) # initialize printer head (0,0) + seq.add_command(KinovaArmInitialize(arm)) + seq.add_command(SonicatorInitialize(sonicator)) + seq.add_command(PSD6SyringePumpInitialize(liquid_handler_pump)) + seq.add_command(PSD6SyringePumpInitialize(solvent_pump)) + seq.add_command(PSD6SyringePumpMoveAbsolute(liquid_handler_pump, volume=0.0, valve_num=PSD6_PORT_AIR_EMPTY, flowrate=PSD6_AIR_PURGE_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpWithdraw(liquid_handler_pump, volume=PSD6_AIR_PURGE_VOLUME_UL, valve_num=PSD6_PORT_AIR_EMPTY, flowrate=PSD6_AIR_PURGE_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpInfuse(liquid_handler_pump, volume=PSD6_AIR_PURGE_VOLUME_UL, valve_num=PSD6_PORT_LIQUID_HANDLER, flowrate=PSD6_AIR_PURGE_FLOWRATE_UL_S)) + for drain_idx in range(PSD6_CLEANING_RESERVOIR_DRAIN_REPEATS): + seq.add_command(PSD6SyringePumpMoveAbsolute(solvent_pump, volume=0.0, valve_num=PSD6_PORT_SONICATOR_RESERVOIR, flowrate=PSD6_CLEANING_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpWithdraw(solvent_pump, volume=PSD6_CLEANING_VOLUME_UL, valve_num=PSD6_PORT_SONICATOR_RESERVOIR, flowrate=PSD6_CLEANING_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpInfuse(solvent_pump, volume=PSD6_CLEANING_VOLUME_UL, valve_num=PSD6_PORT_WASTE, flowrate=PSD6_CLEANING_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpMoveValve(liquid_handler_pump, valve_num=PSD6_PORT_SAFE_IDLE)) + seq.add_command(PSD6SyringePumpMoveValve(solvent_pump, valve_num=PSD6_PORT_SAFE_IDLE)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Home")) + + ## move printer head to sonicator up position(79, 10) to not mess with the arm ## + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=10.0, speed=10.0)) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=79.0, speed=10.0)) # sonicator up position (79,10) + + + ## move arm to liquid dispenser ## + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Ready_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Liq_Handler_Up_Out_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Liq_Handler_Down_Out_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Liq_Handler_Down_Angles")) + seq.add_command(KinovaArmCloseGripper(arm)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Liq_Handler_Up_Angles", delay=1)) + + ## move liquid dispenser to designated solution position(solution map needs to be implemented) ## + seq.add_command(KinovaArmExecuteAction(arm, action_name="Solution_Hotel_Ready_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Solution_1_High_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Solution_1_Up_Angles", delay=5)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Solution_1_Down_Angles")) + + ## withdraw solution using PSD6 pump ## + ## liquid_handler_pump valve map: 1=liquid handler, 2=IPA, 5=air/empty, 6=waste ## + seq.add_command(PSD6SyringePumpMoveAbsolute(liquid_handler_pump, volume=0.0, valve_num=PSD6_PORT_LIQUID_HANDLER, flowrate=PSD6_LOAD_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpWithdraw(liquid_handler_pump, volume=PSD6_LOAD_VOLUME_UL, valve_num=PSD6_PORT_LIQUID_HANDLER, flowrate=PSD6_LOAD_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpInfuse(liquid_handler_pump, volume=PSD6_FIRST_PURGE_VOLUME_UL, valve_num=PSD6_PORT_LIQUID_HANDLER, flowrate=PSD6_PURGE_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpWithdraw(liquid_handler_pump, volume=PSD6_RELOAD_VOLUME_UL, valve_num=PSD6_PORT_LIQUID_HANDLER, flowrate=PSD6_LOAD_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpInfuse(liquid_handler_pump, volume=PSD6_SECOND_PURGE_VOLUME_UL, valve_num=PSD6_PORT_LIQUID_HANDLER, flowrate=PSD6_PURGE_FLOWRATE_UL_S)) + + ## move liquid dispenser to printing position ## + seq.add_command(KinovaArmExecuteAction(arm, action_name="Solution_1_Up_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Solution_Hotel_Ready_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Ready_Angles")) + + ## move arm to Print_Dis_Left_Down position ## + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Dis_Center_High_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Dis_Left_Up_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Dis_Left_Down_Angles", delay=5)) + + ## move printer head to printing position(4,35) ## + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=40.0, speed=10.0)) # lowering printer head (79,40) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=4.0, speed=10.0)) # printing up position (4,40) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=35.0, speed=10.0)) # printing down position (4,35) + + ## add precursor solution using PSD6 pump ## + seq.add_command(PSD6SyringePumpInfuse(liquid_handler_pump, volume=precursor_volume_ul, valve_num=PSD6_PORT_LIQUID_HANDLER, flowrate=PSD6_PURGE_FLOWRATE_UL_S)) + + ## move to meniscus position ## + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=printing_ready_axis2_position, speed=10.0, delay='P')) # printing ready position adjusted by printing_gap (4,printing height) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=2.0, speed=10.0)) # making meniscus stable (2,PH) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=5.0, speed=10.0)) # making meniscus stable (5,PH) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=3.0, speed=10.0)) # making meniscus stable (3,PH) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=20, speed=printing_speed, delay=3)) # printing speed from MongoDB motor_speed (20, PH) + + ## move arm back to Liq_Handler ## + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Ready_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Solution_Hotel_Ready_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Liq_Handler_Up_Angles", delay=1)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Liq_Handler_Down_Angles")) + seq.add_command(KinovaArmOpenGripper(arm)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Liq_Handler_Down_Out_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Liq_Handler_Up_Out_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Ready_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Home")) + + ## end printing and move printer head to sonicator ## + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=10, speed=10.0)) # head up (20,10) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=79, speed=10.0)) # sonicator up position (79,10) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=70, speed=10.0)) # sonicator down position (79,70) + + ## add cleaning solvnet, sonicate ## + for solvent_port, solvent_name in PSD6_CLEANING_SOLVENT_PORTS: + seq.add_command(PSD6SyringePumpMoveAbsolute(solvent_pump, volume=0.0, valve_num=solvent_port, flowrate=PSD6_CLEANING_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpWithdraw(solvent_pump, volume=PSD6_CLEANING_VOLUME_UL, valve_num=solvent_port, flowrate=PSD6_CLEANING_FLOWRATE_UL_S)) + seq.add_command(PSD6SyringePumpInfuse(solvent_pump, volume=PSD6_CLEANING_VOLUME_UL, valve_num=PSD6_PORT_SONICATOR_RESERVOIR, flowrate=PSD6_CLEANING_FLOWRATE_UL_S)) + seq.add_command(SonicatorStartSonicating(sonicator)) + seq.add_command(SonicatorStopSonicating(sonicator, delay=SONICATION_TIME_S)) + + ## mechanical cleaning ## + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=35, speed=10.0)) # head up (79,35) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=45, speed=10.0)) # mechanical cleaning up position (45,35) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=43, speed=10.0)) # mechanical cleaning down position (45,43) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=79, speed=10.0)) # mechanical cleaning through linear movement (79,43) + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=70, speed=10.0)) # sonicator down position (79,70) + seq.add_command(SonicatorStopSonicating(sonicator, delay=SONICATION_TIME_S)) # second sonication + + ## move to drying position (3,35) ## + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=10, speed=10.0)) # head up + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=1, position=3, speed=10.0)) # printing up position + seq.add_command(NewportESP301MoveSpeedAbsolute(esp301, axis_number=2, position=35, speed=10.0)) # printing down position + + return seq + + +def main() -> None: + params = load_recipe_params() + printing_ready_axis2_position = printing_gap_to_axis2_position(float(params["printing_gap"])) + + print("\nLoaded MongoDB parameter set:") + for key, value in params.items(): + print(f" {key}: {value}") + print(f" printing_ready_axis2_position: {printing_ready_axis2_position:.4f} mm") + + seq = build_sequence(params) + + log_file = "logs/example_printing_recipe.log" + invoker = CommandInvoker(seq, log_to_file=True, log_filename=log_file, alert_slack=False) + + print("\nPrinting recipe preview.") + print("Sequence will use MongoDB motor_speed for axis 1 printing move and printing_gap for axis 2 ready position.") + seq.print_command_names() + userinput = input("\ntype 'y' to continue, type anything else to quit: ").strip().lower() + if userinput == "y": + result = invoker.invoke_commands() + print(result) + + +if __name__ == "__main__": + main() diff --git a/examples/example_psd6_1ml_suction_test.py b/examples/example_psd6_1ml_suction_test.py new file mode 100644 index 0000000..3a9a1f6 --- /dev/null +++ b/examples/example_psd6_1ml_suction_test.py @@ -0,0 +1,255 @@ +# PSD/6 1 mL port 1 transfer check +# run from repo root with: +# python -m examples.example_psd6_1ml_suction_test + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from devices.psd6_syringe_pump import PSD6SyringePump + + +PORT = "COM14" +STROKE_VOLUME_UL = 1_000.0 +AIR_PURGE_FLOWRATE_UL_S = 5.0 +LOAD_FLOWRATE_UL_S = 10.0 +PURGE_FLOWRATE_UL_S = 10.0 +DISPENSE_FLOWRATE_UL_S = 10.0 +AIR_SOURCE_PORT = 5 +SOURCE_PORT = 1 +DEST_PORT = 6 +AIR_PURGE_VOLUME_UL = 100.0 +LOAD_VOLUME_UL = 1_000.0 +FIRST_PURGE_VOLUME_UL = 700.0 +RELOAD_VOLUME_UL = 300.0 +SECOND_PURGE_VOLUME_UL = 500.0 +EXPECTED_REMAINING_VOLUME_UL = ( + LOAD_VOLUME_UL - FIRST_PURGE_VOLUME_UL + RELOAD_VOLUME_UL - SECOND_PURGE_VOLUME_UL +) + + +def transfer_volume( + pump: PSD6SyringePump, + source_port: int, + dest_port: int, + volume_ul: float, + flowrate_ul_s: float, + label: str, +) -> bool: + ok, message = pump.withdraw_syringe_volume( + volume_ul, + valve_num=source_port, + flowrate=flowrate_ul_s, + ) + print(f"{label} | withdraw from valve {source_port}: {ok} | {message}", flush=True) + if not ok: + return False + + ok, message = pump.infuse_syringe_volume( + volume_ul, + valve_num=dest_port, + flowrate=flowrate_ul_s, + ) + print(f"{label} | infuse to valve {dest_port}: {ok} | {message}", flush=True) + return ok + + +def main() -> None: + print("Hamilton PSD/6 1 mL port 1 automatic loading check") + print(f"- Port: {PORT}") + print("- Syringe: 1 mL") + print(f"- Air source valve: {AIR_SOURCE_PORT}") + print(f"- Source valve: {SOURCE_PORT} / liquid handler") + print(f"- Destination valve: {DEST_PORT}") + print("- Purpose: air-purge port 1, wet/load the liquid-handler line, and finish with 100 uL nominally loaded.") + print(f"- Air purge flowrate: {AIR_PURGE_FLOWRATE_UL_S:.1f} uL/s") + print(f"- Load flowrate: {LOAD_FLOWRATE_UL_S:.1f} uL/s") + print(f"- Liquid-handler purge flowrate: {PURGE_FLOWRATE_UL_S:.1f} uL/s") + print(f"- Interactive dispense flowrate: {DISPENSE_FLOWRATE_UL_S:.1f} uL/s") + print(f"- Air purge: {AIR_PURGE_VOLUME_UL:.0f} uL from valve {AIR_SOURCE_PORT} to valve {SOURCE_PORT}") + print(f"- Load: withdraw {LOAD_VOLUME_UL:.0f} uL from valve {SOURCE_PORT}") + print(f"- Wetting purge: infuse {FIRST_PURGE_VOLUME_UL:.0f} uL back to valve {SOURCE_PORT}") + print(f"- Reload: withdraw {RELOAD_VOLUME_UL:.0f} uL from valve {SOURCE_PORT}") + print(f"- Final purge: infuse {SECOND_PURGE_VOLUME_UL:.0f} uL back to valve {SOURCE_PORT}") + print(f"- Expected final syringe load: {EXPECTED_REMAINING_VOLUME_UL:.0f} uL") + + confirm = input( + f"This will connect to {PORT}, initialize the pump, air-purge {AIR_SOURCE_PORT}->{SOURCE_PORT}, " + f"then run the automatic 1000/700/300/500 uL loading sequence. Type 'y' to continue: " + ).strip().lower() + if confirm != "y": + print("Transfer check cancelled.") + return + + pump = PSD6SyringePump( + name="psd6_1ml_com14", + port=PORT, + baudrate=9600, + timeout=10.0, + stroke_volume=STROKE_VOLUME_UL, + stroke_steps=6000, + default_flowrate=LOAD_FLOWRATE_UL_S, + port_dead_volumes=[0.0] * 6, + poll_interval=0.1, + ) + + try: + ok, message = pump.start_serial(delay=1.0) + print(f"connect: {ok} | {message}") + if not ok: + return + + ok, message = pump.initialize() + print(f"initialize: {ok} | {message}") + if not ok: + return + + ok, message = pump.move_syringe_absolute_volume( + 0.0, + valve_num=AIR_SOURCE_PORT, + flowrate=AIR_PURGE_FLOWRATE_UL_S, + ) + print(f"reset to 0 uL on valve {AIR_SOURCE_PORT}: {ok} | {message}") + if not ok: + return + + ok = transfer_volume( + pump, + source_port=AIR_SOURCE_PORT, + dest_port=SOURCE_PORT, + volume_ul=AIR_PURGE_VOLUME_UL, + flowrate_ul_s=AIR_PURGE_FLOWRATE_UL_S, + label=f"air purge {AIR_SOURCE_PORT}->{SOURCE_PORT}", + ) + if not ok: + return + + pause_input = input( + f"Air purge to valve {SOURCE_PORT} is complete. Check the liquid-handler line, then type 'y' to continue loading: " + ).strip().lower() + if pause_input != "y": + print("Loading check cancelled after air purge.") + return + + ok, message = pump.move_syringe_absolute_volume( + 0.0, + valve_num=SOURCE_PORT, + flowrate=LOAD_FLOWRATE_UL_S, + ) + print(f"reset to 0 uL on valve {SOURCE_PORT}: {ok} | {message}") + if not ok: + return + + ok, message = pump.withdraw_syringe_volume( + LOAD_VOLUME_UL, + valve_num=SOURCE_PORT, + flowrate=LOAD_FLOWRATE_UL_S, + ) + print(f"load from valve {SOURCE_PORT}: {ok} | {message}") + if not ok: + return + + remaining_loaded_ul = LOAD_VOLUME_UL + print(f"Nominal remaining syringe load: {remaining_loaded_ul:.0f} uL.") + + pause_input = input( + f"Check whether {LOAD_VOLUME_UL:.0f} uL loaded from valve {SOURCE_PORT}, then type 'y' to run the 700/300/500 uL wetting sequence: " + ).strip().lower() + if pause_input != "y": + print("Loading check stopped after full withdraw.") + return + + ok, message = pump.infuse_syringe_volume( + FIRST_PURGE_VOLUME_UL, + valve_num=SOURCE_PORT, + flowrate=PURGE_FLOWRATE_UL_S, + ) + print(f"wetting purge to valve {SOURCE_PORT}: {ok} | {message}") + if not ok: + return + remaining_loaded_ul -= FIRST_PURGE_VOLUME_UL + print(f"Nominal remaining syringe load: {remaining_loaded_ul:.0f} uL.") + + ok, message = pump.withdraw_syringe_volume( + RELOAD_VOLUME_UL, + valve_num=SOURCE_PORT, + flowrate=LOAD_FLOWRATE_UL_S, + ) + print(f"reload from valve {SOURCE_PORT}: {ok} | {message}") + if not ok: + return + remaining_loaded_ul += RELOAD_VOLUME_UL + print(f"Nominal remaining syringe load: {remaining_loaded_ul:.0f} uL.") + + ok, message = pump.infuse_syringe_volume( + SECOND_PURGE_VOLUME_UL, + valve_num=SOURCE_PORT, + flowrate=PURGE_FLOWRATE_UL_S, + ) + print(f"final purge to valve {SOURCE_PORT}: {ok} | {message}") + if not ok: + return + remaining_loaded_ul -= SECOND_PURGE_VOLUME_UL + print(f"Nominal remaining syringe load: {remaining_loaded_ul:.0f} uL.") + + print( + f"Interactive dispensing started. Enter a number in uL to dispense to valve {SOURCE_PORT}, or 'q' to stop." + ) + while remaining_loaded_ul > 0.0: + user_input = input( + f"[{remaining_loaded_ul:.0f} uL nominal remaining] dispense uL to valve {SOURCE_PORT}: " + ).strip().lower() + + if user_input in ("q", "quit", "exit"): + print("Interactive dispensing stopped by user.") + break + + try: + dispense_ul = float(user_input) + except ValueError: + print("Invalid input. Enter a number in uL, or 'q' to stop.") + continue + + if dispense_ul <= 0.0: + print("Dispense volume must be greater than 0 uL.") + continue + if dispense_ul > remaining_loaded_ul: + print( + f"Requested {dispense_ul:.0f} uL but only about {remaining_loaded_ul:.0f} uL remains." + ) + continue + + ok, message = pump.infuse_syringe_volume( + dispense_ul, + valve_num=SOURCE_PORT, + flowrate=DISPENSE_FLOWRATE_UL_S, + ) + print(f"dispense to valve {SOURCE_PORT}: {ok} | {message}") + if not ok: + return + + remaining_loaded_ul -= dispense_ul + print(f"Nominal remaining syringe load: {remaining_loaded_ul:.0f} uL.") + + if remaining_loaded_ul <= 0.0: + print("Syringe nominally empty.") + + valve_position = pump.valve_position() + print(f"reported valve position: {valve_position}") + print(f"Automatic loading check finished. Syringe nominally holds about {remaining_loaded_ul:.0f} uL.") + finally: + deinit_ok, deinit_message = pump.deinitialize(reset_init_flag=True) + print(f"deinitialize: {deinit_ok} | {deinit_message}") + if pump.ser.is_open: + pump.ser.close() + print("serial: closed") + + +if __name__ == "__main__": + main() diff --git a/examples/example_psd6_cleaning_solvent_test.py b/examples/example_psd6_cleaning_solvent_test.py new file mode 100644 index 0000000..3f847f7 --- /dev/null +++ b/examples/example_psd6_cleaning_solvent_test.py @@ -0,0 +1,176 @@ +# PSD/6 25 mL cleaning solvent test +# run from repo root with: +# python -m examples.example_psd6_cleaning_solvent_test + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from devices.psd6_syringe_pump import PSD6SyringePump + + +PORT = "COM17" +STROKE_VOLUME_UL = 25_000.0 +DEFAULT_FLOWRATE_UL_S = 1_000.0 +CLEANING_SOLVENT_TEST_VOLUME_UL = 15_000.0 +RESERVOIR_DRAIN_REPEATS = 3 + +PORT_SONICATOR_RESERVOIR = 1 +PORT_IPA = 2 +PORT_ACETONE = 3 +PORT_TOLUENE = 4 +PORT_AIR_EMPTY = 5 +PORT_WASTE = 6 +PORT_SAFE_IDLE = PORT_AIR_EMPTY + +CLEANING_SOLVENT_PORTS = [ + (PORT_IPA, "IPA"), + (PORT_ACETONE, "acetone"), + (PORT_TOLUENE, "toluene"), +] + + +def transfer_volume( + pump: PSD6SyringePump, + source_port: int, + dest_port: int, + volume_ul: float, + flowrate_ul_s: float, + label: str, +) -> bool: + ok, message = pump.withdraw_syringe_volume( + volume_ul, + valve_num=source_port, + flowrate=flowrate_ul_s, + ) + print(f"{label} | withdraw from valve {source_port}: {ok} | {message}", flush=True) + if not ok: + return False + + ok, message = pump.infuse_syringe_volume( + volume_ul, + valve_num=dest_port, + flowrate=flowrate_ul_s, + ) + print(f"{label} | infuse to valve {dest_port}: {ok} | {message}", flush=True) + return ok + + +def main() -> None: + print("Hamilton PSD/6 25 mL cleaning solvent test") + print(f"- Port: {PORT}") + print(f"- Reservoir valve: {PORT_SONICATOR_RESERVOIR} / sonicator reservoir") + print(f"- Waste valve: {PORT_WASTE}") + print( + f"- Reservoir drain: {RESERVOIR_DRAIN_REPEATS} transfers of " + f"{CLEANING_SOLVENT_TEST_VOLUME_UL / 1000.0:.1f} mL from valve {PORT_SONICATOR_RESERVOIR} to valve {PORT_WASTE}" + ) + print(f"- Cleaning solvent test volume: {CLEANING_SOLVENT_TEST_VOLUME_UL / 1000.0:.1f} mL") + print(f"- Flowrate: {DEFAULT_FLOWRATE_UL_S / 1000.0:.1f} mL/s") + print("- Known cleaning solvent ports:") + for solvent_port, solvent_name in CLEANING_SOLVENT_PORTS: + print(f" - valve {solvent_port}: {solvent_name}") + + confirm = input( + f"This will initialize COM17, drain valve {PORT_SONICATOR_RESERVOIR} to valve {PORT_WASTE} " + f"{RESERVOIR_DRAIN_REPEATS} times, then transfer {CLEANING_SOLVENT_TEST_VOLUME_UL / 1000.0:.1f} mL each from valves 2, 3, and 4 " + f"to valve {PORT_SONICATOR_RESERVOIR}. " + "Type 'y' to continue: " + ).strip().lower() + if confirm != "y": + print("Cleaning solvent test cancelled.") + return + + pump = PSD6SyringePump( + name="psd6_25ml_cleaning_solvent", + port=PORT, + baudrate=9600, + timeout=10.0, + stroke_volume=STROKE_VOLUME_UL, + stroke_steps=6000, + default_flowrate=DEFAULT_FLOWRATE_UL_S, + port_dead_volumes=[0.0] * 6, + poll_interval=0.1, + ) + + try: + ok, message = pump.start_serial(delay=1.0) + print(f"connect: {ok} | {message}", flush=True) + if not ok: + return + + ok, message = pump.initialize() + print(f"initialize: {ok} | {message}", flush=True) + if not ok: + return + + ok, message = pump.move_valve_position(PORT_SAFE_IDLE) + print(f"safe idle move to valve {PORT_SAFE_IDLE}: {ok} | {message}", flush=True) + if not ok: + return + + for drain_idx in range(RESERVOIR_DRAIN_REPEATS): + label = f"reservoir drain {PORT_SONICATOR_RESERVOIR}->{PORT_WASTE} {drain_idx + 1}/{RESERVOIR_DRAIN_REPEATS}" + ok, message = pump.move_syringe_absolute_volume( + 0.0, + valve_num=PORT_SONICATOR_RESERVOIR, + flowrate=DEFAULT_FLOWRATE_UL_S, + ) + print(f"{label} | reset to 0 uL on valve {PORT_SONICATOR_RESERVOIR}: {ok} | {message}", flush=True) + if not ok: + return + + ok = transfer_volume( + pump, + source_port=PORT_SONICATOR_RESERVOIR, + dest_port=PORT_WASTE, + volume_ul=CLEANING_SOLVENT_TEST_VOLUME_UL, + flowrate_ul_s=DEFAULT_FLOWRATE_UL_S, + label=label, + ) + if not ok: + return + + for solvent_port, solvent_name in CLEANING_SOLVENT_PORTS: + label = f"{solvent_name} {CLEANING_SOLVENT_TEST_VOLUME_UL / 1000.0:.1f} mL transfer {solvent_port}->{PORT_SONICATOR_RESERVOIR}" + ok, message = pump.move_syringe_absolute_volume( + 0.0, + valve_num=solvent_port, + flowrate=DEFAULT_FLOWRATE_UL_S, + ) + print(f"{label} | reset to 0 uL on valve {solvent_port}: {ok} | {message}", flush=True) + if not ok: + return + + ok = transfer_volume( + pump, + source_port=solvent_port, + dest_port=PORT_SONICATOR_RESERVOIR, + volume_ul=CLEANING_SOLVENT_TEST_VOLUME_UL, + flowrate_ul_s=DEFAULT_FLOWRATE_UL_S, + label=label, + ) + if not ok: + return + + valve_position = pump.valve_position() + print(f"reported valve position: {valve_position}", flush=True) + finally: + if pump.ser.is_open and pump._is_initialized: + idle_ok, idle_message = pump.move_valve_position(PORT_SAFE_IDLE) + print(f"pre-deinitialize safe idle move to valve {PORT_SAFE_IDLE}: {idle_ok} | {idle_message}", flush=True) + deinit_ok, deinit_message = pump.deinitialize(reset_init_flag=True) + print(f"deinitialize: {deinit_ok} | {deinit_message}", flush=True) + if pump.ser.is_open: + pump.ser.close() + print("serial: closed", flush=True) + + +if __name__ == "__main__": + main() diff --git a/examples/example_psd6_syringe_pump.py b/examples/example_psd6_syringe_pump.py new file mode 100644 index 0000000..8e41256 --- /dev/null +++ b/examples/example_psd6_syringe_pump.py @@ -0,0 +1,190 @@ +# PSD/6 syringe pump smoke test +# run from repo root with: +# python -m examples.example_psd6_syringe_pump + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from devices.psd6_syringe_pump import PSD6SyringePump + + +PORT = "COM17" +STROKE_VOLUME_UL = 25_000.0 +DEFAULT_FLOWRATE_UL_S = 1_000.0 +LARGE_TEST_VOLUME_UL = 24_000.0 +PRIME_TEST_VOLUME_UL = 3_000.0 +PORT_SONICATOR_RESERVOIR = 1 +PORT_IPA = 2 +PORT_AIR_EMPTY = 5 +PORT_WASTE = 6 +PORT_SAFE_IDLE = PORT_AIR_EMPTY + + +def transfer_volume( + pump: PSD6SyringePump, + source_port: int, + dest_port: int, + volume_ul: float, + flowrate_ul_s: float, + label: str, +) -> bool: + ok, message = pump.withdraw_syringe_volume( + volume_ul, + valve_num=source_port, + flowrate=flowrate_ul_s, + ) + print(f"{label} | withdraw from valve {source_port}: {ok} | {message}") + if not ok: + return False + + ok, message = pump.infuse_syringe_volume( + volume_ul, + valve_num=dest_port, + flowrate=flowrate_ul_s, + ) + print(f"{label} | infuse to valve {dest_port}: {ok} | {message}") + return ok + + +def main() -> None: + print("Hamilton PSD/6 smoke test") + print(f"- Port: {PORT}") + print("- Syringe: 25 mL") + print("- Valve: 9998-01, 6-port distribution valve (positions 1-6)") + print("- Tubing: 1/8 in downstream tubing is fine mechanically, but the Hamilton valve ports are still 1/4-28 fittings.") + print("- Port map:") + print(f" - {PORT_SONICATOR_RESERVOIR}: sonicator reservoir") + print(f" - {PORT_IPA}: IPA") + print(f" - {PORT_AIR_EMPTY}: empty / air") + print(f" - {PORT_WASTE}: waste") + print(f"- Large transfer volume is {LARGE_TEST_VOLUME_UL / 1000.0:.1f} mL.") + print(f"- Prime test volume is {PRIME_TEST_VOLUME_UL / 1000.0:.1f} mL.") + print( + f"- The pump initializes with /1ZR, which homes the syringe and returns the valve to position 1, then this example moves it to safe idle port {PORT_SAFE_IDLE}." + ) + + confirm = input( + "This will connect to COM17, move to safe idle 5, air-purge 5->2 three times at 24 mL, then prime 2->6 two times at 3 mL. Type 'y' to continue: " + ).strip().lower() + if confirm != "y": + print("Smoke test cancelled.") + return + + pump = PSD6SyringePump( + name="psd6_com17", + port=PORT, + baudrate=9600, + timeout=10.0, + stroke_volume=STROKE_VOLUME_UL, + stroke_steps=6000, + default_flowrate=DEFAULT_FLOWRATE_UL_S, + port_dead_volumes=[0.0] * 6, + poll_interval=0.1, + ) + + try: + ok, message = pump.start_serial(delay=1.0) + print(f"connect: {ok} | {message}") + if not ok: + return + + ok, message = pump.initialize() + print(f"initialize: {ok} | {message}") + if not ok: + return + + ok, message = pump.move_valve_position(PORT_SAFE_IDLE) + print(f"post-initialize safe idle move to valve {PORT_SAFE_IDLE}: {ok} | {message}") + if not ok: + return + + # Reservoir drain template: keep this block here so it can be copied into + # a recipe later when you want to empty the sonicator reservoir. + # for drain_idx in range(3): + # label = f"reservoir drain {drain_idx + 1}/3" + # ok, message = pump.move_syringe_absolute_volume( + # 0.0, + # valve_num=PORT_SONICATOR_RESERVOIR, + # flowrate=DEFAULT_FLOWRATE_UL_S, + # ) + # print(f"{label} | reset to 0.0 mL on valve {PORT_SONICATOR_RESERVOIR}: {ok} | {message}") + # if not ok: + # return + # + # ok = transfer_volume( + # pump, + # source_port=PORT_SONICATOR_RESERVOIR, + # dest_port=PORT_WASTE, + # volume_ul=LARGE_TEST_VOLUME_UL, + # flowrate_ul_s=DEFAULT_FLOWRATE_UL_S, + # label=label, + # ) + # if not ok: + # return + + for purge_idx in range(3): + label = f"air purge 5->2 {purge_idx + 1}/3" + ok, message = pump.move_syringe_absolute_volume( + 0.0, + valve_num=PORT_AIR_EMPTY, + flowrate=DEFAULT_FLOWRATE_UL_S, + ) + print(f"{label} | reset to 0.0 mL on valve {PORT_AIR_EMPTY}: {ok} | {message}") + if not ok: + return + + ok = transfer_volume( + pump, + source_port=PORT_AIR_EMPTY, + dest_port=PORT_IPA, + volume_ul=LARGE_TEST_VOLUME_UL, + flowrate_ul_s=DEFAULT_FLOWRATE_UL_S, + label=label, + ) + if not ok: + return + + for prime_idx in range(2): + label = f"IPA prime 2->6 {prime_idx + 1}/2" + ok, message = pump.move_syringe_absolute_volume( + 0.0, + valve_num=PORT_IPA, + flowrate=DEFAULT_FLOWRATE_UL_S, + ) + print(f"{label} | reset to 0.0 mL on valve {PORT_IPA}: {ok} | {message}") + if not ok: + return + + ok = transfer_volume( + pump, + source_port=PORT_IPA, + dest_port=PORT_WASTE, + volume_ul=PRIME_TEST_VOLUME_UL, + flowrate_ul_s=DEFAULT_FLOWRATE_UL_S, + label=label, + ) + if not ok: + return + + valve_position = pump.valve_position() + print(f"reported valve position: {valve_position}") + finally: + if pump.ser.is_open and pump._is_initialized: + idle_ok, idle_message = pump.move_valve_position(PORT_SAFE_IDLE) + print(f"pre-deinitialize safe idle move to valve {PORT_SAFE_IDLE}: {idle_ok} | {idle_message}") + deinit_ok, deinit_message = pump.deinitialize(reset_init_flag=True) + print(f"deinitialize: {deinit_ok} | {deinit_message}") + if pump.ser.is_open: + pump.ser.close() + print("serial: closed") + + +if __name__ == "__main__": + main() diff --git a/recipes/user_recipes/.gitignore b/recipes/user_recipes/.gitignore index db372c4..9d37706 100644 --- a/recipes/user_recipes/.gitignore +++ b/recipes/user_recipes/.gitignore @@ -1,3 +1,4 @@ * !.gitignore !ch_apis_imaging_from_mongodb.py +!example_mongodb_recipe_lookup.py diff --git a/recipes/user_recipes/example_mongodb_recipe_lookup.py b/recipes/user_recipes/example_mongodb_recipe_lookup.py new file mode 100644 index 0000000..78e0498 --- /dev/null +++ b/recipes/user_recipes/example_mongodb_recipe_lookup.py @@ -0,0 +1,226 @@ +"""Minimal recipe example that reads a parameter set from MongoDB. + +Run from the repository root: + python ./recipes/user_recipes/example_mongodb_recipe_lookup.py + +Expected .env entries: + MONGO_URI=mongodb://... + MONGO_DB_NAME=... +""" + +import os +import re +import sys +from pathlib import Path +from typing import Dict, List, Tuple + + +ROOT_DIR = Path(__file__).resolve().parents[2] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_sequence import CommandSequence +from mongodb_helper import MongoDBHelper + + +DEFAULT_CAMPAIGN_NAME = "" + + +def load_env(file_paths=(ROOT_DIR / ".env", AAMP_APP_DIR / ".env")) -> None: + for file_path in file_paths: + if not file_path.exists(): + continue + with open(file_path, "r", encoding="utf-8") as file: + for line in file: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + os.environ[key.strip()] = value.strip().strip('"').strip("'") + + +def get_mongo_helper() -> MongoDBHelper: + mongo_uri = os.environ.get("MONGO_URI") + mongo_db_name = os.environ.get("MONGO_DB_NAME") + if not mongo_uri or not mongo_db_name: + raise RuntimeError("MONGO_URI and MONGO_DB_NAME must be set in .env.") + return MongoDBHelper(mongo_uri, mongo_db_name) + + +def fetch_campaign(mongo: MongoDBHelper, campaign_name: str) -> dict: + campaign_doc = mongo.db["campaigns"].find_one({"campaign_name": campaign_name}) + if campaign_doc is None: + available_campaigns = sorted(mongo.db["campaigns"].distinct("campaign_name")) + preview = available_campaigns[:20] + suffix = " ..." if len(available_campaigns) > len(preview) else "" + raise LookupError( + f"Campaign '{campaign_name}' was not found. Available campaigns: {preview}{suffix}" + ) + return campaign_doc + + +def list_batch_numbers(mongo: MongoDBHelper, campaign_doc: dict) -> List[int]: + return sorted(mongo.db["sets"].distinct("batch_no", {"campaign_id": campaign_doc["_id"]})) + + +def list_sample_numbers(mongo: MongoDBHelper, campaign_doc: dict, batch_no: int) -> List[int]: + return sorted( + mongo.db["sets"].distinct( + "sample_no", + { + "campaign_id": campaign_doc["_id"], + "batch_no": batch_no, + }, + ) + ) + + +def prompt_choice(prompt: str, options: List[int]) -> int: + if not options: + raise LookupError(f"No options available for {prompt}.") + + print(f"\nAvailable {prompt}: {options}") + default = options[0] + while True: + raw_value = input(f"Choose {prompt} [{default}]: ").strip() + value = default if not raw_value else int(raw_value) + if value in options: + return value + print(f"{value} is not available. Choose one of: {options}") + + +def fetch_parameter_set( + mongo: MongoDBHelper, + campaign_doc: dict, + batch_no: int, + sample_no: int, +) -> Tuple[dict, dict]: + set_doc = mongo.db["sets"].find_one( + { + "campaign_id": campaign_doc["_id"], + "batch_no": batch_no, + "sample_no": sample_no, + } + ) + if set_doc is None: + available_samples = sorted( + mongo.db["sets"].distinct( + "sample_no", + { + "campaign_id": campaign_doc["_id"], + "batch_no": batch_no, + }, + ) + ) + if available_samples: + raise LookupError( + f"No set found for campaign='{campaign_doc['campaign_name']}', batch_no={batch_no}, " + f"sample_no={sample_no}. Available sample_no values: {available_samples}" + ) + + available_batches = list_batch_numbers(mongo, campaign_doc) + raise LookupError( + f"No set found for campaign='{campaign_doc['campaign_name']}', batch_no={batch_no}, " + f"sample_no={sample_no}. Available batch_no values: {available_batches}" + ) + + return campaign_doc, set_doc + + +def display_value(value): + if isinstance(value, float) and value.is_integer(): + return int(value) + return value + + +def build_recipe_params(batch_no: int, sample_no: int, set_doc: dict) -> Dict[str, object]: + return { + "campaign_name": display_value(set_doc.get("campaign_name", "")), + "batch_no": batch_no, + "sample_no": sample_no, + "polymer_name": display_value(set_doc["polymer_name"]), + "temperature": display_value(set_doc["temperature"]), + "motor_speed": float(set_doc["motor_speed"]), + "printing_gap": display_value(set_doc["printing_gap"]), + "solvent": display_value(set_doc["solvent"]), + "concentration": display_value(set_doc["concentration"]), + "precursor_volume": display_value(set_doc["precursor_volume"]), + } + + +def sanitize_path_component(value: object) -> str: + sanitized = re.sub(r'[<>:"/\\|?*]+', "_", str(value).strip()) + return sanitized or "unnamed" + + +def build_sample_name(params: Dict[str, object]) -> str: + speed = f"{params['motor_speed']:.4f}".rstrip("0").rstrip(".") + parts = [ + f"R{params['batch_no']}", + f"S{params['sample_no']}", + params["polymer_name"], + params["solvent"], + f"{params['concentration']}mgml", + f"{speed}mms", + f"{params['temperature']}C", + f"{params['printing_gap']}um", + f"{params['precursor_volume']}ul", + ] + return sanitize_path_component("_".join(str(part) for part in parts)) + + +def build_sequence(params: Dict[str, object]) -> CommandSequence: + seq = CommandSequence() + + # Add devices and commands here using values from params. + # Example: + # volume_ul = float(params["precursor_volume"]) + # speed_mm_s = float(params["motor_speed"]) + # gap_um = float(params["printing_gap"]) + # + # seq.add_device(...) + # seq.add_command(...) + + return seq + + +def prompt_with_default(prompt: str, default: object) -> str: + raw_value = input(f"{prompt} [{default}]: ").strip() + return raw_value if raw_value else str(default) + + +def main() -> None: + load_env() + mongo = get_mongo_helper() + + campaign_name = prompt_with_default("Campaign name", DEFAULT_CAMPAIGN_NAME) + if not campaign_name: + raise ValueError("Campaign name is required.") + + campaign_doc = fetch_campaign(mongo, campaign_name) + batch_no = prompt_choice("batch_no values", list_batch_numbers(mongo, campaign_doc)) + sample_no = prompt_choice("sample_no values", list_sample_numbers(mongo, campaign_doc, batch_no)) + + campaign_doc, set_doc = fetch_parameter_set(mongo, campaign_doc, batch_no, sample_no) + params = build_recipe_params(batch_no, sample_no, set_doc) + params["campaign_name"] = campaign_doc["campaign_name"] + + print("\nLoaded MongoDB parameter set:") + for key, value in params.items(): + print(f" {key}: {value}") + + print(f"\nSuggested sample name: {build_sample_name(params)}") + + seq = build_sequence(params) + print("\nRecipe command sequence preview:") + if seq.command_list: + seq.print_command_names() + else: + print(" No commands yet. Add devices/commands in build_sequence().") + + +if __name__ == "__main__": + main() From 71adcfe1411c3fd5cfc74242829bb6ca8a742372 Mon Sep 17 00:00:00 2001 From: Hwang Date: Mon, 29 Jun 2026 13:24:30 -0500 Subject: [PATCH 124/125] Improve StellarNet spectrometer merging --- .../stellarnet_spectrometer_commands.py | 51 ++-- aamp_app/devices/stellarnet_spectrometer.py | 260 +++++++++++++++--- aamp_app/util.py | 48 ++-- 3 files changed, 270 insertions(+), 89 deletions(-) diff --git a/aamp_app/commands/stellarnet_spectrometer_commands.py b/aamp_app/commands/stellarnet_spectrometer_commands.py index 0b29ffe..60a89db 100644 --- a/aamp_app/commands/stellarnet_spectrometer_commands.py +++ b/aamp_app/commands/stellarnet_spectrometer_commands.py @@ -1,8 +1,11 @@ -from typing import Tuple, Optional +from typing import List, Optional, Tuple, Union from .command import Command, CommandResult from devices.stellarnet_spectrometer import StellarNetSpectrometer +DetectorSetting = Union[int, List[int], Tuple[int, ...]] +OptionalDetectorSetting = Optional[DetectorSetting] + class SpectrometerParentCommand(Command): """Parent class for all StellarNet Spectrometer commands.""" receiver_cls = StellarNetSpectrometer @@ -35,10 +38,10 @@ class SpectrometerUpdateDark(SpectrometerParentCommand): def __init__( self, receiver: StellarNetSpectrometer, - integration_times: Optional[Tuple[int, ...]] = None, - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1), + integration_times: OptionalDetectorSetting = None, + scans_to_avg: DetectorSetting = (3, 3), + smoothings: DetectorSetting = (0, 0), + xtimings: DetectorSetting = (1, 1), **kwargs): super().__init__(receiver, **kwargs) self._params['integration_times'] = integration_times @@ -59,10 +62,10 @@ class SpectrometerUpdateBlank(SpectrometerParentCommand): def __init__( self, receiver: StellarNetSpectrometer, - integration_times: Optional[Tuple[int, ...]] = None, - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1), + integration_times: OptionalDetectorSetting = None, + scans_to_avg: DetectorSetting = (3, 3), + smoothings: DetectorSetting = (0, 0), + xtimings: DetectorSetting = (1, 1), **kwargs): super().__init__(receiver, **kwargs) self._params['integration_times'] = integration_times @@ -81,9 +84,9 @@ class SpectrometerAdjDefIntegrationTime(SpectrometerParentCommand): def __init__( self, receiver: StellarNetSpectrometer, - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1), + scans_to_avg: DetectorSetting = (3, 3), + smoothings: DetectorSetting = (0, 0), + xtimings: DetectorSetting = (1, 1), target_max_count: int = 52000, tolerance: int = 2000, **kwargs): @@ -110,10 +113,10 @@ def __init__( receiver: StellarNetSpectrometer, save_to_file: bool = True, filename: Optional[str] = None, - integration_times: Optional[Tuple[int, ...]] = None, - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1), + integration_times: OptionalDetectorSetting = None, + scans_to_avg: DetectorSetting = (3, 3), + smoothings: DetectorSetting = (0, 0), + xtimings: DetectorSetting = (1, 1), **kwargs): super().__init__(receiver, **kwargs) self._params['save_to_file'] = save_to_file @@ -139,10 +142,10 @@ def __init__( sample_name: Optional[str] = None, save_to_file: bool = True, repeat_measure: bool = False, - integration_times: Optional[Tuple[int, ...]] = None, - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1), + integration_times: OptionalDetectorSetting = None, + scans_to_avg: DetectorSetting = (3, 3), + smoothings: DetectorSetting = (0, 0), + xtimings: DetectorSetting = (1, 1), absorbance_threshold: float = 0.003, **kwargs): super().__init__(receiver, **kwargs) @@ -173,10 +176,10 @@ def __init__( sample_name: Optional[str] = None, save_to_file: bool = True, repeat_measure: bool = False, - integration_times: Optional[Tuple[int, ...]] = None, - scans_to_avg: Tuple[int, ...] = (3, 3), - smoothings: Tuple[int, ...] = (0, 0), - xtimings: Tuple[int, ...] = (1, 1), + integration_times: OptionalDetectorSetting = None, + scans_to_avg: DetectorSetting = (3, 3), + smoothings: DetectorSetting = (0, 0), + xtimings: DetectorSetting = (1, 1), absorbance_threshold: float = 0.003, **kwargs): super().__init__(receiver, **kwargs) diff --git a/aamp_app/devices/stellarnet_spectrometer.py b/aamp_app/devices/stellarnet_spectrometer.py index 0f05218..0ef8512 100644 --- a/aamp_app/devices/stellarnet_spectrometer.py +++ b/aamp_app/devices/stellarnet_spectrometer.py @@ -27,6 +27,17 @@ class StellarNetSpectrometer(Device): SUPPORTED_SPEC_KEYS = ("UV-Vis", "NIR") save_directory = 'data/spectroscopy/' + MERGE_UV_START = 210.0 + MERGE_NIR_END = 1700.0 + MERGE_OVERLAP_START = 900.0 + MERGE_OVERLAP_END = 1030.0 + MERGE_WINDOW_NM = 10.0 + MERGE_RAW_ABS_TOLERANCE = 0.01 + MERGE_SIGNAL_MIN_P95 = 0.02 + MERGE_SCALE_MIN = 0.5 + MERGE_SCALE_MAX = 2.0 + MERGE_SCALE_FIT_MEDIAN_TOLERANCE = 0.02 + MERGE_FIXED_CROSSOVER_NM = 900.0 def __init__( self, @@ -1365,57 +1376,224 @@ def plot_spec_decay_summary( # hard coded for UV-Vis and NIR # what happens if only 1 spectrometer is connected/being used? def merge_absorbance(self, save_to_file: bool, filename: str, comment_list: List[str]) -> Tuple[bool, str]: - uv_wavelength = np.squeeze(self.wavelength_dict['UV-Vis'].copy()) - nir_wavelength = np.squeeze(self.wavelength_dict['NIR'].copy()) - uv_absorbance = np.squeeze(self.absorbance_dict['UV-Vis'][:,1].copy()) - nir_absorbance = np.squeeze(self.absorbance_dict['NIR'][:,1].copy()) + result, merge_output = self.merge_uv_nir_absorbance_arrays( + self.absorbance_dict['UV-Vis'], + self.absorbance_dict['NIR'], + ) + if not result: + return (False, merge_output) - # start and end of overlapping regions - WAVE_START = 900.0 - WAVE_END = 1030.0 + self.merged_absorbance = merge_output["merged_array"] + metadata = merge_output["metadata"] - merge_result = minimize(self.merge_error, [1, 0], args=(uv_wavelength, uv_absorbance, nir_wavelength, nir_absorbance, WAVE_START, WAVE_END), method='BFGS') + if save_to_file: + os.makedirs(self.save_directory, exist_ok=True) + data = pd.DataFrame() + data['Wavelength'] = np.squeeze(self.merged_absorbance[:, 0].copy()) + data['Absorbance'] = np.squeeze(self.merged_absorbance[:, 1].copy()) - if not merge_result: - return (False, "Failed to merge UV-Vis and NIR absorbance spectra: " + merge_result.message) - else: - scale = merge_result.x[0] - shift = merge_result.x[1] + fullfilename = os.path.join(self.save_directory, filename + '_merged.csv') + comment_list = list(comment_list) + comment_list.append("# merge_method = qc_stitch_scale_only\n") + comment_list.append("# merge_mode = " + str(metadata["merge_mode"]) + "\n") + comment_list.append("# merge_reason = " + str(metadata["merge_reason"]) + "\n") + comment_list.append("# crossover_nm = " + str(metadata["crossover_nm"]) + "\n") + comment_list.append("# scale = " + str(metadata["scale"]) + "\n") + comment_list.append("# shift = " + str(metadata["shift"]) + "\n") + comment_list.append("# overlap_range_nm = " + str((metadata["overlap_start_nm"], metadata["overlap_end_nm"])) + "\n") + comment_list.append("# uv_overlap_p95 = " + str(metadata["uv_overlap_p95"]) + "\n") + comment_list.append("# best_raw_window_diff = " + str(metadata["best_raw_window_diff"]) + "\n") + comment_list.append("# scale_only = " + str(metadata["scale_only"]) + "\n") + comment_list.append("# scaled_median_diff = " + str(metadata["scaled_median_diff"]) + "\n") + comment_list.append("# negative_values_clipped = " + str(metadata["negative_values_clipped"]) + "\n") + with open(fullfilename, 'w') as file: + file.writelines(comment_list) + data.to_csv(fullfilename, mode='a', index_label='Index') - # uv is not modified, nir is adjusted to match uv - uv_array = self.absorbance_dict['UV-Vis'].copy() - nir_wavelength = self.wavelength_dict['NIR'].copy() + return ( + True, + "Successfully merged UV-Vis and NIR absorbance spectra using " + + str(metadata["merge_mode"]) + ) - nir_absorbance = np.expand_dims(self.scale_shift_data([scale, shift], self.absorbance_dict['NIR'][:,1].copy()), axis=1) - nir_array = np.hstack((nir_wavelength, nir_absorbance)) + @staticmethod + def _best_window_median_diff( + wavelengths, + y1, + y2, + window_nm: float, + min_points: int = 5) -> Tuple[float, float]: + best_diff = float('inf') + best_wavelength = float('nan') + for wavelength in wavelengths: + window_index = np.abs(wavelengths - wavelength) <= window_nm + if np.count_nonzero(window_index) < min_points: + continue + median_diff = float(np.median(np.abs(y1[window_index] - y2[window_index]))) + if median_diff < best_diff: + best_diff = median_diff + best_wavelength = float(wavelength) + return best_diff, best_wavelength - UV_START = 210.0 - UV_END = 1030.0 - NIR_START = 900.0 - NIR_END = 1700.0 + @staticmethod + def _sanitize_absorbance_array(array) -> np.ndarray: + clean_array = np.asarray(array, dtype=float) + if clean_array.ndim != 2 or clean_array.shape[1] < 2: + raise ValueError("Absorbance array must have at least two columns.") + clean_array = clean_array[:, :2] + valid_index = np.isfinite(clean_array[:, 0]) & np.isfinite(clean_array[:, 1]) + clean_array = clean_array[valid_index] + return clean_array[clean_array[:, 0].argsort()] - uv_array = self.truncate_ends_by_wavelength(uv_array, UV_START, UV_END) - nir_array = self.truncate_ends_by_wavelength(nir_array, NIR_START, NIR_END) + @staticmethod + def merge_uv_nir_absorbance_arrays(uv_array, nir_array) -> Tuple[bool, Union[dict, str]]: + try: + uv_array = StellarNetSpectrometer._sanitize_absorbance_array(uv_array) + nir_array = StellarNetSpectrometer._sanitize_absorbance_array(nir_array) + except ValueError as exc: + return (False, str(exc)) + + overlap_start = StellarNetSpectrometer.MERGE_OVERLAP_START + overlap_end = StellarNetSpectrometer.MERGE_OVERLAP_END + overlap_index = (nir_array[:, 0] >= overlap_start) & (nir_array[:, 0] <= overlap_end) + overlap_wavelength = nir_array[overlap_index, 0].copy() + nir_overlap = nir_array[overlap_index, 1].copy() + uv_overlap = StellarNetSpectrometer._linear_interpolate( + uv_array[:, 0], + uv_array[:, 1], + overlap_wavelength, + ) - merged_array = np.vstack((uv_array, nir_array)) - merged_array = merged_array[merged_array[:,0].argsort()] - self.merged_absorbance = merged_array - - if save_to_file: - os.makedirs(self.save_directory, exist_ok=True) - data = pd.DataFrame() - data['Wavelength'] = np.squeeze(self.merged_absorbance[:,0].copy()) - data['Absorbance'] = np.squeeze(self.merged_absorbance[:,1].copy()) + valid_overlap = np.isfinite(overlap_wavelength) & np.isfinite(uv_overlap) & np.isfinite(nir_overlap) + overlap_wavelength = overlap_wavelength[valid_overlap] + uv_overlap = uv_overlap[valid_overlap] + nir_overlap = nir_overlap[valid_overlap] + + metadata = { + "merge_mode": "fixed_stitch_invalid_overlap", + "merge_reason": "not enough valid overlap points", + "overlap_start_nm": overlap_start, + "overlap_end_nm": overlap_end, + "window_nm": StellarNetSpectrometer.MERGE_WINDOW_NM, + "raw_abs_tolerance": StellarNetSpectrometer.MERGE_RAW_ABS_TOLERANCE, + "signal_min_p95": StellarNetSpectrometer.MERGE_SIGNAL_MIN_P95, + "scale_min": StellarNetSpectrometer.MERGE_SCALE_MIN, + "scale_max": StellarNetSpectrometer.MERGE_SCALE_MAX, + "scale_fit_median_tolerance": StellarNetSpectrometer.MERGE_SCALE_FIT_MEDIAN_TOLERANCE, + "uv_overlap_p95": float('nan'), + "uv_overlap_median": float('nan'), + "nir_overlap_p95": float('nan'), + "best_raw_window_diff": float('inf'), + "best_raw_window_nm": float('nan'), + "scale_only": float('nan'), + "scaled_median_diff": float('nan'), + "best_scaled_window_diff": float('inf'), + "best_scaled_window_nm": float('nan'), + "scale": 1.0, + "shift": 0.0, + "crossover_nm": StellarNetSpectrometer.MERGE_FIXED_CROSSOVER_NM, + "negative_values_clipped": 0, + } - fullfilename = self.save_directory + filename + '_merged.csv' - comment_list.append("# To merge, NIR data is scaled first then shifted\n") - comment_list.append("# scale = " + str(scale) + "\n") - comment_list.append("# shift = " + str(shift) + "\n") - with open(fullfilename, 'w') as file: - file.writelines(comment_list) - data.to_csv(fullfilename, mode='a', index_label='Index') + if overlap_wavelength.size >= 10: + uv_p95 = float(np.percentile(uv_overlap, 95)) + nir_p95 = float(np.percentile(nir_overlap, 95)) + raw_diff, raw_nm = StellarNetSpectrometer._best_window_median_diff( + overlap_wavelength, + uv_overlap, + nir_overlap, + StellarNetSpectrometer.MERGE_WINDOW_NM, + ) + denom = float(np.sum(nir_overlap * nir_overlap)) + scale_only = float(np.sum(uv_overlap * nir_overlap) / denom) if denom > 0 else float('nan') + scaled_overlap = nir_overlap * scale_only if np.isfinite(scale_only) else np.full_like(nir_overlap, np.nan) + scaled_median_diff = ( + float(np.median(np.abs(uv_overlap - scaled_overlap))) + if np.all(np.isfinite(scaled_overlap)) + else float('nan') + ) + scaled_window_diff, scaled_nm = StellarNetSpectrometer._best_window_median_diff( + overlap_wavelength, + uv_overlap, + scaled_overlap, + StellarNetSpectrometer.MERGE_WINDOW_NM, + ) + + metadata.update({ + "uv_overlap_p95": uv_p95, + "uv_overlap_median": float(np.median(uv_overlap)), + "nir_overlap_p95": nir_p95, + "best_raw_window_diff": raw_diff, + "best_raw_window_nm": raw_nm, + "scale_only": scale_only, + "scaled_median_diff": scaled_median_diff, + "best_scaled_window_diff": scaled_window_diff, + "best_scaled_window_nm": scaled_nm, + }) + + if uv_p95 < StellarNetSpectrometer.MERGE_SIGNAL_MIN_P95: + metadata.update({ + "merge_mode": "fixed_stitch_low_overlap_signal", + "merge_reason": "UV overlap signal is too low for reliable fitting", + "crossover_nm": StellarNetSpectrometer.MERGE_FIXED_CROSSOVER_NM, + "scale": 1.0, + "shift": 0.0, + }) + elif raw_diff <= StellarNetSpectrometer.MERGE_RAW_ABS_TOLERANCE: + metadata.update({ + "merge_mode": "raw_stitch", + "merge_reason": "raw UV and NIR spectra match within window tolerance", + "crossover_nm": raw_nm if np.isfinite(raw_nm) else StellarNetSpectrometer.MERGE_FIXED_CROSSOVER_NM, + "scale": 1.0, + "shift": 0.0, + }) + elif ( + np.isfinite(scale_only) + and StellarNetSpectrometer.MERGE_SCALE_MIN <= scale_only <= StellarNetSpectrometer.MERGE_SCALE_MAX + and scaled_median_diff <= StellarNetSpectrometer.MERGE_SCALE_FIT_MEDIAN_TOLERANCE): + metadata.update({ + "merge_mode": "scale_only_stitch", + "merge_reason": "raw spectra do not meet tolerance; scale-only fit is within bounds", + "crossover_nm": scaled_nm if np.isfinite(scaled_nm) else StellarNetSpectrometer.MERGE_FIXED_CROSSOVER_NM, + "scale": scale_only, + "shift": 0.0, + }) + else: + metadata.update({ + "merge_mode": "fixed_stitch_unreliable_fit", + "merge_reason": "raw match and scale-only fit did not pass QC", + "crossover_nm": StellarNetSpectrometer.MERGE_FIXED_CROSSOVER_NM, + "scale": 1.0, + "shift": 0.0, + }) + + uv_part = StellarNetSpectrometer.truncate_ends_by_wavelength( + uv_array, + StellarNetSpectrometer.MERGE_UV_START, + float(metadata["crossover_nm"]), + ) + nir_adjusted = nir_array.copy() + nir_adjusted[:, 1] = StellarNetSpectrometer.scale_shift_data( + [float(metadata["scale"]), float(metadata["shift"])], + nir_adjusted[:, 1], + ) + nir_part = nir_adjusted[ + np.logical_and( + nir_adjusted[:, 0] > float(metadata["crossover_nm"]), + nir_adjusted[:, 0] < StellarNetSpectrometer.MERGE_NIR_END, + ) + ] + + if uv_part.size == 0 or nir_part.size == 0: + return (False, "Unable to stitch UV-Vis and NIR arrays with crossover " + str(metadata["crossover_nm"])) + + merged_array = np.vstack((uv_part, nir_part)) + merged_array = merged_array[merged_array[:, 0].argsort()] + negative_index = merged_array[:, 1] < 0 + metadata["negative_values_clipped"] = int(np.count_nonzero(negative_index)) + merged_array[negative_index, 1] = 0.0 - return (True, "Successfully merged UV-Vis and NIR absorbance spectra: " + merge_result.message) + return (True, {"merged_array": merged_array, "metadata": metadata}) # y and return are ndarrays @staticmethod diff --git a/aamp_app/util.py b/aamp_app/util.py index fd7de45..5353d71 100644 --- a/aamp_app/util.py +++ b/aamp_app/util.py @@ -2850,7 +2850,7 @@ def default(self, obj): "default_integration_time": { "default": (100,), "type": tuple, - "notes": "Default integration time in ms for each declared spectrometer.", + "notes": "Default integration time in ms. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order.", }, }, }, @@ -2874,10 +2874,10 @@ def default(self, obj): "default_code": "SpectrometerUpdateDark(receiver= '', integration_times=(100,), scans_to_avg=(3,), smoothings=(0,), xtimings=(1,))", "args": { "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, - "integration_times": {"default": (100,), "type": tuple, "notes": "Integration times in ms for each declared spectrometer."}, - "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, - "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, - "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + "integration_times": {"default": (100,), "type": tuple, "notes": "Integration times in ms. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, }, "obj": SpectrometerUpdateDark, }, @@ -2885,10 +2885,10 @@ def default(self, obj): "default_code": "SpectrometerUpdateBlank(receiver= '', integration_times=(100,), scans_to_avg=(3,), smoothings=(0,), xtimings=(1,))", "args": { "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, - "integration_times": {"default": (100,), "type": tuple, "notes": "Integration times in ms for each declared spectrometer."}, - "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, - "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, - "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + "integration_times": {"default": (100,), "type": tuple, "notes": "Integration times in ms. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, }, "obj": SpectrometerUpdateBlank, }, @@ -2896,9 +2896,9 @@ def default(self, obj): "default_code": "SpectrometerAdjDefIntegrationTime(receiver= '', scans_to_avg=(3,), smoothings=(0,), xtimings=(1,), target_max_count=52000, tolerance=2000)", "args": { "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, - "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, - "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, - "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, "target_max_count": {"default": 52000, "type": int, "notes": "Target detector max count used to tune the default integration time."}, "tolerance": {"default": 2000, "type": int, "notes": "Allowed deviation from the target max count."}, }, @@ -2910,10 +2910,10 @@ def default(self, obj): "receiver": {"default": "StellarNetSpectrometer", "type": str, "notes": "Name of the device"}, "save_to_file": {"default": True, "type": bool, "notes": "Save absorbance CSV output to the spectrometer save directory."}, "filename": {"default": None, "type": str, "notes": "Optional output filename prefix. Uses a timestamp when omitted."}, - "integration_times": {"default": (100,), "type": tuple, "notes": "Integration times in ms for each declared spectrometer."}, - "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, - "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, - "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + "integration_times": {"default": (100,), "type": tuple, "notes": "Integration times in ms. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, }, "obj": SpectrometerGetAbsorbance, }, @@ -2924,10 +2924,10 @@ def default(self, obj): "sample_name": {"default": "sample", "type": str, "notes": "Sample name used to build absorbance filenames."}, "save_to_file": {"default": True, "type": bool, "notes": "Save absorbance CSV output to the spectrometer save directory."}, "repeat_measure": {"default": False, "type": bool, "notes": "Retained for compatibility with the older workflow."}, - "integration_times": {"default": None, "type": tuple, "notes": "Optional integration times in ms. Uses the device default integration time when omitted."}, - "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, - "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, - "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + "integration_times": {"default": None, "type": tuple, "notes": "Optional integration times in ms. Uses the device default when omitted; otherwise a single value applies to all selected spectrometers or tuple/list values follow spec_keys order."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, "absorbance_threshold": {"default": 0.003, "type": float, "notes": "Compatibility placeholder for the older repeat-measure workflow."}, }, "obj": SpectrometerGetAbsorbancebyname, @@ -2939,10 +2939,10 @@ def default(self, obj): "sample_name": {"default": "sample", "type": str, "notes": "Sample name used to build photon count filenames."}, "save_to_file": {"default": True, "type": bool, "notes": "Save photon count CSV output to the spectrometer save directory."}, "repeat_measure": {"default": False, "type": bool, "notes": "Retained for compatibility with the older workflow."}, - "integration_times": {"default": None, "type": tuple, "notes": "Optional integration times in ms. Uses the device default integration time when omitted."}, - "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values for each declared spectrometer."}, - "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values for each declared spectrometer."}, - "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values for each declared spectrometer."}, + "integration_times": {"default": None, "type": tuple, "notes": "Optional integration times in ms. Uses the device default when omitted; otherwise a single value applies to all selected spectrometers or tuple/list values follow spec_keys order."}, + "scans_to_avg": {"default": (3,), "type": tuple, "notes": "Scans-to-average values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "smoothings": {"default": (0,), "type": tuple, "notes": "Smoothing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, + "xtimings": {"default": (1,), "type": tuple, "notes": "X timing values. A single value applies to all selected spectrometers; tuple/list values follow spec_keys order."}, "absorbance_threshold": {"default": 0.003, "type": float, "notes": "Compatibility placeholder for the older repeat-measure workflow."}, }, "obj": SpectrometerGetPhotoncountsbyname, From 0d6740923ce3aeb1c4f562f1fc5ff875459bbec9 Mon Sep 17 00:00:00 2001 From: Hwang Date: Mon, 29 Jun 2026 13:36:49 -0500 Subject: [PATCH 125/125] Add remaining hardware examples --- examples/example_festo_solenoid_valve.py | 10 +- examples/example_film_characterization.py | 202 ++++++++++++++++++ examples/example_p4pp_rotation_sweep.py | 164 ++++++++++++++ .../example_sciencetech_uhe_nl_solar_sim.py | 22 +- examples/example_sonicator.py | 2 +- 5 files changed, 383 insertions(+), 17 deletions(-) create mode 100644 examples/example_film_characterization.py create mode 100644 examples/example_p4pp_rotation_sweep.py diff --git a/examples/example_festo_solenoid_valve.py b/examples/example_festo_solenoid_valve.py index 0bb2705..8dae248 100644 --- a/examples/example_festo_solenoid_valve.py +++ b/examples/example_festo_solenoid_valve.py @@ -55,16 +55,16 @@ def main() -> None: seq.add_command(FestoConnect(valve)) seq.add_command(FestoInitialize(valve)) seq.add_command(FestoCloseAll(valve)) - seq.add_command(FestoValveOpen(valve, valve_num=PIN4_VALVE_NUM, delay=1.0)) - seq.add_command(FestoValveClosed(valve, valve_num=PIN4_VALVE_NUM, delay=5)) - seq.add_command(FestoValveOpen(valve, valve_num=PIN8_VALVE_NUM, delay=1.0)) - seq.add_command(FestoValveClosed(valve, valve_num=PIN8_VALVE_NUM, delay=5)) + seq.add_command(FestoValveOpen(valve, valve_num=PIN8_VALVE_NUM, delay=5.0)) + seq.add_command(FestoValveClosed(valve, valve_num=PIN8_VALVE_NUM, delay=60)) + # seq.add_command(FestoValveOpen(valve, valve_num=PIN4_VALVE_NUM, delay=45.0)) + # seq.add_command(FestoValveClosed(valve, valve_num=PIN4_VALVE_NUM, delay=15)) seq.add_command(FestoCloseAll(valve)) seq.add_command(FestoDeinitialize(valve)) print("\nAssumptions:") print("- Arduino sketch: to_implement/festo_solenoid_valve_multiple/Festo_Multiple.ino") - print("- With the current sketch, valve_num=2 uses Arduino pin D8 and valve_num=3 uses D4.") + print("- With the current sketch, valve_num=2 uses Arduino pin D8 and valve_num=3 uses D4.") print("- valve_num=1 is still mapped to D12 unless you remap the sketch.") print("- The Arduino pin must drive a MOSFET/relay/driver board, not the valve coil directly.") print("- The valve coil power must come from an external supply with flyback protection.") diff --git a/examples/example_film_characterization.py b/examples/example_film_characterization.py new file mode 100644 index 0000000..a040127 --- /dev/null +++ b/examples/example_film_characterization.py @@ -0,0 +1,202 @@ +# Film characterization example combining Kinova handling, APIS rotation, and P4PP probing. +# run from root using 'python -m examples.example_film_characterization' + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_invoker import CommandInvoker +from command_sequence import CommandSequence +from devices.apis import APIS +from devices.kinova_arm import KinovaArm +from devices.p4pp import P4PP +from commands.apis_commands import * +from commands.kinova_arm_commands import * +from commands.p4pp_commands import * + + +APIS_PORT = "COM8" +P4PP_PORT = "COM19" +SAMPLE_ANGLES_DEG = [0, 45, 90] +P4PP_TOUCHDOWN_MM = 47.0 +P4PP_ROTATION_CLEARANCE_MM = 40.0 +P4PP_ROTATION_STEP_DEG = 45.0 +P4PP_CONTACT_DELAY_S = 5.0 +P4PP_MEASUREMENT_RESISTOR_OHMS = 681.0 + + +def add_legacy_apis_rotation_commands( + seq: CommandSequence, + apis: APIS, + sample_angles_deg, + sample_delay_s: float = 0.5, +) -> None: + # Keep the same logical flow as the old polarizer example: + # polarizer at 0 deg, rotate the sample through each angle, + # then polarizer at 90 deg and repeat. + seq.add_command(APISRotatePolarizer(apis, angle_deg=0.0)) + for angle in sample_angles_deg: + seq.add_command(APISRotateSample(apis, angle_deg=float(angle), delay=sample_delay_s)) + + seq.add_command(APISRotatePolarizer(apis, angle_deg=90.0)) + for angle in sample_angles_deg: + seq.add_command(APISRotateSample(apis, angle_deg=float(angle), delay=sample_delay_s)) + + seq.add_command(APISRotatePolarizer(apis, angle_deg=0.0)) + seq.add_command(APISRotateSample(apis, angle_deg=0.0)) + + +def add_p4pp_characterization_commands(seq: CommandSequence, p4pp: P4PP) -> None: + # Measure at 0 deg, 45 deg, then 90 deg using 45 deg relative steps. + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=P4PP_TOUCHDOWN_MM)) + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=P4PP_ROTATION_CLEARANCE_MM, delay=P4PP_CONTACT_DELAY_S)) + + seq.add_command(P4PPMoveRotationalRelative(p4pp, distance_deg=P4PP_ROTATION_STEP_DEG)) + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=P4PP_TOUCHDOWN_MM)) + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=P4PP_ROTATION_CLEARANCE_MM, delay=P4PP_CONTACT_DELAY_S)) + + seq.add_command(P4PPMoveRotationalRelative(p4pp, distance_deg=P4PP_ROTATION_STEP_DEG)) + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=P4PP_TOUCHDOWN_MM)) + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=P4PP_ROTATION_CLEARANCE_MM, delay=P4PP_CONTACT_DELAY_S)) + seq.add_command(P4PPHomeAll(p4pp)) + + + +def main() -> None: + seq = CommandSequence() + + arm = KinovaArm("arm") + apis = APIS( + name="apis", + port=APIS_PORT, + baudrate=9600, + timeout=0.5, + connection_wait_s=2.0, + settling_time_s=1.5, + command_delay_s=0.05, + max_retries=3, + use_camera=False, + ) + p4pp = P4PP( + name="p4pp", + port=P4PP_PORT, + baudrate=115200, + timeout=0.2, + startup_delay=2.0, + command_timeout=10.0, + motion_timeout=60.0, + home_timeout=60.0, + measure_timeout=30.0, + poll_interval=0.2, + rotation_safety_linear_mm=45.0, + measurement_resistor_ohms=P4PP_MEASUREMENT_RESISTOR_OHMS, + save_directory="data/resistance/", + ) + + seq.add_device(arm) + seq.add_device(apis) + seq.add_device(p4pp) + + seq.add_command(KinovaArmConnect(arm)) + seq.add_command(APISConnect(apis)) + seq.add_command(P4PPConnect(p4pp)) + + seq.add_command(APISInitialize(apis)) + seq.add_command(APISHome(apis)) + seq.add_command(P4PPInitialize(p4pp)) + seq.add_command(P4PPSetMeasurementResistor(p4pp, resistor_ohms=P4PP_MEASUREMENT_RESISTOR_OHMS)) + seq.add_command(P4PPHomeAll(p4pp)) + seq.add_command(KinovaArmInitialize(arm)) + + seq.add_command(KinovaArmExecuteAction(arm, action_name="Home")) + seq.add_command(KinovaArmOpenGripper(arm)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_High_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_Up_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_Down_Angles")) + seq.add_command(KinovaArmCloseGripper(arm)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_Up_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_High_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Ready_Angles")) + + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Down_Out_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Down_In_Angles", delay=5)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Up_In_Angles", delay=5)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_High_In_Angles", delay="P")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Print_Ready_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_High_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="UV_Intermediate_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_Intermediate_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_High_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_Up_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_Down_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_Down_Out_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_Intermediate_Angles")) + + # Image sequence + add_legacy_apis_rotation_commands( + seq=seq, + apis=apis, + sample_angles_deg=SAMPLE_ANGLES_DEG, + ) + + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_Down_Out_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_Down_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_Up_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_High_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_Intermediate_Angles")) + + seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_Intermediate_Angles", delay=5)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_High_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_Up_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_Down_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_Down_Out_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_Intermediate_Angles")) + + + # Resistance sequence + # add_p4pp_characterization_commands(seq=seq, p4pp=p4pp) + + # seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_Down_Out_Angles", delay=5)) + # seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_Down_Angles")) + # seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_Up_Angles")) + # seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_High_Angles")) + # seq.add_command(KinovaArmExecuteAction(arm, action_name="Resistance_Intermediate_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Image_Intermediate_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="UV_Intermediate_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_High_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_Up_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_Down_Angles")) + seq.add_command(KinovaArmOpenGripper(arm)) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_Up_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Sub_Handler_High_Angles")) + seq.add_command(KinovaArmExecuteAction(arm, action_name="Home")) + + + # Resistance sequence + add_p4pp_characterization_commands(seq=seq, p4pp=p4pp) + + + seq.add_command(APISDeinitialize(apis)) + seq.add_command(P4PPDeinitialize(p4pp)) + + log_file = "logs/example_film_characterization.log" + invoker = CommandInvoker(seq, log_to_file=True, log_filename=log_file, alert_slack=False) + + print("This example combines Kinova handling, APIS rotation, and P4PP characterization.") + print("APIS camera support is disabled here; the APIS portion rotates the film before the P4PP probing sequence.") + print("The P4PP portion probes at 0 deg, 45 deg, and 90 deg using 47 mm contact and 40 mm rotation clearance.") + seq.print_command_names() + userinput = input("\ntype 'y' to continue, type anything else to quit: ").strip().lower() + if userinput == "y": + result = invoker.invoke_commands() + print(result) + + +if __name__ == "__main__": + main() diff --git a/examples/example_p4pp_rotation_sweep.py b/examples/example_p4pp_rotation_sweep.py new file mode 100644 index 0000000..a740187 --- /dev/null +++ b/examples/example_p4pp_rotation_sweep.py @@ -0,0 +1,164 @@ +# P4PP rotation sweep measurement +# run from root using 'python -m examples.example_p4pp_rotation_sweep' + +import csv +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[1] +AAMP_APP_DIR = ROOT_DIR / "aamp_app" +for path in (ROOT_DIR, AAMP_APP_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + +from command_sequence import CommandSequence +from command_invoker import CommandInvoker +from commands.command import CommandResult +from commands.p4pp_commands import ( + P4PPConnect, + P4PPDeinitialize, + P4PPHomeAll, + P4PPInitialize, + P4PPMeasure, + P4PPMoveLinearAbsolute, + P4PPMoveRotationalAbsolute, + P4PPParentCommand, + P4PPRefreshPosition, + P4PPSaveMeasurementCsv, + P4PPSetMeasurementResistor, +) +from devices.p4pp import P4PP + + +P4PP_PORT = "COM19" +SAMPLE_NAME = "sample_name" +SAVE_DIRECTORY = "data/resistance" +MEASUREMENT_CYCLES = 25 +MEASUREMENT_RESISTOR_OHMS = 681.0 +RETRACT_LINEAR_MM = 40.0 +MEASURE_LINEAR_MM = 47.0 +ANGLE_START_DEG = 0 +ANGLE_STOP_DEG = 180 +ANGLE_STEP_DEG = 5 + + +class P4PPSaveRotationSweepSummary(P4PPParentCommand): + """Append the latest P4PP rotation sweep average/std result to a merged CSV.""" + + def __init__(self, receiver: P4PP, angle_deg: float, sample_id: str, csv_path: str, notes: str = "", **kwargs): + super().__init__(receiver, **kwargs) + self._params["angle_deg"] = angle_deg + self._params["sample_id"] = sample_id + self._params["csv_path"] = csv_path + self._params["notes"] = notes + + def execute(self) -> None: + if self._receiver.latest_result is None: + self._result = CommandResult(False, "No P4PP measurement result is available to summarize.") + return + + csv_path = self._params["csv_path"] + Path(csv_path).parent.mkdir(parents=True, exist_ok=True) + file_exists = Path(csv_path).is_file() + resistor_info = self._receiver.get_measurement_resistor_info() + _, linear_position_mm = self._receiver.get_linear_position_mm() + _, rotational_position_deg = self._receiver.get_rotational_position_deg() + cycle_results = self._receiver.cycle_results + + row = { + "sample_id": self._params["sample_id"], + "angle_deg": self._params["angle_deg"], + "linear_position_mm": linear_position_mm, + "rotational_position_deg": rotational_position_deg, + "cycles": len(cycle_results) if cycle_results else 1, + "r_set_ohms": resistor_info["R_set"], + "r_sheet_avg": self._receiver.latest_result, + "r_sheet_std": self._receiver.latest_std if self._receiver.latest_std is not None else "", + "notes": self._params["notes"], + } + + with open(csv_path, "a", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter(handle, fieldnames=list(row.keys())) + if not file_exists: + writer.writeheader() + writer.writerow(row) + + self._result = CommandResult(True, "Successfully saved P4PP rotation sweep summary to " + csv_path) + + +def main() -> None: + seq = CommandSequence() + + p4pp = P4PP( + name="p4pp", + port=P4PP_PORT, + baudrate=115200, + timeout=0.2, + startup_delay=2.0, + command_timeout=10.0, + motion_timeout=60.0, + home_timeout=60.0, + measure_timeout=60.0, + poll_interval=0.2, + rotation_safety_linear_mm=45.0, + measurement_resistor_ohms=MEASUREMENT_RESISTOR_OHMS, + save_directory=SAVE_DIRECTORY, + ) + seq.add_device(p4pp) + + sample_dir = Path(SAVE_DIRECTORY) / SAMPLE_NAME + measurement_csv_path = sample_dir / f"{SAMPLE_NAME}_p4pp_rotation_sweep_measurements.csv" + merged_csv_path = sample_dir / f"{SAMPLE_NAME}_p4pp_rotation_sweep_merged.csv" + angles_deg = list(range(ANGLE_START_DEG, ANGLE_STOP_DEG, ANGLE_STEP_DEG)) + + seq.add_command(P4PPConnect(p4pp)) + seq.add_command(P4PPInitialize(p4pp)) + seq.add_command(P4PPHomeAll(p4pp)) + seq.add_command(P4PPSetMeasurementResistor(p4pp, resistor_ohms=MEASUREMENT_RESISTOR_OHMS)) + + for angle_deg in angles_deg: + # Retract below the rotation safety limit before changing angle. + sample_id = f"{SAMPLE_NAME}_{angle_deg}deg" + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=RETRACT_LINEAR_MM)) + seq.add_command(P4PPMoveRotationalAbsolute(p4pp, position_deg=float(angle_deg))) + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=MEASURE_LINEAR_MM)) + seq.add_command(P4PPMeasure(p4pp, cycles=MEASUREMENT_CYCLES)) + seq.add_command( + P4PPSaveMeasurementCsv( + p4pp, + sample_id=sample_id, + csv_path=str(measurement_csv_path), + notes="P4PP 0:180:5 rotation sweep at 47 mm linear position", + ) + ) + seq.add_command( + P4PPSaveRotationSweepSummary( + p4pp, + angle_deg=float(angle_deg), + sample_id=sample_id, + csv_path=str(merged_csv_path), + notes="P4PP 0:180:5 rotation sweep avg/std summary", + ) + ) + + seq.add_command(P4PPMoveLinearAbsolute(p4pp, position_mm=RETRACT_LINEAR_MM)) + seq.add_command(P4PPRefreshPosition(p4pp)) + seq.add_command(P4PPDeinitialize(p4pp)) + + log_file = "logs/example_p4pp_rotation_sweep.log" + invoker = CommandInvoker(seq, log_to_file=True, log_filename=log_file, alert_slack=False) + + print("This P4PP example homes the axes, selects the 681 ohm resistor,") + print(f"then measures {len(angles_deg)} angles from 0 to 175 deg in 5 deg steps.") + print(f"Each angle is measured with {MEASUREMENT_CYCLES} cycles at {MEASURE_LINEAR_MM} mm.") + print(f"Detailed results will be appended to {measurement_csv_path}.") + print(f"Merged avg/std results will be appended to {merged_csv_path}.") + seq.print_command_names() + userinput = input("\ntype 'y' to continue, type anything else to quit: ").strip().lower() + if userinput == "y": + invoker.invoke_commands() + + +if __name__ == "__main__": + main() diff --git a/examples/example_sciencetech_uhe_nl_solar_sim.py b/examples/example_sciencetech_uhe_nl_solar_sim.py index 9fce551..44e383e 100644 --- a/examples/example_sciencetech_uhe_nl_solar_sim.py +++ b/examples/example_sciencetech_uhe_nl_solar_sim.py @@ -52,8 +52,8 @@ def main() -> None: seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="lamp")) seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="shutter")) seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="attenuator")) - seq.add_command(SciencetechUHENLSolarSimCloseShutter(simulator)) - seq.add_command(SciencetechUHENLSolarSimSetAttenuator(simulator, percent=75)) + seq.add_command(SciencetechUHENLSolarSimOpenShutter(simulator)) + seq.add_command(SciencetechUHENLSolarSimSetAttenuator(simulator, percent=35)) seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="attenuator")) seq.add_command(SciencetechUHENLSolarSimEnableArcLamp(simulator)) seq.add_command(SciencetechUHENLSolarSimGetStatus(simulator)) @@ -62,17 +62,17 @@ def main() -> None: seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="current")) seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="voltage")) seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="power")) - seq.add_command(SciencetechUHENLSolarSimOpenShutter(simulator)) - seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="shutter")) - seq.add_command(SciencetechUHENLSolarSimCloseShutter(simulator)) - seq.add_command(SciencetechUHENLSolarSimOpenAttenuator(simulator)) - seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="attenuator")) - seq.add_command(SciencetechUHENLSolarSimDisableArcLamp(simulator)) - seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="lamp")) - seq.add_command(SciencetechUHENLSolarSimDeinitialize(simulator, close_serial=True)) + # seq.add_command(SciencetechUHENLSolarSimOpenShutter(simulator)) + # seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="shutter")) + # seq.add_command(SciencetechUHENLSolarSimCloseShutter(simulator)) + # seq.add_command(SciencetechUHENLSolarSimOpenAttenuator(simulator)) + # seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="attenuator")) + # seq.add_command(SciencetechUHENLSolarSimDisableArcLamp(simulator)) + # seq.add_command(SciencetechUHENLSolarSimGetFeedback(simulator, feedback_type="lamp")) + # seq.add_command(SciencetechUHENLSolarSimDeinitialize(simulator, close_serial=True)) print("\nThis smoke test verifies the full UHE-NL control path.") - print("Sequence: initialize with lamp OFF, close shutter, set attenuator to 75%, ignite lamp,") + print("Sequence: initialize with lamp OFF, close shutter, set attenuator to 35%, ignite lamp,") print("check output/current/voltage/power, open and close shutter, reopen attenuator to 100%,") print("turn lamp OFF, then deinitialize while leaving cooling on for cooldown.") diff --git a/examples/example_sonicator.py b/examples/example_sonicator.py index 061c1c4..2e42f83 100644 --- a/examples/example_sonicator.py +++ b/examples/example_sonicator.py @@ -50,7 +50,7 @@ def main() -> None: seq.add_command(SonicatorGetStatus(sonicator)) seq.add_command(SonicatorStartSonicating(sonicator)) seq.add_command(SonicatorGetStatus(sonicator, delay=2.0)) - seq.add_command(SonicatorStopSonicating(sonicator, delay=5.0)) + seq.add_command(SonicatorStopSonicating(sonicator, delay=30.0)) seq.add_command(SonicatorGetStatus(sonicator)) seq.add_command(SonicatorDeinitialize(sonicator, close_serial=True))