pychemstation 0.10.3__py3-none-any.whl → 0.10.5__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/__init__.py +1 -1
- pychemstation/analysis/__init__.py +1 -6
- pychemstation/analysis/base_spectrum.py +7 -7
- pychemstation/{utils → analysis}/chromatogram.py +24 -4
- pychemstation/analysis/process_report.py +189 -90
- pychemstation/control/__init__.py +5 -2
- pychemstation/control/controllers/__init__.py +2 -7
- pychemstation/control/controllers/comm.py +56 -32
- pychemstation/control/controllers/devices/device.py +59 -24
- pychemstation/control/controllers/devices/injector.py +33 -10
- pychemstation/control/controllers/tables/__init__.py +4 -0
- pychemstation/control/controllers/tables/method.py +241 -151
- pychemstation/control/controllers/tables/sequence.py +226 -107
- pychemstation/control/controllers/tables/table.py +216 -132
- pychemstation/control/hplc.py +89 -75
- pychemstation/generated/__init__.py +0 -2
- pychemstation/generated/pump_method.py +15 -19
- pychemstation/utils/injector_types.py +1 -1
- pychemstation/utils/macro.py +11 -10
- pychemstation/utils/method_types.py +2 -1
- pychemstation/utils/parsing.py +0 -11
- pychemstation/utils/sequence_types.py +2 -3
- pychemstation/utils/spec_utils.py +2 -3
- pychemstation/utils/table_types.py +10 -9
- pychemstation/utils/tray_types.py +45 -36
- {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/METADATA +5 -4
- pychemstation-0.10.5.dist-info/RECORD +36 -0
- pychemstation/control/controllers/tables/ms.py +0 -21
- pychemstation-0.10.3.dist-info/RECORD +0 -37
- {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/WHEEL +0 -0
- {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/licenses/LICENSE +0 -0
@@ -9,6 +9,7 @@ been processed.
|
|
9
9
|
|
10
10
|
Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
|
11
11
|
"""
|
12
|
+
|
12
13
|
import os
|
13
14
|
import time
|
14
15
|
from typing import Optional, Union
|
@@ -34,11 +35,12 @@ class CommunicationController:
|
|
34
35
|
MAX_CMD_NO = 255
|
35
36
|
|
36
37
|
def __init__(
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
self,
|
39
|
+
comm_dir: str,
|
40
|
+
cmd_file: str = "cmd",
|
41
|
+
reply_file: str = "reply",
|
42
|
+
offline: bool = False,
|
43
|
+
debug: bool = False,
|
42
44
|
):
|
43
45
|
"""
|
44
46
|
:param comm_dir:
|
@@ -46,26 +48,27 @@ class CommunicationController:
|
|
46
48
|
:param reply_file: Name of reply file
|
47
49
|
:param debug: whether to save log of sent commands
|
48
50
|
"""
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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.")
|
57
59
|
|
58
|
-
|
59
|
-
|
60
|
-
|
60
|
+
# Create files for Chemstation to communicate with Python
|
61
|
+
open(self.cmd_file, "a").close()
|
62
|
+
open(self.reply_file, "a").close()
|
61
63
|
|
62
|
-
|
64
|
+
self.reset_cmd_counter()
|
63
65
|
|
64
|
-
|
65
|
-
|
66
|
+
# Initialize row counter for table operations
|
67
|
+
self._most_recent_hplc_status: Status = self.get_status()
|
68
|
+
self.send("Local Rows")
|
66
69
|
|
67
70
|
def get_num_val(self, cmd: str) -> Union[int, float]:
|
68
|
-
tries =
|
71
|
+
tries = 10
|
69
72
|
for _ in range(tries):
|
70
73
|
self.send(Command.GET_NUM_VAL_CMD.value.format(cmd=cmd))
|
71
74
|
res = self.receive()
|
@@ -74,7 +77,7 @@ class CommunicationController:
|
|
74
77
|
raise RuntimeError("Failed to get number.")
|
75
78
|
|
76
79
|
def get_text_val(self, cmd: str) -> str:
|
77
|
-
tries =
|
80
|
+
tries = 10
|
78
81
|
for _ in range(tries):
|
79
82
|
self.send(Command.GET_TEXT_VAL_CMD.value.format(cmd=cmd))
|
80
83
|
res = self.receive()
|
@@ -91,9 +94,15 @@ class CommunicationController:
|
|
91
94
|
time.sleep(1)
|
92
95
|
|
93
96
|
try:
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
+
res = self.receive()
|
98
|
+
if res.is_err():
|
99
|
+
return HPLCErrorStatus.NORESPONSE
|
100
|
+
if res.is_ok():
|
101
|
+
parsed_response = self.receive().value.string_response
|
102
|
+
self._most_recent_hplc_status = str_to_status(parsed_response)
|
103
|
+
return self._most_recent_hplc_status
|
104
|
+
else:
|
105
|
+
raise RuntimeError("Failed to get status")
|
97
106
|
except IOError:
|
98
107
|
return HPLCErrorStatus.NORESPONSE
|
99
108
|
except IndexError:
|
@@ -144,7 +153,8 @@ class CommunicationController:
|
|
144
153
|
:raises IOError: Could not read reply file.
|
145
154
|
:return: Potential ChemStation response
|
146
155
|
"""
|
147
|
-
err: Optional[Union[OSError, IndexError]] = None
|
156
|
+
err: Optional[Union[OSError, IndexError, ValueError]] = None
|
157
|
+
err_msg = ""
|
148
158
|
for _ in range(num_attempts):
|
149
159
|
time.sleep(1)
|
150
160
|
|
@@ -157,7 +167,11 @@ class CommunicationController:
|
|
157
167
|
|
158
168
|
try:
|
159
169
|
first_line = response.splitlines()[0]
|
160
|
-
|
170
|
+
try:
|
171
|
+
response_no = int(first_line.split()[0])
|
172
|
+
except ValueError as e:
|
173
|
+
err = e
|
174
|
+
err_msg = f"Caused by {first_line}"
|
161
175
|
except IndexError as e:
|
162
176
|
err = e
|
163
177
|
continue
|
@@ -168,7 +182,9 @@ class CommunicationController:
|
|
168
182
|
else:
|
169
183
|
continue
|
170
184
|
else:
|
171
|
-
return Err(
|
185
|
+
return Err(
|
186
|
+
f"Failed to receive reply to command #{cmd_no} due to {err} caused by {err_msg}."
|
187
|
+
)
|
172
188
|
|
173
189
|
def sleepy_send(self, cmd: Union[Command, str]):
|
174
190
|
self.send("Sleep 0.1")
|
@@ -194,7 +210,7 @@ class CommunicationController:
|
|
194
210
|
def receive(self) -> Result[Response, str]:
|
195
211
|
"""Returns messages received in reply file.
|
196
212
|
|
197
|
-
:return: ChemStation response
|
213
|
+
:return: ChemStation response
|
198
214
|
"""
|
199
215
|
num_response_prefix = "Numerical Responses:"
|
200
216
|
str_response_prefix = "String Responses:"
|
@@ -203,10 +219,18 @@ class CommunicationController:
|
|
203
219
|
lines = possible_response.ok_value.splitlines()
|
204
220
|
for line in lines:
|
205
221
|
if str_response_prefix in line and num_response_prefix in line:
|
206
|
-
string_responses_dirty, _, numerical_responses = line.partition(
|
207
|
-
|
208
|
-
|
209
|
-
|
222
|
+
string_responses_dirty, _, numerical_responses = line.partition(
|
223
|
+
num_response_prefix
|
224
|
+
)
|
225
|
+
_, _, string_responses = string_responses_dirty.partition(
|
226
|
+
str_response_prefix
|
227
|
+
)
|
228
|
+
return Ok(
|
229
|
+
Response(
|
230
|
+
string_response=string_responses.strip(),
|
231
|
+
num_response=float(numerical_responses.strip()),
|
232
|
+
)
|
233
|
+
)
|
210
234
|
return Err("Could not retrieve HPLC response")
|
211
235
|
else:
|
212
236
|
return Err(f"Could not establish response to HPLC: {possible_response}")
|
@@ -1,39 +1,74 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import abc
|
2
|
-
from typing import
|
4
|
+
from typing import Union
|
3
5
|
|
4
|
-
from result import
|
6
|
+
from result import Err, Ok
|
5
7
|
|
6
|
-
from ....analysis.process_report import AgilentReport, ReportType
|
7
8
|
from ....control.controllers import CommunicationController
|
8
|
-
from ....
|
9
|
-
from ....utils.
|
10
|
-
from ....utils.table_types import T, Table
|
11
|
-
|
9
|
+
from ....utils.macro import Command, Response
|
10
|
+
from ....utils.table_types import RegisterFlag, Table, TableOperation
|
12
11
|
|
13
|
-
class DeviceController(TableController, abc.ABC):
|
14
12
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
13
|
+
class DeviceController(abc.ABC):
|
14
|
+
def __init__(
|
15
|
+
self, controller: CommunicationController, table: Table, offline: bool
|
16
|
+
):
|
17
|
+
self.table_locator = table
|
18
|
+
self.controller = controller
|
19
|
+
self.offline = offline
|
21
20
|
|
22
21
|
@abc.abstractmethod
|
23
22
|
def get_row(self, row: int):
|
24
23
|
pass
|
25
24
|
|
26
|
-
def
|
27
|
-
|
25
|
+
def get_text(self, row: int, col_name: RegisterFlag) -> str:
|
26
|
+
return self.controller.get_text_val(
|
27
|
+
TableOperation.GET_ROW_TEXT.value.format(
|
28
|
+
register=self.table_locator.register,
|
29
|
+
table_name=self.table_locator.name,
|
30
|
+
row=row,
|
31
|
+
col_name=col_name.value,
|
32
|
+
)
|
33
|
+
)
|
28
34
|
|
29
|
-
def
|
30
|
-
|
35
|
+
def get_num(self, row: int, col_name: RegisterFlag) -> Union[int, float]:
|
36
|
+
return self.controller.get_num_val(
|
37
|
+
TableOperation.GET_ROW_VAL.value.format(
|
38
|
+
register=self.table_locator.register,
|
39
|
+
table_name=self.table_locator.name,
|
40
|
+
row=row,
|
41
|
+
col_name=col_name.value,
|
42
|
+
)
|
43
|
+
)
|
31
44
|
|
32
|
-
def
|
33
|
-
|
45
|
+
def get_num_rows(self) -> Ok[Response] | Err[str]:
|
46
|
+
self.send(
|
47
|
+
TableOperation.GET_NUM_ROWS.value.format(
|
48
|
+
register=self.table_locator.register,
|
49
|
+
table_name=self.table_locator.name,
|
50
|
+
col_name=RegisterFlag.NUM_ROWS,
|
51
|
+
)
|
52
|
+
)
|
53
|
+
self.send(
|
54
|
+
Command.GET_ROWS_CMD.value.format(
|
55
|
+
register=self.table_locator.register,
|
56
|
+
table_name=self.table_locator.name,
|
57
|
+
col_name=RegisterFlag.NUM_ROWS,
|
58
|
+
)
|
59
|
+
)
|
60
|
+
res = self.controller.receive()
|
34
61
|
|
35
|
-
|
36
|
-
|
62
|
+
if res.is_ok():
|
63
|
+
self.send("Sleep 0.1")
|
64
|
+
self.send("Print Rows")
|
65
|
+
return res
|
66
|
+
else:
|
67
|
+
return Err("No rows could be read.")
|
37
68
|
|
38
|
-
def
|
39
|
-
|
69
|
+
def send(self, cmd: Union[Command, str]):
|
70
|
+
if not self.controller:
|
71
|
+
raise RuntimeError(
|
72
|
+
"Communication controller must be initialized before sending command. It is currently in offline mode."
|
73
|
+
)
|
74
|
+
self.controller.send(cmd)
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
from ....control.controllers import CommunicationController
|
3
2
|
from ....utils.injector_types import (
|
4
3
|
Draw,
|
@@ -11,19 +10,26 @@ from ....utils.injector_types import (
|
|
11
10
|
SourceType,
|
12
11
|
Wait,
|
13
12
|
)
|
13
|
+
from ....utils.macro import Response
|
14
14
|
from ....utils.table_types import RegisterFlag, Table
|
15
15
|
from ....utils.tray_types import Tray
|
16
16
|
from .device import DeviceController
|
17
17
|
|
18
18
|
|
19
19
|
class InjectorController(DeviceController):
|
20
|
-
|
21
|
-
|
20
|
+
def __init__(
|
21
|
+
self, controller: CommunicationController, table: Table, offline: bool
|
22
|
+
):
|
22
23
|
super().__init__(controller, table, offline)
|
23
24
|
|
24
25
|
def get_row(self, row: int) -> InjectorFunction:
|
25
26
|
def return_tray_loc() -> Tray:
|
26
|
-
|
27
|
+
raise NotImplementedError
|
28
|
+
# unit = self.get_text(row, RegisterFlag.DRAW_LOCATION_UNIT)
|
29
|
+
# tray = self.get_text(row, RegisterFlag.DRAW_LOCATION_TRAY)
|
30
|
+
# x = self.get_text(row, RegisterFlag.DRAW_LOCATION_ROW)
|
31
|
+
# y = self.get_text(row, RegisterFlag.DRAW_LOCATION_COLUMN)
|
32
|
+
# return FiftyFourVialPlate.from_str("P1-A1")
|
27
33
|
|
28
34
|
function = self.get_text(row, RegisterFlag.FUNCTION)
|
29
35
|
if function == "Wait":
|
@@ -34,17 +40,34 @@ class InjectorController(DeviceController):
|
|
34
40
|
# TODO: better error handling
|
35
41
|
is_source = SourceType(self.get_text(row, RegisterFlag.DRAW_SOURCE))
|
36
42
|
is_volume = Mode(self.get_text(row, RegisterFlag.DRAW_VOLUME))
|
37
|
-
vol =
|
43
|
+
vol = (
|
44
|
+
self.get_num(row, RegisterFlag.DRAW_VOLUME_VALUE)
|
45
|
+
if is_volume == Mode.SET
|
46
|
+
else None
|
47
|
+
)
|
38
48
|
if is_source is SourceType.SPECIFIC_LOCATION:
|
39
49
|
return Draw(amount=vol, source=return_tray_loc())
|
40
50
|
elif is_source is SourceType.LOCATION:
|
41
|
-
return Draw(
|
51
|
+
return Draw(
|
52
|
+
amount=vol, location=self.get_text(row, RegisterFlag.DRAW_LOCATION)
|
53
|
+
)
|
42
54
|
elif function == "Remote":
|
43
|
-
return Remote(
|
44
|
-
|
55
|
+
return Remote(
|
56
|
+
command=RemoteCommand(self.get_text(row, RegisterFlag.REMOTE)),
|
57
|
+
duration=int(self.get_num(row, RegisterFlag.REMOTE_DUR)),
|
58
|
+
)
|
59
|
+
raise ValueError("No valid function found.")
|
45
60
|
|
46
61
|
def load(self) -> InjectorTable:
|
47
62
|
rows = self.get_num_rows()
|
48
63
|
if rows.is_ok():
|
49
|
-
|
50
|
-
|
64
|
+
row_response = rows.value
|
65
|
+
if isinstance(row_response, Response):
|
66
|
+
return InjectorTable(
|
67
|
+
functions=[
|
68
|
+
self.get_row(i) for i in range(int(row_response.num_response))
|
69
|
+
]
|
70
|
+
)
|
71
|
+
elif rows.is_err():
|
72
|
+
return InjectorTable(functions=[])
|
73
|
+
raise ValueError("Unexpected error")
|