pychemstation 0.10.5__py3-none-any.whl → 0.10.7__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.
Files changed (29) hide show
  1. pychemstation/analysis/__init__.py +8 -1
  2. pychemstation/control/README.md +1 -1
  3. pychemstation/control/controllers/__init__.py +2 -2
  4. pychemstation/control/controllers/abc_tables/__init__.py +0 -0
  5. pychemstation/control/controllers/abc_tables/abc_comm.py +155 -0
  6. pychemstation/control/controllers/abc_tables/device.py +20 -0
  7. pychemstation/control/controllers/{tables/table.py → abc_tables/run.py} +58 -201
  8. pychemstation/control/controllers/abc_tables/table.py +230 -0
  9. pychemstation/control/controllers/comm.py +26 -101
  10. pychemstation/control/controllers/{tables → data_aq}/method.py +12 -15
  11. pychemstation/control/controllers/{tables → data_aq}/sequence.py +168 -119
  12. pychemstation/control/controllers/devices/__init__.py +3 -0
  13. pychemstation/control/controllers/devices/injector.py +61 -28
  14. pychemstation/control/hplc.py +42 -26
  15. pychemstation/utils/injector_types.py +22 -2
  16. pychemstation/utils/macro.py +11 -0
  17. pychemstation/utils/mocking/__init__.py +0 -0
  18. pychemstation/utils/mocking/mock_comm.py +5 -0
  19. pychemstation/utils/mocking/mock_hplc.py +2 -0
  20. pychemstation/utils/sequence_types.py +22 -2
  21. pychemstation/utils/table_types.py +6 -0
  22. pychemstation/utils/tray_types.py +36 -1
  23. {pychemstation-0.10.5.dist-info → pychemstation-0.10.7.dist-info}/METADATA +3 -3
  24. pychemstation-0.10.7.dist-info/RECORD +42 -0
  25. pychemstation/control/controllers/devices/device.py +0 -74
  26. pychemstation-0.10.5.dist-info/RECORD +0 -36
  27. /pychemstation/control/controllers/{tables → data_aq}/__init__.py +0 -0
  28. {pychemstation-0.10.5.dist-info → pychemstation-0.10.7.dist-info}/WHEEL +0 -0
  29. {pychemstation-0.10.5.dist-info → pychemstation-0.10.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,11 @@
1
1
  from .process_report import CSVProcessor
2
2
  from .process_report import TXTProcessor
3
+ from .chromatogram import AgilentChannelChromatogramData
4
+ from .chromatogram import AgilentHPLCChromatogram
3
5
 
4
- __all__ = ["CSVProcessor", "TXTProcessor"]
6
+ __all__ = [
7
+ "CSVProcessor",
8
+ "TXTProcessor",
9
+ "AgilentChannelChromatogramData",
10
+ "AgilentHPLCChromatogram",
11
+ ]
@@ -10,7 +10,7 @@ DATA_DIR_2 = "C:\\Users\\Public\\Documents\\ChemStation\\2\\Data"
10
10
  DATA_DIR_3 = "C:\\Users\\Public\\Documents\\ChemStation\\3\\Data"
11
11
 
12
12
  # Initialize HPLC Controller
13
- hplc_controller = HPLCController(data_dirs=[DATA_DIR_2, DATA_DIR_3],
13
+ hplc_controller = HPLCController(extra_data_dirs=[DATA_DIR_2, DATA_DIR_3],
14
14
  comm_dir=DEFAULT_COMMAND_PATH,
15
15
  method_dir=DEFAULT_METHOD_DIR,
16
16
  sequence_dir=SEQUENCE_DIR)
@@ -3,6 +3,6 @@
3
3
  """
4
4
 
5
5
  from .comm import CommunicationController
6
- from . import tables
6
+ from . import data_aq
7
7
 
8
- __all__ = ["CommunicationController", "tables"]
8
+ __all__ = ["CommunicationController", "data_aq"]
@@ -0,0 +1,155 @@
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 Status, HPLCAvailStatus, Command, Response
22
+
23
+
24
+ class ABCCommunicationController(abc.ABC):
25
+ """
26
+ Abstract class representing the communication controller.
27
+ """
28
+
29
+ # maximum command number
30
+ MAX_CMD_NO = 255
31
+
32
+ def __init__(
33
+ self,
34
+ comm_dir: str,
35
+ cmd_file: str = "cmd",
36
+ reply_file: str = "reply",
37
+ offline: bool = False,
38
+ debug: bool = False,
39
+ ):
40
+ """
41
+ :param comm_dir:
42
+ :param cmd_file: Name of command file
43
+ :param reply_file: Name of reply file
44
+ :param debug: whether to save log of sent commands
45
+ """
46
+ if not offline:
47
+ self.debug = debug
48
+ if os.path.isdir(comm_dir):
49
+ self.cmd_file = os.path.join(comm_dir, cmd_file)
50
+ self.reply_file = os.path.join(comm_dir, reply_file)
51
+ self.cmd_no = 0
52
+ else:
53
+ raise FileNotFoundError(f"comm_dir: {comm_dir} not found.")
54
+
55
+ # Create files for Chemstation to communicate with Python
56
+ open(self.cmd_file, "a").close()
57
+ open(self.reply_file, "a").close()
58
+
59
+ self.reset_cmd_counter()
60
+ self._most_recent_hplc_status: Status = self.get_status()
61
+ self.send("Local Rows")
62
+
63
+ @abstractmethod
64
+ def get_num_val(self, cmd: str) -> Union[int, float]:
65
+ pass
66
+
67
+ @abstractmethod
68
+ def get_text_val(self, cmd: str) -> str:
69
+ pass
70
+
71
+ @abstractmethod
72
+ def get_status(self) -> Status:
73
+ pass
74
+
75
+ @abstractmethod
76
+ def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
77
+ pass
78
+
79
+ @abstractmethod
80
+ def _receive(self, cmd_no: int, num_attempts=100) -> Result[str, str]:
81
+ pass
82
+
83
+ def set_status(self):
84
+ """Updates current status of HPLC machine"""
85
+ self._most_recent_hplc_status = self.get_status()
86
+
87
+ def check_if_not_running(self) -> bool:
88
+ """Checks if HPLC machine is in an available state, meaning a state that data is not being written.
89
+
90
+ :return: whether the HPLC machine is in a safe state to retrieve data back."""
91
+ self.set_status()
92
+ hplc_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
93
+ time.sleep(5)
94
+ self.set_status()
95
+ hplc_actually_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
96
+ return hplc_avail and hplc_actually_avail
97
+
98
+ def sleepy_send(self, cmd: Union[Command, str]):
99
+ self.send("Sleep 0.1")
100
+ self.send(cmd)
101
+ self.send("Sleep 0.1")
102
+
103
+ def send(self, cmd: Union[Command, str]):
104
+ """Sends a command to Chemstation.
105
+
106
+ :param cmd: Command to be sent to HPLC
107
+ """
108
+ if self.cmd_no == self.MAX_CMD_NO:
109
+ self.reset_cmd_counter()
110
+
111
+ cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
112
+ self.cmd_no += 1
113
+ self._send(cmd_to_send, self.cmd_no)
114
+ if self.debug:
115
+ f = open("out.txt", "a")
116
+ f.write(cmd_to_send + "\n")
117
+ f.close()
118
+
119
+ def receive(self) -> Result[Response, str]:
120
+ """Returns messages received in reply file.
121
+
122
+ :return: ChemStation response
123
+ """
124
+ num_response_prefix = "Numerical Responses:"
125
+ str_response_prefix = "String Responses:"
126
+ possible_response = self._receive(self.cmd_no)
127
+ if possible_response.is_ok():
128
+ lines = possible_response.ok_value.splitlines()
129
+ for line in lines:
130
+ if str_response_prefix in line and num_response_prefix in line:
131
+ string_responses_dirty, _, numerical_responses = line.partition(
132
+ num_response_prefix
133
+ )
134
+ _, _, string_responses = string_responses_dirty.partition(
135
+ str_response_prefix
136
+ )
137
+ return Ok(
138
+ Response(
139
+ string_response=string_responses.strip(),
140
+ num_response=float(numerical_responses.strip()),
141
+ )
142
+ )
143
+ return Err("Could not retrieve HPLC response")
144
+ else:
145
+ return Err(f"Could not establish response to HPLC: {possible_response}")
146
+
147
+ def reset_cmd_counter(self):
148
+ """Resets the command counter."""
149
+ self._send(Command.RESET_COUNTER_CMD.value, cmd_no=self.MAX_CMD_NO + 1)
150
+ self._receive(cmd_no=self.MAX_CMD_NO + 1)
151
+ self.cmd_no = 0
152
+
153
+ def stop_macro(self):
154
+ """Stops Macro execution. Connection will be lost."""
155
+ self.send(Command.STOP_MACRO_CMD)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+
5
+ from ....control.controllers import CommunicationController
6
+ from ....utils.table_types import Table
7
+ from .table import ABCTableController
8
+
9
+
10
+ class DeviceController(ABCTableController, ABC):
11
+ def __init__(
12
+ self, controller: CommunicationController, table: Table, offline: bool
13
+ ):
14
+ super().__init__(controller=controller, table=table)
15
+ self.offline = offline
16
+
17
+ def __new__(cls, *args, **kwargs):
18
+ if cls is ABCTableController:
19
+ raise TypeError(f"only children of '{cls.__name__}' may be instantiated")
20
+ return object.__new__(cls)
@@ -11,11 +11,16 @@ import math
11
11
  import os
12
12
  import time
13
13
  import warnings
14
- from typing import Dict, List, Optional, Tuple, Union
14
+ from typing import Dict, List, Optional, Tuple, Union, Set
15
15
 
16
16
  import polling
17
17
  import rainbow as rb
18
- from result import Err, Result, Ok
18
+ from result import Err, Ok, Result
19
+
20
+ from pychemstation.analysis.chromatogram import (
21
+ AgilentChannelChromatogramData,
22
+ AgilentHPLCChromatogram,
23
+ )
19
24
 
20
25
  from ....analysis.process_report import (
21
26
  AgilentReport,
@@ -24,19 +29,16 @@ from ....analysis.process_report import (
24
29
  TXTProcessor,
25
30
  )
26
31
  from ....control.controllers.comm import CommunicationController
27
- from pychemstation.analysis.chromatogram import (
28
- AgilentChannelChromatogramData,
29
- AgilentHPLCChromatogram,
30
- )
31
- from ....utils.macro import Command, HPLCRunningStatus, Response
32
+ from ....utils.macro import Command, HPLCRunningStatus
32
33
  from ....utils.method_types import MethodDetails
33
34
  from ....utils.sequence_types import SequenceTable
34
- from ....utils.table_types import RegisterFlag, Table, TableOperation, T
35
+ from ....utils.table_types import T, Table
36
+ from .table import ABCTableController
35
37
 
36
38
  TableType = Union[MethodDetails, SequenceTable]
37
39
 
38
40
 
39
- class TableController(abc.ABC):
41
+ class RunController(ABCTableController, abc.ABC):
40
42
  def __init__(
41
43
  self,
42
44
  controller: Optional[CommunicationController],
@@ -45,11 +47,11 @@ class TableController(abc.ABC):
45
47
  table: Table,
46
48
  offline: bool = False,
47
49
  ):
48
- self.controller = controller
49
- self.table_locator = table
50
+ super().__init__(controller=controller, table=table)
50
51
  self.table_state: Optional[TableType] = None
51
52
  self.curr_run_starting_time: Optional[float] = None
52
53
  self.timeout: Optional[float] = None
54
+ self.current_run_child_files: Set[str] = set()
53
55
 
54
56
  if not offline:
55
57
  if src and not os.path.isdir(src):
@@ -76,179 +78,34 @@ class TableController(abc.ABC):
76
78
  self.uv: Dict[int, AgilentHPLCChromatogram] = {}
77
79
  self.data_files: List = []
78
80
 
79
- def receive(self) -> Result[Response, str]:
80
- if self.controller:
81
- for _ in range(10):
82
- try:
83
- return self.controller.receive()
84
- except IndexError:
85
- continue
86
- return Err("Could not parse response")
87
- else:
88
- raise ValueError("Controller is offline!")
89
-
90
- def send(self, cmd: Union[Command, str]):
91
- if not self.controller:
92
- raise RuntimeError(
93
- "Communication controller must be initialized before sending command. It is currently in offline mode."
94
- )
95
- self.controller.send(cmd)
96
-
97
- def sleepy_send(self, cmd: Union[Command, str]):
98
- if self.controller:
99
- self.controller.sleepy_send(cmd)
100
- else:
101
- raise ValueError("Controller is offline")
102
-
103
- def sleep(self, seconds: int):
104
- """
105
- Tells the HPLC to wait for a specified number of seconds.
106
-
107
- :param seconds: number of seconds to wait
108
- """
109
- self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
110
-
111
- def get_num(self, row: int, col_name: RegisterFlag) -> Union[int, float]:
112
- if self.controller:
113
- return self.controller.get_num_val(
114
- TableOperation.GET_ROW_VAL.value.format(
115
- register=self.table_locator.register,
116
- table_name=self.table_locator.name,
117
- row=row,
118
- col_name=col_name.value,
119
- )
120
- )
121
- else:
122
- raise ValueError("Controller is offline")
123
-
124
- def get_text(self, row: int, col_name: RegisterFlag) -> str:
125
- if self.controller:
126
- return self.controller.get_text_val(
127
- TableOperation.GET_ROW_TEXT.value.format(
128
- register=self.table_locator.register,
129
- table_name=self.table_locator.name,
130
- row=row,
131
- col_name=col_name.value,
132
- )
133
- )
134
- else:
135
- raise ValueError("Controller is offline")
136
-
137
- def add_new_col_num(self, col_name: RegisterFlag, val: Union[int, float]):
138
- self.sleepy_send(
139
- TableOperation.NEW_COL_VAL.value.format(
140
- register=self.table_locator.register,
141
- table_name=self.table_locator.name,
142
- col_name=col_name,
143
- val=val,
144
- )
145
- )
146
-
147
- def add_new_col_text(self, col_name: RegisterFlag, val: str):
148
- self.sleepy_send(
149
- TableOperation.NEW_COL_TEXT.value.format(
150
- register=self.table_locator.register,
151
- table_name=self.table_locator.name,
152
- col_name=col_name,
153
- val=val,
154
- )
155
- )
156
-
157
- def _edit_row_num(
158
- self, col_name: RegisterFlag, val: Union[int, float], row: Optional[int] = None
159
- ):
160
- self.sleepy_send(
161
- TableOperation.EDIT_ROW_VAL.value.format(
162
- register=self.table_locator.register,
163
- table_name=self.table_locator.name,
164
- row=row if row is not None else "Rows",
165
- col_name=col_name,
166
- val=val,
167
- )
168
- )
169
-
170
- def _edit_row_text(
171
- self, col_name: RegisterFlag, val: str, row: Optional[int] = None
172
- ):
173
- self.sleepy_send(
174
- TableOperation.EDIT_ROW_TEXT.value.format(
175
- register=self.table_locator.register,
176
- table_name=self.table_locator.name,
177
- row=row if row is not None else "Rows",
178
- col_name=col_name,
179
- val=val,
180
- )
181
- )
81
+ def __new__(cls, *args, **kwargs):
82
+ if cls is RunController:
83
+ raise TypeError(f"only children of '{cls.__name__}' may be instantiated")
84
+ return object.__new__(cls)
182
85
 
183
86
  @abc.abstractmethod
184
- def get_row(self, row: int):
87
+ def fuzzy_match_most_recent_folder(
88
+ self, most_recent_folder: T, child_files: Set[str]
89
+ ) -> Result[T, str]:
185
90
  pass
186
91
 
187
- def delete_row(self, row: int):
188
- self.sleepy_send(
189
- TableOperation.DELETE_ROW.value.format(
190
- register=self.table_locator.register,
191
- table_name=self.table_locator.name,
192
- row=row,
193
- )
194
- )
195
-
196
- def add_row(self):
197
- """
198
- Adds a row to the provided table for currently loaded method or sequence.
199
- """
200
- self.sleepy_send(
201
- TableOperation.NEW_ROW.value.format(
202
- register=self.table_locator.register, table_name=self.table_locator.name
203
- )
204
- )
205
-
206
- def delete_table(self):
207
- """
208
- Deletes the table for the current loaded method or sequence.
209
- """
210
- self.sleepy_send(
211
- TableOperation.DELETE_TABLE.value.format(
212
- register=self.table_locator.register, table_name=self.table_locator.name
213
- )
214
- )
215
-
216
- def new_table(self):
217
- """
218
- Creates the table for the currently loaded method or sequence.
219
- """
220
- self.send(
221
- TableOperation.CREATE_TABLE.value.format(
222
- register=self.table_locator.register, table_name=self.table_locator.name
223
- )
224
- )
92
+ @abc.abstractmethod
93
+ def get_data(
94
+ self, custom_path: Optional[str] = None
95
+ ) -> Union[List[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
96
+ pass
225
97
 
226
- def get_num_rows(self) -> Result[Response, str]:
227
- self.send(
228
- TableOperation.GET_NUM_ROWS.value.format(
229
- register=self.table_locator.register,
230
- table_name=self.table_locator.name,
231
- col_name=RegisterFlag.NUM_ROWS,
232
- )
233
- )
234
- self.send(
235
- Command.GET_ROWS_CMD.value.format(
236
- register=self.table_locator.register,
237
- table_name=self.table_locator.name,
238
- col_name=RegisterFlag.NUM_ROWS,
239
- )
240
- )
241
- if self.controller:
242
- res = self.controller.receive()
243
- else:
244
- raise ValueError("Controller is offline")
98
+ @abc.abstractmethod
99
+ def get_data_uv(
100
+ self, custom_path: str | None = None
101
+ ) -> Dict[int, AgilentHPLCChromatogram]:
102
+ pass
245
103
 
246
- if res.is_ok():
247
- self.send("Sleep 0.1")
248
- self.send("Print Rows")
249
- return res
250
- else:
251
- return Err("No rows could be read.")
104
+ @abc.abstractmethod
105
+ def get_report(
106
+ self, custom_path: str, report_type: ReportType = ReportType.TXT
107
+ ) -> List[AgilentReport]:
108
+ pass
252
109
 
253
110
  def check_hplc_is_running(self) -> bool:
254
111
  if self.controller:
@@ -269,6 +126,12 @@ class TableController(abc.ABC):
269
126
 
270
127
  def check_hplc_run_finished(self) -> Tuple[float, bool]:
271
128
  if self.controller:
129
+ try:
130
+ _, current_run_file = self.get_current_run_data_dir_file()
131
+ sample_file, extension, _ = current_run_file.partition(".D")
132
+ self.current_run_child_files.add(sample_file)
133
+ except Exception:
134
+ pass
272
135
  done_running = self.controller.check_if_not_running()
273
136
  if self.curr_run_starting_time and self.timeout:
274
137
  time_passed = time.time() - self.curr_run_starting_time
@@ -290,6 +153,7 @@ class TableController(abc.ABC):
290
153
 
291
154
  :return: Data file object containing most recent run file information.
292
155
  """
156
+ self.current_run_child_files = set()
293
157
  if self.timeout is not None:
294
158
  finished_run = False
295
159
  minutes = math.ceil(self.timeout / 60)
@@ -319,7 +183,9 @@ class TableController(abc.ABC):
319
183
  else:
320
184
  raise ValueError("Timeout value is None, no comparison can be made.")
321
185
 
322
- check_folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
186
+ check_folder = self.fuzzy_match_most_recent_folder(
187
+ self.data_files[-1], self.current_run_child_files
188
+ )
323
189
  if check_folder.is_ok() and finished_run:
324
190
  return check_folder
325
191
  elif check_folder.is_ok():
@@ -334,28 +200,6 @@ class TableController(abc.ABC):
334
200
  return self.data_files[-1]
335
201
  return Err("Run did not complete as expected")
336
202
 
337
- @abc.abstractmethod
338
- def fuzzy_match_most_recent_folder(self, most_recent_folder: T) -> Result[T, str]:
339
- pass
340
-
341
- @abc.abstractmethod
342
- def get_data(
343
- self, custom_path: Optional[str] = None
344
- ) -> Union[List[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
345
- pass
346
-
347
- @abc.abstractmethod
348
- def get_data_uv(
349
- self, custom_path: str | None = None
350
- ) -> Dict[int, AgilentHPLCChromatogram]:
351
- pass
352
-
353
- @abc.abstractmethod
354
- def get_report(
355
- self, custom_path: str, report_type: ReportType = ReportType.TXT
356
- ) -> List[AgilentReport]:
357
- pass
358
-
359
203
  def get_uv_spectrum(self, path: str):
360
204
  data_uv = rb.agilent.chemstation.parse_file(os.path.join(path, "DAD1.UV"))
361
205
  times = data_uv.xlabels
@@ -397,3 +241,16 @@ class TableController(abc.ABC):
397
241
  def _reset_time(self):
398
242
  self.curr_run_starting_time = None
399
243
  self.timeout = None
244
+
245
+ def get_current_run_data_dir_file(self) -> Tuple[str, str]:
246
+ self.send(Command.GET_CURRENT_RUN_DATA_DIR)
247
+ full_path_name = self.receive()
248
+ self.send(Command.GET_CURRENT_RUN_DATA_FILE)
249
+ current_sample_file = self.receive()
250
+ if full_path_name.is_ok() and current_sample_file.is_ok():
251
+ return (
252
+ full_path_name.ok_value.string_response,
253
+ current_sample_file.ok_value.string_response,
254
+ )
255
+ else:
256
+ raise ValueError("Couldn't read data dir and file.")