pychemstation 0.5.1__py3-none-any.whl → 0.5.3.dev1__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.
@@ -13,7 +13,7 @@ Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
13
13
  import os
14
14
  import time
15
15
 
16
- from ..utils.constants import MAX_CMD_NO
16
+ from result import Result, Ok, Err
17
17
  from ..utils.macro import *
18
18
  from ..utils.method_types import *
19
19
 
@@ -23,6 +23,9 @@ class CommunicationController:
23
23
  Class that communicates with Agilent using Macros
24
24
  """
25
25
 
26
+ # maximum command number
27
+ MAX_CMD_NO = 255
28
+
26
29
  def __init__(
27
30
  self,
28
31
  comm_dir: str,
@@ -48,7 +51,23 @@ class CommunicationController:
48
51
 
49
52
  self.reset_cmd_counter()
50
53
 
51
- def get_status(self) -> list[Union[HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus]]:
54
+ def get_num_val(self, cmd: str) -> Union[int, float, Err]:
55
+ self.send(Command.GET_NUM_VAL_CMD.value.format(cmd=cmd))
56
+ res = self.receive()
57
+ if res.is_ok():
58
+ return res.ok_value.num_response
59
+ else:
60
+ raise RuntimeError("Failed to get number.")
61
+
62
+ def get_text_val(self, cmd: str) -> str:
63
+ self.send(Command.GET_TEXT_VAL_CMD.value.format(cmd=cmd))
64
+ res = self.receive()
65
+ if res.is_ok():
66
+ return res.ok_value.string_response
67
+ else:
68
+ raise RuntimeError("Failed to get string")
69
+
70
+ def get_status(self) -> Union[HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus]:
52
71
  """Get device status(es).
53
72
 
54
73
  :return: list of ChemStation's current status
@@ -57,33 +76,28 @@ class CommunicationController:
57
76
  time.sleep(1)
58
77
 
59
78
  try:
60
- parsed_response = self.receive().splitlines()[1].split()[1:]
61
- recieved_status = [str_to_status(res) for res in parsed_response]
62
- self._most_recent_hplc_status = recieved_status[0]
63
- return recieved_status
79
+ parsed_response = self.receive().value.string_response
80
+ self._most_recent_hplc_status = str_to_status(parsed_response)
81
+ return self._most_recent_hplc_status
64
82
  except IOError:
65
- return [HPLCErrorStatus.NORESPONSE]
83
+ return HPLCErrorStatus.NORESPONSE
66
84
  except IndexError:
67
- return [HPLCErrorStatus.MALFORMED]
85
+ return HPLCErrorStatus.MALFORMED
68
86
 
69
87
  def set_status(self):
70
88
  """Updates current status of HPLC machine"""
71
89
  self._most_recent_hplc_status = self.get_status()[0]
72
90
 
73
- def _check_data_status(self, data_path: str) -> bool:
91
+ def check_if_running(self, data_path: str) -> bool:
74
92
  """Checks if HPLC machine is in an available state, meaning a state that data is not being written.
75
93
 
76
94
  :return: whether the HPLC machine is in a safe state to retrieve data back."""
77
- old_status = self._most_recent_hplc_status
78
95
  self.set_status()
79
96
  file_exists = os.path.exists(data_path)
80
- done_writing_data = isinstance(self._most_recent_hplc_status,
81
- HPLCAvailStatus) and old_status != self._most_recent_hplc_status and file_exists
97
+ hplc_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
98
+ done_writing_data = hplc_avail and file_exists
82
99
  return done_writing_data
83
100
 
84
- def check_data(self, data_path: str) -> bool:
85
- return self._check_data_status(data_path)
86
-
87
101
  def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
88
102
  """Low-level execution primitive. Sends a command string to HPLC.
89
103
 
@@ -106,13 +120,13 @@ class CommunicationController:
106
120
  else:
107
121
  raise IOError(f"Failed to send command #{cmd_no}: {cmd}.") from err
108
122
 
109
- def _receive(self, cmd_no: int, num_attempts=100) -> str:
123
+ def _receive(self, cmd_no: int, num_attempts=100) -> Result[str, str]:
110
124
  """Low-level execution primitive. Recives a response from HPLC.
111
125
 
112
126
  :param cmd_no: Command number
113
127
  :param num_attempts: Number of retries to open reply file
114
128
  :raises IOError: Could not read reply file.
115
- :return: ChemStation response
129
+ :return: Potential ChemStation response
116
130
  """
117
131
  err = None
118
132
  for _ in range(num_attempts):
@@ -134,11 +148,11 @@ class CommunicationController:
134
148
 
135
149
  # check that response corresponds to sent command
136
150
  if response_no == cmd_no:
137
- return response
151
+ return Ok(response)
138
152
  else:
139
153
  continue
140
154
  else:
141
- raise IOError(f"Failed to receive reply to command #{cmd_no}.") from err
155
+ return Err(f"Failed to receive reply to command #{cmd_no} due to {err}.")
142
156
 
143
157
  def sleepy_send(self, cmd: Union[Command, str]):
144
158
  self.send("Sleep 0.1")
@@ -150,24 +164,37 @@ class CommunicationController:
150
164
 
151
165
  :param cmd: Command to be sent to HPLC
152
166
  """
153
- if self.cmd_no == MAX_CMD_NO:
167
+ if self.cmd_no == self.MAX_CMD_NO:
154
168
  self.reset_cmd_counter()
155
169
 
156
170
  cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
157
171
  self.cmd_no += 1
158
172
  self._send(cmd_to_send, self.cmd_no)
159
173
 
160
- def receive(self) -> str:
174
+ def receive(self) -> Result[Response, str]:
161
175
  """Returns messages received in reply file.
162
176
 
163
177
  :return: ChemStation response
164
178
  """
165
- return self._receive(self.cmd_no)
179
+ num_response_prefix = "Numerical Responses:"
180
+ str_response_prefix = "String Responses:"
181
+ possible_response = self._receive(self.cmd_no)
182
+ if Ok(possible_response):
183
+ lines = possible_response.value.splitlines()
184
+ for line in lines:
185
+ if str_response_prefix in line and num_response_prefix in line:
186
+ string_responses_dirty, _, numerical_responses = line.partition(num_response_prefix)
187
+ _, _, string_responses = string_responses_dirty.partition(str_response_prefix)
188
+ return Ok(Response(string_response=string_responses.strip(),
189
+ num_response=float(numerical_responses.strip())))
190
+ return Err(f"Could not retrieve HPLC response")
191
+ else:
192
+ return Err(f"Could not establish response to HPLC: {possible_response}")
166
193
 
167
194
  def reset_cmd_counter(self):
168
195
  """Resets the command counter."""
169
- self._send(Command.RESET_COUNTER_CMD.value, cmd_no=MAX_CMD_NO + 1)
170
- self._receive(cmd_no=MAX_CMD_NO + 1)
196
+ self._send(Command.RESET_COUNTER_CMD.value, cmd_no=self.MAX_CMD_NO + 1)
197
+ self._receive(cmd_no=self.MAX_CMD_NO + 1)
171
198
  self.cmd_no = 0
172
199
 
173
200
  def stop_macro(self):
@@ -9,12 +9,24 @@ from typing import Union, Optional
9
9
  from ..control import CommunicationController
10
10
  from ..control.table import MethodController, SequenceController
11
11
  from ..utils.chromatogram import AgilentHPLCChromatogram
12
- from ..utils.macro import Command, HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus
12
+ from ..utils.macro import Command, HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus, Response
13
13
  from ..utils.method_types import MethodTimetable
14
14
  from ..utils.sequence_types import SequenceTable, SequenceEntry
15
+ from ..utils.table_types import Table
15
16
 
16
17
 
17
18
  class HPLCController:
19
+ # tables
20
+ METHOD_TIMETABLE = Table(
21
+ register="RCPMP1Method[1]",
22
+ name="Timetable"
23
+ )
24
+
25
+ SEQUENCE_TABLE = Table(
26
+ register="_sequence[1]",
27
+ name="SeqTable1"
28
+ )
29
+
18
30
  def __init__(self,
19
31
  comm_dir: str,
20
32
  data_dir: str,
@@ -27,16 +39,23 @@ class HPLCController:
27
39
  :raises FileNotFoundError: If either `data_dir`, `method_dir` or `comm_dir` is not a valid directory.
28
40
  """
29
41
  self.comm = CommunicationController(comm_dir=comm_dir)
30
- self.method_controller = MethodController(controller=self.comm, src=method_dir, data_dir=data_dir)
31
- self.sequence_controller = SequenceController(controller=self.comm, src=sequence_dir, data_dir=data_dir)
42
+ self.method_controller = MethodController(controller=self.comm,
43
+ src=method_dir,
44
+ data_dir=data_dir,
45
+ table=self.METHOD_TIMETABLE)
46
+ self.sequence_controller = SequenceController(controller=self.comm,
47
+ src=sequence_dir,
48
+ data_dir=data_dir,
49
+ table=self.SEQUENCE_TABLE,
50
+ method_dir=method_dir)
32
51
 
33
52
  def send(self, cmd: Union[Command, str]):
34
53
  self.comm.send(cmd)
35
54
 
36
- def receive(self) -> str:
37
- return self.comm.receive()
55
+ def receive(self) -> Response:
56
+ return self.comm.receive().value
38
57
 
39
- def status(self) -> list[HPLCRunningStatus | HPLCAvailStatus | HPLCErrorStatus]:
58
+ def status(self) -> Union[HPLCRunningStatus | HPLCAvailStatus | HPLCErrorStatus]:
40
59
  return self.comm.get_status()
41
60
 
42
61
  def switch_method(self, method_name: str):
@@ -56,7 +75,7 @@ class HPLCController:
56
75
  Allows the user to switch between pre-programmed sequences. The sequence name does not need the '.S' extension.
57
76
  For example. for the method named 'mySeq.S', only 'mySeq' is needed.
58
77
 
59
- :param seq_name: The name of the sequence file
78
+ :param sequence_name: The name of the sequence file
60
79
  """
61
80
  self.sequence_controller.switch(sequence_name)
62
81
 
@@ -82,12 +101,13 @@ class HPLCController:
82
101
  """
83
102
  self.sequence_controller.run(sequence_table)
84
103
 
85
- def edit_method(self, updated_method: MethodTimetable):
104
+ def edit_method(self, updated_method: MethodTimetable, save: bool = False):
86
105
  """Updated the currently loaded method in ChemStation with provided values.
87
106
 
88
107
  :param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
108
+ :param save:
89
109
  """
90
- self.method_controller.edit(updated_method)
110
+ self.method_controller.edit(updated_method, save)
91
111
 
92
112
  def edit_sequence(self, updated_sequence: SequenceTable):
93
113
  """
@@ -107,23 +127,29 @@ class HPLCController:
107
127
  """
108
128
  self.sequence_controller.edit_row(row, num)
109
129
 
110
- def get_last_run_method_data(self) -> Optional[dict[str, AgilentHPLCChromatogram]]:
130
+ def get_last_run_method_data(self) -> dict[str, AgilentHPLCChromatogram]:
111
131
  """
112
132
  Returns the last run method data.
113
133
  """
114
- data_valid, data = self.method_controller.get_data()
115
- if data_valid:
116
- return data
117
- return None
134
+ return self.method_controller.get_data()
118
135
 
119
- def get_last_run_sequence_data(self) -> Optional[list[dict[str, AgilentHPLCChromatogram]]]:
136
+ def get_last_run_sequence_data(self) -> list[dict[str, AgilentHPLCChromatogram]]:
120
137
  """
121
138
  Returns data for all rows in the last run sequence data.
122
139
  """
123
- data_valid, data = self.sequence_controller.get_data()
124
- if data_valid:
125
- return data
126
- return None
140
+ return self.sequence_controller.get_data()
141
+
142
+ def load_method(self) -> MethodTimetable:
143
+ """
144
+ Returns the currently loaded method, including its timetable.
145
+ """
146
+ return self.method_controller.load()
147
+
148
+ def load_sequence(self) -> SequenceTable:
149
+ """
150
+ Returns the currently loaded sequence.
151
+ """
152
+ return self.sequence_controller.load()
127
153
 
128
154
  def standby(self):
129
155
  """Switches all modules in standby mode. All lamps and pumps are switched off."""
@@ -7,11 +7,10 @@ from xsdata.formats.dataclass.parsers import XmlParser
7
7
  from .. import CommunicationController
8
8
  from ...control.table.table_controller import TableController
9
9
  from ...generated import PumpMethod, DadMethod, SolventElement
10
- from ...utils.chromatogram import TIME_FORMAT
11
- from ...utils.constants import METHOD_TIMETABLE
10
+ from ...utils.chromatogram import TIME_FORMAT, AgilentHPLCChromatogram
12
11
  from ...utils.macro import Command
13
12
  from ...utils.method_types import PType, TimeTableEntry, Param, MethodTimetable, HPLCMethodParams
14
- from ...utils.table_types import RegisterFlag, TableOperation
13
+ from ...utils.table_types import RegisterFlag, TableOperation, Table
15
14
 
16
15
 
17
16
  class MethodController(TableController):
@@ -19,10 +18,46 @@ class MethodController(TableController):
19
18
  Class containing method related logic
20
19
  """
21
20
 
22
- def __init__(self, controller: CommunicationController, src: str, data_dir: str):
23
- super().__init__(controller, src, data_dir)
21
+ def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table):
22
+ super().__init__(controller, src, data_dir, table)
24
23
 
25
- def is_loaded(self, method_name: str):
24
+ def get_method_params(self) -> HPLCMethodParams:
25
+ return HPLCMethodParams(organic_modifier=self.controller.get_num_val(
26
+ cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
27
+ register=self.table.register,
28
+ register_flag=RegisterFlag.SOLVENT_B_COMPOSITION
29
+ )
30
+ ),
31
+ flow=self.controller.get_num_val(
32
+ cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
33
+ register=self.table.register,
34
+ register_flag=RegisterFlag.FLOW
35
+ )
36
+ ),
37
+ maximum_run_time=self.controller.get_num_val(
38
+ cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
39
+ register=self.table.register,
40
+ register_flag=RegisterFlag.MAX_TIME
41
+ )
42
+ ),
43
+ )
44
+
45
+ def get_row(self, row: int) -> TimeTableEntry:
46
+ return TimeTableEntry(start_time=self.get_num(row, RegisterFlag.TIME),
47
+ organic_modifer=self.get_num(row, RegisterFlag.TIMETABLE_SOLVENT_B_COMPOSITION),
48
+ flow=None)
49
+
50
+ def load(self) -> MethodTimetable:
51
+ rows = self.get_num_rows()
52
+ if rows.is_ok():
53
+ timetable_rows = [self.get_row(r + 1) for r in range(int(rows.ok_value.num_response))]
54
+ return MethodTimetable(
55
+ first_row=self.get_method_params(),
56
+ subsequent_rows=timetable_rows)
57
+ else:
58
+ raise RuntimeError(rows.err_value)
59
+
60
+ def current_method(self, method_name: str):
26
61
  """
27
62
  Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
28
63
 
@@ -30,7 +65,7 @@ class MethodController(TableController):
30
65
  :return: True if method is already loaded
31
66
  """
32
67
  self.send(Command.GET_METHOD_CMD)
33
- parsed_response = self.receive().splitlines()[1].split()[1:][0]
68
+ parsed_response = self.receive()
34
69
  return method_name in parsed_response
35
70
 
36
71
  def switch(self, method_name: str):
@@ -49,18 +84,12 @@ class MethodController(TableController):
49
84
  time.sleep(2)
50
85
  self.send(Command.GET_METHOD_CMD)
51
86
  time.sleep(2)
87
+ res = self.receive()
88
+ if res.is_ok():
89
+ parsed_response = res.ok_value.string_response
90
+ assert parsed_response == f"{method_name}.M", "Switching Methods failed."
52
91
 
53
- # check that method switched
54
- for _ in range(10):
55
- try:
56
- parsed_response = self.receive().splitlines()[1].split()[1:][0]
57
- break
58
- except IndexError:
59
- continue
60
-
61
- assert parsed_response == f"{method_name}.M", "Switching Methods failed."
62
-
63
- def load(self, method_name: str):
92
+ def load_from_disk(self, method_name: str) -> MethodTimetable:
64
93
  """
65
94
  Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
66
95
  organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
@@ -91,25 +120,10 @@ class MethodController(TableController):
91
120
 
92
121
  return MethodTimetable(
93
122
  first_row=HPLCMethodParams(
94
- organic_modifier=Param(val=organic_modifier.percentage,
95
- chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
96
- ptype=PType.NUM),
97
- flow=Param(val=method.flow,
98
- chemstation_key=RegisterFlag.FLOW,
99
- ptype=PType.NUM),
100
- maximum_run_time=Param(val=method.stop_time,
101
- chemstation_key=RegisterFlag.MAX_TIME,
102
- ptype=PType.NUM),
103
- temperature=Param(val=None,
104
- chemstation_key=[RegisterFlag.COLUMN_OVEN_TEMP1,
105
- RegisterFlag.COLUMN_OVEN_TEMP2],
106
- ptype=PType.NUM),
107
- inj_vol=Param(val=None,
108
- chemstation_key=None,
109
- ptype=PType.NUM),
110
- equ_time=Param(val=None,
111
- chemstation_key=None,
112
- ptype=PType.NUM)),
123
+ organic_modifier=organic_modifier.percentage,
124
+ flow=method.flow,
125
+ maximum_run_time=method.stop_time,
126
+ temperature=-1),
113
127
  subsequent_rows=[
114
128
  TimeTableEntry(
115
129
  start_time=tte.time,
@@ -124,7 +138,7 @@ class MethodController(TableController):
124
138
  else:
125
139
  raise FileNotFoundError
126
140
 
127
- def edit(self, updated_method: MethodTimetable):
141
+ def edit(self, updated_method: MethodTimetable, save: bool):
128
142
  """Updated the currently loaded method in ChemStation with provided values.
129
143
 
130
144
  :param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
@@ -138,13 +152,9 @@ class MethodController(TableController):
138
152
  flow: Param = Param(val=updated_method.first_row.flow,
139
153
  chemstation_key=RegisterFlag.FLOW,
140
154
  ptype=PType.NUM)
141
- temperature: Param = Param(val=updated_method.first_row.temperature,
142
- chemstation_key=[RegisterFlag.COLUMN_OVEN_TEMP1,
143
- RegisterFlag.COLUMN_OVEN_TEMP2],
144
- ptype=PType.NUM)
145
155
 
146
156
  # Method settings required for all runs
147
- self.delete_table(METHOD_TIMETABLE)
157
+ self.delete_table()
148
158
  self._update_param(initial_organic_modifier)
149
159
  self._update_param(flow)
150
160
  self._update_param(Param(val="Set",
@@ -159,18 +169,23 @@ class MethodController(TableController):
159
169
 
160
170
  self._update_method_timetable(updated_method.subsequent_rows)
161
171
 
172
+ if save:
173
+ self.send(Command.SAVE_METHOD_CMD.value.format(
174
+ commit_msg=f"saved method at {str(time.time())}"
175
+ ))
176
+
162
177
  def _update_method_timetable(self, timetable_rows: list[TimeTableEntry]):
163
178
  self.sleepy_send('Local Rows')
164
- self._get_table_rows(METHOD_TIMETABLE)
179
+ self.get_num_rows()
165
180
 
166
181
  self.sleepy_send('DelTab RCPMP1Method[1], "Timetable"')
167
- res = self._get_table_rows(METHOD_TIMETABLE)
168
- while "ERROR" not in res:
182
+ res = self.get_num_rows()
183
+ while not res.is_err():
169
184
  self.sleepy_send('DelTab RCPMP1Method[1], "Timetable"')
170
- res = self._get_table_rows(METHOD_TIMETABLE)
185
+ res = self.get_num_rows()
171
186
 
172
187
  self.sleepy_send('NewTab RCPMP1Method[1], "Timetable"')
173
- self._get_table_rows(METHOD_TIMETABLE)
188
+ self.get_num_rows()
174
189
 
175
190
  for i, row in enumerate(timetable_rows):
176
191
  if i == 0:
@@ -188,7 +203,7 @@ class MethodController(TableController):
188
203
  self.send('Sleep 1')
189
204
  else:
190
205
  self.sleepy_send('InsTabRow RCPMP1Method[1], "Timetable"')
191
- self._get_table_rows(METHOD_TIMETABLE)
206
+ self.get_num_rows()
192
207
 
193
208
  self.sleepy_send(
194
209
  f'SetTabText RCPMP1Method[1], "Timetable", Rows, "Function", "SolventComposition"')
@@ -200,7 +215,7 @@ class MethodController(TableController):
200
215
  self.send("Sleep 1")
201
216
  self.sleepy_send("DownloadRCMethod PMP1")
202
217
  self.send("Sleep 1")
203
- self._get_table_rows(METHOD_TIMETABLE)
218
+ self.get_num_rows()
204
219
 
205
220
  def _update_param(self, method_param: Param):
206
221
  """Change a method parameter, changes what is visibly seen in Chemstation GUI.
@@ -208,7 +223,7 @@ class MethodController(TableController):
208
223
 
209
224
  :param method_param: a parameter to update for currently loaded method.
210
225
  """
211
- register = METHOD_TIMETABLE.register
226
+ register = self.table.register
212
227
  setting_command = TableOperation.UPDATE_OBJ_HDR_VAL if method_param.ptype == PType.NUM else TableOperation.UPDATE_OBJ_HDR_TEXT
213
228
  if isinstance(method_param.chemstation_key, list):
214
229
  for register_flag in method_param.chemstation_key:
@@ -238,21 +253,22 @@ class MethodController(TableController):
238
253
  :param experiment_name: Name of the experiment
239
254
  """
240
255
  timestamp = time.strftime(TIME_FORMAT)
241
-
242
256
  self.send(Command.RUN_METHOD_CMD.value.format(data_dir=self.data_dir,
243
257
  experiment_name=experiment_name,
244
258
  timestamp=timestamp))
245
259
 
246
- folder_name = f"{experiment_name}_{timestamp}.D"
247
- self.data_files.append(os.path.join(self.data_dir, folder_name))
260
+ if self.check_hplc_is_running():
261
+ folder_name = f"{experiment_name}_{timestamp}.D"
262
+ self.data_files.append(os.path.join(self.data_dir, folder_name))
263
+
264
+ run_completed = self.check_hplc_done_running()
265
+
266
+ if not run_completed.is_ok():
267
+ raise RuntimeError("Run did not complete as expected")
248
268
 
249
269
  def retrieve_recent_data_files(self) -> str:
250
270
  return self.data_files[-1]
251
271
 
252
- def get_data(self) -> tuple[bool, Any]:
253
- data_ready = self.data_ready()
254
- if data_ready:
255
- self.get_spectrum(self.data_files[-1])
256
- return data_ready, self.spectra
257
- else:
258
- return False, None
272
+ def get_data(self) -> dict[str, AgilentHPLCChromatogram]:
273
+ self.get_spectrum(self.data_files[-1])
274
+ return self.spectra