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
@@ -0,0 +1,230 @@
1
+ """
2
+ Abstract module containing shared logic for Method and Sequence tables.
3
+
4
+ Authors: Lucy Hao
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import abc
10
+ from typing import Optional, Union
11
+
12
+ from result import Err, Result
13
+
14
+ from ....control.controllers.comm import CommunicationController
15
+ from ....utils.macro import Command, Response
16
+ from ....utils.method_types import MethodDetails
17
+ from ....utils.sequence_types import SequenceTable
18
+ from ....utils.table_types import RegisterFlag, Table, TableOperation
19
+
20
+ TableType = Union[MethodDetails, SequenceTable]
21
+
22
+
23
+ class ABCTableController(abc.ABC):
24
+ def __init__(
25
+ self,
26
+ controller: Optional[CommunicationController],
27
+ table: Table,
28
+ ):
29
+ self.controller = controller
30
+ self.table_locator = table
31
+ self.table_state: Optional[TableType] = None
32
+
33
+ def __new__(cls, *args, **kwargs):
34
+ if cls is ABCTableController:
35
+ raise TypeError(f"only children of '{cls.__name__}' may be instantiated")
36
+ return object.__new__(cls, *args, **kwargs)
37
+
38
+ def receive(self) -> Result[Response, str]:
39
+ if self.controller:
40
+ for _ in range(10):
41
+ try:
42
+ return self.controller.receive()
43
+ except IndexError:
44
+ continue
45
+ return Err("Could not parse response")
46
+ else:
47
+ raise ValueError("Controller is offline!")
48
+
49
+ def send(self, cmd: Union[Command, str]):
50
+ if not self.controller:
51
+ raise RuntimeError(
52
+ "Communication controller must be initialized before sending command. It is currently in offline mode."
53
+ )
54
+ self.controller.send(cmd)
55
+
56
+ def sleepy_send(self, cmd: Union[Command, str]):
57
+ if self.controller:
58
+ self.controller.sleepy_send(cmd)
59
+ else:
60
+ raise ValueError("Controller is offline")
61
+
62
+ def sleep(self, seconds: int):
63
+ """
64
+ Tells the HPLC to wait for a specified number of seconds.
65
+
66
+ :param seconds: number of seconds to wait
67
+ """
68
+ self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
69
+
70
+ def get_num(self, row: int, col_name: RegisterFlag) -> Union[int, float]:
71
+ if self.controller:
72
+ return self.controller.get_num_val(
73
+ TableOperation.GET_ROW_VAL.value.format(
74
+ register=self.table_locator.register,
75
+ table_name=self.table_locator.name,
76
+ row=row,
77
+ col_name=col_name.value,
78
+ )
79
+ )
80
+ else:
81
+ raise ValueError("Controller is offline")
82
+
83
+ def get_text(self, row: int, col_name: RegisterFlag) -> str:
84
+ if self.controller:
85
+ return self.controller.get_text_val(
86
+ TableOperation.GET_ROW_TEXT.value.format(
87
+ register=self.table_locator.register,
88
+ table_name=self.table_locator.name,
89
+ row=row,
90
+ col_name=col_name.value,
91
+ )
92
+ )
93
+ else:
94
+ raise ValueError("Controller is offline")
95
+
96
+ def add_new_col_num(self, col_name: RegisterFlag, val: Union[int, float]):
97
+ if not (isinstance(val, int) or isinstance(val, float)):
98
+ raise ValueError(f"{val} must be an int or float.")
99
+ self.sleepy_send(
100
+ TableOperation.NEW_COL_VAL.value.format(
101
+ register=self.table_locator.register,
102
+ table_name=self.table_locator.name,
103
+ col_name=col_name,
104
+ val=val,
105
+ )
106
+ )
107
+
108
+ def add_new_col_text(self, col_name: RegisterFlag, val: str):
109
+ if not isinstance(val, str):
110
+ raise ValueError(f"{val} must be a str.")
111
+ self.sleepy_send(
112
+ TableOperation.NEW_COL_TEXT.value.format(
113
+ register=self.table_locator.register,
114
+ table_name=self.table_locator.name,
115
+ col_name=col_name,
116
+ val=val,
117
+ )
118
+ )
119
+
120
+ def _edit_row_num(
121
+ self, col_name: RegisterFlag, val: Union[int, float], row: Optional[int] = None
122
+ ):
123
+ if not (isinstance(val, int) or isinstance(val, float)):
124
+ raise ValueError(f"{val} must be an int or float.")
125
+ if row:
126
+ num_rows = self.get_num_rows()
127
+ if num_rows.is_ok():
128
+ if num_rows.ok_value.num_response < row:
129
+ raise ValueError("Not enough rows to edit!")
130
+
131
+ self.sleepy_send(
132
+ TableOperation.EDIT_ROW_VAL.value.format(
133
+ register=self.table_locator.register,
134
+ table_name=self.table_locator.name,
135
+ row=row if row is not None else "Rows",
136
+ col_name=col_name,
137
+ val=val,
138
+ )
139
+ )
140
+
141
+ def _edit_row_text(
142
+ self, col_name: RegisterFlag, val: str, row: Optional[int] = None
143
+ ):
144
+ if not isinstance(val, str):
145
+ raise ValueError(f"{val} must be a str.")
146
+ if row:
147
+ num_rows = self.get_num_rows()
148
+ if num_rows.is_ok():
149
+ if num_rows.ok_value.num_response < row:
150
+ raise ValueError("Not enough rows to edit!")
151
+
152
+ self.sleepy_send(
153
+ TableOperation.EDIT_ROW_TEXT.value.format(
154
+ register=self.table_locator.register,
155
+ table_name=self.table_locator.name,
156
+ row=row if row is not None else "Rows",
157
+ col_name=col_name,
158
+ val=val,
159
+ )
160
+ )
161
+
162
+ @abc.abstractmethod
163
+ def get_row(self, row: int):
164
+ pass
165
+
166
+ def delete_row(self, row: int):
167
+ self.sleepy_send(
168
+ TableOperation.DELETE_ROW.value.format(
169
+ register=self.table_locator.register,
170
+ table_name=self.table_locator.name,
171
+ row=row,
172
+ )
173
+ )
174
+
175
+ def add_row(self):
176
+ """
177
+ Adds a row to the provided table for currently loaded method or sequence.
178
+ """
179
+ self.sleepy_send(
180
+ TableOperation.NEW_ROW.value.format(
181
+ register=self.table_locator.register, table_name=self.table_locator.name
182
+ )
183
+ )
184
+
185
+ def delete_table(self):
186
+ """
187
+ Deletes the table for the current loaded method or sequence.
188
+ """
189
+ self.sleepy_send(
190
+ TableOperation.DELETE_TABLE.value.format(
191
+ register=self.table_locator.register, table_name=self.table_locator.name
192
+ )
193
+ )
194
+
195
+ def new_table(self):
196
+ """
197
+ Creates the table for the currently loaded method or sequence.
198
+ """
199
+ self.send(
200
+ TableOperation.CREATE_TABLE.value.format(
201
+ register=self.table_locator.register, table_name=self.table_locator.name
202
+ )
203
+ )
204
+
205
+ def get_num_rows(self) -> Result[Response, str]:
206
+ self.send(
207
+ TableOperation.GET_NUM_ROWS.value.format(
208
+ register=self.table_locator.register,
209
+ table_name=self.table_locator.name,
210
+ col_name=RegisterFlag.NUM_ROWS,
211
+ )
212
+ )
213
+ self.send(
214
+ Command.GET_ROWS_CMD.value.format(
215
+ register=self.table_locator.register,
216
+ table_name=self.table_locator.name,
217
+ col_name=RegisterFlag.NUM_ROWS,
218
+ )
219
+ )
220
+ if self.controller:
221
+ res = self.controller.receive()
222
+ else:
223
+ raise ValueError("Controller is offline")
224
+
225
+ if res.is_ok():
226
+ self.send("Sleep 0.1")
227
+ self.send("Print Rows")
228
+ return res
229
+ else:
230
+ return Err("No rows could be read.")
@@ -10,30 +10,25 @@ been processed.
10
10
  Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
11
11
  """
12
12
 
13
- import os
14
13
  import time
15
- from typing import Optional, Union
14
+ from typing import Optional, Union, Tuple, List
16
15
 
17
16
  from result import Err, Ok, Result
18
17
 
19
18
  from ...utils.macro import (
20
- str_to_status,
21
- HPLCAvailStatus,
22
- HPLCErrorStatus,
23
19
  Command,
20
+ HPLCErrorStatus,
24
21
  Status,
25
- Response,
22
+ str_to_status,
26
23
  )
24
+ from .abc_tables.abc_comm import ABCCommunicationController
27
25
 
28
26
 
29
- class CommunicationController:
27
+ class CommunicationController(ABCCommunicationController):
30
28
  """
31
29
  Class that communicates with Agilent using Macros
32
30
  """
33
31
 
34
- # maximum command number
35
- MAX_CMD_NO = 255
36
-
37
32
  def __init__(
38
33
  self,
39
34
  comm_dir: str,
@@ -48,24 +43,7 @@ class CommunicationController:
48
43
  :param reply_file: Name of reply file
49
44
  :param debug: whether to save log of sent commands
50
45
  """
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
-
66
- # Initialize row counter for table operations
67
- self._most_recent_hplc_status: Status = self.get_status()
68
- self.send("Local Rows")
46
+ super().__init__(comm_dir, cmd_file, reply_file, offline, debug)
69
47
 
70
48
  def get_num_val(self, cmd: str) -> Union[int, float]:
71
49
  tries = 10
@@ -98,7 +76,7 @@ class CommunicationController:
98
76
  if res.is_err():
99
77
  return HPLCErrorStatus.NORESPONSE
100
78
  if res.is_ok():
101
- parsed_response = self.receive().value.string_response
79
+ parsed_response = self.receive().ok_value.string_response
102
80
  self._most_recent_hplc_status = str_to_status(parsed_response)
103
81
  return self._most_recent_hplc_status
104
82
  else:
@@ -108,21 +86,6 @@ class CommunicationController:
108
86
  except IndexError:
109
87
  return HPLCErrorStatus.MALFORMED
110
88
 
111
- def set_status(self):
112
- """Updates current status of HPLC machine"""
113
- self._most_recent_hplc_status = self.get_status()
114
-
115
- def check_if_not_running(self) -> bool:
116
- """Checks if HPLC machine is in an available state, meaning a state that data is not being written.
117
-
118
- :return: whether the HPLC machine is in a safe state to retrieve data back."""
119
- self.set_status()
120
- hplc_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
121
- time.sleep(5)
122
- self.set_status()
123
- hplc_actually_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
124
- return hplc_avail and hplc_actually_avail
125
-
126
89
  def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
127
90
  """Low-level execution primitive. Sends a command string to HPLC.
128
91
 
@@ -186,61 +149,23 @@ class CommunicationController:
186
149
  f"Failed to receive reply to command #{cmd_no} due to {err} caused by {err_msg}."
187
150
  )
188
151
 
189
- def sleepy_send(self, cmd: Union[Command, str]):
190
- self.send("Sleep 0.1")
191
- self.send(cmd)
192
- self.send("Sleep 0.1")
193
-
194
- def send(self, cmd: Union[Command, str]):
195
- """Sends a command to Chemstation.
196
-
197
- :param cmd: Command to be sent to HPLC
198
- """
199
- if self.cmd_no == self.MAX_CMD_NO:
200
- self.reset_cmd_counter()
201
-
202
- cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
203
- self.cmd_no += 1
204
- self._send(cmd_to_send, self.cmd_no)
205
- if self.debug:
206
- f = open("out.txt", "a")
207
- f.write(cmd_to_send + "\n")
208
- f.close()
209
-
210
- def receive(self) -> Result[Response, str]:
211
- """Returns messages received in reply file.
212
-
213
- :return: ChemStation response
214
- """
215
- num_response_prefix = "Numerical Responses:"
216
- str_response_prefix = "String Responses:"
217
- possible_response = self._receive(self.cmd_no)
218
- if possible_response.is_ok():
219
- lines = possible_response.ok_value.splitlines()
220
- for line in lines:
221
- if str_response_prefix in line and num_response_prefix in line:
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
- )
234
- return Err("Could not retrieve HPLC response")
152
+ def get_chemstation_dirs(self) -> Tuple[str, str, List[str]]:
153
+ method_dir, sequence_dir, data_dirs = None, None, None
154
+ self.send(Command.GET_METHOD_DIR)
155
+ res = self.receive()
156
+ if res.is_ok():
157
+ method_dir = res.ok_value.string_response
158
+ self.send(Command.GET_SEQUENCE_DIR)
159
+ res = self.receive()
160
+ if res.is_ok():
161
+ sequence_dir = res.ok_value.string_response
162
+ self.send(Command.GET_DATA_DIRS)
163
+ res = self.receive()
164
+ if res.is_ok():
165
+ data_dirs = res.ok().string_response.split("|")
166
+ if method_dir and sequence_dir and data_dirs:
167
+ return method_dir, sequence_dir, data_dirs
235
168
  else:
236
- return Err(f"Could not establish response to HPLC: {possible_response}")
237
-
238
- def reset_cmd_counter(self):
239
- """Resets the command counter."""
240
- self._send(Command.RESET_COUNTER_CMD.value, cmd_no=self.MAX_CMD_NO + 1)
241
- self._receive(cmd_no=self.MAX_CMD_NO + 1)
242
- self.cmd_no = 0
243
-
244
- def stop_macro(self):
245
- """Stops Macro execution. Connection will be lost."""
246
- self.send(Command.STOP_MACRO_CMD)
169
+ raise ValueError(
170
+ "Please provide the method, sequence and data directories, could not be found."
171
+ )
@@ -3,10 +3,11 @@ from __future__ import annotations
3
3
  import os
4
4
  import time
5
5
  import warnings
6
- from typing import List, Optional, Union, Dict
6
+ from typing import List, Optional, Union, Dict, Set
7
7
 
8
8
  from result import Err, Ok, Result
9
9
 
10
+ from ..abc_tables.run import RunController
10
11
  from ....analysis.process_report import AgilentReport, ReportType
11
12
  from ....control.controllers import CommunicationController
12
13
  from pychemstation.analysis.chromatogram import (
@@ -24,10 +25,9 @@ from ....utils.method_types import (
24
25
  )
25
26
  from ....utils.table_types import RegisterFlag, Table, TableOperation, T
26
27
  from ..devices.injector import InjectorController
27
- from .table import TableController
28
28
 
29
29
 
30
- class MethodController(TableController):
30
+ class MethodController(RunController):
31
31
  """
32
32
  Class containing method related logic
33
33
  """
@@ -413,8 +413,6 @@ class MethodController(TableController):
413
413
  :param stall_while_running: whether to stall or immediately return
414
414
  :param add_timestamp: if should add timestamp to experiment name
415
415
  """
416
-
417
- folder_name = ""
418
416
  hplc_is_running = False
419
417
  tries = 0
420
418
  while tries < 10 and not hplc_is_running:
@@ -427,18 +425,15 @@ class MethodController(TableController):
427
425
  else experiment_name,
428
426
  )
429
427
  )
430
- folder_name = (
431
- f"{experiment_name}_{timestamp}.D"
432
- if add_timestamp
433
- else f"{experiment_name}.D"
434
- )
428
+
435
429
  hplc_is_running = self.check_hplc_is_running()
436
430
  tries += 1
437
431
 
432
+ data_dir, data_file = self.get_current_run_data_dir_file()
438
433
  if not hplc_is_running:
439
434
  raise RuntimeError("Method failed to start.")
440
435
 
441
- self.data_files.append(os.path.join(self.data_dirs[0], folder_name))
436
+ self.data_files.append(os.path.join(os.path.normpath(data_dir), data_file))
442
437
  self.timeout = (self.get_total_runtime()) * 60
443
438
 
444
439
  if stall_while_running:
@@ -446,18 +441,20 @@ class MethodController(TableController):
446
441
  if run_completed.is_ok():
447
442
  self.data_files[-1] = run_completed.ok_value
448
443
  else:
449
- raise RuntimeError("Run error has occurred.")
444
+ raise RuntimeError(f"Run error has occurred:{run_completed.err_value}.")
450
445
  else:
451
- folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
446
+ folder = self.fuzzy_match_most_recent_folder(self.data_files[-1], None)
452
447
  while folder.is_err():
453
- folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
448
+ folder = self.fuzzy_match_most_recent_folder(self.data_files[-1], None)
454
449
  if folder.is_ok():
455
450
  self.data_files[-1] = folder.ok_value
456
451
  else:
457
452
  warning = f"Data folder {self.data_files[-1]} may not exist, returning and will check again after run is done."
458
453
  warnings.warn(warning)
459
454
 
460
- def fuzzy_match_most_recent_folder(self, most_recent_folder: T) -> Result[str, str]:
455
+ def fuzzy_match_most_recent_folder(
456
+ self, most_recent_folder: T, child_dirs: Optional[Set[str]]
457
+ ) -> Result[str, str]:
461
458
  if isinstance(most_recent_folder, str) or isinstance(most_recent_folder, bytes):
462
459
  if os.path.exists(most_recent_folder):
463
460
  return Ok(most_recent_folder)