pychemstation 0.5.12__py3-none-any.whl → 0.5.13.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pychemstation/control/controllers/__init__.py +8 -3
- pychemstation/control/controllers/devices/__init__.py +0 -0
- pychemstation/control/controllers/devices/column.py +12 -0
- pychemstation/control/controllers/devices/device.py +23 -0
- pychemstation/control/controllers/devices/pump.py +43 -0
- pychemstation/control/controllers/tables/__init__.py +0 -0
- pychemstation/control/controllers/tables/method.py +346 -0
- pychemstation/control/controllers/tables/sequence.py +199 -0
- pychemstation/control/controllers/tables/table.py +287 -0
- pychemstation/utils/injector_types.py +30 -0
- pychemstation/utils/macro.py +1 -0
- pychemstation/utils/pump_types.py +7 -0
- pychemstation/utils/sequence_types.py +3 -2
- pychemstation/utils/tray_types.py +4 -0
- {pychemstation-0.5.12.dist-info → pychemstation-0.5.13.dev2.dist-info}/METADATA +3 -2
- {pychemstation-0.5.12.dist-info → pychemstation-0.5.13.dev2.dist-info}/RECORD +20 -9
- tests/test_inj.py +38 -0
- {pychemstation-0.5.12.dist-info → pychemstation-0.5.13.dev2.dist-info}/LICENSE +0 -0
- {pychemstation-0.5.12.dist-info → pychemstation-0.5.13.dev2.dist-info}/WHEEL +0 -0
- {pychemstation-0.5.12.dist-info → pychemstation-0.5.13.dev2.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
"""
|
2
|
+
.. include:: README.md
|
3
|
+
"""
|
4
|
+
|
4
5
|
from .comm import CommunicationController
|
6
|
+
from .devices.pump import PumpController
|
7
|
+
from .devices.column import ColumnController
|
8
|
+
from .tables.method import MethodController
|
9
|
+
from .tables.sequence import SequenceController
|
File without changes
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from ....control.controllers import CommunicationController
|
2
|
+
from .device import DeviceController
|
3
|
+
from ....utils.table_types import Table
|
4
|
+
|
5
|
+
|
6
|
+
class ColumnController(DeviceController):
|
7
|
+
|
8
|
+
def __init__(self, controller: CommunicationController, table: Table):
|
9
|
+
super().__init__(controller, table)
|
10
|
+
|
11
|
+
def get_row(self, row: int):
|
12
|
+
pass
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import abc
|
2
|
+
from typing import Union
|
3
|
+
|
4
|
+
from ....control.controllers import CommunicationController
|
5
|
+
from ....control.controllers.tables.table import TableController
|
6
|
+
from ....utils.chromatogram import AgilentChannelChromatogramData
|
7
|
+
from ....utils.table_types import Table
|
8
|
+
|
9
|
+
|
10
|
+
class DeviceController(TableController, abc.ABC):
|
11
|
+
|
12
|
+
def __init__(self, controller: CommunicationController, table: Table):
|
13
|
+
super().__init__(controller, None, None, table)
|
14
|
+
|
15
|
+
@abc.abstractmethod
|
16
|
+
def get_row(self, row: int):
|
17
|
+
pass
|
18
|
+
|
19
|
+
def retrieve_recent_data_files(self):
|
20
|
+
raise NotImplementedError
|
21
|
+
|
22
|
+
def get_data(self) -> Union[list[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
|
23
|
+
raise NotImplementedError
|
@@ -0,0 +1,43 @@
|
|
1
|
+
from ....control.controllers import CommunicationController
|
2
|
+
from .device import DeviceController
|
3
|
+
from ....utils.pump_types import Pump
|
4
|
+
from ....utils.table_types import Table
|
5
|
+
|
6
|
+
|
7
|
+
class PumpController(DeviceController):
|
8
|
+
|
9
|
+
def __init__(self, controller: CommunicationController, table: Table):
|
10
|
+
super().__init__(controller, table)
|
11
|
+
self.A1 = Pump(in_use=True, solvent="A1")
|
12
|
+
self.B1 = Pump(in_use=True, solvent="B1")
|
13
|
+
self.A2 = Pump(in_use=False, solvent="A2")
|
14
|
+
self.B2 = Pump(in_use=False, solvent="B2")
|
15
|
+
|
16
|
+
def validate_pumps(self):
|
17
|
+
invalid_A_pump_usage = self.A1.in_use and self.A2.in_use
|
18
|
+
invalid_B_pump_usage = self.B1.in_use and self.B2.in_use
|
19
|
+
if invalid_A_pump_usage or invalid_B_pump_usage:
|
20
|
+
raise AttributeError
|
21
|
+
|
22
|
+
def switch_pump(self, num: int, pump: str):
|
23
|
+
if pump == "A":
|
24
|
+
if num == 1:
|
25
|
+
self.A1.in_use = True
|
26
|
+
self.A2.in_use = False
|
27
|
+
elif num == 2:
|
28
|
+
self.A1.in_use = False
|
29
|
+
self.A2.in_use = True
|
30
|
+
elif pump == "B":
|
31
|
+
if num == 1:
|
32
|
+
self.B1.in_use = True
|
33
|
+
self.B2.in_use = False
|
34
|
+
elif num == 2:
|
35
|
+
self.B1.in_use = False
|
36
|
+
self.B2.in_use = True
|
37
|
+
self.purge()
|
38
|
+
|
39
|
+
def purge(self):
|
40
|
+
pass
|
41
|
+
|
42
|
+
def get_row(self, row: int):
|
43
|
+
pass
|
File without changes
|
@@ -0,0 +1,346 @@
|
|
1
|
+
import os
|
2
|
+
import time
|
3
|
+
|
4
|
+
from xsdata.formats.dataclass.parsers import XmlParser
|
5
|
+
|
6
|
+
from .table import TableController
|
7
|
+
from ....control.controllers import CommunicationController
|
8
|
+
from ....generated import PumpMethod, DadMethod, SolventElement
|
9
|
+
from ....utils.chromatogram import TIME_FORMAT, AgilentChannelChromatogramData
|
10
|
+
from ....utils.macro import *
|
11
|
+
from ....utils.method_types import *
|
12
|
+
from ....utils.table_types import *
|
13
|
+
|
14
|
+
|
15
|
+
class MethodController(TableController):
|
16
|
+
"""
|
17
|
+
Class containing method related logic
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table):
|
21
|
+
super().__init__(controller, src, data_dir, table)
|
22
|
+
|
23
|
+
def check(self) -> str:
|
24
|
+
time.sleep(2)
|
25
|
+
self.send(Command.GET_METHOD_CMD)
|
26
|
+
time.sleep(2)
|
27
|
+
res = self.receive()
|
28
|
+
if res.is_ok():
|
29
|
+
return res.ok_value.string_response
|
30
|
+
return "ERROR"
|
31
|
+
|
32
|
+
def get_method_params(self) -> HPLCMethodParams:
|
33
|
+
return HPLCMethodParams(organic_modifier=self.controller.get_num_val(
|
34
|
+
cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
|
35
|
+
register=self.table.register,
|
36
|
+
register_flag=RegisterFlag.SOLVENT_B_COMPOSITION
|
37
|
+
)
|
38
|
+
),
|
39
|
+
flow=self.controller.get_num_val(
|
40
|
+
cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
|
41
|
+
register=self.table.register,
|
42
|
+
register_flag=RegisterFlag.FLOW
|
43
|
+
)
|
44
|
+
),
|
45
|
+
)
|
46
|
+
|
47
|
+
def get_row(self, row: int) -> TimeTableEntry:
|
48
|
+
flow = None
|
49
|
+
om = None
|
50
|
+
|
51
|
+
try:
|
52
|
+
flow = self.get_num(row, RegisterFlag.TIMETABLE_FLOW)
|
53
|
+
except RuntimeError:
|
54
|
+
pass
|
55
|
+
try:
|
56
|
+
om = self.get_num(row, RegisterFlag.TIMETABLE_SOLVENT_B_COMPOSITION)
|
57
|
+
except RuntimeError:
|
58
|
+
pass
|
59
|
+
|
60
|
+
return TimeTableEntry(start_time=self.get_num(row, RegisterFlag.TIME),
|
61
|
+
organic_modifer=om,
|
62
|
+
flow=flow)
|
63
|
+
|
64
|
+
def get_timetable(self, rows: int):
|
65
|
+
uncoalesced_timetable_rows = [self.get_row(r + 1) for r in range(rows)]
|
66
|
+
timetable_rows = {}
|
67
|
+
for row in uncoalesced_timetable_rows:
|
68
|
+
time_key = str(row.start_time)
|
69
|
+
if time_key not in timetable_rows.keys():
|
70
|
+
timetable_rows[time_key] = TimeTableEntry(start_time=row.start_time,
|
71
|
+
flow=row.flow,
|
72
|
+
organic_modifer=row.organic_modifer)
|
73
|
+
else:
|
74
|
+
if row.flow:
|
75
|
+
timetable_rows[time_key].flow = row.flow
|
76
|
+
if row.organic_modifer:
|
77
|
+
timetable_rows[time_key].organic_modifer = row.organic_modifer
|
78
|
+
entries = list(timetable_rows.values())
|
79
|
+
entries.sort(key=lambda e: e.start_time)
|
80
|
+
return entries
|
81
|
+
|
82
|
+
def load(self) -> MethodDetails:
|
83
|
+
rows = self.get_num_rows()
|
84
|
+
if rows.is_ok():
|
85
|
+
self.send(Command.GET_METHOD_CMD)
|
86
|
+
res = self.receive()
|
87
|
+
method_name = res.ok_value.string_response
|
88
|
+
timetable_rows = self.get_timetable(int(rows.ok_value.num_response))
|
89
|
+
params = self.get_method_params()
|
90
|
+
stop_time = self.controller.get_num_val(
|
91
|
+
cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
|
92
|
+
register=self.table.register,
|
93
|
+
register_flag=RegisterFlag.MAX_TIME))
|
94
|
+
post_time = self.controller.get_num_val(
|
95
|
+
cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
|
96
|
+
register=self.table.register,
|
97
|
+
register_flag=RegisterFlag.POST_TIME))
|
98
|
+
self.table_state = MethodDetails(name=method_name,
|
99
|
+
timetable=timetable_rows,
|
100
|
+
stop_time=stop_time,
|
101
|
+
post_time=post_time,
|
102
|
+
params=params)
|
103
|
+
return self.table_state
|
104
|
+
else:
|
105
|
+
raise RuntimeError(rows.err_value)
|
106
|
+
|
107
|
+
def current_method(self, method_name: str):
|
108
|
+
"""
|
109
|
+
Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
|
110
|
+
|
111
|
+
:param method_name: a Chemstation method
|
112
|
+
:return: True if method is already loaded
|
113
|
+
"""
|
114
|
+
self.send(Command.GET_METHOD_CMD)
|
115
|
+
parsed_response = self.receive()
|
116
|
+
return method_name in parsed_response
|
117
|
+
|
118
|
+
def switch(self, method_name: str):
|
119
|
+
"""
|
120
|
+
Allows the user to switch between pre-programmed methods. No need to append '.M'
|
121
|
+
to the end of the method name. For example. for the method named 'General-Poroshell.M',
|
122
|
+
only 'General-Poroshell' is needed.
|
123
|
+
|
124
|
+
:param method_name: any available method in Chemstation method directory
|
125
|
+
:raise IndexError: Response did not have expected format. Try again.
|
126
|
+
:raise AssertionError: The desired method is not selected. Try again.
|
127
|
+
"""
|
128
|
+
self.send(Command.SWITCH_METHOD_CMD.value.format(method_dir=self.src,
|
129
|
+
method_name=method_name))
|
130
|
+
|
131
|
+
time.sleep(2)
|
132
|
+
self.send(Command.GET_METHOD_CMD)
|
133
|
+
time.sleep(2)
|
134
|
+
res = self.receive()
|
135
|
+
if res.is_ok():
|
136
|
+
parsed_response = res.ok_value.string_response
|
137
|
+
assert parsed_response == f"{method_name}.M", "Switching Methods failed."
|
138
|
+
self.table_state = None
|
139
|
+
|
140
|
+
def load_from_disk(self, method_name: str) -> MethodDetails:
|
141
|
+
"""
|
142
|
+
Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
|
143
|
+
organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
|
144
|
+
only two solvents are being used.
|
145
|
+
|
146
|
+
:param method_name: name of method to load details of
|
147
|
+
:raises FileNotFoundError: Method does not exist
|
148
|
+
:return: method details
|
149
|
+
"""
|
150
|
+
method_folder = f"{method_name}.M"
|
151
|
+
method_path = os.path.join(self.src, method_folder, "AgilentPumpDriver1.RapidControl.MethodXML.xml")
|
152
|
+
dad_path = os.path.join(self.src, method_folder, "Agilent1200erDadDriver1.RapidControl.MethodXML.xml")
|
153
|
+
|
154
|
+
if os.path.exists(os.path.join(self.src, f"{method_name}.M")):
|
155
|
+
parser = XmlParser()
|
156
|
+
method = parser.parse(method_path, PumpMethod)
|
157
|
+
dad = parser.parse(dad_path, DadMethod)
|
158
|
+
|
159
|
+
organic_modifier: Optional[SolventElement] = None
|
160
|
+
aq_modifier: Optional[SolventElement] = None
|
161
|
+
|
162
|
+
if len(method.solvent_composition.solvent_element) == 2:
|
163
|
+
for solvent in method.solvent_composition.solvent_element:
|
164
|
+
if solvent.channel == "Channel_A":
|
165
|
+
aq_modifier = solvent
|
166
|
+
elif solvent.channel == "Channel_B":
|
167
|
+
organic_modifier = solvent
|
168
|
+
|
169
|
+
self.table_state = MethodDetails(name=method_name,
|
170
|
+
params=HPLCMethodParams(organic_modifier=organic_modifier.percentage,
|
171
|
+
flow=method.flow),
|
172
|
+
stop_time=method.stop_time.stop_time_value,
|
173
|
+
post_time=method.post_time.post_time_value,
|
174
|
+
timetable=[TimeTableEntry(start_time=tte.time,
|
175
|
+
organic_modifer=tte.percent_b,
|
176
|
+
flow=method.flow
|
177
|
+
) for tte in method.timetable.timetable_entry],
|
178
|
+
dad_wavelengthes=dad.signals.signal)
|
179
|
+
return self.table_state
|
180
|
+
else:
|
181
|
+
raise FileNotFoundError
|
182
|
+
|
183
|
+
def edit(self, updated_method: MethodDetails, save: bool):
|
184
|
+
"""Updated the currently loaded method in ChemStation with provided values.
|
185
|
+
|
186
|
+
:param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
|
187
|
+
:param save: if false only modifies the method, otherwise saves to disk
|
188
|
+
"""
|
189
|
+
self.table_state = updated_method
|
190
|
+
initial_organic_modifier: Param = Param(val=updated_method.params.organic_modifier,
|
191
|
+
chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
|
192
|
+
ptype=PType.NUM)
|
193
|
+
max_time: Param = Param(val=updated_method.stop_time,
|
194
|
+
chemstation_key=RegisterFlag.MAX_TIME,
|
195
|
+
ptype=PType.NUM)
|
196
|
+
post_time: Param = Param(val=updated_method.post_time,
|
197
|
+
chemstation_key=RegisterFlag.POST_TIME,
|
198
|
+
ptype=PType.NUM)
|
199
|
+
flow: Param = Param(val=updated_method.params.flow,
|
200
|
+
chemstation_key=RegisterFlag.FLOW,
|
201
|
+
ptype=PType.NUM)
|
202
|
+
|
203
|
+
# Method settings required for all runs
|
204
|
+
self.update_method_params(flow, initial_organic_modifier, max_time, post_time)
|
205
|
+
self._update_method_timetable(updated_method.timetable)
|
206
|
+
|
207
|
+
if save:
|
208
|
+
self.send(Command.SAVE_METHOD_CMD.value.format(
|
209
|
+
commit_msg=f"saved method at {str(time.time())}"
|
210
|
+
))
|
211
|
+
|
212
|
+
def update_method_params(self, flow, initial_organic_modifier, max_time, post_time):
|
213
|
+
self.delete_table()
|
214
|
+
self._update_param(initial_organic_modifier)
|
215
|
+
self._update_param(flow)
|
216
|
+
if self.table_state.stop_time:
|
217
|
+
self._update_param(Param(val="Set", chemstation_key=RegisterFlag.STOPTIME_MODE, ptype=PType.STR))
|
218
|
+
self._update_param(max_time)
|
219
|
+
else:
|
220
|
+
self._update_param(Param(val="Off", chemstation_key=RegisterFlag.STOPTIME_MODE, ptype=PType.STR))
|
221
|
+
if self.table_state.post_time:
|
222
|
+
self._update_param(Param(val="Set", chemstation_key=RegisterFlag.POSTIME_MODE, ptype=PType.STR))
|
223
|
+
self._update_param(post_time)
|
224
|
+
else:
|
225
|
+
self._update_param(Param(val="Off", chemstation_key=RegisterFlag.POSTIME_MODE, ptype=PType.STR))
|
226
|
+
self.download()
|
227
|
+
|
228
|
+
def _update_param(self, method_param: Param):
|
229
|
+
"""Change a method parameter, changes what is visibly seen in Chemstation GUI.
|
230
|
+
(changes the first row in the timetable)
|
231
|
+
|
232
|
+
:param method_param: a parameter to update for currently loaded method.
|
233
|
+
"""
|
234
|
+
register = self.table.register
|
235
|
+
setting_command = TableOperation.UPDATE_OBJ_HDR_VAL if method_param.ptype == PType.NUM else TableOperation.UPDATE_OBJ_HDR_TEXT
|
236
|
+
if isinstance(method_param.chemstation_key, list):
|
237
|
+
for register_flag in method_param.chemstation_key:
|
238
|
+
self.send(setting_command.value.format(register=register,
|
239
|
+
register_flag=register_flag,
|
240
|
+
val=method_param.val))
|
241
|
+
else:
|
242
|
+
self.send(setting_command.value.format(register=register,
|
243
|
+
register_flag=method_param.chemstation_key,
|
244
|
+
val=method_param.val))
|
245
|
+
time.sleep(2)
|
246
|
+
|
247
|
+
def download(self):
|
248
|
+
self.send('Sleep 1')
|
249
|
+
self.sleepy_send("DownloadRCMethod PMP1")
|
250
|
+
self.send('Sleep 1')
|
251
|
+
|
252
|
+
def edit_row(self, row: TimeTableEntry, first_row: bool = False):
|
253
|
+
if first_row:
|
254
|
+
if row.organic_modifer:
|
255
|
+
self.add_row()
|
256
|
+
self.add_new_col_text(col_name=RegisterFlag.FUNCTION, val=RegisterFlag.SOLVENT_COMPOSITION.value)
|
257
|
+
self.add_new_col_num(col_name=RegisterFlag.TIME, val=row.start_time)
|
258
|
+
self.add_new_col_num(col_name=RegisterFlag.TIMETABLE_SOLVENT_B_COMPOSITION, val=row.organic_modifer)
|
259
|
+
if row.flow:
|
260
|
+
self.add_row()
|
261
|
+
self.get_num_rows()
|
262
|
+
self.edit_row_text(col_name=RegisterFlag.FUNCTION, val=RegisterFlag.FLOW.value)
|
263
|
+
self.add_new_col_num(col_name=RegisterFlag.TIMETABLE_FLOW, val=row.flow)
|
264
|
+
self.edit_row_num(col_name=RegisterFlag.TIMETABLE_FLOW, val=row.flow)
|
265
|
+
self.download()
|
266
|
+
else:
|
267
|
+
if row.organic_modifer:
|
268
|
+
self.add_row()
|
269
|
+
self.get_num_rows()
|
270
|
+
self.edit_row_text(col_name=RegisterFlag.FUNCTION, val=RegisterFlag.SOLVENT_COMPOSITION.value)
|
271
|
+
self.edit_row_num(col_name=RegisterFlag.TIME, val=row.start_time)
|
272
|
+
self.edit_row_num(col_name=RegisterFlag.TIMETABLE_SOLVENT_B_COMPOSITION, val=row.organic_modifer)
|
273
|
+
self.download()
|
274
|
+
if row.flow:
|
275
|
+
self.add_row()
|
276
|
+
self.get_num_rows()
|
277
|
+
self.edit_row_text(col_name=RegisterFlag.FUNCTION, val=RegisterFlag.FLOW.value)
|
278
|
+
self.edit_row_num(col_name=RegisterFlag.TIMETABLE_FLOW, val=row.flow)
|
279
|
+
self.edit_row_num(col_name=RegisterFlag.TIME, val=row.start_time)
|
280
|
+
self.download()
|
281
|
+
|
282
|
+
def _update_method_timetable(self, timetable_rows: list[TimeTableEntry]):
|
283
|
+
self.get_num_rows()
|
284
|
+
|
285
|
+
self.delete_table()
|
286
|
+
res = self.get_num_rows()
|
287
|
+
while not res.is_err():
|
288
|
+
self.delete_table()
|
289
|
+
res = self.get_num_rows()
|
290
|
+
|
291
|
+
self.new_table()
|
292
|
+
self.get_num_rows()
|
293
|
+
|
294
|
+
for i, row in enumerate(timetable_rows):
|
295
|
+
self.edit_row(row=row, first_row=i == 0)
|
296
|
+
|
297
|
+
def stop(self):
|
298
|
+
"""
|
299
|
+
Stops the method run. A dialog window will pop up and manual intervention may be required.\
|
300
|
+
"""
|
301
|
+
self.send(Command.STOP_METHOD_CMD)
|
302
|
+
|
303
|
+
def run(self, experiment_name: str, stall_while_running: bool = True):
|
304
|
+
"""
|
305
|
+
This is the preferred method to trigger a run.
|
306
|
+
Starts the currently selected method, storing data
|
307
|
+
under the <data_dir>/<experiment_name>.D folder.
|
308
|
+
The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
|
309
|
+
Device must be ready.
|
310
|
+
|
311
|
+
:param experiment_name: Name of the experiment
|
312
|
+
"""
|
313
|
+
if not self.table_state:
|
314
|
+
self.table_state = self.load()
|
315
|
+
|
316
|
+
folder_name = ""
|
317
|
+
hplc_is_running = self.check_hplc_is_running()
|
318
|
+
while not hplc_is_running:
|
319
|
+
timestamp = time.strftime(TIME_FORMAT)
|
320
|
+
self.send(Command.RUN_METHOD_CMD.value.format(data_dir=self.data_dir,
|
321
|
+
experiment_name=experiment_name,
|
322
|
+
timestamp=timestamp))
|
323
|
+
folder_name = f"{experiment_name}_{timestamp}.D"
|
324
|
+
hplc_is_running = self.check_hplc_is_running()
|
325
|
+
|
326
|
+
self.data_files.append(os.path.join(self.data_dir, folder_name))
|
327
|
+
|
328
|
+
if stall_while_running:
|
329
|
+
run_completed = self.check_hplc_done_running(method=self.table_state)
|
330
|
+
if run_completed.is_ok():
|
331
|
+
self.data_files[-1] = run_completed.ok_value
|
332
|
+
else:
|
333
|
+
raise RuntimeError("Run error has occurred.")
|
334
|
+
else:
|
335
|
+
self.data_files[-1].dir = self.fuzzy_match_most_recent_folder(folder_name).ok_value
|
336
|
+
|
337
|
+
def retrieve_recent_data_files(self) -> str:
|
338
|
+
return self.data_files[-1]
|
339
|
+
|
340
|
+
def get_data(self, custom_path: Optional[str] = None,
|
341
|
+
read_uv: bool = False) -> AgilentChannelChromatogramData:
|
342
|
+
if not custom_path:
|
343
|
+
self.get_spectrum(self.data_files[-1], read_uv)
|
344
|
+
else:
|
345
|
+
self.get_spectrum(custom_path, read_uv)
|
346
|
+
return AgilentChannelChromatogramData(**self.spectra) if not read_uv else self.uv
|
@@ -0,0 +1,199 @@
|
|
1
|
+
import os
|
2
|
+
import time
|
3
|
+
from typing import Optional, Union
|
4
|
+
|
5
|
+
from .table import TableController, ChromData
|
6
|
+
from ....control.controllers.comm import CommunicationController
|
7
|
+
from ....utils.chromatogram import SEQUENCE_TIME_FORMAT, AgilentChannelChromatogramData
|
8
|
+
from ....utils.macro import Command
|
9
|
+
from ....utils.sequence_types import SequenceTable, SequenceEntry, SequenceDataFiles, InjectionSource, SampleType
|
10
|
+
from ....utils.table_types import RegisterFlag, Table
|
11
|
+
from ....utils.tray_types import TenVialColumn, FiftyFourVialPlate
|
12
|
+
|
13
|
+
|
14
|
+
class SequenceController(TableController):
|
15
|
+
"""
|
16
|
+
Class containing sequence related logic
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table, method_dir: str):
|
20
|
+
self.method_dir = method_dir
|
21
|
+
super().__init__(controller, src, data_dir, table)
|
22
|
+
|
23
|
+
def load(self) -> SequenceTable:
|
24
|
+
rows = self.get_num_rows()
|
25
|
+
self.send(Command.GET_SEQUENCE_CMD)
|
26
|
+
seq_name = self.receive()
|
27
|
+
|
28
|
+
if rows.is_ok() and seq_name.is_ok():
|
29
|
+
self.table_state = SequenceTable(
|
30
|
+
name=seq_name.ok_value.string_response.partition(".S")[0],
|
31
|
+
rows=[self.get_row(r + 1) for r in range(int(rows.ok_value.num_response))])
|
32
|
+
return self.table_state
|
33
|
+
raise RuntimeError(rows.err_value)
|
34
|
+
|
35
|
+
def get_row(self, row: int) -> SequenceEntry:
|
36
|
+
sample_name = self.get_text(row, RegisterFlag.NAME)
|
37
|
+
vial_location = int(self.get_num(row, RegisterFlag.VIAL_LOCATION))
|
38
|
+
method = self.get_text(row, RegisterFlag.METHOD)
|
39
|
+
num_inj = int(self.get_num(row, RegisterFlag.NUM_INJ))
|
40
|
+
inj_vol = int(self.get_text(row, RegisterFlag.INJ_VOL))
|
41
|
+
inj_source = InjectionSource(self.get_text(row, RegisterFlag.INJ_SOR))
|
42
|
+
sample_type = SampleType(self.get_num(row, RegisterFlag.SAMPLE_TYPE))
|
43
|
+
return SequenceEntry(sample_name=sample_name,
|
44
|
+
vial_location=vial_location,
|
45
|
+
method=None if len(method) == 0 else method,
|
46
|
+
num_inj=num_inj,
|
47
|
+
inj_vol=inj_vol,
|
48
|
+
inj_source=inj_source,
|
49
|
+
sample_type=sample_type)
|
50
|
+
|
51
|
+
def check(self) -> str:
|
52
|
+
time.sleep(2)
|
53
|
+
self.send(Command.GET_SEQUENCE_CMD)
|
54
|
+
time.sleep(2)
|
55
|
+
res = self.receive()
|
56
|
+
if res.is_ok():
|
57
|
+
return res.ok_value.string_response
|
58
|
+
return "ERROR"
|
59
|
+
|
60
|
+
def switch(self, seq_name: str):
|
61
|
+
"""
|
62
|
+
Switch to the specified sequence. The sequence name does not need the '.S' extension.
|
63
|
+
|
64
|
+
:param seq_name: The name of the sequence file
|
65
|
+
"""
|
66
|
+
self.send(f'_SeqFile$ = "{seq_name}.S"')
|
67
|
+
self.send(f'_SeqPath$ = "{self.src}"')
|
68
|
+
self.send(Command.SWITCH_SEQUENCE_CMD)
|
69
|
+
time.sleep(2)
|
70
|
+
self.send(Command.GET_SEQUENCE_CMD)
|
71
|
+
time.sleep(2)
|
72
|
+
parsed_response = self.receive().value.string_response
|
73
|
+
|
74
|
+
assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
|
75
|
+
self.table_state = None
|
76
|
+
|
77
|
+
def edit(self, sequence_table: SequenceTable):
|
78
|
+
"""
|
79
|
+
Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
|
80
|
+
If you would only like to edit a single row of a sequence table, use `edit_sequence_table_row` instead.
|
81
|
+
|
82
|
+
:param sequence_table:
|
83
|
+
"""
|
84
|
+
self.table_state = sequence_table
|
85
|
+
rows = self.get_num_rows()
|
86
|
+
if rows.is_ok():
|
87
|
+
existing_row_num = rows.value.num_response
|
88
|
+
wanted_row_num = len(sequence_table.rows)
|
89
|
+
while existing_row_num != wanted_row_num:
|
90
|
+
if wanted_row_num > existing_row_num:
|
91
|
+
self.add_row()
|
92
|
+
elif wanted_row_num < existing_row_num:
|
93
|
+
self.delete_row(int(existing_row_num))
|
94
|
+
self.send(Command.SAVE_SEQUENCE_CMD)
|
95
|
+
existing_row_num = self.get_num_rows().ok_value.num_response
|
96
|
+
self.send(Command.SWITCH_SEQUENCE_CMD)
|
97
|
+
|
98
|
+
for i, row in enumerate(sequence_table.rows):
|
99
|
+
self.edit_row(row=row, row_num=i + 1)
|
100
|
+
self.sleep(1)
|
101
|
+
self.send(Command.SAVE_SEQUENCE_CMD)
|
102
|
+
self.send(Command.SWITCH_SEQUENCE_CMD)
|
103
|
+
|
104
|
+
def edit_row(self, row: SequenceEntry, row_num: int):
|
105
|
+
"""
|
106
|
+
Edits a row in the sequence table. If a row does NOT exist, a new one will be created.
|
107
|
+
|
108
|
+
:param row: sequence row entry with updated information
|
109
|
+
:param row_num: the row to edit, based on 1-based indexing
|
110
|
+
"""
|
111
|
+
num_rows = self.get_num_rows()
|
112
|
+
if num_rows.is_ok():
|
113
|
+
while num_rows.ok_value.num_response < row_num:
|
114
|
+
self.add_row()
|
115
|
+
self.send(Command.SAVE_SEQUENCE_CMD)
|
116
|
+
num_rows = self.get_num_rows()
|
117
|
+
|
118
|
+
if row.vial_location:
|
119
|
+
loc = row.vial_location
|
120
|
+
if isinstance(loc, TenVialColumn):
|
121
|
+
loc = row.vial_location.value
|
122
|
+
elif isinstance(loc, FiftyFourVialPlate):
|
123
|
+
loc = row.vial_location.value()
|
124
|
+
self.edit_row_num(row=row_num, col_name=RegisterFlag.VIAL_LOCATION, val=loc)
|
125
|
+
|
126
|
+
if row.method:
|
127
|
+
possible_path = os.path.join(self.method_dir, row.method) + ".M\\"
|
128
|
+
method = row.method
|
129
|
+
if os.path.exists(possible_path):
|
130
|
+
method = os.path.join(self.method_dir, row.method)
|
131
|
+
self.edit_row_text(row=row_num, col_name=RegisterFlag.METHOD, val=method)
|
132
|
+
|
133
|
+
if row.num_inj:
|
134
|
+
self.edit_row_num(row=row_num, col_name=RegisterFlag.NUM_INJ, val=row.num_inj)
|
135
|
+
|
136
|
+
if row.inj_vol:
|
137
|
+
self.edit_row_text(row=row_num, col_name=RegisterFlag.INJ_VOL, val=row.inj_vol)
|
138
|
+
|
139
|
+
if row.inj_source:
|
140
|
+
self.edit_row_text(row=row_num, col_name=RegisterFlag.INJ_SOR, val=row.inj_source.value)
|
141
|
+
|
142
|
+
if row.sample_name:
|
143
|
+
self.edit_row_text(row=row_num, col_name=RegisterFlag.NAME, val=row.sample_name)
|
144
|
+
if row.data_file:
|
145
|
+
self.edit_row_text(row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.data_file)
|
146
|
+
else:
|
147
|
+
self.edit_row_text(row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.sample_name)
|
148
|
+
|
149
|
+
if row.sample_type:
|
150
|
+
self.edit_row_num(row=row_num, col_name=RegisterFlag.SAMPLE_TYPE, val=row.sample_type.value)
|
151
|
+
|
152
|
+
self.send(Command.SAVE_SEQUENCE_CMD)
|
153
|
+
|
154
|
+
def run(self, stall_while_running: bool = True):
|
155
|
+
"""
|
156
|
+
Starts the currently loaded sequence, storing data
|
157
|
+
under the <data_dir>/<sequence table name> folder.
|
158
|
+
Device must be ready.
|
159
|
+
"""
|
160
|
+
if not self.table_state:
|
161
|
+
self.table_state = self.load()
|
162
|
+
|
163
|
+
timestamp = time.strftime(SEQUENCE_TIME_FORMAT)
|
164
|
+
self.send(Command.RUN_SEQUENCE_CMD.value)
|
165
|
+
|
166
|
+
if self.check_hplc_is_running():
|
167
|
+
folder_name = f"{self.table_state.name} {timestamp}"
|
168
|
+
self.data_files.append(SequenceDataFiles(dir=folder_name,
|
169
|
+
sequence_name=self.table_state.name))
|
170
|
+
|
171
|
+
if stall_while_running:
|
172
|
+
run_completed = self.check_hplc_done_running(sequence=self.table_state)
|
173
|
+
if run_completed.is_ok():
|
174
|
+
self.data_files[-1].dir = run_completed.value
|
175
|
+
else:
|
176
|
+
raise RuntimeError("Run error has occurred.")
|
177
|
+
else:
|
178
|
+
self.data_files[-1].dir = self.fuzzy_match_most_recent_folder(folder_name).ok_value
|
179
|
+
|
180
|
+
def retrieve_recent_data_files(self) -> str:
|
181
|
+
sequence_data_files: SequenceDataFiles = self.data_files[-1]
|
182
|
+
return sequence_data_files.dir
|
183
|
+
|
184
|
+
def get_data(self, custom_path: Optional[str] = None,
|
185
|
+
read_uv: bool = False) -> list[AgilentChannelChromatogramData]:
|
186
|
+
parent_dir = self.data_files[-1].dir if not custom_path else custom_path
|
187
|
+
subdirs = [x[0] for x in os.walk(self.data_dir)]
|
188
|
+
potential_folders = sorted(list(filter(lambda d: parent_dir in d, subdirs)))
|
189
|
+
self.data_files[-1].child_dirs = [f for f in potential_folders if
|
190
|
+
parent_dir in f and ".M" not in f and ".D" in f]
|
191
|
+
|
192
|
+
spectra: list[Union[AgilentChannelChromatogramData, ChromData]] = []
|
193
|
+
all_w_spectra: list[Union[AgilentChannelChromatogramData, ChromData]] = []
|
194
|
+
for row in self.data_files[-1].child_dirs:
|
195
|
+
self.get_spectrum(row, read_uv)
|
196
|
+
spectra.append(AgilentChannelChromatogramData(**self.spectra))
|
197
|
+
all_w_spectra.append(self.uv)
|
198
|
+
|
199
|
+
return spectra if not read_uv else all_w_spectra
|
@@ -0,0 +1,287 @@
|
|
1
|
+
"""
|
2
|
+
Abstract module containing shared logic for Method and Sequence tables.
|
3
|
+
|
4
|
+
Authors: Lucy Hao
|
5
|
+
"""
|
6
|
+
|
7
|
+
import abc
|
8
|
+
import os
|
9
|
+
from dataclasses import dataclass
|
10
|
+
from typing import Union, Optional
|
11
|
+
|
12
|
+
import numpy as np
|
13
|
+
import polling
|
14
|
+
from rainbow import DataFile
|
15
|
+
from result import Result, Ok, Err
|
16
|
+
import pandas as pd
|
17
|
+
import rainbow as rb
|
18
|
+
|
19
|
+
from ....control.controllers.comm import CommunicationController
|
20
|
+
from ....utils.chromatogram import AgilentHPLCChromatogram, AgilentChannelChromatogramData
|
21
|
+
from ....utils.macro import Command, HPLCRunningStatus, Response
|
22
|
+
from ....utils.method_types import MethodDetails
|
23
|
+
from ....utils.sequence_types import SequenceDataFiles, SequenceTable
|
24
|
+
from ....utils.table_types import Table, TableOperation, RegisterFlag
|
25
|
+
|
26
|
+
TableType = Union[MethodDetails, SequenceTable]
|
27
|
+
|
28
|
+
|
29
|
+
@dataclass
|
30
|
+
class ChromData:
|
31
|
+
x: np.array
|
32
|
+
y: np.array
|
33
|
+
|
34
|
+
|
35
|
+
class TableController(abc.ABC):
|
36
|
+
|
37
|
+
def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table):
|
38
|
+
self.controller = controller
|
39
|
+
self.table = table
|
40
|
+
self.table_state: Optional[TableType] = None
|
41
|
+
|
42
|
+
if os.path.isdir(src):
|
43
|
+
self.src: str = src
|
44
|
+
else:
|
45
|
+
raise FileNotFoundError(f"dir: {src} not found.")
|
46
|
+
|
47
|
+
if os.path.isdir(data_dir):
|
48
|
+
self.data_dir: str = data_dir
|
49
|
+
else:
|
50
|
+
raise FileNotFoundError(f"dir: {data_dir} not found.")
|
51
|
+
|
52
|
+
self.spectra: dict[str, Optional[AgilentHPLCChromatogram]] = {
|
53
|
+
"A": AgilentHPLCChromatogram(self.data_dir),
|
54
|
+
"B": AgilentHPLCChromatogram(self.data_dir),
|
55
|
+
"C": AgilentHPLCChromatogram(self.data_dir),
|
56
|
+
"D": AgilentHPLCChromatogram(self.data_dir),
|
57
|
+
"E": AgilentHPLCChromatogram(self.data_dir),
|
58
|
+
"F": AgilentHPLCChromatogram(self.data_dir),
|
59
|
+
"G": AgilentHPLCChromatogram(self.data_dir),
|
60
|
+
"H": AgilentHPLCChromatogram(self.data_dir),
|
61
|
+
}
|
62
|
+
|
63
|
+
self.data_files: Union[list[SequenceDataFiles], list[str]] = []
|
64
|
+
|
65
|
+
self.uv = None
|
66
|
+
|
67
|
+
# Initialize row counter for table operations
|
68
|
+
self.send('Local Rows')
|
69
|
+
|
70
|
+
def receive(self) -> Result[Response, str]:
|
71
|
+
for _ in range(10):
|
72
|
+
try:
|
73
|
+
return self.controller.receive()
|
74
|
+
except IndexError:
|
75
|
+
continue
|
76
|
+
return Err("Could not parse response")
|
77
|
+
|
78
|
+
def send(self, cmd: Union[Command, str]):
|
79
|
+
if not self.controller:
|
80
|
+
raise RuntimeError(
|
81
|
+
"Communication controller must be initialized before sending command. It is currently in offline mode.")
|
82
|
+
self.controller.send(cmd)
|
83
|
+
|
84
|
+
def sleepy_send(self, cmd: Union[Command, str]):
|
85
|
+
self.controller.sleepy_send(cmd)
|
86
|
+
|
87
|
+
def sleep(self, seconds: int):
|
88
|
+
"""
|
89
|
+
Tells the HPLC to wait for a specified number of seconds.
|
90
|
+
|
91
|
+
:param seconds: number of seconds to wait
|
92
|
+
"""
|
93
|
+
self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
|
94
|
+
|
95
|
+
def get_num(self, row: int, col_name: RegisterFlag) -> float:
|
96
|
+
return self.controller.get_num_val(TableOperation.GET_ROW_VAL.value.format(register=self.table.register,
|
97
|
+
table_name=self.table.name,
|
98
|
+
row=row,
|
99
|
+
col_name=col_name.value))
|
100
|
+
|
101
|
+
def get_text(self, row: int, col_name: RegisterFlag) -> str:
|
102
|
+
return self.controller.get_text_val(TableOperation.GET_ROW_TEXT.value.format(register=self.table.register,
|
103
|
+
table_name=self.table.name,
|
104
|
+
row=row,
|
105
|
+
col_name=col_name.value))
|
106
|
+
|
107
|
+
def add_new_col_num(self,
|
108
|
+
col_name: RegisterFlag,
|
109
|
+
val: Union[int, float]):
|
110
|
+
self.sleepy_send(TableOperation.NEW_COL_VAL.value.format(
|
111
|
+
register=self.table.register,
|
112
|
+
table_name=self.table.name,
|
113
|
+
col_name=col_name,
|
114
|
+
val=val))
|
115
|
+
|
116
|
+
def add_new_col_text(self,
|
117
|
+
col_name: RegisterFlag,
|
118
|
+
val: str):
|
119
|
+
self.sleepy_send(TableOperation.NEW_COL_TEXT.value.format(
|
120
|
+
register=self.table.register,
|
121
|
+
table_name=self.table.name,
|
122
|
+
col_name=col_name,
|
123
|
+
val=val))
|
124
|
+
|
125
|
+
def edit_row_num(self,
|
126
|
+
col_name: RegisterFlag,
|
127
|
+
val: Union[int, float],
|
128
|
+
row: Optional[int] = None):
|
129
|
+
self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(
|
130
|
+
register=self.table.register,
|
131
|
+
table_name=self.table.name,
|
132
|
+
row=row if row is not None else 'Rows',
|
133
|
+
col_name=col_name,
|
134
|
+
val=val))
|
135
|
+
|
136
|
+
def edit_row_text(self,
|
137
|
+
col_name: RegisterFlag,
|
138
|
+
val: str,
|
139
|
+
row: Optional[int] = None):
|
140
|
+
self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(
|
141
|
+
register=self.table.register,
|
142
|
+
table_name=self.table.name,
|
143
|
+
row=row if row is not None else 'Rows',
|
144
|
+
col_name=col_name,
|
145
|
+
val=val))
|
146
|
+
|
147
|
+
@abc.abstractmethod
|
148
|
+
def get_row(self, row: int):
|
149
|
+
pass
|
150
|
+
|
151
|
+
def delete_row(self, row: int):
|
152
|
+
self.sleepy_send(TableOperation.DELETE_ROW.value.format(register=self.table.register,
|
153
|
+
table_name=self.table.name,
|
154
|
+
row=row))
|
155
|
+
|
156
|
+
def add_row(self):
|
157
|
+
"""
|
158
|
+
Adds a row to the provided table for currently loaded method or sequence.
|
159
|
+
Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
|
160
|
+
You can also provide your own table.
|
161
|
+
|
162
|
+
:param table: the table to add a new row to
|
163
|
+
"""
|
164
|
+
self.sleepy_send(TableOperation.NEW_ROW.value.format(register=self.table.register,
|
165
|
+
table_name=self.table.name))
|
166
|
+
|
167
|
+
def delete_table(self):
|
168
|
+
"""
|
169
|
+
Deletes the table for the current loaded method or sequence.
|
170
|
+
Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
|
171
|
+
You can also provide your own table.
|
172
|
+
|
173
|
+
:param table: the table to delete
|
174
|
+
"""
|
175
|
+
self.sleepy_send(TableOperation.DELETE_TABLE.value.format(register=self.table.register,
|
176
|
+
table_name=self.table.name))
|
177
|
+
|
178
|
+
def new_table(self):
|
179
|
+
"""
|
180
|
+
Creates the table for the currently loaded method or sequence. Import either the SEQUENCE_TABLE or
|
181
|
+
METHOD_TIMETABLE from hein_analytical_control.constants. You can also provide your own table.
|
182
|
+
|
183
|
+
:param table: the table to create
|
184
|
+
"""
|
185
|
+
self.send(TableOperation.CREATE_TABLE.value.format(register=self.table.register,
|
186
|
+
table_name=self.table.name))
|
187
|
+
|
188
|
+
def get_num_rows(self) -> Result[Response, str]:
|
189
|
+
self.send(TableOperation.GET_NUM_ROWS.value.format(register=self.table.register,
|
190
|
+
table_name=self.table.name,
|
191
|
+
col_name=RegisterFlag.NUM_ROWS))
|
192
|
+
self.send(Command.GET_ROWS_CMD.value.format(register=self.table.register,
|
193
|
+
table_name=self.table.name,
|
194
|
+
col_name=RegisterFlag.NUM_ROWS))
|
195
|
+
res = self.controller.receive()
|
196
|
+
|
197
|
+
if res.is_ok():
|
198
|
+
self.send("Sleep 0.1")
|
199
|
+
self.send('Print Rows')
|
200
|
+
return res
|
201
|
+
else:
|
202
|
+
return Err("No rows could be read.")
|
203
|
+
|
204
|
+
def check_hplc_is_running(self) -> bool:
|
205
|
+
started_running = polling.poll(
|
206
|
+
lambda: isinstance(self.controller.get_status(), HPLCRunningStatus),
|
207
|
+
step=5,
|
208
|
+
max_tries=40)
|
209
|
+
return started_running
|
210
|
+
|
211
|
+
def check_hplc_done_running(self,
|
212
|
+
method: Optional[MethodDetails] = None,
|
213
|
+
sequence: Optional[SequenceTable] = None) -> Result[str, str]:
|
214
|
+
"""
|
215
|
+
Checks if ChemStation has finished running and can read data back
|
216
|
+
|
217
|
+
:param method: if you are running a method and want to read back data, the timeout period will be adjusted to be longer than the method's runtime
|
218
|
+
:param sequence: if you are running a sequence and want to read back data, the timeout period will be adjusted to be longer than the sequence's runtime
|
219
|
+
:return: Return True if data can be read back, else False.
|
220
|
+
"""
|
221
|
+
timeout = 10 * 60
|
222
|
+
if method:
|
223
|
+
timeout = ((method.stop_time + method.post_time + 3) * 60)
|
224
|
+
if sequence:
|
225
|
+
timeout *= len(sequence.rows)
|
226
|
+
|
227
|
+
most_recent_folder = self.retrieve_recent_data_files()
|
228
|
+
finished_run = polling.poll(
|
229
|
+
lambda: self.controller.check_if_running(),
|
230
|
+
timeout=timeout,
|
231
|
+
step=50)
|
232
|
+
|
233
|
+
check_folder = self.fuzzy_match_most_recent_folder(most_recent_folder)
|
234
|
+
if check_folder.is_ok() and finished_run:
|
235
|
+
return check_folder
|
236
|
+
elif check_folder.is_ok():
|
237
|
+
finished_run = polling.poll(
|
238
|
+
lambda: self.controller.check_if_running(),
|
239
|
+
timeout=timeout,
|
240
|
+
step=50)
|
241
|
+
if finished_run:
|
242
|
+
return check_folder
|
243
|
+
return check_folder
|
244
|
+
else:
|
245
|
+
return Err("Run did not complete as expected")
|
246
|
+
|
247
|
+
def fuzzy_match_most_recent_folder(self, most_recent_folder) -> Result[str, str]:
|
248
|
+
if os.path.exists(most_recent_folder):
|
249
|
+
return Ok(most_recent_folder)
|
250
|
+
|
251
|
+
subdirs = [x[0] for x in os.walk(self.data_dir)]
|
252
|
+
potential_folders = sorted(list(filter(lambda d: most_recent_folder in d, subdirs)))
|
253
|
+
parent_dirs = []
|
254
|
+
for folder in potential_folders:
|
255
|
+
path = os.path.normpath(folder)
|
256
|
+
split_folder = path.split(os.sep)
|
257
|
+
if most_recent_folder in split_folder[-1]:
|
258
|
+
parent_dirs.append(folder)
|
259
|
+
parent_dir = sorted(parent_dirs, reverse=True)[0]
|
260
|
+
return Ok(parent_dir)
|
261
|
+
|
262
|
+
@abc.abstractmethod
|
263
|
+
def retrieve_recent_data_files(self):
|
264
|
+
pass
|
265
|
+
|
266
|
+
@abc.abstractmethod
|
267
|
+
def get_data(self) -> Union[list[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
|
268
|
+
pass
|
269
|
+
|
270
|
+
def get_uv_spectrum(self, path: str):
|
271
|
+
data_uv: DataFile = rb.agilent.chemstation.parse_file(os.path.join(path, "DAD1.UV"))
|
272
|
+
zipped_data = zip(data_uv.ylabels, data_uv.data)
|
273
|
+
self.uv = {str(w_a[0]): ChromData(x=data_uv.xlabels, y=w_a[1]) for w_a in zipped_data}
|
274
|
+
|
275
|
+
def get_spectrum(self, data_path: str, read_uv: bool = False):
|
276
|
+
"""
|
277
|
+
Load chromatogram for any channel in spectra dictionary.
|
278
|
+
"""
|
279
|
+
if read_uv:
|
280
|
+
self.get_uv_spectrum(data_path)
|
281
|
+
|
282
|
+
for channel, spec in self.spectra.items():
|
283
|
+
try:
|
284
|
+
spec.load_spectrum(data_path=data_path, channel=channel)
|
285
|
+
except FileNotFoundError:
|
286
|
+
self.spectra[channel] = None
|
287
|
+
print(f"No data at channel: {channel}")
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from pychemstation.utils.tray_types import Tray
|
5
|
+
|
6
|
+
|
7
|
+
@dataclass
|
8
|
+
class Draw:
|
9
|
+
amount: float
|
10
|
+
source: Tray
|
11
|
+
speed: Any
|
12
|
+
offset: Any
|
13
|
+
|
14
|
+
|
15
|
+
@dataclass
|
16
|
+
class Wait:
|
17
|
+
time: int
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class Inject:
|
22
|
+
pass
|
23
|
+
|
24
|
+
|
25
|
+
InjectorFunction = [Draw, Wait, Inject]
|
26
|
+
|
27
|
+
|
28
|
+
@dataclass
|
29
|
+
class InjectorTable:
|
30
|
+
functions: list[InjectorFunction]
|
pychemstation/utils/macro.py
CHANGED
@@ -34,6 +34,7 @@ class Command(Enum):
|
|
34
34
|
INSTRUMENT_OFF = 'macro "SHUTDOWN.MAC" ,go'
|
35
35
|
INSTRUMENT_ON = 'LIDoOperation "TURN_ON"'
|
36
36
|
|
37
|
+
# Method and Sequence Related
|
37
38
|
GET_METHOD_CMD = "response$ = _MethFile$"
|
38
39
|
GET_ROWS_CMD = 'response_num = TabHdrVal({register}, "{table_name}", "{col_name}")'
|
39
40
|
SWITCH_METHOD_CMD = 'LoadMethod "{method_dir}", "{method_name}.M"'
|
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|
4
4
|
from enum import Enum
|
5
5
|
from typing import Optional, Union
|
6
6
|
|
7
|
-
from pychemstation.utils.tray_types import TenVialColumn
|
7
|
+
from pychemstation.utils.tray_types import TenVialColumn, Tray
|
8
8
|
|
9
9
|
|
10
10
|
@dataclass
|
@@ -39,7 +39,8 @@ class InjectionSource(Enum):
|
|
39
39
|
@dataclass
|
40
40
|
class SequenceEntry:
|
41
41
|
sample_name: str
|
42
|
-
vial_location:
|
42
|
+
vial_location: Tray
|
43
|
+
data_file: Optional[str] = None
|
43
44
|
method: Optional[str] = None
|
44
45
|
num_inj: Optional[int] = 1
|
45
46
|
inj_vol: Optional[int] = 2
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
from enum import Enum
|
5
|
+
from typing import Union
|
5
6
|
|
6
7
|
|
7
8
|
class Num(Enum):
|
@@ -50,3 +51,6 @@ class TenVialColumn(Enum):
|
|
50
51
|
EIGHT = 8
|
51
52
|
NINE = 9
|
52
53
|
TEN = 10
|
54
|
+
|
55
|
+
|
56
|
+
Tray = Union[FiftyFourVialPlate, TenVialColumn]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pychemstation
|
3
|
-
Version: 0.5.
|
3
|
+
Version: 0.5.13.dev2
|
4
4
|
Summary: Library to interact with Chemstation software, primarily used in Hein lab
|
5
5
|
Home-page: https://gitlab.com/heingroup/device-api/pychemstation
|
6
6
|
Author: Lucy Hao
|
@@ -14,6 +14,7 @@ Requires-Dist: polling
|
|
14
14
|
Requires-Dist: seabreeze
|
15
15
|
Requires-Dist: xsdata
|
16
16
|
Requires-Dist: result
|
17
|
+
Requires-Dist: rainbow
|
17
18
|
|
18
19
|
# Agilent HPLC Macro Control
|
19
20
|
|
@@ -99,7 +100,7 @@ our [GitLab](https://gitlab.com/heingroup/device-api/pychemstation)!
|
|
99
100
|
|
100
101
|
## Authors and Acknowledgements
|
101
102
|
|
102
|
-
Lucy Hao
|
103
|
+
Lucy Hao, Maria Politi
|
103
104
|
|
104
105
|
- Adapted from [**AnalyticalLabware**](https://github.com/croningp/analyticallabware), created by members in the Cronin
|
105
106
|
Group. Copyright © Cronin Group, used under the [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) license.
|
@@ -6,11 +6,19 @@ pychemstation/analysis/utils.py,sha256=ISupAOb_yqA4_DZRK9v18UL-XjUQccAicIJKb1VMn
|
|
6
6
|
pychemstation/control/__init__.py,sha256=4xTy8X-mkn_PPZKr7w9rnj1wZhtmTesbQptPhpYmKXs,64
|
7
7
|
pychemstation/control/comm.py,sha256=u44g1hTluQ0yUG93Un-QAshScoDpgYRrZfFTgweP5tY,7386
|
8
8
|
pychemstation/control/hplc.py,sha256=5xC5q-hrAn5hKdz-ZwT_Dlas9LWLV27jZvhCKj7Lzg4,8761
|
9
|
-
pychemstation/control/controllers/__init__.py,sha256=
|
9
|
+
pychemstation/control/controllers/__init__.py,sha256=EM6LBNSTJqYVatmnvPq0P-S3q0POA88c-y64zL79I_I,252
|
10
10
|
pychemstation/control/controllers/comm.py,sha256=IU4I_Q42VNCNUlVi93MxCmw2EBY9hiBDkU9FxubKg3c,7441
|
11
11
|
pychemstation/control/controllers/method.py,sha256=XUclB7lQ_SIkquR58MBmmi9drHIPEq9AR8VprTLenvI,15503
|
12
12
|
pychemstation/control/controllers/sequence.py,sha256=kYNxxck2I-q9mZDEZwG8bJ_99FfLmunS13EAHOS65wU,8288
|
13
13
|
pychemstation/control/controllers/table_controller.py,sha256=70ovnIjLKkJborS1ztk445Mv42TtUM9jUniaQmZuyWQ,11031
|
14
|
+
pychemstation/control/controllers/devices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
pychemstation/control/controllers/devices/column.py,sha256=SCpCnVFZFUM9LM51MbWkVcBRayN3WFxy7lz9gs2PYeY,348
|
16
|
+
pychemstation/control/controllers/devices/device.py,sha256=SF1JK93FjmACnYrlKvldX3gEeA21qnXZegeNhc9QJGQ,738
|
17
|
+
pychemstation/control/controllers/devices/pump.py,sha256=DJQh4lNXEraeC1CWrsKmsITOjuYlRI3tih_XRB3F1hg,1404
|
18
|
+
pychemstation/control/controllers/tables/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
+
pychemstation/control/controllers/tables/method.py,sha256=TlP7FwEmN-QGwewoxXNuzLBAzDipRms9TMTgdAaKJmM,16033
|
20
|
+
pychemstation/control/controllers/tables/sequence.py,sha256=Yq31YhcIhmu1A3iVEFV_d_N68QHABGVOkqn1QrvyKu0,8766
|
21
|
+
pychemstation/control/controllers/tables/table.py,sha256=LAxDNKY2sEypikYR6v6o8SiuP70Ay-FYcvuCfVSFhsQ,11621
|
14
22
|
pychemstation/control/table/__init__.py,sha256=RgMN4uIWHdNUHpGRBWdzmzAbk7XEKl6Y-qtqWCxzSZU,124
|
15
23
|
pychemstation/control/table/method.py,sha256=THVoGomSXff_CTU3eAYme0BYwkPzab5UgZKsiZ29QSk,12196
|
16
24
|
pychemstation/control/table/sequence.py,sha256=Eri52AnbE3BGthfrRSvYKYciquUzvHKo0lYUTySYYE8,10542
|
@@ -20,20 +28,23 @@ pychemstation/generated/dad_method.py,sha256=0W8Z5WDtF5jpIcudMqb7XrkTnR2EGg_QOCs
|
|
20
28
|
pychemstation/generated/pump_method.py,sha256=sUhE2Oo00nzVcoONtq3EMWsN4wLSryXbG8f3EeViWKg,12174
|
21
29
|
pychemstation/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
30
|
pychemstation/utils/chromatogram.py,sha256=-q3_hL9GTyi4C95os7IwAiOrkTM4EXIiigm-nW9pFmM,3221
|
23
|
-
pychemstation/utils/
|
31
|
+
pychemstation/utils/injector_types.py,sha256=xOkvlX_cH_QmuKbyXj8Lc2JEA_6g82az1zB4oTwcKN0,386
|
32
|
+
pychemstation/utils/macro.py,sha256=OCQRRC-f46RlhenZfe7ldSgxBhzcZ5JlW3Ej-9OfeSI,2847
|
24
33
|
pychemstation/utils/method_types.py,sha256=e7c2nWub6pT_S8ZF6sd7uXYm9ZBno0mARnzc1hnSHPY,1516
|
25
34
|
pychemstation/utils/parsing.py,sha256=bnFIsZZwFy9NKzVUf517yN-ogzQbm0hp_aho3KUD6Is,9317
|
26
|
-
pychemstation/utils/
|
35
|
+
pychemstation/utils/pump_types.py,sha256=HWQHxscGn19NTrfYBwQRCO2VcYfwyko7YfBO5uDhEm4,93
|
36
|
+
pychemstation/utils/sequence_types.py,sha256=4cNpmRdPLN5oGN7ozHgT21E65aBO8vV3ZcRXMOQ3EA8,1084
|
27
37
|
pychemstation/utils/table_types.py,sha256=mlbxPAiPvO_EBba5OSzuJcpCL0srrC7uUfm_lKsOsmA,2557
|
28
|
-
pychemstation/utils/tray_types.py,sha256=
|
38
|
+
pychemstation/utils/tray_types.py,sha256=MaHN36rhcEI5mAY95VU8hfP9HhAlngQvMYq-2oyC0hc,764
|
29
39
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
40
|
tests/constants.py,sha256=iU6knsNpQGnBfGcC8VAj5SrCDcHD3jBZLhQdF6UUwY4,2452
|
31
41
|
tests/test_comb.py,sha256=EDE1Ve0A_EK4qro9imZsrD0xXrQN8hAowiOWPFtw3dM,5515
|
32
42
|
tests/test_comm.py,sha256=EYOpVXzEMQLGhhKYDPO-KaLcJdPSMPTD9Y4jSI0yVQY,2516
|
43
|
+
tests/test_inj.py,sha256=yaPGZoHiOC3ZSgsmrtiqp8QtSo2bMxB9FJhaFlOpad0,1412
|
33
44
|
tests/test_method.py,sha256=r1Q1irqiVzs31QuTYLX3u_A0FpX8rIAQ1L4WOk9tLbk,2473
|
34
45
|
tests/test_sequence.py,sha256=Nz2iqp1cJgw6kcQvnwSkfBmhxpOH62PoEu6o_5rO-PY,4929
|
35
|
-
pychemstation-0.5.
|
36
|
-
pychemstation-0.5.
|
37
|
-
pychemstation-0.5.
|
38
|
-
pychemstation-0.5.
|
39
|
-
pychemstation-0.5.
|
46
|
+
pychemstation-0.5.13.dev2.dist-info/LICENSE,sha256=9bdF75gIf1MecZ7oymqWgJREVz7McXPG-mjqrTmzzD8,18658
|
47
|
+
pychemstation-0.5.13.dev2.dist-info/METADATA,sha256=KRy25kSutKC7MXyLmp6YYcIkiIRh9s0GzglAVa1rudg,4372
|
48
|
+
pychemstation-0.5.13.dev2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
49
|
+
pychemstation-0.5.13.dev2.dist-info/top_level.txt,sha256=zXfKu_4nYWwPHo3OsuhshMNC3SPkcoTGCyODjURaghY,20
|
50
|
+
pychemstation-0.5.13.dev2.dist-info/RECORD,,
|
tests/test_inj.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import os
|
2
|
+
import unittest
|
3
|
+
|
4
|
+
from pychemstation.control import HPLCController
|
5
|
+
from tests.constants import *
|
6
|
+
|
7
|
+
|
8
|
+
class TestInj(unittest.TestCase):
|
9
|
+
def setUp(self):
|
10
|
+
path_constants = room(254)
|
11
|
+
for path in path_constants:
|
12
|
+
if not os.path.exists(path):
|
13
|
+
self.fail(
|
14
|
+
f"{path} does not exist on your system. If you would like to run tests, please change this path.")
|
15
|
+
|
16
|
+
self.hplc_controller = HPLCController(comm_dir=path_constants[0],
|
17
|
+
method_dir=path_constants[1],
|
18
|
+
data_dir=path_constants[2],
|
19
|
+
sequence_dir=path_constants[3])
|
20
|
+
|
21
|
+
def test_load_inj(self):
|
22
|
+
self.hplc_controller.switch_method(DEFAULT_METHOD)
|
23
|
+
try:
|
24
|
+
gp_mtd = self.hplc_controller.method_controller.load_from_disk(DEFAULT_METHOD)
|
25
|
+
self.assertTrue(gp_mtd.first_row.organic_modifier == 5)
|
26
|
+
except Exception as e:
|
27
|
+
self.fail(f"Should have not failed, {e}")
|
28
|
+
|
29
|
+
def test_edit_inj(self):
|
30
|
+
self.hplc_controller.method_controller.switch(DEFAULT_METHOD)
|
31
|
+
new_method = gen_rand_method()
|
32
|
+
try:
|
33
|
+
self.hplc_controller.edit_method(new_method)
|
34
|
+
except Exception as e:
|
35
|
+
self.fail(f"Should have not failed: {e}")
|
36
|
+
|
37
|
+
if __name__ == '__main__':
|
38
|
+
unittest.main()
|
File without changes
|
File without changes
|
File without changes
|