pychemstation 0.10.0__py3-none-any.whl → 0.10.2__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 (33) hide show
  1. pychemstation/control/README.md +132 -0
  2. pychemstation/control/controllers/README.md +1 -0
  3. {pychemstation-0.10.0.dist-info → pychemstation-0.10.2.dist-info}/METADATA +11 -9
  4. {pychemstation-0.10.0.dist-info → pychemstation-0.10.2.dist-info}/RECORD +6 -31
  5. {pychemstation-0.10.0.dist-info → pychemstation-0.10.2.dist-info}/WHEEL +1 -2
  6. pychemstation/analysis/spec_utils.py +0 -304
  7. pychemstation/analysis/utils.py +0 -63
  8. pychemstation/control/comm.py +0 -206
  9. pychemstation/control/controllers/devices/column.py +0 -12
  10. pychemstation/control/controllers/devices/dad.py +0 -0
  11. pychemstation/control/controllers/devices/pump.py +0 -43
  12. pychemstation/control/controllers/method.py +0 -338
  13. pychemstation/control/controllers/sequence.py +0 -190
  14. pychemstation/control/controllers/table_controller.py +0 -266
  15. pychemstation/control/table/__init__.py +0 -3
  16. pychemstation/control/table/method.py +0 -274
  17. pychemstation/control/table/sequence.py +0 -210
  18. pychemstation/control/table/table_controller.py +0 -201
  19. pychemstation-0.10.0.dist-info/top_level.txt +0 -2
  20. tests/__init__.py +0 -0
  21. tests/constants.py +0 -134
  22. tests/test_comb.py +0 -136
  23. tests/test_comm.py +0 -65
  24. tests/test_inj.py +0 -39
  25. tests/test_method.py +0 -99
  26. tests/test_nightly.py +0 -80
  27. tests/test_offline_stable.py +0 -69
  28. tests/test_online_stable.py +0 -275
  29. tests/test_proc_rep.py +0 -52
  30. tests/test_runs_stable.py +0 -225
  31. tests/test_sequence.py +0 -125
  32. tests/test_stable.py +0 -276
  33. {pychemstation-0.10.0.dist-info → pychemstation-0.10.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,190 +0,0 @@
1
- import os
2
- import time
3
- from typing import Optional
4
-
5
- from ...control.controllers.comm import CommunicationController
6
- from ...control.controllers.table_controller import TableController
7
- from ...utils.chromatogram import SEQUENCE_TIME_FORMAT, AgilentChannelChromatogramData
8
- from ...utils.macro import Command
9
- from ...utils.sequence_types import SequenceTable, SequenceEntry, SequenceDataFiles, InjectionSource, SampleType
10
- from ...utils.table_types import RegisterFlag, Table
11
- from ...utils.tray_types import TenVialColumn
12
-
13
-
14
- class SequenceController(TableController):
15
- """
16
- Class containing sequence related logic
17
- """
18
-
19
- def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table, method_dir: str):
20
- self.method_dir = method_dir
21
- super().__init__(controller, src, data_dir, table)
22
-
23
- def load(self) -> SequenceTable:
24
- rows = self.get_num_rows()
25
- self.send(Command.GET_SEQUENCE_CMD)
26
- seq_name = self.receive()
27
-
28
- if rows.is_ok() and seq_name.is_ok():
29
- self.table_state = SequenceTable(
30
- name=seq_name.ok_value.string_response.partition(".S")[0],
31
- rows=[self.get_row(r + 1) for r in range(int(rows.ok_value.num_response))])
32
- return self.table_state
33
- raise RuntimeError(rows.err_value)
34
-
35
- def get_row(self, row: int) -> SequenceEntry:
36
- sample_name = self.get_text(row, RegisterFlag.NAME)
37
- vial_location = int(self.get_num(row, RegisterFlag.VIAL_LOCATION))
38
- method = self.get_text(row, RegisterFlag.METHOD)
39
- num_inj = int(self.get_num(row, RegisterFlag.NUM_INJ))
40
- inj_vol = int(self.get_text(row, RegisterFlag.INJ_VOL))
41
- inj_source = InjectionSource(self.get_text(row, RegisterFlag.INJ_SOR))
42
- sample_type = SampleType(self.get_num(row, RegisterFlag.SAMPLE_TYPE))
43
- return SequenceEntry(sample_name=sample_name,
44
- vial_location=vial_location,
45
- method=None if len(method) == 0 else method,
46
- num_inj=num_inj,
47
- inj_vol=inj_vol,
48
- inj_source=inj_source,
49
- sample_type=sample_type)
50
-
51
- def check(self) -> str:
52
- time.sleep(2)
53
- self.send(Command.GET_SEQUENCE_CMD)
54
- time.sleep(2)
55
- res = self.receive()
56
- if res.is_ok():
57
- return res.ok_value.string_response
58
- return "ERROR"
59
-
60
- def switch(self, seq_name: str):
61
- """
62
- Switch to the specified sequence. The sequence name does not need the '.S' extension.
63
-
64
- :param seq_name: The name of the sequence file
65
- """
66
- self.send(f'_SeqFile$ = "{seq_name}.S"')
67
- self.send(f'_SeqPath$ = "{self.src}"')
68
- self.send(Command.SWITCH_SEQUENCE_CMD)
69
- time.sleep(2)
70
- self.send(Command.GET_SEQUENCE_CMD)
71
- time.sleep(2)
72
- parsed_response = self.receive().value.string_response
73
-
74
- assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
75
- self.table_state = None
76
-
77
- def edit(self, sequence_table: SequenceTable):
78
- """
79
- Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
80
- If you would only like to edit a single row of a sequence table, use `edit_sequence_table_row` instead.
81
-
82
- :param sequence_table:
83
- """
84
- self.table_state = sequence_table
85
- rows = self.get_num_rows()
86
- if rows.is_ok():
87
- existing_row_num = rows.value.num_response
88
- wanted_row_num = len(sequence_table.rows)
89
- while existing_row_num != wanted_row_num:
90
- if wanted_row_num > existing_row_num:
91
- self.add_row()
92
- elif wanted_row_num < existing_row_num:
93
- self.delete_row(int(existing_row_num))
94
- self.send(Command.SAVE_SEQUENCE_CMD)
95
- existing_row_num = self.get_num_rows().ok_value.num_response
96
- self.send(Command.SWITCH_SEQUENCE_CMD)
97
-
98
- for i, row in enumerate(sequence_table.rows):
99
- self.edit_row(row=row, row_num=i + 1)
100
- self.sleep(1)
101
- self.send(Command.SAVE_SEQUENCE_CMD)
102
- self.send(Command.SWITCH_SEQUENCE_CMD)
103
-
104
- def edit_row(self, row: SequenceEntry, row_num: int):
105
- """
106
- Edits a row in the sequence table. If a row does NOT exist, a new one will be created.
107
-
108
- :param row: sequence row entry with updated information
109
- :param row_num: the row to edit, based on 1-based indexing
110
- """
111
- num_rows = self.get_num_rows()
112
- if num_rows.is_ok():
113
- while num_rows.ok_value.num_response < row_num:
114
- self.add_row()
115
- self.send(Command.SAVE_SEQUENCE_CMD)
116
- num_rows = self.get_num_rows()
117
-
118
- if row.vial_location:
119
- loc = row.vial_location
120
- if isinstance(row.vial_location, TenVialColumn):
121
- loc = row.vial_location.value
122
- self.edit_row_num(row=row_num, col_name=RegisterFlag.VIAL_LOCATION, val=loc)
123
-
124
- if row.method:
125
- possible_path = os.path.join(self.method_dir, row.method) + ".M\\"
126
- method = row.method
127
- if os.path.exists(possible_path):
128
- method = os.path.join(self.method_dir, row.method)
129
- self.edit_row_text(row=row_num, col_name=RegisterFlag.METHOD, val=method)
130
-
131
- if row.num_inj:
132
- self.edit_row_num(row=row_num, col_name=RegisterFlag.NUM_INJ, val=row.num_inj)
133
-
134
- if row.inj_vol:
135
- self.edit_row_text(row=row_num, col_name=RegisterFlag.INJ_VOL, val=row.inj_vol)
136
-
137
- if row.inj_source:
138
- self.edit_row_text(row=row_num, col_name=RegisterFlag.INJ_SOR, val=row.inj_source.value)
139
-
140
- if row.sample_name:
141
- self.edit_row_text(row=row_num, col_name=RegisterFlag.NAME, val=row.sample_name)
142
- self.edit_row_text(row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.sample_name)
143
-
144
- if row.sample_type:
145
- self.edit_row_num(row=row_num, col_name=RegisterFlag.SAMPLE_TYPE, val=row.sample_type.value)
146
-
147
- self.send(Command.SAVE_SEQUENCE_CMD)
148
-
149
- def run(self, stall_while_running: bool = True):
150
- """
151
- Starts the currently loaded sequence, storing data
152
- under the <data_dir>/<sequence table name> folder.
153
- Device must be ready.
154
- """
155
- if not self.table_state:
156
- self.table_state = self.load()
157
-
158
- timestamp = time.strftime(SEQUENCE_TIME_FORMAT)
159
- self.send(Command.RUN_SEQUENCE_CMD.value)
160
-
161
- if self.check_hplc_is_running():
162
- folder_name = f"{self.table_state.name} {timestamp}"
163
- self.data_files.append(SequenceDataFiles(dir=folder_name,
164
- sequence_name=self.table_state.name))
165
-
166
- if stall_while_running:
167
- run_completed = self.check_hplc_done_running(sequence=self.table_state)
168
- if run_completed.is_ok():
169
- self.data_files[-1].dir = run_completed.value
170
- else:
171
- raise RuntimeError("Run error has occurred.")
172
- else:
173
- self.data_files[-1].dir = self.fuzzy_match_most_recent_folder(folder_name).ok_value
174
-
175
- def retrieve_recent_data_files(self) -> str:
176
- sequence_data_files: SequenceDataFiles = self.data_files[-1]
177
- return sequence_data_files.dir
178
-
179
- def get_data(self, custom_path: Optional[str] = None) -> list[AgilentChannelChromatogramData]:
180
- parent_dir = self.data_files[-1].dir if not custom_path else custom_path
181
- subdirs = [x[0] for x in os.walk(self.data_dir)]
182
- potential_folders = sorted(list(filter(lambda d: parent_dir in d, subdirs)))
183
- self.data_files[-1].child_dirs = [f for f in potential_folders if
184
- parent_dir in f and ".M" not in f and ".D" in f]
185
-
186
- spectra: list[AgilentChannelChromatogramData] = []
187
- for row in self.data_files[-1].child_dirs:
188
- self.get_spectrum(row)
189
- spectra.append(AgilentChannelChromatogramData(**self.spectra))
190
- return spectra
@@ -1,266 +0,0 @@
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 MethodDetails
18
- from ...utils.sequence_types import SequenceDataFiles, SequenceTable
19
- from ...utils.table_types import Table, TableOperation, RegisterFlag
20
-
21
- TableType = Union[MethodDetails, SequenceTable]
22
-
23
-
24
- class TableController(abc.ABC):
25
-
26
- def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table):
27
- self.controller = controller
28
- self.table = table
29
- self.table_state: Optional[TableType] = None
30
-
31
- if os.path.isdir(src):
32
- self.src: str = src
33
- else:
34
- raise FileNotFoundError(f"dir: {src} not found.")
35
-
36
- if os.path.isdir(data_dir):
37
- self.data_dir: str = data_dir
38
- else:
39
- raise FileNotFoundError(f"dir: {data_dir} not found.")
40
-
41
- self.spectra: dict[str, Optional[AgilentHPLCChromatogram]] = {
42
- "A": AgilentHPLCChromatogram(self.data_dir),
43
- "B": AgilentHPLCChromatogram(self.data_dir),
44
- "C": AgilentHPLCChromatogram(self.data_dir),
45
- "D": AgilentHPLCChromatogram(self.data_dir),
46
- "E": AgilentHPLCChromatogram(self.data_dir),
47
- "F": AgilentHPLCChromatogram(self.data_dir),
48
- "G": AgilentHPLCChromatogram(self.data_dir),
49
- "H": AgilentHPLCChromatogram(self.data_dir),
50
- }
51
-
52
- self.data_files: Union[list[SequenceDataFiles], list[str]] = []
53
-
54
- # Initialize row counter for table operations
55
- self.send('Local Rows')
56
-
57
- def receive(self) -> Result[Response, str]:
58
- for _ in range(10):
59
- try:
60
- return self.controller.receive()
61
- except IndexError:
62
- continue
63
- return Err("Could not parse response")
64
-
65
- def send(self, cmd: Union[Command, str]):
66
- if not self.controller:
67
- raise RuntimeError(
68
- "Communication controller must be initialized before sending command. It is currently in offline mode.")
69
- self.controller.send(cmd)
70
-
71
- def sleepy_send(self, cmd: Union[Command, str]):
72
- self.controller.sleepy_send(cmd)
73
-
74
- def sleep(self, seconds: int):
75
- """
76
- Tells the HPLC to wait for a specified number of seconds.
77
-
78
- :param seconds: number of seconds to wait
79
- """
80
- self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
81
-
82
- def get_num(self, row: int, col_name: RegisterFlag) -> float:
83
- return self.controller.get_num_val(TableOperation.GET_ROW_VAL.value.format(register=self.table.register,
84
- table_name=self.table.name,
85
- row=row,
86
- col_name=col_name.value))
87
-
88
- def get_text(self, row: int, col_name: RegisterFlag) -> str:
89
- return self.controller.get_text_val(TableOperation.GET_ROW_TEXT.value.format(register=self.table.register,
90
- table_name=self.table.name,
91
- row=row,
92
- col_name=col_name.value))
93
-
94
- def add_new_col_num(self,
95
- col_name: RegisterFlag,
96
- val: Union[int, float]):
97
- self.sleepy_send(TableOperation.NEW_COL_VAL.value.format(
98
- register=self.table.register,
99
- table_name=self.table.name,
100
- col_name=col_name,
101
- val=val))
102
-
103
- def add_new_col_text(self,
104
- col_name: RegisterFlag,
105
- val: str):
106
- self.sleepy_send(TableOperation.NEW_COL_TEXT.value.format(
107
- register=self.table.register,
108
- table_name=self.table.name,
109
- col_name=col_name,
110
- val=val))
111
-
112
- def edit_row_num(self,
113
- col_name: RegisterFlag,
114
- val: Union[int, float],
115
- row: Optional[int] = None):
116
- self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(
117
- register=self.table.register,
118
- table_name=self.table.name,
119
- row=row if row is not None else 'Rows',
120
- col_name=col_name,
121
- val=val))
122
-
123
- def edit_row_text(self,
124
- col_name: RegisterFlag,
125
- val: str,
126
- row: Optional[int] = None):
127
- self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(
128
- register=self.table.register,
129
- table_name=self.table.name,
130
- row=row if row is not None else 'Rows',
131
- col_name=col_name,
132
- val=val))
133
-
134
- @abc.abstractmethod
135
- def get_row(self, row: int):
136
- pass
137
-
138
- def delete_row(self, row: int):
139
- self.sleepy_send(TableOperation.DELETE_ROW.value.format(register=self.table.register,
140
- table_name=self.table.name,
141
- row=row))
142
-
143
- def add_row(self):
144
- """
145
- Adds a row to the provided table for currently loaded method or sequence.
146
- Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
147
- You can also provide your own table.
148
-
149
- :param table: the table to add a new row to
150
- """
151
- self.sleepy_send(TableOperation.NEW_ROW.value.format(register=self.table.register,
152
- table_name=self.table.name))
153
-
154
- def delete_table(self):
155
- """
156
- Deletes the table for the current loaded method or sequence.
157
- Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
158
- You can also provide your own table.
159
-
160
- :param table: the table to delete
161
- """
162
- self.sleepy_send(TableOperation.DELETE_TABLE.value.format(register=self.table.register,
163
- table_name=self.table.name))
164
-
165
- def new_table(self):
166
- """
167
- Creates the table for the currently loaded method or sequence. Import either the SEQUENCE_TABLE or
168
- METHOD_TIMETABLE from hein_analytical_control.constants. You can also provide your own table.
169
-
170
- :param table: the table to create
171
- """
172
- self.send(TableOperation.CREATE_TABLE.value.format(register=self.table.register,
173
- table_name=self.table.name))
174
-
175
- def get_num_rows(self) -> Result[Response, str]:
176
- self.send(TableOperation.GET_NUM_ROWS.value.format(register=self.table.register,
177
- table_name=self.table.name,
178
- col_name=RegisterFlag.NUM_ROWS))
179
- self.send(Command.GET_ROWS_CMD.value.format(register=self.table.register,
180
- table_name=self.table.name,
181
- col_name=RegisterFlag.NUM_ROWS))
182
- res = self.controller.receive()
183
-
184
- if res.is_ok():
185
- self.send("Sleep 0.1")
186
- self.send('Print Rows')
187
- return res
188
- else:
189
- return Err("No rows could be read.")
190
-
191
- def check_hplc_is_running(self) -> bool:
192
- started_running = polling.poll(
193
- lambda: isinstance(self.controller.get_status(), HPLCRunningStatus),
194
- step=5,
195
- max_tries=100)
196
- return started_running
197
-
198
- def check_hplc_done_running(self,
199
- method: Optional[MethodDetails] = None,
200
- sequence: Optional[SequenceTable] = None) -> Result[str, str]:
201
- """
202
- Checks if ChemStation has finished running and can read data back
203
-
204
- :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
205
- :param sequence: if you are running a sequence and want to read back data, the timeout period will be adjusted to be longer than the sequence's runtime
206
- :return: Return True if data can be read back, else False.
207
- """
208
- timeout = 10 * 60
209
- if method:
210
- timeout = ((method.stop_time + method.post_time + 3) * 60)
211
- if sequence:
212
- timeout *= len(sequence.rows)
213
-
214
- most_recent_folder = self.retrieve_recent_data_files()
215
- finished_run = polling.poll(
216
- lambda: self.controller.check_if_running(),
217
- timeout=timeout,
218
- step=50)
219
-
220
- check_folder = self.fuzzy_match_most_recent_folder(most_recent_folder)
221
- if check_folder.is_ok() and finished_run:
222
- return check_folder
223
- elif check_folder.is_ok():
224
- finished_run = polling.poll(
225
- lambda: self.controller.check_if_running(),
226
- timeout=timeout,
227
- step=50)
228
- if finished_run:
229
- return check_folder
230
- return check_folder
231
- else:
232
- return Err("Run did not complete as expected")
233
-
234
- def fuzzy_match_most_recent_folder(self, most_recent_folder) -> Result[str, str]:
235
- if os.path.exists(most_recent_folder):
236
- return Ok(most_recent_folder)
237
-
238
- subdirs = [x[0] for x in os.walk(self.data_dir)]
239
- potential_folders = sorted(list(filter(lambda d: most_recent_folder in d, subdirs)))
240
- parent_dirs = []
241
- for folder in potential_folders:
242
- path = os.path.normpath(folder)
243
- split_folder = path.split(os.sep)
244
- if most_recent_folder in split_folder[-1]:
245
- parent_dirs.append(folder)
246
- parent_dir = sorted(parent_dirs, reverse=True)[0]
247
- return Ok(parent_dir)
248
-
249
- @abc.abstractmethod
250
- def retrieve_recent_data_files(self):
251
- pass
252
-
253
- @abc.abstractmethod
254
- def get_data(self) -> Union[list[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
255
- pass
256
-
257
- def get_spectrum(self, data_file: str):
258
- """
259
- Load chromatogram for any channel in spectra dictionary.
260
- """
261
- for channel, spec in self.spectra.items():
262
- try:
263
- spec.load_spectrum(data_path=data_file, channel=channel)
264
- except FileNotFoundError:
265
- self.spectra[channel] = None
266
- print(f"No data at channel: {channel}")
@@ -1,3 +0,0 @@
1
- from .method import MethodController
2
- from .sequence import SequenceController
3
- from .table_controller import TableController