pychemstation 0.5.4__py3-none-any.whl → 0.5.6.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.
@@ -1,5 +1,4 @@
1
1
  """
2
2
  .. include:: README.md
3
3
  """
4
- from .comm import CommunicationController
5
4
  from .hplc import HPLCController
@@ -0,0 +1,4 @@
1
+ from .method import MethodController
2
+ from .sequence import SequenceController
3
+ from .table_controller import TableController
4
+ from .comm import CommunicationController
@@ -0,0 +1,206 @@
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 os
14
+ import time
15
+
16
+ from result import Result, Ok, Err
17
+
18
+ from ...utils.macro import *
19
+
20
+
21
+ class CommunicationController:
22
+ """
23
+ Class that communicates with Agilent using Macros
24
+ """
25
+
26
+ # maximum command number
27
+ MAX_CMD_NO = 255
28
+
29
+ def __init__(
30
+ self,
31
+ comm_dir: str,
32
+ cmd_file: str = "cmd",
33
+ reply_file: str = "reply",
34
+ ):
35
+ """
36
+ :param comm_dir:
37
+ :param cmd_file: Name of command file
38
+ :param reply_file: Name of reply file
39
+ """
40
+ if os.path.isdir(comm_dir):
41
+ self.cmd_file = os.path.join(comm_dir, cmd_file)
42
+ self.reply_file = os.path.join(comm_dir, reply_file)
43
+ self.cmd_no = 0
44
+ else:
45
+ raise FileNotFoundError(f"comm_dir: {comm_dir} not found.")
46
+ self._most_recent_hplc_status = None
47
+
48
+ # Create files for Chemstation to communicate with Python
49
+ open(self.cmd_file, "a").close()
50
+ open(self.reply_file, "a").close()
51
+
52
+ self.reset_cmd_counter()
53
+
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]:
71
+ """Get device status(es).
72
+
73
+ :return: list of ChemStation's current status
74
+ """
75
+ self.send(Command.GET_STATUS_CMD)
76
+ time.sleep(1)
77
+
78
+ try:
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
82
+ except IOError:
83
+ return HPLCErrorStatus.NORESPONSE
84
+ except IndexError:
85
+ return HPLCErrorStatus.MALFORMED
86
+
87
+ def set_status(self):
88
+ """Updates current status of HPLC machine"""
89
+ self._most_recent_hplc_status = self.get_status()
90
+
91
+ def check_if_running(self) -> bool:
92
+ """Checks if HPLC machine is in an available state, meaning a state that data is not being written.
93
+
94
+ :return: whether the HPLC machine is in a safe state to retrieve data back."""
95
+ self.set_status()
96
+ hplc_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
97
+ time.sleep(30)
98
+ self.set_status()
99
+ hplc_actually_avail = isinstance(self._most_recent_hplc_status, HPLCAvailStatus)
100
+ return hplc_avail and hplc_actually_avail
101
+
102
+ def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
103
+ """Low-level execution primitive. Sends a command string to HPLC.
104
+
105
+ :param cmd: string to be sent to HPLC
106
+ :param cmd_no: Command number
107
+ :param num_attempts: Number of attempts to send the command before raising exception.
108
+ :raises IOError: Could not write to command file.
109
+ """
110
+ err = None
111
+ for _ in range(num_attempts):
112
+ time.sleep(1)
113
+ try:
114
+ with open(self.cmd_file, "w", encoding="utf8") as cmd_file:
115
+ cmd_file.write(f"{cmd_no} {cmd}")
116
+ except IOError as e:
117
+ err = e
118
+ continue
119
+ else:
120
+ return
121
+ else:
122
+ raise IOError(f"Failed to send command #{cmd_no}: {cmd}.") from err
123
+
124
+ def _receive(self, cmd_no: int, num_attempts=100) -> Result[str, str]:
125
+ """Low-level execution primitive. Recives a response from HPLC.
126
+
127
+ :param cmd_no: Command number
128
+ :param num_attempts: Number of retries to open reply file
129
+ :raises IOError: Could not read reply file.
130
+ :return: Potential ChemStation response
131
+ """
132
+ err = None
133
+ for _ in range(num_attempts):
134
+ time.sleep(1)
135
+
136
+ try:
137
+ with open(self.reply_file, "r", encoding="utf_16") as reply_file:
138
+ response = reply_file.read()
139
+ except OSError as e:
140
+ err = e
141
+ continue
142
+
143
+ try:
144
+ first_line = response.splitlines()[0]
145
+ response_no = int(first_line.split()[0])
146
+ except IndexError as e:
147
+ err = e
148
+ continue
149
+
150
+ # check that response corresponds to sent command
151
+ if response_no == cmd_no:
152
+ return Ok(response)
153
+ else:
154
+ continue
155
+ else:
156
+ return Err(f"Failed to receive reply to command #{cmd_no} due to {err}.")
157
+
158
+ def sleepy_send(self, cmd: Union[Command, str]):
159
+ self.send("Sleep 0.1")
160
+ self.send(cmd)
161
+ self.send("Sleep 0.1")
162
+
163
+ def send(self, cmd: Union[Command, str]):
164
+ """Sends a command to Chemstation.
165
+
166
+ :param cmd: Command to be sent to HPLC
167
+ """
168
+ if self.cmd_no == self.MAX_CMD_NO:
169
+ self.reset_cmd_counter()
170
+
171
+ cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
172
+ self.cmd_no += 1
173
+ self._send(cmd_to_send, self.cmd_no)
174
+ f = open("out.txt", "a")
175
+ f.write(cmd_to_send + "\n")
176
+ f.close()
177
+
178
+ def receive(self) -> Result[Response, str]:
179
+ """Returns messages received in reply file.
180
+
181
+ :return: ChemStation response
182
+ """
183
+ num_response_prefix = "Numerical Responses:"
184
+ str_response_prefix = "String Responses:"
185
+ possible_response = self._receive(self.cmd_no)
186
+ if Ok(possible_response):
187
+ lines = possible_response.value.splitlines()
188
+ for line in lines:
189
+ if str_response_prefix in line and num_response_prefix in line:
190
+ string_responses_dirty, _, numerical_responses = line.partition(num_response_prefix)
191
+ _, _, string_responses = string_responses_dirty.partition(str_response_prefix)
192
+ return Ok(Response(string_response=string_responses.strip(),
193
+ num_response=float(numerical_responses.strip())))
194
+ return Err(f"Could not retrieve HPLC response")
195
+ else:
196
+ return Err(f"Could not establish response to HPLC: {possible_response}")
197
+
198
+ def reset_cmd_counter(self):
199
+ """Resets the command counter."""
200
+ self._send(Command.RESET_COUNTER_CMD.value, cmd_no=self.MAX_CMD_NO + 1)
201
+ self._receive(cmd_no=self.MAX_CMD_NO + 1)
202
+ self.cmd_no = 0
203
+
204
+ def stop_macro(self):
205
+ """Stops Macro execution. Connection will be lost."""
206
+ self.send(Command.STOP_MACRO_CMD)
@@ -0,0 +1,284 @@
1
+ import os
2
+ import time
3
+ from typing import Optional
4
+
5
+ from xsdata.formats.dataclass.parsers import XmlParser
6
+
7
+ from ...control.controllers.table_controller import TableController
8
+ from ...control.controllers.comm import CommunicationController
9
+ from ...generated import PumpMethod, DadMethod, SolventElement
10
+ from ...utils.chromatogram import TIME_FORMAT, AgilentChannelChromatogramData
11
+ from ...utils.macro import Command
12
+ from ...utils.method_types import PType, TimeTableEntry, Param, MethodTimetable, HPLCMethodParams
13
+ from ...utils.table_types import RegisterFlag, TableOperation, Table
14
+
15
+
16
+ class MethodController(TableController):
17
+ """
18
+ Class containing method related logic
19
+ """
20
+
21
+ def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table):
22
+ super().__init__(controller, src, data_dir, table)
23
+
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
+ self.table_state = MethodTimetable(
55
+ first_row=self.get_method_params(),
56
+ subsequent_rows=timetable_rows)
57
+ return self.table_state
58
+ else:
59
+ raise RuntimeError(rows.err_value)
60
+
61
+ def current_method(self, method_name: str):
62
+ """
63
+ Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
64
+
65
+ :param method_name: a Chemstation method
66
+ :return: True if method is already loaded
67
+ """
68
+ self.send(Command.GET_METHOD_CMD)
69
+ parsed_response = self.receive()
70
+ return method_name in parsed_response
71
+
72
+ def switch(self, method_name: str):
73
+ """
74
+ Allows the user to switch between pre-programmed methods. No need to append '.M'
75
+ to the end of the method name. For example. for the method named 'General-Poroshell.M',
76
+ only 'General-Poroshell' is needed.
77
+
78
+ :param method_name: any available method in Chemstation method directory
79
+ :raise IndexError: Response did not have expected format. Try again.
80
+ :raise AssertionError: The desired method is not selected. Try again.
81
+ """
82
+ self.send(Command.SWITCH_METHOD_CMD.value.format(method_dir=self.src,
83
+ method_name=method_name))
84
+
85
+ time.sleep(2)
86
+ self.send(Command.GET_METHOD_CMD)
87
+ time.sleep(2)
88
+ res = self.receive()
89
+ if res.is_ok():
90
+ parsed_response = res.ok_value.string_response
91
+ assert parsed_response == f"{method_name}.M", "Switching Methods failed."
92
+ self.table_state = None
93
+
94
+ def load_from_disk(self, method_name: str) -> MethodTimetable:
95
+ """
96
+ Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
97
+ organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
98
+ only two solvents are being used.
99
+
100
+ :param method_name: name of method to load details of
101
+ :raises FileNotFoundError: Method does not exist
102
+ :return: method details
103
+ """
104
+ method_folder = f"{method_name}.M"
105
+ method_path = os.path.join(self.src, method_folder, "AgilentPumpDriver1.RapidControl.MethodXML.xml")
106
+ dad_path = os.path.join(self.src, method_folder, "Agilent1200erDadDriver1.RapidControl.MethodXML.xml")
107
+
108
+ if os.path.exists(os.path.join(self.src, f"{method_name}.M")):
109
+ parser = XmlParser()
110
+ method = parser.parse(method_path, PumpMethod)
111
+ dad = parser.parse(dad_path, DadMethod)
112
+
113
+ organic_modifier: Optional[SolventElement] = None
114
+ aq_modifier: Optional[SolventElement] = None
115
+
116
+ if len(method.solvent_composition.solvent_element) == 2:
117
+ for solvent in method.solvent_composition.solvent_element:
118
+ if solvent.channel == "Channel_A":
119
+ aq_modifier = solvent
120
+ elif solvent.channel == "Channel_B":
121
+ organic_modifier = solvent
122
+
123
+ self.table_state = MethodTimetable(
124
+ first_row=HPLCMethodParams(
125
+ organic_modifier=organic_modifier.percentage,
126
+ flow=method.flow,
127
+ maximum_run_time=method.stop_time,
128
+ temperature=-1),
129
+ subsequent_rows=[
130
+ TimeTableEntry(
131
+ start_time=tte.time,
132
+ organic_modifer=tte.percent_b,
133
+ flow=method.flow
134
+ ) for tte in method.timetable.timetable_entry
135
+ ],
136
+ dad_wavelengthes=dad.signals.signal,
137
+ organic_modifier=organic_modifier,
138
+ modifier_a=aq_modifier
139
+ )
140
+ return self.table_state
141
+ else:
142
+ raise FileNotFoundError
143
+
144
+ def edit(self, updated_method: MethodTimetable, save: bool):
145
+ """Updated the currently loaded method in ChemStation with provided values.
146
+
147
+ :param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
148
+ """
149
+ initial_organic_modifier: Param = Param(val=updated_method.first_row.organic_modifier,
150
+ chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
151
+ ptype=PType.NUM)
152
+ max_time: Param = Param(val=updated_method.first_row.maximum_run_time,
153
+ chemstation_key=RegisterFlag.MAX_TIME,
154
+ ptype=PType.NUM)
155
+ flow: Param = Param(val=updated_method.first_row.flow,
156
+ chemstation_key=RegisterFlag.FLOW,
157
+ ptype=PType.NUM)
158
+
159
+ # Method settings required for all runs
160
+ self.delete_table()
161
+ self._update_param(initial_organic_modifier)
162
+ self._update_param(flow)
163
+ self._update_param(Param(val="Set",
164
+ chemstation_key=RegisterFlag.STOPTIME_MODE,
165
+ ptype=PType.STR))
166
+ self._update_param(max_time)
167
+ self._update_param(Param(val="Off",
168
+ chemstation_key=RegisterFlag.POSTIME_MODE,
169
+ ptype=PType.STR))
170
+
171
+ self.send("DownloadRCMethod PMP1")
172
+
173
+ self._update_method_timetable(updated_method.subsequent_rows)
174
+
175
+ if save:
176
+ self.send(Command.SAVE_METHOD_CMD.value.format(
177
+ commit_msg=f"saved method at {str(time.time())}"
178
+ ))
179
+
180
+ def _update_method_timetable(self, timetable_rows: list[TimeTableEntry]):
181
+ self.sleepy_send('Local Rows')
182
+ self.get_num_rows()
183
+
184
+ self.sleepy_send('DelTab RCPMP1Method[1], "Timetable"')
185
+ res = self.get_num_rows()
186
+ while not res.is_err():
187
+ self.sleepy_send('DelTab RCPMP1Method[1], "Timetable"')
188
+ res = self.get_num_rows()
189
+
190
+ self.sleepy_send('NewTab RCPMP1Method[1], "Timetable"')
191
+ self.get_num_rows()
192
+
193
+ for i, row in enumerate(timetable_rows):
194
+ if i == 0:
195
+ self.send('Sleep 1')
196
+ self.sleepy_send('InsTabRow RCPMP1Method[1], "Timetable"')
197
+ self.send('Sleep 1')
198
+
199
+ self.sleepy_send('NewColText RCPMP1Method[1], "Timetable", "Function", "SolventComposition"')
200
+ self.sleepy_send(f'NewColVal RCPMP1Method[1], "Timetable", "Time", {row.start_time}')
201
+ self.sleepy_send(
202
+ f'NewColVal RCPMP1Method[1], "Timetable", "SolventCompositionPumpChannel2_Percentage", {row.organic_modifer}')
203
+
204
+ self.send('Sleep 1')
205
+ self.sleepy_send("DownloadRCMethod PMP1")
206
+ self.send('Sleep 1')
207
+ else:
208
+ self.sleepy_send('InsTabRow RCPMP1Method[1], "Timetable"')
209
+ self.get_num_rows()
210
+
211
+ self.sleepy_send(
212
+ f'SetTabText RCPMP1Method[1], "Timetable", Rows, "Function", "SolventComposition"')
213
+ self.sleepy_send(
214
+ f'SetTabVal RCPMP1Method[1], "Timetable", Rows, "Time", {row.start_time}')
215
+ self.sleepy_send(
216
+ f'SetTabVal RCPMP1Method[1], "Timetable", Rows, "SolventCompositionPumpChannel2_Percentage", {row.organic_modifer}')
217
+
218
+ self.send("Sleep 1")
219
+ self.sleepy_send("DownloadRCMethod PMP1")
220
+ self.send("Sleep 1")
221
+ self.get_num_rows()
222
+
223
+ def _update_param(self, method_param: Param):
224
+ """Change a method parameter, changes what is visibly seen in Chemstation GUI.
225
+ (changes the first row in the timetable)
226
+
227
+ :param method_param: a parameter to update for currently loaded method.
228
+ """
229
+ register = self.table.register
230
+ setting_command = TableOperation.UPDATE_OBJ_HDR_VAL if method_param.ptype == PType.NUM else TableOperation.UPDATE_OBJ_HDR_TEXT
231
+ if isinstance(method_param.chemstation_key, list):
232
+ for register_flag in method_param.chemstation_key:
233
+ self.send(setting_command.value.format(register=register,
234
+ register_flag=register_flag,
235
+ val=method_param.val))
236
+ else:
237
+ self.send(setting_command.value.format(register=register,
238
+ register_flag=method_param.chemstation_key,
239
+ val=method_param.val))
240
+ time.sleep(2)
241
+
242
+ def stop(self):
243
+ """
244
+ Stops the method run. A dialog window will pop up and manual intervention may be required.\
245
+ """
246
+ self.send(Command.STOP_METHOD_CMD)
247
+
248
+ def run(self, experiment_name: str, stall_while_running: bool = True):
249
+ """
250
+ This is the preferred method to trigger a run.
251
+ Starts the currently selected method, storing data
252
+ under the <data_dir>/<experiment_name>.D folder.
253
+ The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
254
+ Device must be ready.
255
+
256
+ :param experiment_name: Name of the experiment
257
+ """
258
+ timestamp = time.strftime(TIME_FORMAT)
259
+ self.send(Command.RUN_METHOD_CMD.value.format(data_dir=self.data_dir,
260
+ experiment_name=experiment_name,
261
+ timestamp=timestamp))
262
+
263
+ if self.check_hplc_is_running():
264
+ folder_name = f"{experiment_name}_{timestamp}.D"
265
+ self.data_files.append(os.path.join(self.data_dir, folder_name))
266
+
267
+ if stall_while_running:
268
+ run_completed = self.check_hplc_done_running(method=self.table_state)
269
+ if run_completed.is_ok():
270
+ self.data_files[-1] = run_completed.value
271
+ else:
272
+ raise RuntimeError("Run error has occurred.")
273
+ else:
274
+ self.data_files[-1].dir = self.fuzzy_match_most_recent_folder(folder_name).ok_value
275
+
276
+ def retrieve_recent_data_files(self) -> str:
277
+ return self.data_files[-1]
278
+
279
+ def get_data(self, custom_path: Optional[str] = None) -> AgilentChannelChromatogramData:
280
+ if not custom_path:
281
+ self.get_spectrum(self.data_files[-1])
282
+ else:
283
+ self.get_spectrum(custom_path)
284
+ return AgilentChannelChromatogramData(**self.spectra)
@@ -0,0 +1,213 @@
1
+ from typing import Optional
2
+
3
+
4
+ import os
5
+ import time
6
+
7
+ from ...control.controllers.table_controller import TableController
8
+ from ...control.controllers.comm import CommunicationController
9
+ from ...utils.chromatogram import SEQUENCE_TIME_FORMAT, AgilentChannelChromatogramData
10
+ from ...utils.macro import Command
11
+ from ...utils.sequence_types import SequenceTable, SequenceEntry, SequenceDataFiles, InjectionSource, SampleType
12
+ from ...utils.table_types import TableOperation, RegisterFlag, Table
13
+
14
+
15
+ class SequenceController(TableController):
16
+ """
17
+ Class containing sequence related logic
18
+ """
19
+
20
+ def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table, method_dir: str):
21
+ self.method_dir = method_dir
22
+ super().__init__(controller, src, data_dir, table)
23
+
24
+ def load(self) -> SequenceTable:
25
+ rows = self.get_num_rows()
26
+ self.send(Command.GET_SEQUENCE_CMD)
27
+ seq_name = self.receive()
28
+
29
+ if rows.is_ok() and seq_name.is_ok():
30
+ self.table_state = SequenceTable(
31
+ name=seq_name.ok_value.string_response.partition(".S")[0],
32
+ rows=[self.get_row(r + 1) for r in range(int(rows.ok_value.num_response))])
33
+ return self.table_state
34
+ raise RuntimeError(rows.err_value)
35
+
36
+ def get_row(self, row: int) -> SequenceEntry:
37
+ sample_name = self.get_text(row, RegisterFlag.NAME)
38
+ vial_location = int(self.get_num(row, RegisterFlag.VIAL_LOCATION))
39
+ method = self.get_text(row, RegisterFlag.METHOD)
40
+ num_inj = int(self.get_num(row, RegisterFlag.NUM_INJ))
41
+ inj_vol = int(self.get_text(row, RegisterFlag.INJ_VOL))
42
+ inj_source = InjectionSource(self.get_text(row, RegisterFlag.INJ_SOR))
43
+ sample_type = SampleType(self.get_num(row, RegisterFlag.SAMPLE_TYPE))
44
+ return SequenceEntry(sample_name=sample_name,
45
+ vial_location=vial_location,
46
+ method=None if len(method) == 0 else method,
47
+ num_inj=num_inj,
48
+ inj_vol=inj_vol,
49
+ inj_source=inj_source,
50
+ sample_type=sample_type, )
51
+
52
+ def switch(self, seq_name: str):
53
+ """
54
+ Switch to the specified sequence. The sequence name does not need the '.S' extension.
55
+
56
+ :param seq_name: The name of the sequence file
57
+ """
58
+ self.send(f'_SeqFile$ = "{seq_name}.S"')
59
+ self.send(f'_SeqPath$ = "{self.src}"')
60
+ self.send(Command.SWITCH_SEQUENCE_CMD)
61
+ time.sleep(2)
62
+ self.send(Command.GET_SEQUENCE_CMD)
63
+ time.sleep(2)
64
+ parsed_response = self.receive().value.string_response
65
+
66
+ assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
67
+ self.table_state = None
68
+
69
+ def edit(self, sequence_table: SequenceTable):
70
+ """
71
+ Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
72
+ If you would only like to edit a single row of a sequence table, use `edit_sequence_table_row` instead.
73
+
74
+ :param sequence_table:
75
+ """
76
+
77
+ rows = self.get_num_rows()
78
+ if rows.is_ok():
79
+ existing_row_num = rows.value.num_response
80
+ wanted_row_num = len(sequence_table.rows)
81
+ while existing_row_num != wanted_row_num:
82
+ if wanted_row_num > existing_row_num:
83
+ self.add_row()
84
+ elif wanted_row_num < existing_row_num:
85
+ self.delete_row(int(existing_row_num))
86
+ self.send(Command.SAVE_SEQUENCE_CMD)
87
+ existing_row_num = self.get_num_rows().ok_value.num_response
88
+ self.send(Command.SWITCH_SEQUENCE_CMD)
89
+
90
+ for i, row in enumerate(sequence_table.rows):
91
+ self.edit_row(row=row, row_num=i + 1)
92
+ self.sleep(1)
93
+ self.send(Command.SAVE_SEQUENCE_CMD)
94
+ self.send(Command.SWITCH_SEQUENCE_CMD)
95
+
96
+ def edit_row(self, row: SequenceEntry, row_num: int):
97
+ """
98
+ Edits a row in the sequence table. If a row does NOT exist, a new one will be created.
99
+
100
+ :param row: sequence row entry with updated information
101
+ :param row_num: the row to edit, based on 1-based indexing
102
+ """
103
+ num_rows = self.get_num_rows()
104
+ if num_rows.is_ok():
105
+ while num_rows.ok_value.num_response < row_num:
106
+ self.add_row()
107
+ self.send(Command.SAVE_SEQUENCE_CMD)
108
+ num_rows = self.get_num_rows()
109
+
110
+ table_register = self.table.register
111
+ table_name = self.table.name
112
+
113
+ if row.vial_location:
114
+ loc = row.vial_location
115
+ if isinstance(row.vial_location, InjectionSource):
116
+ loc = row.vial_location.value
117
+ self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=table_register,
118
+ table_name=table_name,
119
+ row=row_num,
120
+ col_name=RegisterFlag.VIAL_LOCATION,
121
+ val=loc))
122
+ if row.method:
123
+ possible_path = os.path.join(self.method_dir, row.method) + ".M\\"
124
+ method = row.method
125
+ if os.path.exists(possible_path):
126
+ method = os.path.join(self.method_dir, row.method)
127
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=table_register,
128
+ table_name=table_name,
129
+ row=row_num,
130
+ col_name=RegisterFlag.METHOD,
131
+ val=method))
132
+
133
+ if row.num_inj:
134
+ self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=table_register,
135
+ table_name=table_name,
136
+ row=row_num,
137
+ col_name=RegisterFlag.NUM_INJ,
138
+ val=row.num_inj))
139
+
140
+ if row.inj_vol:
141
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=table_register,
142
+ table_name=table_name,
143
+ row=row_num,
144
+ col_name=RegisterFlag.INJ_VOL,
145
+ val=row.inj_vol))
146
+
147
+ if row.inj_source:
148
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=table_register,
149
+ table_name=table_name,
150
+ row=row_num,
151
+ col_name=RegisterFlag.INJ_SOR,
152
+ val=row.inj_source.value))
153
+
154
+ if row.sample_name:
155
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=table_register,
156
+ table_name=table_name,
157
+ row=row_num,
158
+ col_name=RegisterFlag.NAME,
159
+ val=row.sample_name))
160
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=table_register,
161
+ table_name=table_name,
162
+ row=row_num,
163
+ col_name=RegisterFlag.DATA_FILE,
164
+ val=row.sample_name))
165
+ if row.sample_type:
166
+ self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=table_register,
167
+ table_name=table_name,
168
+ row=row_num,
169
+ col_name=RegisterFlag.SAMPLE_TYPE,
170
+ val=row.sample_type.value))
171
+
172
+ self.send(Command.SAVE_SEQUENCE_CMD)
173
+
174
+ def run(self, stall_while_running: bool = True):
175
+ """
176
+ Starts the currently loaded sequence, storing data
177
+ under the <data_dir>/<sequence table name> folder.
178
+ Device must be ready.
179
+ """
180
+ timestamp = time.strftime(SEQUENCE_TIME_FORMAT)
181
+ seq_table = self.load()
182
+ self.send(Command.RUN_SEQUENCE_CMD.value)
183
+
184
+ if self.check_hplc_is_running():
185
+ folder_name = f"{seq_table.name} {timestamp}"
186
+ self.data_files.append(SequenceDataFiles(dir=folder_name,
187
+ sequence_name=seq_table.name))
188
+
189
+ if stall_while_running:
190
+ run_completed = self.check_hplc_done_running(sequence=seq_table)
191
+ if run_completed.is_ok():
192
+ self.data_files[-1].dir = run_completed.value
193
+ else:
194
+ raise RuntimeError("Run error has occurred.")
195
+ else:
196
+ self.data_files[-1].dir = self.fuzzy_match_most_recent_folder(folder_name).ok_value
197
+
198
+ def retrieve_recent_data_files(self):
199
+ sequence_data_files: SequenceDataFiles = self.data_files[-1]
200
+ return sequence_data_files.dir
201
+
202
+ def get_data(self, custom_path:Optional[str] = None ) -> list[AgilentChannelChromatogramData]:
203
+ parent_dir = self.data_files[-1].dir if not custom_path else custom_path
204
+ subdirs = [x[0] for x in os.walk(self.data_dir)]
205
+ potential_folders = sorted(list(filter(lambda d: parent_dir in d, subdirs)))
206
+ self.data_files[-1].child_dirs = [f for f in potential_folders if
207
+ parent_dir in f and ".M" not in f and ".D" in f]
208
+
209
+ spectra: list[AgilentChannelChromatogramData] = []
210
+ for row in self.data_files[-1].child_dirs:
211
+ self.get_spectrum(row)
212
+ spectra.append(AgilentChannelChromatogramData(**self.spectra))
213
+ return spectra
@@ -0,0 +1,208 @@
1
+ """
2
+ Abstract module containing shared logic for Method and Sequence tables.
3
+
4
+ Authors: Lucy Hao
5
+ """
6
+
7
+ import abc
8
+ import os
9
+ from typing import Union, Optional
10
+
11
+ import polling
12
+ from result import Result, Ok, Err
13
+
14
+ from ...control.controllers.comm import CommunicationController
15
+ from ...utils.chromatogram import AgilentHPLCChromatogram, AgilentChannelChromatogramData
16
+ from ...utils.macro import Command, HPLCRunningStatus, Response
17
+ from ...utils.method_types import MethodTimetable
18
+ from ...utils.sequence_types import SequenceDataFiles, SequenceTable
19
+ from ...utils.table_types import Table, TableOperation, RegisterFlag
20
+
21
+
22
+ class TableController(abc.ABC):
23
+
24
+ def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table):
25
+ self.controller = controller
26
+ self.table = table
27
+ self.table_state : Optional[TableController] = None
28
+
29
+ if os.path.isdir(src):
30
+ self.src: str = src
31
+ else:
32
+ raise FileNotFoundError(f"dir: {src} not found.")
33
+
34
+ if os.path.isdir(data_dir):
35
+ self.data_dir: str = data_dir
36
+ else:
37
+ raise FileNotFoundError(f"dir: {data_dir} not found.")
38
+
39
+ self.spectra: dict[str, AgilentHPLCChromatogram] = {
40
+ "A": AgilentHPLCChromatogram(self.data_dir),
41
+ "B": AgilentHPLCChromatogram(self.data_dir),
42
+ "C": AgilentHPLCChromatogram(self.data_dir),
43
+ "D": AgilentHPLCChromatogram(self.data_dir),
44
+ }
45
+
46
+ self.data_files: Union[list[SequenceDataFiles], list[str]] = []
47
+
48
+ def receive(self) -> Result[Response, str]:
49
+ for _ in range(10):
50
+ try:
51
+ return self.controller.receive()
52
+ except IndexError:
53
+ continue
54
+ return Err("Could not parse response")
55
+
56
+ def send(self, cmd: Union[Command, str]):
57
+ self.controller.send(cmd)
58
+
59
+ def sleepy_send(self, cmd: Union[Command, str]):
60
+ self.controller.sleepy_send(cmd)
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) -> float:
71
+ return self.controller.get_num_val(TableOperation.GET_ROW_VAL.value.format(register=self.table.register,
72
+ table_name=self.table.name,
73
+ row=row,
74
+ col_name=col_name.value))
75
+
76
+ def get_text(self, row: int, col_name: RegisterFlag) -> str:
77
+ return self.controller.get_text_val(TableOperation.GET_ROW_TEXT.value.format(register=self.table.register,
78
+ table_name=self.table.name,
79
+ row=row,
80
+ col_name=col_name.value))
81
+
82
+ @abc.abstractmethod
83
+ def get_row(self, row: int):
84
+ pass
85
+
86
+ def delete_row(self, row: int):
87
+ self.sleepy_send(TableOperation.DELETE_ROW.value.format(register=self.table.register,
88
+ table_name=self.table.name,
89
+ row=row))
90
+
91
+ def add_row(self):
92
+ """
93
+ Adds a row to the provided table for currently loaded method or sequence.
94
+ Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
95
+ You can also provide your own table.
96
+
97
+ :param table: the table to add a new row to
98
+ """
99
+ self.sleepy_send(TableOperation.NEW_ROW.value.format(register=self.table.register,
100
+ table_name=self.table.name))
101
+
102
+ def delete_table(self):
103
+ """
104
+ Deletes the table for the current loaded method or sequence.
105
+ Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
106
+ You can also provide your own table.
107
+
108
+ :param table: the table to delete
109
+ """
110
+ self.sleepy_send(TableOperation.DELETE_TABLE.value.format(register=self.table.register,
111
+ table_name=self.table.name))
112
+
113
+ def new_table(self):
114
+ """
115
+ Creates the table for the currently loaded method or sequence. Import either the SEQUENCE_TABLE or
116
+ METHOD_TIMETABLE from hein_analytical_control.constants. You can also provide your own table.
117
+
118
+ :param table: the table to create
119
+ """
120
+ self.send(TableOperation.CREATE_TABLE.value.format(register=self.table.register,
121
+ table_name=self.table.name))
122
+
123
+ def get_num_rows(self) -> Result[Response, str]:
124
+ self.send(TableOperation.GET_NUM_ROWS.value.format(register=self.table.register,
125
+ table_name=self.table.name,
126
+ col_name=RegisterFlag.NUM_ROWS))
127
+ self.send(Command.GET_ROWS_CMD.value.format(register=self.table.register,
128
+ table_name=self.table.name,
129
+ col_name=RegisterFlag.NUM_ROWS))
130
+ res = self.controller.receive()
131
+
132
+ if res.is_ok():
133
+ self.send("Sleep 0.1")
134
+ self.send('Print Rows')
135
+ return res
136
+ else:
137
+ return Err("No rows could be read.")
138
+
139
+ def check_hplc_is_running(self) -> bool:
140
+ started_running = polling.poll(
141
+ lambda: isinstance(self.controller.get_status(), HPLCRunningStatus),
142
+ step=5,
143
+ max_tries=100)
144
+ return started_running
145
+
146
+ def check_hplc_done_running(self,
147
+ method: Optional[MethodTimetable] = None,
148
+ sequence: Optional[SequenceTable] = None) -> Result[str, str]:
149
+ """
150
+ Checks if ChemStation has finished running and can read data back
151
+
152
+ :param method: if you are running a method and want to read back data, the timeout period will be adjusted to be longer than the method's runtime
153
+ :return: Return True if data can be read back, else False.
154
+ """
155
+ timeout = 10 * 60
156
+ if method:
157
+ timeout = ((method.first_row.maximum_run_time + 5) * 60)
158
+ if sequence:
159
+ timeout *= len(sequence.rows)
160
+
161
+ most_recent_folder = self.retrieve_recent_data_files()
162
+ finished_run = polling.poll(
163
+ lambda: self.controller.check_if_running(),
164
+ timeout=timeout,
165
+ step=18)
166
+
167
+ check_folder = self.fuzzy_match_most_recent_folder(most_recent_folder)
168
+ if check_folder.is_ok() and finished_run:
169
+ return check_folder
170
+ elif check_folder.is_ok():
171
+ finished_run = polling.poll(
172
+ lambda: self.controller.check_if_running(),
173
+ timeout=timeout,
174
+ step=12)
175
+ if finished_run:
176
+ return check_folder
177
+ else:
178
+ return Err("Run did not complete as expected")
179
+
180
+ def fuzzy_match_most_recent_folder(self, most_recent_folder) -> Result[str, str]:
181
+ if os.path.exists(most_recent_folder):
182
+ return Ok(most_recent_folder)
183
+
184
+ subdirs = [x[0] for x in os.walk(self.data_dir)]
185
+ potential_folders = sorted(list(filter(lambda d: most_recent_folder in d, subdirs)))
186
+ parent_dirs = []
187
+ for folder in potential_folders:
188
+ path = os.path.normpath(folder)
189
+ split_folder = path.split(os.sep)
190
+ if most_recent_folder in split_folder[-1]:
191
+ parent_dirs.append(folder)
192
+ parent_dir = sorted(parent_dirs, reverse=True)[0]
193
+ return Ok(parent_dir)
194
+
195
+ @abc.abstractmethod
196
+ def retrieve_recent_data_files(self):
197
+ pass
198
+
199
+ @abc.abstractmethod
200
+ def get_data(self) -> Union[list[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
201
+ pass
202
+
203
+ def get_spectrum(self, data_file: str):
204
+ """
205
+ Load chromatogram for any channel in spectra dictionary.
206
+ """
207
+ for channel, spec in self.spectra.items():
208
+ spec.load_spectrum(data_path=data_file, channel=channel)
@@ -6,9 +6,8 @@ Authors: Lucy Hao
6
6
 
7
7
  from typing import Union, Optional
8
8
 
9
- from ..control import CommunicationController
10
- from ..control.table import MethodController, SequenceController
11
- from ..utils.chromatogram import AgilentHPLCChromatogram
9
+ from ..control.controllers import MethodController, SequenceController, CommunicationController
10
+ from ..utils.chromatogram import AgilentHPLCChromatogram, AgilentChannelChromatogramData
12
11
  from ..utils.macro import Command, HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus, Response
13
12
  from ..utils.method_types import MethodTimetable
14
13
  from ..utils.sequence_types import SequenceTable, SequenceEntry
@@ -65,8 +64,8 @@ class HPLCController:
65
64
  only 'General-Poroshell' is needed.
66
65
 
67
66
  :param method_name: any available method in Chemstation method directory
68
- :raise IndexError: Response did not have expected format. Try again.
69
- :raise AssertionError: The desired method is not selected. Try again.
67
+ :raises IndexError: Response did not have expected format. Try again.
68
+ :raises AssertionError: The desired method is not selected. Try again.
70
69
  """
71
70
  self.method_controller.switch(method_name)
72
71
 
@@ -127,13 +126,13 @@ class HPLCController:
127
126
  """
128
127
  self.sequence_controller.edit_row(row, num)
129
128
 
130
- def get_last_run_method_data(self) -> dict[str, AgilentHPLCChromatogram]:
129
+ def get_last_run_method_data(self) -> AgilentChannelChromatogramData:
131
130
  """
132
131
  Returns the last run method data.
133
132
  """
134
133
  return self.method_controller.get_data()
135
134
 
136
- def get_last_run_sequence_data(self) -> list[dict[str, AgilentHPLCChromatogram]]:
135
+ def get_last_run_sequence_data(self) -> list[AgilentChannelChromatogramData]:
137
136
  """
138
137
  Returns data for all rows in the last run sequence data.
139
138
  """
@@ -12,8 +12,8 @@ from typing import Union, Optional
12
12
  import polling
13
13
  from result import Result, Ok, Err
14
14
 
15
- from ...control import CommunicationController
16
- from ...utils.chromatogram import AgilentHPLCChromatogram
15
+ from ...control.controllers.comm import CommunicationController
16
+ from ...utils.chromatogram import AgilentHPLCChromatogram, AgilentChannelChromatogramData
17
17
  from ...utils.macro import Command, HPLCRunningStatus, Response
18
18
  from ...utils.method_types import MethodTimetable
19
19
  from ...utils.sequence_types import SequenceDataFiles, SequenceTable
@@ -139,8 +139,8 @@ class TableController(abc.ABC):
139
139
  def check_hplc_is_running(self) -> bool:
140
140
  started_running = polling.poll(
141
141
  lambda: isinstance(self.controller.get_status(), HPLCRunningStatus),
142
- step=30,
143
- max_tries=10)
142
+ step=5,
143
+ max_tries=100)
144
144
  return started_running
145
145
 
146
146
  def check_hplc_done_running(self,
@@ -162,32 +162,35 @@ class TableController(abc.ABC):
162
162
  finished_run = polling.poll(
163
163
  lambda: self.controller.check_if_running(),
164
164
  timeout=timeout,
165
- step=60
165
+ step=12
166
166
  )
167
167
 
168
168
  if finished_run:
169
169
  if os.path.exists(most_recent_folder):
170
170
  return Ok(most_recent_folder)
171
171
  else:
172
- subdirs = [x[0] for x in os.walk(self.data_dir)]
173
- potential_folders = sorted(list(filter(lambda d: most_recent_folder in d, subdirs)))
174
- parent_dirs = []
175
- for folder in potential_folders:
176
- path = os.path.normpath(folder)
177
- split_folder = path.split(os.sep)
178
- if most_recent_folder in split_folder[-1]:
179
- parent_dirs.append(folder)
180
- parent_dir = sorted(parent_dirs, reverse=True)[0]
181
- return Ok(parent_dir)
172
+ return self.fuzzy_match_most_recent_folder(most_recent_folder)
182
173
  else:
183
174
  return Err("Run did not complete as expected")
184
175
 
176
+ def fuzzy_match_most_recent_folder(self, most_recent_folder) -> Result[str, str]:
177
+ subdirs = [x[0] for x in os.walk(self.data_dir)]
178
+ potential_folders = sorted(list(filter(lambda d: most_recent_folder in d, subdirs)))
179
+ parent_dirs = []
180
+ for folder in potential_folders:
181
+ path = os.path.normpath(folder)
182
+ split_folder = path.split(os.sep)
183
+ if most_recent_folder in split_folder[-1]:
184
+ parent_dirs.append(folder)
185
+ parent_dir = sorted(parent_dirs, reverse=True)[0]
186
+ return Ok(parent_dir)
187
+
185
188
  @abc.abstractmethod
186
189
  def retrieve_recent_data_files(self):
187
190
  pass
188
191
 
189
192
  @abc.abstractmethod
190
- def get_data(self) -> tuple[bool,]:
193
+ def get_data(self) -> Union[list[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
191
194
  pass
192
195
 
193
196
  def get_spectrum(self, data_file: str):
@@ -1,17 +1,14 @@
1
1
  """Module for HPLC chromatogram data loading and manipulating"""
2
2
 
3
3
  import os
4
- import logging
5
4
  import time
5
+ from dataclasses import dataclass
6
6
 
7
7
  import numpy as np
8
8
 
9
9
  from .parsing import CHFile
10
10
  from ..analysis import AbstractSpectrum
11
11
 
12
- # Chemstation data path
13
- DATA_DIR = r"C:\Chem32\1\Data"
14
-
15
12
  # standard filenames for spectral data
16
13
  CHANNELS = {"A": "01", "B": "02", "C": "03", "D": "04"}
17
14
 
@@ -19,7 +16,7 @@ ACQUISITION_PARAMETERS = "acq.txt"
19
16
 
20
17
  # format used in acquisition parameters
21
18
  TIME_FORMAT = "%Y-%m-%d %H-%M-%S"
22
- SEQUENCE_TIME_FORMAT = "%Y-%m-%d %H"
19
+ SEQUENCE_TIME_FORMAT = "%Y-%m-%d %H-%M"
23
20
 
24
21
 
25
22
  class AgilentHPLCChromatogram(AbstractSpectrum):
@@ -105,26 +102,10 @@ class AgilentHPLCChromatogram(AbstractSpectrum):
105
102
  np.savez_compressed(npz_file, times=data.times, values=data.values)
106
103
  return np.array(data.times), np.array(data.values)
107
104
 
108
- def extract_peakarea(self, experiment_dir: str):
109
- """
110
- Reads processed data from Chemstation report files.
111
105
 
112
- Args:
113
- experiment_dir: .D directory with the report files
114
- """
115
- # filename = os.path.join(experiment_dir, f"REPORT{CHANNELS[channel]}.csv")
116
- # TODO parse file properly
117
- # data = np.genfromtxt(filename, delimiter=',')
118
- # return data
119
- pass
120
-
121
- def default_processing(self):
122
- """
123
- Processes the chromatogram in place.
124
- """
125
- # trim first 5 min and last 3 min of run
126
- self.trim(5, 25)
127
- # parameters found to work best for chromatogram data
128
- self.correct_baseline(lmbd=1e5, p=0.0001, n_iter=10)
129
- # get all peaks in processed chromatogram
130
- self.find_peaks()
106
+ @dataclass
107
+ class AgilentChannelChromatogramData:
108
+ A: AgilentHPLCChromatogram
109
+ B: AgilentHPLCChromatogram
110
+ C: AgilentHPLCChromatogram
111
+ D: AgilentHPLCChromatogram
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pychemstation
3
- Version: 0.5.4
3
+ Version: 0.5.6.dev1
4
4
  Summary: Library to interact with Chemstation software, primarily used in Hein lab
5
5
  Home-page: https://gitlab.com/heingroup/device-api/pychemstation
6
6
  Author: Lucy Hao
@@ -41,7 +41,7 @@ pip install pychemstation
41
41
  where you will put your
42
42
  MACRO file(s).
43
43
  3. Download the [
44
- `hplc_talk.mac`](https://github.com/croningp/analyticallabware/blob/master/AnalyticalLabware/devices/Agilent/hplctalk.mac).
44
+ `hplc_talk.mac`](https://gitlab.com/heingroup/device-api/pychemstation/-/blob/main/tests/hplc_talk.mac).
45
45
  - On line 69, change the path name up to `\cmd` and `\reply`. For instance, you should have:
46
46
  `MonitorFile "[my path]\cmd", "[my path]\reply"`
47
47
  - and then add this file to the folder from the previous step.
@@ -78,13 +78,12 @@ hplc_controller = HPLCController(data_dir=DATA_DIR,
78
78
  hplc_controller.preprun()
79
79
  hplc_controller.switch_method(method_name=DEFAULT_METHOD)
80
80
  hplc_controller.run_method(experiment_name="Run 10")
81
- data_status_valid, chrom = hplc_controller.get_last_run_method_data()
81
+ chrom = hplc_controller.get_last_run_method_data()
82
82
 
83
- if data_status_valid:
84
- # afterwards, save, analyze or plot the data!
85
- values = {"x": chrom.x, "y": chrom.y}
86
- chromatogram_data = pd.DataFrame.from_dict(values)
87
- chromatogram_data.to_csv("Run 10.csv", index=False)
83
+ # afterwards, save, analyze or plot the data!
84
+ values = {"x": chrom.A.x, "y": chrom.A.y}
85
+ chromatogram_data = pd.DataFrame.from_dict(values)
86
+ chromatogram_data.to_csv("Run 10.csv", index=False)
88
87
  ```
89
88
 
90
89
  ## Adding your own MACROs
@@ -103,3 +102,6 @@ Lucy Hao
103
102
 
104
103
  - Adapted from [**AnalyticalLabware**](https://github.com/croningp/analyticallabware), created by members in the Cronin
105
104
  Group. Copyright © Cronin Group, used under the [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) license.
105
+ - Adapted from the [MACROS](https://github.com/Bourne-Group/HPLCMethodOptimisationGUI)
106
+ used in [**Operator-free HPLC automated method development guided by Bayesian optimization**](https://pubs.rsc.org/en/content/articlelanding/2024/dd/d4dd00062e),
107
+ created by members in the Bourne Group. Copyright © Bourne Group, used under the [MIT](https://opensource.org/license/mit) license.
@@ -3,18 +3,23 @@ pychemstation/analysis/__init__.py,sha256=EWoU47iyn9xGS-b44zK9eq50bSjOV4AC5dvt42
3
3
  pychemstation/analysis/base_spectrum.py,sha256=FBvwzLtF9mdqW7f8ETY9G4cpfJ-SzbiSkZq9EtXcSXo,17045
4
4
  pychemstation/analysis/spec_utils.py,sha256=8NZMV0dtfxZLARjWzM5ks0tgEYQv_SKUiZzba2IoKgw,10505
5
5
  pychemstation/analysis/utils.py,sha256=ISupAOb_yqA4_DZRK9v18UL-XjUQccAicIJKb1VMnGg,2055
6
- pychemstation/control/__init__.py,sha256=aH9cPf-ljrVeVhN0K3cyEcAavmPXCjhhOnpLNf8qLqE,106
6
+ pychemstation/control/__init__.py,sha256=4xTy8X-mkn_PPZKr7w9rnj1wZhtmTesbQptPhpYmKXs,64
7
7
  pychemstation/control/comm.py,sha256=u44g1hTluQ0yUG93Un-QAshScoDpgYRrZfFTgweP5tY,7386
8
- pychemstation/control/hplc.py,sha256=huEUl90Ylvzvy2TXWUjhyDU4PazrK7Z51M8spquHvVg,6719
8
+ pychemstation/control/hplc.py,sha256=L1-cBQ7-thuITx1Mqq3lYXo832nEQtRJEX7nlLvRYrg,6732
9
+ pychemstation/control/controllers/__init__.py,sha256=di3ytLIK-35XC_THw4IjNaOtCUTe7GuEOFb-obmREw4,166
10
+ pychemstation/control/controllers/comm.py,sha256=iltKMNfdp_8INA5Vss9asI2LxJam_Ro3YWBHBQFr4t0,7353
11
+ pychemstation/control/controllers/method.py,sha256=GNGvGUF9LUCcHAe37SIdnQQ_RzwgnU2ba1oWLXnOp8w,12816
12
+ pychemstation/control/controllers/sequence.py,sha256=f4ZhzkM2hY8xk8dUyiCpmZ5fwU4ev2-CNSbyFeFVe_w,10963
13
+ pychemstation/control/controllers/table_controller.py,sha256=DnusCG5ySR8gA-oe3K_EJfAPlYhmW33epiCi4YVUKJg,8648
9
14
  pychemstation/control/table/__init__.py,sha256=RgMN4uIWHdNUHpGRBWdzmzAbk7XEKl6Y-qtqWCxzSZU,124
10
15
  pychemstation/control/table/method.py,sha256=THVoGomSXff_CTU3eAYme0BYwkPzab5UgZKsiZ29QSk,12196
11
16
  pychemstation/control/table/sequence.py,sha256=Eri52AnbE3BGthfrRSvYKYciquUzvHKo0lYUTySYYE8,10542
12
- pychemstation/control/table/table_controller.py,sha256=F5lv1DlPkEDcFmtwVT-E9Kq075jnWIN-iZIM3_mTt5E,8115
17
+ pychemstation/control/table/table_controller.py,sha256=HVNYUXqtyFTAvb67fa3RO5RHgmBTFMsYRHKpiXdYcfs,8313
13
18
  pychemstation/generated/__init__.py,sha256=GAoZFAYbPVEJDkcOw3e1rgOqd7TCW0HyKNPM8OMehMg,1005
14
19
  pychemstation/generated/dad_method.py,sha256=0W8Z5WDtF5jpIcudMqb7XrkTnR2EGg_QOCsHRFQ0rmM,8402
15
20
  pychemstation/generated/pump_method.py,sha256=sUhE2Oo00nzVcoONtq3EMWsN4wLSryXbG8f3EeViWKg,12174
16
21
  pychemstation/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- pychemstation/utils/chromatogram.py,sha256=35nvEh6prVsWO6lMHYgGuidUOFHv954_7MNf0Az3Fz4,3759
22
+ pychemstation/utils/chromatogram.py,sha256=RiKWQG2FIlDzddhgABV-WIJg_BsCTL_wPfyYGikyxoM,3097
18
23
  pychemstation/utils/macro.py,sha256=BAIcE_dppNffwrSqGq8gh0ccE9YAJfQFQZHXJgA1WtA,2586
19
24
  pychemstation/utils/method_types.py,sha256=YngbyHg96JSFnvhm5Zd7wJvLTQPPQsLbvbyz3HlGLYY,862
20
25
  pychemstation/utils/parsing.py,sha256=bnFIsZZwFy9NKzVUf517yN-ogzQbm0hp_aho3KUD6Is,9317
@@ -23,12 +28,12 @@ pychemstation/utils/table_types.py,sha256=cN51Ry2pammDdk85cabVH3qkchjKKIzZfAH87P
23
28
  pychemstation/utils/tray_types.py,sha256=UUDED-IAf-8FmPVZezuWSiIQE_HgiZQMV2sTqu4oZw8,177
24
29
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
30
  tests/constants.py,sha256=lLwT_QaVriivL5llkul6RxXxPriwUp1E-Y3G9fejvbo,2092
26
- tests/test_comb.py,sha256=faF4764oJEZ2vhG9-zLmQxjATseipRKXrStehuDCXOI,5638
31
+ tests/test_comb.py,sha256=CF6nSxUMBJeUjA-IfgPcBGV5oIG4aH1-RA26oIATa-s,5640
27
32
  tests/test_comm.py,sha256=1ZZd0UrIBOKe91wzA-XI-gSRgXmId9mLWYSMeche82Y,2973
28
33
  tests/test_method.py,sha256=uCPpZVYKPz1CNWwhmBo_8TH0ku2V0ZpDZJj3f8iINB4,2440
29
34
  tests/test_sequence.py,sha256=yIQGhUTehtHz6D1ai5W6AlP0zes2icF0VdQ0IGJ2CbQ,4901
30
- pychemstation-0.5.4.dist-info/LICENSE,sha256=9bdF75gIf1MecZ7oymqWgJREVz7McXPG-mjqrTmzzD8,18658
31
- pychemstation-0.5.4.dist-info/METADATA,sha256=TN5NWM9_QmES6TmIm_pXGvQxlNTQsHJDiP1wd1dRktA,3992
32
- pychemstation-0.5.4.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
33
- pychemstation-0.5.4.dist-info/top_level.txt,sha256=zXfKu_4nYWwPHo3OsuhshMNC3SPkcoTGCyODjURaghY,20
34
- pychemstation-0.5.4.dist-info/RECORD,,
35
+ pychemstation-0.5.6.dev1.dist-info/LICENSE,sha256=9bdF75gIf1MecZ7oymqWgJREVz7McXPG-mjqrTmzzD8,18658
36
+ pychemstation-0.5.6.dev1.dist-info/METADATA,sha256=7Pj5MObTqRmX3B19nhA70BzBRyHSA7bDku7O1-D89lE,4312
37
+ pychemstation-0.5.6.dev1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
38
+ pychemstation-0.5.6.dev1.dist-info/top_level.txt,sha256=zXfKu_4nYWwPHo3OsuhshMNC3SPkcoTGCyODjURaghY,20
39
+ pychemstation-0.5.6.dev1.dist-info/RECORD,,
tests/test_comb.py CHANGED
@@ -28,14 +28,14 @@ class TestCombinations(unittest.TestCase):
28
28
  first_row=HPLCMethodParams(
29
29
  organic_modifier=5,
30
30
  flow=0.65,
31
- maximum_run_time=2),
31
+ maximum_run_time=1),
32
32
  subsequent_rows=[
33
33
  TimeTableEntry(
34
34
  start_time=0.10,
35
35
  organic_modifer=5,
36
36
  flow=0.34),
37
37
  TimeTableEntry(
38
- start_time=1,
38
+ start_time=0.5,
39
39
  organic_modifer=98,
40
40
  flow=0.55)])
41
41
  self.hplc_controller.edit_method(rand_method, save=True)