Skip to content

Commit c0700c8

Browse files
authored
added functions to count module commands per run (#14797)
<!-- Thanks for taking the time to open a pull request! Please make sure you've read the "Opening Pull Requests" section of our Contributing Guide: https://github.com/Opentrons/opentrons/blob/edge/CONTRIBUTING.md#opening-pull-requests To ensure your code is reviewed quickly and thoroughly, please fill out the sections below to the best of your ability! --> # Overview Functions to Count Module commands per run # Test Plan - looked at run logs and used cmd f to double check command counts/times # Changelog Added a function for the thermocycler, temperature module, and heater shaker to count values of interest for lifetime test comparison Added those dictionaries to larger dictionary to be included on run sheet # Review requests <!-- Describe any requests for your reviewers here. --> # Risk assessment - These functions are not set up to handle multiples of the same module in a protocol. It will group total commands together - some modules do not deactivate at the end of the run. To get total on time, the protocol completedAt timestamp is used.
1 parent 048a533 commit c0700c8

File tree

5 files changed

+233
-39
lines changed

5 files changed

+233
-39
lines changed

abr-testing/abr_testing/automation/jira_tool.py

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def issues_on_board(self, board_id: str) -> List[str]:
4444
def open_issue(self, issue_key: str) -> None:
4545
"""Open issue on web browser."""
4646
url = f"{self.url}/browse/{issue_key}"
47+
print(f"Opening at {url}.")
4748
webbrowser.open(url)
4849

4950
def create_ticket(

abr-testing/abr_testing/data_collection/abr_google_drive.py

+12-27
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import gspread # type: ignore[import]
77
from datetime import datetime, timedelta
88
from abr_testing.data_collection import read_robot_logs
9-
from typing import Set, Dict, Any
9+
from typing import Set, Dict, Any, Tuple, List
1010
from abr_testing.automation import google_drive_tool, google_sheets_tool
1111

1212

@@ -31,7 +31,7 @@ def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]:
3131

3232
def create_data_dictionary(
3333
runs_to_save: Set[str], storage_directory: str
34-
) -> Dict[Any, Dict[str, Any]]:
34+
) -> Tuple[Dict[Any, Dict[str, Any]], List]:
3535
"""Pull data from run files and format into a dictionary."""
3636
runs_and_robots = {}
3737
for filename in os.listdir(storage_directory):
@@ -100,12 +100,17 @@ def create_data_dictionary(
100100
"Right Mount": right_pipette,
101101
"Extension": extension,
102102
}
103-
row_2 = {**row, **all_modules}
103+
tc_dict = read_robot_logs.thermocycler_commands(file_results)
104+
hs_dict = read_robot_logs.hs_commands(file_results)
105+
tm_dict = read_robot_logs.temperature_module_commands(file_results)
106+
notes = {"Note1": "", "Note2": ""}
107+
row_2 = {**row, **all_modules, **notes, **hs_dict, **tm_dict, **tc_dict}
108+
headers = list(row_2.keys())
104109
runs_and_robots[run_id] = row_2
105110
else:
106111
os.remove(file_path)
107112
print(f"Run ID: {run_id} has a run time of 0 minutes. Run removed.")
108-
return runs_and_robots
113+
return runs_and_robots, headers
109114

110115

111116
if __name__ == "__main__":
@@ -175,29 +180,9 @@ def create_data_dictionary(
175180
run_ids_on_gd, run_ids_on_gs
176181
)
177182
# Add missing runs to google sheet
178-
runs_and_robots = create_data_dictionary(missing_runs_from_gs, storage_directory)
179-
headers = [
180-
"Robot",
181-
"Run_ID",
182-
"Protocol_Name",
183-
"Software Version",
184-
"Date",
185-
"Start_Time",
186-
"End_Time",
187-
"Run_Time (min)",
188-
"Errors",
189-
"Error_Code",
190-
"Error_Type",
191-
"Error_Instrument",
192-
"Error_Level",
193-
"Left Mount",
194-
"Right Mount",
195-
"Extension",
196-
"heaterShakerModuleV1",
197-
"temperatureModuleV2",
198-
"magneticBlockV1",
199-
"thermocyclerModuleV2",
200-
]
183+
runs_and_robots, headers = create_data_dictionary(
184+
missing_runs_from_gs, storage_directory
185+
)
201186
read_robot_logs.write_to_local_and_google_sheet(
202187
runs_and_robots, storage_directory, google_sheet_name, google_sheet, headers
203188
)

abr-testing/abr_testing/data_collection/abr_robot_error.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def get_error_info_from_robot(
4444
# JIRA Ticket Fields
4545
failure_level = "Level " + str(error_level) + " Failure"
4646
components = [failure_level, "Flex-RABR"]
47+
components = ["Flex-RABR"]
4748
affects_version = results["API_Version"]
4849
parent = results.get("robot_name", "")
4950
print(parent)
@@ -141,10 +142,15 @@ def get_error_info_from_robot(
141142
whole_description_str,
142143
saved_file_path,
143144
) = get_error_info_from_robot(ip, one_run, storage_directory)
145+
# get calibration data
146+
saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets(
147+
ip, storage_directory
148+
)
144149
print(f"Making ticket for run: {one_run} on robot {robot}.")
145150
# TODO: make argument or see if I can get rid of with using board_id.
146151
project_key = "RABR"
147152
parent_key = project_key + "-" + robot[-1]
153+
issues_ids = ticket.issues_on_board(board_id)
148154
issue_url, issue_key = ticket.create_ticket(
149155
summary,
150156
whole_description_str,
@@ -158,8 +164,4 @@ def get_error_info_from_robot(
158164
)
159165
ticket.open_issue(issue_key)
160166
ticket.post_attachment_to_ticket(issue_key, saved_file_path)
161-
# get calibration data
162-
saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets(
163-
ip, storage_directory
164-
)
165167
ticket.post_attachment_to_ticket(issue_key, saved_file_path_calibration)

abr-testing/abr_testing/data_collection/error_levels.csv

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Prefix,Error Code,Description,Categories,Level of Failure,
2020
2,2009,Early Capactivive Sense Trigger,A Robot Action Failed,4,
2121
2,2010,Innacrruate Non Contact Sweep,A Robot Action Failed,3,
2222
2,2011,Misaligned Gantry,A Robot Action Failed,3,
23-
2,2012,Unmatched Tip Presence States,A Robot Action Failed,3-4,
23+
2,2012,Unmatched Tip Presence States,A Robot Action Failed, 4,
2424
2,2013,Position Unknown,A Robot Action Failed,4,
2525
2,2014,Execution Cancelled,A Robot Action Failed, 4,
2626
2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3,
@@ -31,18 +31,18 @@ Prefix,Error Code,Description,Categories,Level of Failure,
3131
3,3004,Tip Drop Failed,A Robot Interaction Failed,4,
3232
3,3005,Unexpeted Tip Removal,A Robot Interaction Failed,4,
3333
3,3006,Pipette Overpressure,A Robot Interaction Failed,3,
34-
3,3008,E-Stop Activated,A Robot Interaction Failed,Not an error,
34+
3,3008,E-Stop Activated,A Robot Interaction Failed,5, Not an error,
3535
3,3009,E-Stop Not Present,A Robot Interaction Failed,5,
3636
3,3010,Pipette Not Present,A Robot Interaction Failed,5,
3737
3,3011,Gripper Not Present,A Robot Interaction Failed,5,
3838
3,3012,Unexpected Tip Attach,A Robot Interaction Failed,4,
39-
3,3013,Firmware Update Required,A Robot Interaction Failed,Not an error,
39+
3,3013,Firmware Update Required,A Robot Interaction Failed,5, Not an error,
4040
3,3014,Invalid ID Actuator,A Robot Interaction Failed,3,
4141
3,3015,Module Not Pesent,A Robot Interaction Failed,5,Not an error
4242
3,3016,Invalid Instrument Data,A Robot Interaction Failed,3,
4343
3,3017,Invalid Liquid Class Name,A Robot Interaction Failed,5,Not an error
4444
3,3018,Tip Detector Not Found,A Robot Interaction Failed,3,
45-
4,4000,General Error,A Software Error Occured,2-4,How severe does a general error get
45+
4,4000,General Error,A Software Error Occured,4,How severe does a general error get
4646
4,4001,Robot In Use,A Software Error Occured,5,Not an error
4747
4,4002,API Removed,A Software Error Occured,5,used an old app on a new robot
4848
4,4003,Not Supported On Robot Type,A Software Error Occured,5,Not an error

abr-testing/abr_testing/data_collection/read_robot_logs.py

+210-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
saved in a local directory.
66
"""
77
import csv
8-
import datetime
8+
from datetime import datetime
99
import os
1010
from abr_testing.data_collection.error_levels import ERROR_LEVELS_PATH
1111
from typing import List, Dict, Any, Tuple, Set
@@ -14,6 +14,210 @@
1414
import requests
1515

1616

17+
def command_time(command: Dict[str, str]) -> Tuple[float, float]:
18+
"""Calculate total create and complete time per command."""
19+
try:
20+
create_time = datetime.strptime(
21+
command.get("createdAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
22+
)
23+
start_time = datetime.strptime(
24+
command.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
25+
)
26+
complete_time = datetime.strptime(
27+
command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
28+
)
29+
create_to_start = (start_time - create_time).total_seconds()
30+
start_to_complete = (complete_time - start_time).total_seconds()
31+
except ValueError:
32+
create_to_start = 0
33+
start_to_complete = 0
34+
return create_to_start, start_to_complete
35+
36+
37+
def hs_commands(file_results: Dict[str, Any]) -> Dict[str, float]:
38+
"""Gets total latch engagements, homes, rotations and total on time (sec) for heater shaker."""
39+
# TODO: modify for cases that have more than 1 heater shaker.
40+
commandData = file_results.get("commands", "")
41+
hs_latch_count: float = 0.0
42+
hs_temp: float = 0.0
43+
hs_home_count: float = 0.0
44+
hs_speed: float = 0.0
45+
hs_rotations: Dict[str, float] = dict()
46+
hs_temps: Dict[str, float] = dict()
47+
temp_time = None
48+
shake_time = None
49+
for command in commandData:
50+
commandType = command["commandType"]
51+
# Heatershaker
52+
# Latch count
53+
if (
54+
commandType == "heaterShaker/closeLabwareLatch"
55+
or commandType == "heaterShaker/openLabwareLatch"
56+
):
57+
hs_latch_count += 1
58+
# Home count
59+
elif commandType == "heaterShaker/deactivateShaker":
60+
hs_home_count += 1
61+
deactivate_time = datetime.strptime(
62+
command.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
63+
)
64+
if temp_time is not None and deactivate_time > temp_time:
65+
temp_duration = (deactivate_time - temp_time).total_seconds()
66+
hs_temps[hs_temp] = hs_temps.get(hs_temp, 0.0) + temp_duration
67+
if shake_time is not None and deactivate_time > shake_time:
68+
shake_duration = (deactivate_time - shake_time).total_seconds()
69+
hs_rotations[hs_speed] = hs_rotations.get(hs_speed, 0.0) + (
70+
(hs_speed * shake_duration) / 60
71+
)
72+
# of Rotations
73+
elif commandType == "heaterShaker/setAndWaitForShakeSpeed":
74+
hs_speed = command["params"]["rpm"]
75+
shake_time = datetime.strptime(
76+
command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
77+
)
78+
# On Time
79+
elif commandType == "heaterShaker/setTargetTemperature":
80+
# if heater shaker temp is not deactivated.
81+
hs_temp = command["params"]["celsius"]
82+
temp_time = datetime.strptime(
83+
command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
84+
)
85+
86+
hs_total_rotations = sum(hs_rotations.values())
87+
hs_total_temp_time = sum(hs_temps.values())
88+
hs_dict = {
89+
"Heatershaker # of Latch Engagements": hs_latch_count,
90+
"Heatershaker # of Homes": hs_home_count,
91+
"Heatershaker # of Rotations": hs_total_rotations,
92+
"Heatershaker Temp On Time (sec)": hs_total_temp_time,
93+
}
94+
return hs_dict
95+
96+
97+
def temperature_module_commands(file_results: Dict[str, Any]) -> Dict[str, float]:
98+
"""Get # of temp changes and total temp on time for temperature module from run log."""
99+
# TODO: modify for cases that have more than 1 temperature module.
100+
tm_temp_change = 0
101+
tm_temps: Dict[str, float] = dict()
102+
temp_time = None
103+
deactivate_time = None
104+
commandData = file_results.get("commands", "")
105+
for command in commandData:
106+
commandType = command["commandType"]
107+
if commandType == "temperatureModule/setTargetTemperature":
108+
tm_temp = command["params"]["celsius"]
109+
tm_temp_change += 1
110+
if commandType == "temperatureModule/waitForTemperature":
111+
temp_time = datetime.strptime(
112+
command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
113+
)
114+
if commandType == "temperatureModule/deactivate":
115+
deactivate_time = datetime.strptime(
116+
command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
117+
)
118+
if temp_time is not None and deactivate_time > temp_time:
119+
temp_duration = (deactivate_time - temp_time).total_seconds()
120+
tm_temps[tm_temp] = tm_temps.get(tm_temp, 0.0) + temp_duration
121+
if temp_time is not None and deactivate_time is None:
122+
# If temperature module is not deactivated, protocol completedAt time stamp used.
123+
protocol_end = datetime.strptime(
124+
file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
125+
)
126+
temp_duration = (protocol_end - temp_time).total_seconds()
127+
tm_temps[tm_temp] = tm_temps.get(tm_temp, 0.0) + temp_duration
128+
tm_total_temp_time = sum(tm_temps.values())
129+
tm_dict = {
130+
"Temp Module # of Temp Changes": tm_temp_change,
131+
"Temp Module Temp On Time (sec)": tm_total_temp_time,
132+
}
133+
return tm_dict
134+
135+
136+
def thermocycler_commands(file_results: Dict[str, Any]) -> Dict[str, float]:
137+
"""Counts # of lid engagements, temp changes, and temp sustaining mins."""
138+
# TODO: modify for cases that have more than 1 thermocycler.
139+
commandData = file_results.get("commands", "")
140+
lid_engagements: float = 0.0
141+
block_temp_changes: float = 0.0
142+
lid_temp_changes: float = 0.0
143+
lid_temps: Dict[str, float] = dict()
144+
block_temps: Dict[str, float] = dict()
145+
lid_on_time = None
146+
lid_off_time = None
147+
block_on_time = None
148+
block_off_time = None
149+
for command in commandData:
150+
commandType = command["commandType"]
151+
if (
152+
commandType == "thermocycler/openLid"
153+
or commandType == "thermocycler/closeLid"
154+
):
155+
lid_engagements += 1
156+
if commandType == "thermocycler/setTargetBlockTemperature":
157+
block_temp = command["params"]["celsius"]
158+
block_temp_changes += 1
159+
block_on_time = datetime.strptime(
160+
command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
161+
)
162+
if commandType == "thermocycler/setTargetLidTemperature":
163+
lid_temp_changes += 1
164+
lid_temp = command["params"]["celsius"]
165+
lid_on_time = datetime.strptime(
166+
command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
167+
)
168+
if commandType == "thermocycler/deactivateLid":
169+
lid_off_time = datetime.strptime(
170+
command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
171+
)
172+
if lid_on_time is not None and lid_off_time > lid_on_time:
173+
lid_duration = (lid_off_time - lid_on_time).total_seconds()
174+
lid_temps[lid_temp] = lid_temps.get(lid_temp, 0.0) + lid_duration
175+
if commandType == "thermocycler/deactivateBlock":
176+
block_off_time = datetime.strptime(
177+
command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
178+
)
179+
if block_on_time is not None and block_off_time > block_on_time:
180+
block_duration = (block_off_time - block_on_time).total_seconds()
181+
block_temps[block_temp] = (
182+
block_temps.get(block_temp, 0.0) + block_duration
183+
)
184+
if commandType == "thermocycler/runProfile":
185+
profile = command["params"]["profile"]
186+
total_changes = len(profile)
187+
block_temp_changes += total_changes
188+
for cycle in profile:
189+
block_temp = cycle["celsius"]
190+
block_time = cycle["holdSeconds"]
191+
block_temps[block_temp] = block_temps.get(block_temp, 0.0) + block_time
192+
if block_on_time is not None and block_off_time is None:
193+
# If thermocycler block not deactivated protocol completedAt time stamp used
194+
protocol_end = datetime.strptime(
195+
file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
196+
)
197+
temp_duration = (protocol_end - block_on_time).total_seconds()
198+
block_temps[block_temp] = block_temps.get(block_temp, 0.0) + temp_duration
199+
if lid_on_time is not None and lid_off_time is None:
200+
# If thermocycler lid not deactivated protocol completedAt time stamp used
201+
protocol_end = datetime.strptime(
202+
file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z"
203+
)
204+
temp_duration = (protocol_end - lid_on_time).total_seconds()
205+
lid_temps[lid_temp] = block_temps.get(lid_temp, 0.0) + temp_duration
206+
207+
block_total_time = sum(block_temps.values())
208+
lid_total_time = sum(lid_temps.values())
209+
210+
tc_dict = {
211+
"Thermocycler # of Lid Engagements": lid_engagements,
212+
"Thermocycler Block # of Temp Changes": block_temp_changes,
213+
"Thermocycler Block Temp On Time (sec)": block_total_time,
214+
"Thermocycler Lid # of Temp Changes": lid_temp_changes,
215+
"Thermocycler Lid Temp On Time (sec)": lid_total_time,
216+
}
217+
218+
return tc_dict
219+
220+
17221
def create_abr_data_sheet(
18222
storage_directory: str, file_name: str, headers: List[str]
19223
) -> str:
@@ -112,7 +316,7 @@ def read_abr_data_sheet(
112316
runs_in_sheet.add(run_id)
113317
print(f"There are {str(len(runs_in_sheet))} runs documented in the ABR sheet.")
114318
# Read Google Sheet
115-
google_sheet.check_token()
319+
google_sheet.token_check()
116320
google_sheet.write_header(headers)
117321
google_sheet.update_row_index()
118322
return runs_in_sheet
@@ -189,7 +393,7 @@ def get_calibration_offsets(
189393
health_data = response.json()
190394
robot_name = health_data.get("name", "")
191395
api_version = health_data.get("api_version", "")
192-
pull_date_timestamp = datetime.datetime.now()
396+
pull_date_timestamp = datetime.now()
193397
date = pull_date_timestamp.date().isoformat()
194398
file_date = str(pull_date_timestamp).replace(":", "").split(".")[0]
195399
calibration["Robot"] = robot_name
@@ -219,5 +423,7 @@ def get_calibration_offsets(
219423
)
220424
deck: Dict[str, Any] = response.json()
221425
calibration["Deck"] = deck.get("deckCalibration", "")
222-
saved_file_path = save_run_log_to_json(ip, calibration, storage_directory)
426+
save_name = ip + "_calibration.json"
427+
saved_file_path = os.path.join(storage_directory, save_name)
428+
json.dump(calibration, open(saved_file_path, mode="w"))
223429
return saved_file_path, calibration

0 commit comments

Comments
 (0)