pychemstation 0.10.5__py3-none-any.whl → 0.10.6__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/analysis/__init__.py +8 -1
- pychemstation/control/controllers/__init__.py +2 -2
- pychemstation/control/controllers/abc_tables/__init__.py +0 -0
- pychemstation/control/controllers/abc_tables/device.py +15 -0
- pychemstation/control/controllers/{tables/table.py → abc_tables/run.py} +24 -195
- pychemstation/control/controllers/abc_tables/table.py +221 -0
- pychemstation/control/controllers/comm.py +3 -99
- pychemstation/control/controllers/{tables → data_aq}/method.py +2 -2
- pychemstation/control/controllers/{tables → data_aq}/sequence.py +72 -53
- pychemstation/control/controllers/devices/__init__.py +3 -0
- pychemstation/control/controllers/devices/injector.py +57 -24
- pychemstation/control/hplc.py +5 -1
- pychemstation/utils/injector_types.py +22 -2
- pychemstation/utils/mocking/__init__.py +0 -0
- pychemstation/utils/mocking/abc_comm.py +160 -0
- pychemstation/utils/mocking/mock_comm.py +5 -0
- pychemstation/utils/mocking/mock_hplc.py +2 -0
- pychemstation/utils/sequence_types.py +19 -0
- pychemstation/utils/table_types.py +6 -0
- pychemstation/utils/tray_types.py +36 -1
- {pychemstation-0.10.5.dist-info → pychemstation-0.10.6.dist-info}/METADATA +1 -1
- pychemstation-0.10.6.dist-info/RECORD +42 -0
- pychemstation/control/controllers/devices/device.py +0 -74
- pychemstation-0.10.5.dist-info/RECORD +0 -36
- /pychemstation/control/controllers/{tables → data_aq}/__init__.py +0 -0
- {pychemstation-0.10.5.dist-info → pychemstation-0.10.6.dist-info}/WHEEL +0 -0
- {pychemstation-0.10.5.dist-info → pychemstation-0.10.6.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
import time
|
3
3
|
import warnings
|
4
|
-
from typing import Dict, List, Optional,
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
5
5
|
|
6
6
|
from result import Err, Ok, Result
|
7
7
|
from typing_extensions import override
|
@@ -11,7 +11,6 @@ from pychemstation.analysis.chromatogram import (
|
|
11
11
|
AgilentChannelChromatogramData,
|
12
12
|
AgilentHPLCChromatogram,
|
13
13
|
)
|
14
|
-
from . import MethodController
|
15
14
|
|
16
15
|
from ....analysis.process_report import AgilentReport, ReportType
|
17
16
|
from ....control.controllers.comm import CommunicationController
|
@@ -25,10 +24,11 @@ from ....utils.sequence_types import (
|
|
25
24
|
)
|
26
25
|
from ....utils.table_types import RegisterFlag, T, Table
|
27
26
|
from ....utils.tray_types import FiftyFourVialPlate, TenVialColumn, Tray
|
28
|
-
from .
|
27
|
+
from ..abc_tables.run import RunController
|
28
|
+
from . import MethodController
|
29
29
|
|
30
30
|
|
31
|
-
class SequenceController(
|
31
|
+
class SequenceController(RunController):
|
32
32
|
"""
|
33
33
|
Class containing sequence related logic
|
34
34
|
"""
|
@@ -146,13 +146,12 @@ class SequenceController(TableController):
|
|
146
146
|
if rows.is_ok():
|
147
147
|
existing_row_num = rows.ok_value.num_response
|
148
148
|
wanted_row_num = len(sequence_table.rows)
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
149
|
+
for i in range(int(existing_row_num)):
|
150
|
+
self.delete_row(int(existing_row_num - i))
|
151
|
+
self.send(Command.SAVE_SEQUENCE_CMD)
|
152
|
+
for i in range(int(wanted_row_num)):
|
153
|
+
self.add_row()
|
154
154
|
self.send(Command.SAVE_SEQUENCE_CMD)
|
155
|
-
existing_row_num = self.get_num_rows().ok_value.num_response
|
156
155
|
self.send(Command.SWITCH_SEQUENCE_CMD)
|
157
156
|
|
158
157
|
for i, row in enumerate(sequence_table.rows):
|
@@ -175,55 +174,68 @@ class SequenceController(TableController):
|
|
175
174
|
self.send(Command.SAVE_SEQUENCE_CMD)
|
176
175
|
num_rows = self.get_num_rows()
|
177
176
|
if row.vial_location:
|
178
|
-
|
179
|
-
loc_num = -1
|
180
|
-
if isinstance(loc, TenVialColumn):
|
181
|
-
loc_num = loc.value
|
182
|
-
elif isinstance(loc, FiftyFourVialPlate):
|
183
|
-
loc_num = loc.value()
|
184
|
-
self._edit_row_num(
|
185
|
-
row=row_num, col_name=RegisterFlag.VIAL_LOCATION, val=loc_num
|
186
|
-
)
|
177
|
+
self.edit_vial_location(row.vial_location, row_num)
|
187
178
|
if row.method:
|
188
|
-
|
189
|
-
possible_path = os.path.join(method_dir, row.method) + ".M\\"
|
190
|
-
method = row.method
|
191
|
-
if os.path.exists(possible_path):
|
192
|
-
method = os.path.join(method_dir, row.method)
|
193
|
-
self._edit_row_text(row=row_num, col_name=RegisterFlag.METHOD, val=method)
|
179
|
+
self.edit_method_name(row.method, row_num)
|
194
180
|
if row.num_inj:
|
195
|
-
self.
|
196
|
-
row=row_num, col_name=RegisterFlag.NUM_INJ, val=row.num_inj
|
197
|
-
)
|
181
|
+
self.edit_num_injections(row.num_inj, row_num)
|
198
182
|
if row.inj_vol:
|
199
|
-
self.
|
200
|
-
row=row_num, col_name=RegisterFlag.INJ_VOL, val=str(row.inj_vol)
|
201
|
-
)
|
183
|
+
self.edit_injection_volume(row.inj_vol, row_num)
|
202
184
|
if row.inj_source:
|
203
|
-
self.
|
204
|
-
row=row_num, col_name=RegisterFlag.INJ_SOR, val=row.inj_source.value
|
205
|
-
)
|
185
|
+
self.edit_injection_source(row.inj_source, row_num)
|
206
186
|
if row.sample_name:
|
207
|
-
self.
|
208
|
-
|
209
|
-
)
|
210
|
-
|
211
|
-
|
212
|
-
row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.data_file
|
213
|
-
)
|
214
|
-
else:
|
215
|
-
self._edit_row_text(
|
216
|
-
row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.sample_name
|
217
|
-
)
|
187
|
+
self.edit_sample_name(row.sample_name, row_num)
|
188
|
+
if row.data_file:
|
189
|
+
self.edit_data_file(row.data_file, row_num)
|
190
|
+
elif row.sample_name and not row.data_file:
|
191
|
+
self.edit_data_file(row.sample_name, row_num)
|
218
192
|
if row.sample_type:
|
219
|
-
self.
|
220
|
-
row=row_num,
|
221
|
-
col_name=RegisterFlag.SAMPLE_TYPE,
|
222
|
-
val=row.sample_type.value,
|
223
|
-
)
|
224
|
-
|
193
|
+
self.edit_sample_type(row.sample_type, row_num)
|
225
194
|
self.send(Command.SAVE_SEQUENCE_CMD)
|
226
195
|
|
196
|
+
def edit_sample_type(self, sample_type: SampleType, row_num: int):
|
197
|
+
self._edit_row_num(
|
198
|
+
row=row_num,
|
199
|
+
col_name=RegisterFlag.SAMPLE_TYPE,
|
200
|
+
val=sample_type.value,
|
201
|
+
)
|
202
|
+
|
203
|
+
def edit_data_file(self, data_file: str, row_num: int):
|
204
|
+
self._edit_row_text(row=row_num, col_name=RegisterFlag.DATA_FILE, val=data_file)
|
205
|
+
|
206
|
+
def edit_sample_name(self, sample_name: str, row_num: int):
|
207
|
+
self._edit_row_text(row=row_num, col_name=RegisterFlag.NAME, val=sample_name)
|
208
|
+
|
209
|
+
def edit_injection_source(self, inj_source: InjectionSource, row_num: int):
|
210
|
+
self._edit_row_text(
|
211
|
+
row=row_num, col_name=RegisterFlag.INJ_SOR, val=inj_source.value
|
212
|
+
)
|
213
|
+
|
214
|
+
def edit_injection_volume(self, inj_vol: Union[int, float], row_num: int):
|
215
|
+
self._edit_row_text(
|
216
|
+
row=row_num, col_name=RegisterFlag.INJ_VOL, val=str(inj_vol)
|
217
|
+
)
|
218
|
+
|
219
|
+
def edit_num_injections(self, num_inj: int, row_num: int):
|
220
|
+
self._edit_row_num(row=row_num, col_name=RegisterFlag.NUM_INJ, val=num_inj)
|
221
|
+
|
222
|
+
def edit_method_name(self, method: str, row_num: int):
|
223
|
+
method_dir = self.method_controller.src
|
224
|
+
possible_path = os.path.join(method_dir, method) + ".M\\"
|
225
|
+
if os.path.exists(possible_path):
|
226
|
+
method = os.path.join(method_dir, method)
|
227
|
+
self._edit_row_text(row=row_num, col_name=RegisterFlag.METHOD, val=method)
|
228
|
+
|
229
|
+
def edit_vial_location(self, loc: Tray, row_num: int):
|
230
|
+
loc_num = -1
|
231
|
+
if isinstance(loc, TenVialColumn):
|
232
|
+
loc_num = loc.value
|
233
|
+
elif isinstance(loc, FiftyFourVialPlate):
|
234
|
+
loc_num = loc.value()
|
235
|
+
self._edit_row_num(
|
236
|
+
row=row_num, col_name=RegisterFlag.VIAL_LOCATION, val=loc_num
|
237
|
+
)
|
238
|
+
|
227
239
|
def run(self, stall_while_running: bool = True):
|
228
240
|
"""
|
229
241
|
Starts the currently loaded sequence, storing data
|
@@ -262,7 +274,14 @@ class SequenceController(TableController):
|
|
262
274
|
self.send(Command.RUN_SEQUENCE_CMD.value)
|
263
275
|
self.timeout = total_runtime * 60
|
264
276
|
|
265
|
-
|
277
|
+
tries = 10
|
278
|
+
hplc_running = False
|
279
|
+
for _ in range(tries):
|
280
|
+
hplc_running = self.check_hplc_is_running()
|
281
|
+
if hplc_running:
|
282
|
+
break
|
283
|
+
|
284
|
+
if hplc_running:
|
266
285
|
folder_name = f"{self.table_state.name} {timestamp}"
|
267
286
|
data_file = SequenceDataFiles(
|
268
287
|
dir=folder_name, sequence_name=self.table_state.name
|
@@ -276,7 +295,7 @@ class SequenceController(TableController):
|
|
276
295
|
else:
|
277
296
|
warnings.warn("Run may have not completed.")
|
278
297
|
else:
|
279
|
-
raise RuntimeError("Sequence run
|
298
|
+
raise RuntimeError("Sequence run may not have started.")
|
280
299
|
|
281
300
|
@override
|
282
301
|
def fuzzy_match_most_recent_folder(
|
@@ -1,19 +1,21 @@
|
|
1
|
+
from ..abc_tables.device import DeviceController
|
1
2
|
from ....control.controllers import CommunicationController
|
2
3
|
from ....utils.injector_types import (
|
3
4
|
Draw,
|
4
5
|
Inject,
|
5
|
-
InjectorFunction,
|
6
6
|
InjectorTable,
|
7
7
|
Mode,
|
8
8
|
Remote,
|
9
9
|
RemoteCommand,
|
10
10
|
SourceType,
|
11
11
|
Wait,
|
12
|
+
DrawDefault,
|
13
|
+
DrawDefaultVolume,
|
14
|
+
DrawDefaultLocation,
|
12
15
|
)
|
13
16
|
from ....utils.macro import Response
|
14
17
|
from ....utils.table_types import RegisterFlag, Table
|
15
|
-
from ....utils.tray_types import Tray
|
16
|
-
from .device import DeviceController
|
18
|
+
from ....utils.tray_types import Tray, FiftyFourVialPlate, TenVialColumn, LocationPlus
|
17
19
|
|
18
20
|
|
19
21
|
class InjectorController(DeviceController):
|
@@ -22,14 +24,32 @@ class InjectorController(DeviceController):
|
|
22
24
|
):
|
23
25
|
super().__init__(controller, table, offline)
|
24
26
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
def try_vial_location(self, val: str) -> Tray:
|
28
|
+
try:
|
29
|
+
return FiftyFourVialPlate.from_str(val)
|
30
|
+
except Exception:
|
31
|
+
try:
|
32
|
+
return TenVialColumn(int(val))
|
33
|
+
except Exception:
|
34
|
+
raise ValueError("Location could not be identified.")
|
35
|
+
|
36
|
+
def get_row(
|
37
|
+
self, row: int
|
38
|
+
) -> (
|
39
|
+
Draw
|
40
|
+
| DrawDefaultVolume
|
41
|
+
| Inject
|
42
|
+
| Wait
|
43
|
+
| DrawDefault
|
44
|
+
| DrawDefaultLocation
|
45
|
+
| Remote
|
46
|
+
):
|
47
|
+
def return_location_plus() -> Tray:
|
48
|
+
unit = self.get_num(row, RegisterFlag.DRAW_LOCATION_UNIT)
|
49
|
+
tray = self.get_num(row, RegisterFlag.DRAW_LOCATION_TRAY)
|
50
|
+
row_ = self.get_num(row, RegisterFlag.DRAW_LOCATION_ROW)
|
51
|
+
col = self.get_num(row, RegisterFlag.DRAW_LOCATION_COLUMN)
|
52
|
+
return LocationPlus(int(unit), int(tray), int(row_), int(col))
|
33
53
|
|
34
54
|
function = self.get_text(row, RegisterFlag.FUNCTION)
|
35
55
|
if function == "Wait":
|
@@ -37,20 +57,32 @@ class InjectorController(DeviceController):
|
|
37
57
|
elif function == "Inject":
|
38
58
|
return Inject()
|
39
59
|
elif function == "Draw":
|
40
|
-
# TODO: better error handling
|
41
60
|
is_source = SourceType(self.get_text(row, RegisterFlag.DRAW_SOURCE))
|
42
61
|
is_volume = Mode(self.get_text(row, RegisterFlag.DRAW_VOLUME))
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
62
|
+
if is_volume is not Mode.SET:
|
63
|
+
if is_source == SourceType.DEFAULT:
|
64
|
+
return DrawDefault()
|
65
|
+
elif is_source is SourceType.SPECIFIC_LOCATION:
|
66
|
+
return DrawDefaultVolume(location=return_location_plus())
|
67
|
+
elif is_source is SourceType.LOCATION:
|
68
|
+
return DrawDefaultVolume(
|
69
|
+
location=self.try_vial_location(
|
70
|
+
self.get_text(row, RegisterFlag.DRAW_LOCATION)
|
71
|
+
)
|
72
|
+
)
|
73
|
+
else:
|
74
|
+
vol = self.get_num(row, RegisterFlag.DRAW_VOLUME_VALUE)
|
75
|
+
if is_source == SourceType.DEFAULT:
|
76
|
+
return DrawDefaultLocation(amount=vol)
|
77
|
+
elif is_source is SourceType.SPECIFIC_LOCATION:
|
78
|
+
return Draw(amount=vol, location=return_location_plus())
|
79
|
+
elif is_source is SourceType.LOCATION:
|
80
|
+
return Draw(
|
81
|
+
amount=vol,
|
82
|
+
location=self.try_vial_location(
|
83
|
+
self.get_text(row, RegisterFlag.DRAW_LOCATION)
|
84
|
+
),
|
85
|
+
)
|
54
86
|
elif function == "Remote":
|
55
87
|
return Remote(
|
56
88
|
command=RemoteCommand(self.get_text(row, RegisterFlag.REMOTE)),
|
@@ -65,7 +97,8 @@ class InjectorController(DeviceController):
|
|
65
97
|
if isinstance(row_response, Response):
|
66
98
|
return InjectorTable(
|
67
99
|
functions=[
|
68
|
-
self.get_row(i
|
100
|
+
self.get_row(i + 1)
|
101
|
+
for i in range(int(row_response.num_response))
|
69
102
|
]
|
70
103
|
)
|
71
104
|
elif rows.is_err():
|
pychemstation/control/hplc.py
CHANGED
@@ -13,9 +13,10 @@ from pychemstation.analysis.chromatogram import (
|
|
13
13
|
AgilentHPLCChromatogram,
|
14
14
|
)
|
15
15
|
from .controllers.devices.injector import InjectorController
|
16
|
-
from .controllers.
|
16
|
+
from .controllers.data_aq.sequence import SequenceController, MethodController
|
17
17
|
from ..analysis.process_report import AgilentReport, ReportType
|
18
18
|
from ..control.controllers import CommunicationController
|
19
|
+
from ..utils.injector_types import InjectorTable
|
19
20
|
from ..utils.macro import Command, Response, Status
|
20
21
|
from ..utils.method_types import MethodDetails
|
21
22
|
from ..utils.sequence_types import SequenceTable
|
@@ -278,6 +279,9 @@ class HPLCController:
|
|
278
279
|
"""Returns the currently loaded sequence."""
|
279
280
|
return self.sequence_controller.load()
|
280
281
|
|
282
|
+
def load_injector_program(self) -> InjectorTable:
|
283
|
+
return self.method_controller.injector_controller.load()
|
284
|
+
|
281
285
|
def standby(self):
|
282
286
|
"""Switches all modules in standby mode. All lamps and pumps are switched off."""
|
283
287
|
self.send(Command.STANDBY_CMD)
|
@@ -19,10 +19,25 @@ class Mode(Enum):
|
|
19
19
|
@dataclass
|
20
20
|
class Draw:
|
21
21
|
amount: Optional[float] = None
|
22
|
-
location: Optional[
|
22
|
+
location: Optional[Tray] = None
|
23
23
|
source: Optional[Tray] = None
|
24
24
|
|
25
25
|
|
26
|
+
@dataclass
|
27
|
+
class DrawDefaultVolume:
|
28
|
+
location: Optional[Tray] = None
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class DrawDefaultLocation:
|
33
|
+
amount: Optional[float] = None
|
34
|
+
|
35
|
+
|
36
|
+
@dataclass
|
37
|
+
class DrawDefault:
|
38
|
+
pass
|
39
|
+
|
40
|
+
|
26
41
|
@dataclass
|
27
42
|
class Wait:
|
28
43
|
duration: Union[int, float]
|
@@ -35,6 +50,9 @@ class Inject:
|
|
35
50
|
|
36
51
|
class RemoteCommand(Enum):
|
37
52
|
START = "START"
|
53
|
+
NOT_READY = "NOT_READY"
|
54
|
+
STOP = "STOP"
|
55
|
+
READY = "READY"
|
38
56
|
PREPARE = "PREPARE"
|
39
57
|
|
40
58
|
|
@@ -44,7 +62,9 @@ class Remote:
|
|
44
62
|
duration: int
|
45
63
|
|
46
64
|
|
47
|
-
InjectorFunction = Union[
|
65
|
+
InjectorFunction = Union[
|
66
|
+
Draw, DrawDefault, DrawDefaultVolume, DrawDefaultLocation, Wait, Inject, Remote
|
67
|
+
]
|
48
68
|
|
49
69
|
|
50
70
|
@dataclass
|
File without changes
|
@@ -0,0 +1,160 @@
|
|
1
|
+
"""
|
2
|
+
Module to provide API for the communication with Agilent HPLC systems.
|
3
|
+
|
4
|
+
HPLCController sends commands to Chemstation software via a command file.
|
5
|
+
Answers are received via reply file. On the Chemstation side, a custom
|
6
|
+
Macro monitors the command file, executes commands and writes to the reply file.
|
7
|
+
Each command is given a number (cmd_no) to keep track of which commands have
|
8
|
+
been processed.
|
9
|
+
|
10
|
+
Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
|
11
|
+
"""
|
12
|
+
|
13
|
+
import abc
|
14
|
+
import os
|
15
|
+
import time
|
16
|
+
from abc import abstractmethod
|
17
|
+
from typing import Union
|
18
|
+
|
19
|
+
from result import Err, Ok, Result
|
20
|
+
|
21
|
+
from ...utils.macro import (
|
22
|
+
HPLCAvailStatus,
|
23
|
+
Command,
|
24
|
+
Status,
|
25
|
+
Response,
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
class ABCCommunicationController(abc.ABC):
|
30
|
+
"""
|
31
|
+
Abstract class representing the communication controller.
|
32
|
+
"""
|
33
|
+
|
34
|
+
# maximum command number
|
35
|
+
MAX_CMD_NO = 255
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
comm_dir: str,
|
40
|
+
cmd_file: str = "cmd",
|
41
|
+
reply_file: str = "reply",
|
42
|
+
offline: bool = False,
|
43
|
+
debug: bool = False,
|
44
|
+
):
|
45
|
+
"""
|
46
|
+
:param comm_dir:
|
47
|
+
:param cmd_file: Name of command file
|
48
|
+
:param reply_file: Name of reply file
|
49
|
+
:param debug: whether to save log of sent commands
|
50
|
+
"""
|
51
|
+
if not offline:
|
52
|
+
self.debug = debug
|
53
|
+
if os.path.isdir(comm_dir):
|
54
|
+
self.cmd_file = os.path.join(comm_dir, cmd_file)
|
55
|
+
self.reply_file = os.path.join(comm_dir, reply_file)
|
56
|
+
self.cmd_no = 0
|
57
|
+
else:
|
58
|
+
raise FileNotFoundError(f"comm_dir: {comm_dir} not found.")
|
59
|
+
|
60
|
+
# Create files for Chemstation to communicate with Python
|
61
|
+
open(self.cmd_file, "a").close()
|
62
|
+
open(self.reply_file, "a").close()
|
63
|
+
|
64
|
+
self.reset_cmd_counter()
|
65
|
+
self._most_recent_hplc_status: Status = self.get_status()
|
66
|
+
self.send("Local Rows")
|
67
|
+
|
68
|
+
@abstractmethod
|
69
|
+
def get_num_val(self, cmd: str) -> Union[int, float]:
|
70
|
+
pass
|
71
|
+
|
72
|
+
@abstractmethod
|
73
|
+
def get_text_val(self, cmd: str) -> str:
|
74
|
+
pass
|
75
|
+
|
76
|
+
@abstractmethod
|
77
|
+
def get_status(self) -> Status:
|
78
|
+
pass
|
79
|
+
|
80
|
+
@abstractmethod
|
81
|
+
def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
|
82
|
+
pass
|
83
|
+
|
84
|
+
@abstractmethod
|
85
|
+
def _receive(self, cmd_no: int, num_attempts=100) -> Result[str, str]:
|
86
|
+
pass
|
87
|
+
|
88
|
+
def set_status(self):
|
89
|
+
"""Updates current status of HPLC machine"""
|
90
|
+
self._most_recent_hplc_status = self.get_status()
|
91
|
+
|
92
|
+
def check_if_not_running(self) -> bool:
|
93
|
+
"""Checks if HPLC machine is in an available state, meaning a state that data is not being written.
|
94
|
+
|
95
|
+
:return: whether the HPLC machine is in a safe state to retrieve data back."""
|
96
|
+
self.set_status()
|
97
|
+
hplc_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
|
98
|
+
time.sleep(5)
|
99
|
+
self.set_status()
|
100
|
+
hplc_actually_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
|
101
|
+
return hplc_avail and hplc_actually_avail
|
102
|
+
|
103
|
+
def sleepy_send(self, cmd: Union[Command, str]):
|
104
|
+
self.send("Sleep 0.1")
|
105
|
+
self.send(cmd)
|
106
|
+
self.send("Sleep 0.1")
|
107
|
+
|
108
|
+
def send(self, cmd: Union[Command, str]):
|
109
|
+
"""Sends a command to Chemstation.
|
110
|
+
|
111
|
+
:param cmd: Command to be sent to HPLC
|
112
|
+
"""
|
113
|
+
if self.cmd_no == self.MAX_CMD_NO:
|
114
|
+
self.reset_cmd_counter()
|
115
|
+
|
116
|
+
cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
|
117
|
+
self.cmd_no += 1
|
118
|
+
self._send(cmd_to_send, self.cmd_no)
|
119
|
+
if self.debug:
|
120
|
+
f = open("out.txt", "a")
|
121
|
+
f.write(cmd_to_send + "\n")
|
122
|
+
f.close()
|
123
|
+
|
124
|
+
def receive(self) -> Result[Response, str]:
|
125
|
+
"""Returns messages received in reply file.
|
126
|
+
|
127
|
+
:return: ChemStation response
|
128
|
+
"""
|
129
|
+
num_response_prefix = "Numerical Responses:"
|
130
|
+
str_response_prefix = "String Responses:"
|
131
|
+
possible_response = self._receive(self.cmd_no)
|
132
|
+
if possible_response.is_ok():
|
133
|
+
lines = possible_response.ok_value.splitlines()
|
134
|
+
for line in lines:
|
135
|
+
if str_response_prefix in line and num_response_prefix in line:
|
136
|
+
string_responses_dirty, _, numerical_responses = line.partition(
|
137
|
+
num_response_prefix
|
138
|
+
)
|
139
|
+
_, _, string_responses = string_responses_dirty.partition(
|
140
|
+
str_response_prefix
|
141
|
+
)
|
142
|
+
return Ok(
|
143
|
+
Response(
|
144
|
+
string_response=string_responses.strip(),
|
145
|
+
num_response=float(numerical_responses.strip()),
|
146
|
+
)
|
147
|
+
)
|
148
|
+
return Err("Could not retrieve HPLC response")
|
149
|
+
else:
|
150
|
+
return Err(f"Could not establish response to HPLC: {possible_response}")
|
151
|
+
|
152
|
+
def reset_cmd_counter(self):
|
153
|
+
"""Resets the command counter."""
|
154
|
+
self._send(Command.RESET_COUNTER_CMD.value, cmd_no=self.MAX_CMD_NO + 1)
|
155
|
+
self._receive(cmd_no=self.MAX_CMD_NO + 1)
|
156
|
+
self.cmd_no = 0
|
157
|
+
|
158
|
+
def stop_macro(self):
|
159
|
+
"""Stops Macro execution. Connection will be lost."""
|
160
|
+
self.send(Command.STOP_MACRO_CMD)
|
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import os.path
|
3
4
|
from enum import Enum
|
4
5
|
from typing import Optional, List
|
5
6
|
from dataclasses import dataclass, field
|
@@ -51,3 +52,21 @@ class SequenceEntry:
|
|
51
52
|
class SequenceTable:
|
52
53
|
name: str
|
53
54
|
rows: list[SequenceEntry]
|
55
|
+
|
56
|
+
def __eq__(self, other):
|
57
|
+
equal = False
|
58
|
+
if not isinstance(other, SequenceTable):
|
59
|
+
return False
|
60
|
+
|
61
|
+
for self_row, other_row in zip(self.rows, other.rows):
|
62
|
+
equal |= self_row.vial_location == other_row.vial_location
|
63
|
+
equal |= self_row.data_file == other_row.data_file
|
64
|
+
equal |= (
|
65
|
+
os.path.split(os.path.normpath(self_row.method))[-1]
|
66
|
+
== os.path.split(os.path.normpath(other_row.method))[-1]
|
67
|
+
)
|
68
|
+
equal |= self_row.num_inj == other_row.num_inj
|
69
|
+
equal |= self_row.inj_vol == other_row.inj_vol
|
70
|
+
equal |= self_row.inj_source == other_row.inj_source
|
71
|
+
equal |= self_row.sample_name == other_row.sample_name
|
72
|
+
equal |= self_row.sample_type == other_row.sample_type
|
@@ -43,6 +43,12 @@ class RegisterFlag(Enum):
|
|
43
43
|
FLOW = "Flow"
|
44
44
|
MAX_TIME = "StopTime_Time"
|
45
45
|
POST_TIME = "PostTime_Time" # TODO: check
|
46
|
+
SIGNAL_A = "Signal_Wavelength"
|
47
|
+
SIGNAL_B = "Signal2_Wavelength"
|
48
|
+
SIGNAL_C = "Signal3_Wavelength"
|
49
|
+
SIGNAL_D = "Signal4_Wavelength"
|
50
|
+
SIGNAL_E = "Signal5_Wavelength"
|
51
|
+
SIGNAL_A_USED = "Signal1_IsUsed"
|
46
52
|
COLUMN_OVEN_TEMP1 = "TemperatureControl_Temperature"
|
47
53
|
COLUMN_OVEN_TEMP2 = "TemperatureControl2_Temperature"
|
48
54
|
STOPTIME_MODE = "StopTime_Mode"
|
@@ -71,6 +71,22 @@ class Letter(Enum):
|
|
71
71
|
else:
|
72
72
|
raise ValueError("Letter must be one of A to F")
|
73
73
|
|
74
|
+
@classmethod
|
75
|
+
def from_int(cls, num: int) -> Letter:
|
76
|
+
letter_mapping = {
|
77
|
+
"A": Letter.A,
|
78
|
+
"B": Letter.B,
|
79
|
+
"C": Letter.C,
|
80
|
+
"D": Letter.D,
|
81
|
+
"E": Letter.E,
|
82
|
+
"F": Letter.F,
|
83
|
+
}
|
84
|
+
|
85
|
+
if num <= len(letter_mapping):
|
86
|
+
return list(letter_mapping.values())[num]
|
87
|
+
else:
|
88
|
+
raise ValueError("Letter must be one of A to F")
|
89
|
+
|
74
90
|
|
75
91
|
@dataclass
|
76
92
|
class FiftyFourVialPlate:
|
@@ -91,6 +107,17 @@ class FiftyFourVialPlate:
|
|
91
107
|
def value(self) -> int:
|
92
108
|
return self.plate.value + self.letter.value + self.num.value
|
93
109
|
|
110
|
+
@classmethod
|
111
|
+
def from_tray_row_col(cls, tray: int, row: int, col: int):
|
112
|
+
try:
|
113
|
+
return FiftyFourVialPlate(
|
114
|
+
plate=Plate.from_num(tray),
|
115
|
+
letter=Letter.from_int(row),
|
116
|
+
num=Num.from_num(col),
|
117
|
+
)
|
118
|
+
except Exception:
|
119
|
+
raise ValueError("Could not parse tray location.")
|
120
|
+
|
94
121
|
@classmethod
|
95
122
|
def from_str(cls, loc: str):
|
96
123
|
"""
|
@@ -190,4 +217,12 @@ class TenVialColumn(Enum):
|
|
190
217
|
TEN = 10
|
191
218
|
|
192
219
|
|
193
|
-
|
220
|
+
@dataclass
|
221
|
+
class LocationPlus:
|
222
|
+
unit: int
|
223
|
+
tray: int
|
224
|
+
row: int
|
225
|
+
col: int
|
226
|
+
|
227
|
+
|
228
|
+
Tray = Union[FiftyFourVialPlate, TenVialColumn, LocationPlus]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pychemstation
|
3
|
-
Version: 0.10.
|
3
|
+
Version: 0.10.6
|
4
4
|
Summary: Library to interact with Chemstation software, primarily used in Hein lab
|
5
5
|
Project-URL: Documentation, https://pychemstation-e5a086.gitlab.io/pychemstation.html
|
6
6
|
Project-URL: Repository, https://gitlab.com/heingroup/device-api/pychemstation
|