pychemstation 0.8.3__py3-none-any.whl → 0.8.6__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/__init__.py +1 -1
  2. pychemstation/analysis/__init__.py +4 -1
  3. pychemstation/analysis/base_spectrum.py +4 -4
  4. pychemstation/{utils → analysis}/chromatogram.py +4 -7
  5. pychemstation/analysis/process_report.py +121 -74
  6. pychemstation/control/README.md +22 -46
  7. pychemstation/control/__init__.py +5 -0
  8. pychemstation/control/controllers/__init__.py +2 -0
  9. pychemstation/control/controllers/comm.py +41 -18
  10. pychemstation/control/controllers/devices/device.py +27 -14
  11. pychemstation/control/controllers/devices/injector.py +33 -89
  12. pychemstation/control/controllers/tables/method.py +266 -111
  13. pychemstation/control/controllers/tables/ms.py +7 -4
  14. pychemstation/control/controllers/tables/sequence.py +171 -82
  15. pychemstation/control/controllers/tables/table.py +192 -116
  16. pychemstation/control/hplc.py +117 -83
  17. pychemstation/generated/__init__.py +0 -2
  18. pychemstation/generated/dad_method.py +1 -1
  19. pychemstation/generated/pump_method.py +15 -19
  20. pychemstation/utils/injector_types.py +1 -1
  21. pychemstation/utils/macro.py +12 -11
  22. pychemstation/utils/method_types.py +3 -2
  23. pychemstation/{analysis/utils.py → utils/num_utils.py} +2 -2
  24. pychemstation/utils/parsing.py +1 -11
  25. pychemstation/utils/sequence_types.py +4 -5
  26. pychemstation/{analysis → utils}/spec_utils.py +1 -2
  27. pychemstation/utils/table_types.py +10 -9
  28. pychemstation/utils/tray_types.py +48 -38
  29. {pychemstation-0.8.3.dist-info → pychemstation-0.8.6.dist-info}/METADATA +64 -23
  30. pychemstation-0.8.6.dist-info/RECORD +37 -0
  31. pychemstation-0.8.3.dist-info/RECORD +0 -37
  32. {pychemstation-0.8.3.dist-info → pychemstation-0.8.6.dist-info}/WHEEL +0 -0
  33. {pychemstation-0.8.3.dist-info → pychemstation-0.8.6.dist-info}/licenses/LICENSE +0 -0
pychemstation/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """
2
2
  .. include:: ../README.md
3
- """
3
+ """
@@ -1 +1,4 @@
1
- from .base_spectrum import AbstractSpectrum
1
+ from .process_report import CSVProcessor
2
+ from .process_report import TXTProcessor
3
+
4
+ __all__ = ["CSVProcessor", "TXTProcessor"]
@@ -6,12 +6,12 @@ from abc import ABC, abstractmethod
6
6
  import matplotlib.pyplot as plt
7
7
  import numpy as np
8
8
  from scipy import (
9
- sparse,
10
- signal,
11
9
  integrate,
10
+ signal,
11
+ sparse,
12
12
  )
13
13
 
14
- from .utils import interpolate_to_index, find_nearest_value_index
14
+ from ..utils.num_utils import find_nearest_value_index, interpolate_to_index
15
15
 
16
16
 
17
17
  class AbstractSpectrum(ABC):
@@ -249,7 +249,7 @@ class AbstractSpectrum(ABC):
249
249
  os.makedirs(path, exist_ok=True)
250
250
  fig.savefig(os.path.join(path, f"{filename}.png"), dpi=150)
251
251
 
252
- def find_peaks(self, threshold=1, min_width=.1, min_dist=None, area=None):
252
+ def find_peaks(self, threshold=1, min_width=0.1, min_dist=None, area=None):
253
253
  """Finds all peaks above the threshold with at least min_width width.
254
254
 
255
255
  Args:
@@ -6,16 +6,14 @@ from dataclasses import dataclass
6
6
 
7
7
  import numpy as np
8
8
 
9
- from .parsing import CHFile
10
- from ..analysis import AbstractSpectrum
11
9
 
12
- # standard filenames for spectral data
13
- CHANNELS = {"A": "01", "B": "02", "C": "03", "D": "04"}
10
+ from ..utils.parsing import CHFile
11
+ from ..analysis.base_spectrum import AbstractSpectrum
14
12
 
15
13
  ACQUISITION_PARAMETERS = "acq.txt"
16
14
 
17
15
  # format used in acquisition parameters
18
- TIME_FORMAT = "%Y-%m-%d-%H-%M-%S"
16
+ TIME_FORMAT = "%Y-%m-%d %H-%M-%S"
19
17
  SEQUENCE_TIME_FORMAT = "%Y-%m-%d %H-%M"
20
18
 
21
19
 
@@ -39,12 +37,11 @@ class AgilentHPLCChromatogram(AbstractSpectrum):
39
37
  }
40
38
 
41
39
  def __init__(self, path=None, autosaving=False):
42
-
43
40
  if path is not None:
44
41
  os.makedirs(path, exist_ok=True)
45
42
  self.path = path
46
43
  else:
47
- self.path = os.path.join(".", "hplc_data")
44
+ self.path = os.path.join("../utils", "hplc_data")
48
45
  os.makedirs(self.path, exist_ok=True)
49
46
 
50
47
  super().__init__(path=path, autosaving=autosaving)
@@ -4,26 +4,32 @@ import re
4
4
  from abc import abstractmethod
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum
7
- from typing import List, AnyStr, Dict, Optional, Pattern
7
+ from typing import AnyStr, Dict, List, Optional, Pattern
8
8
 
9
9
  import pandas as pd
10
- from aghplctools.ingestion.text import _no_peaks_re, _area_report_re, _header_block_re, _signal_info_re, \
11
- _signal_table_re, chunk_string
12
- from result import Result, Err, Ok
10
+ from aghplctools.ingestion.text import (
11
+ _area_report_re,
12
+ _header_block_re,
13
+ _no_peaks_re,
14
+ _signal_info_re,
15
+ _signal_table_re,
16
+ chunk_string,
17
+ )
18
+ from result import Err, Ok, Result
13
19
 
14
- from pychemstation.utils.chromatogram import AgilentHPLCChromatogram
15
- from pychemstation.utils.tray_types import Tray, FiftyFourVialPlate
20
+ from ..analysis.chromatogram import AgilentHPLCChromatogram
21
+ from ..utils.tray_types import FiftyFourVialPlate, Tray
16
22
 
17
23
 
18
24
  @dataclass
19
25
  class AgilentPeak:
26
+ peak_number: Optional[int]
20
27
  retention_time: float
28
+ peak_type: Optional[str]
21
29
  width: float
22
30
  area: float
23
31
  height: float
24
- peak_number: Optional[int]
25
- peak_type: Optional[str]
26
- height_percent: Optional[float]
32
+ area_percent: Optional[float]
27
33
 
28
34
 
29
35
  @dataclass
@@ -67,12 +73,14 @@ class CSVProcessor(ReportProcessor):
67
73
  """
68
74
  Method to parse details from CSV report.
69
75
 
70
- :returns: subset of complete report details, specifically the sample location, solvents in pumps,
76
+ :return: subset of complete report details, specifically the sample location, solvents in pumps,
71
77
  and list of peaks at each wavelength channel.
72
78
  """
73
- labels = os.path.join(self.path, f'REPORT00.CSV')
79
+ labels = os.path.join(self.path, "REPORT00.CSV")
74
80
  if os.path.exists(labels):
75
- df_labels: Dict[int, Dict[int: AnyStr]] = pd.read_csv(labels, encoding="utf-16", header=None).to_dict()
81
+ df_labels: Dict[int, Dict[int:AnyStr]] = pd.read_csv(
82
+ labels, encoding="utf-16", header=None
83
+ ).to_dict()
76
84
  vial_location = []
77
85
  signals = {}
78
86
  solvents = {}
@@ -85,18 +93,33 @@ class CSVProcessor(ReportProcessor):
85
93
  elif val == "Number of Signals":
86
94
  num_signals = int(df_labels[1][pos])
87
95
  for s in range(1, num_signals + 1):
88
- df = pd.read_csv(os.path.join(self.path, f'REPORT0{s}.CSV'),
89
- encoding="utf-16", header=None)
96
+ df = pd.read_csv(
97
+ os.path.join(self.path, f"REPORT0{s}.CSV"),
98
+ encoding="utf-16",
99
+ header=None,
100
+ )
90
101
  peaks = df.apply(lambda row: AgilentPeak(*row), axis=1)
91
- wavelength = df_labels[1][pos + s].partition(",4 Ref=off")[0][-3:]
92
- signals[wavelength] = peaks
102
+ try:
103
+ wavelength = df_labels[1][pos + s].partition(",4 Ref=off")[
104
+ 0
105
+ ][-3:]
106
+ wavelength = int(wavelength)
107
+ signals[wavelength] = list(peaks)
108
+ except (IndexError, ValueError):
109
+ # TODO: Ask about the MS signals
110
+ pass
93
111
  break
94
112
 
95
- return Ok(AgilentReport(
96
- signals=[Signals(wavelength=w, peaks=s, data=None) for w, s in signals.items()],
97
- vial_location=FiftyFourVialPlate.from_int(int(vial_location)),
98
- solvents=solvents
99
- ))
113
+ return Ok(
114
+ AgilentReport(
115
+ signals=[
116
+ Signals(wavelength=w, peaks=s, data=None)
117
+ for w, s in signals.items()
118
+ ],
119
+ vial_location=FiftyFourVialPlate.from_int(int(vial_location)),
120
+ solvents=solvents,
121
+ )
122
+ )
100
123
 
101
124
  return Err("No report found")
102
125
 
@@ -105,34 +128,39 @@ class TXTProcessor(ReportProcessor):
105
128
  """
106
129
  Regex matches for column and unit combinations, courtesy of Veronica Lai.
107
130
  """
131
+
108
132
  _column_re_dictionary = {
109
- 'Peak': { # peak index
110
- '#': '[ ]+(?P<Peak>[\d]+)', # number
133
+ "Peak": { # peak index
134
+ "#": "[ ]+(?P<Peak>[\d]+)", # number
111
135
  },
112
- 'RetTime': { # retention time
113
- '[min]': '(?P<RetTime>[\d]+.[\d]+)', # minutes
136
+ "RetTime": { # retention time
137
+ "[min]": "(?P<RetTime>[\d]+.[\d]+)", # minutes
114
138
  },
115
- 'Type': { # peak type
116
- '': '(?P<Type>[A-Z]{1,3}(?: [A-Z]{1,2})*)', # todo this is different from <4.8.8 aghplc tools
139
+ "Type": { # peak type
140
+ "": "(?P<Type>[A-Z]{1,3}(?: [A-Z]{1,2})*)", # todo this is different from <4.8.8 aghplc tools
117
141
  },
118
- 'Width': { # peak width
119
- '[min]': '(?P<Width>[\d]+.[\d]+[e+-]*[\d]+)',
142
+ "Width": { # peak width
143
+ "[min]": "(?P<Width>[\d]+.[\d]+[e+-]*[\d]+)",
120
144
  },
121
- 'Area': { # peak area
122
- '[mAU*s]': '(?P<Area>[\d]+.[\d]+[e+-]*[\d]+)', # area units
123
- '%': '(?P<percent>[\d]+.[\d]+[e+-]*[\d]+)', # percent
145
+ "Area": { # peak area
146
+ "[mAU*s]": "(?P<Area>[\d]+.[\d]+[e+-]*[\d]+)", # area units
147
+ "%": "(?P<percent>[\d]+.[\d]+[e+-]*[\d]+)", # percent
124
148
  },
125
- 'Height': { # peak height
126
- '[mAU]': '(?P<Height>[\d]+.[\d]+[e+-]*[\d]+)',
149
+ "Height": { # peak height
150
+ "[mAU]": "(?P<Height>[\d]+.[\d]+[e+-]*[\d]+)",
127
151
  },
128
- 'Name': {
129
- '': '(?P<Name>[^\s]+(?:\s[^\s]+)*)', # peak name
152
+ "Name": {
153
+ "": "(?P<Name>[^\s]+(?:\s[^\s]+)*)", # peak name
130
154
  },
131
155
  }
132
156
 
133
- def __init__(self, path: str, min_ret_time: int = 0,
134
- max_ret_time: int = 999,
135
- target_wavelength_range: List[int] = range(200, 300)):
157
+ def __init__(
158
+ self,
159
+ path: str,
160
+ min_ret_time: int = 0,
161
+ max_ret_time: int = 999,
162
+ target_wavelength_range: List[int] = range(200, 300),
163
+ ):
136
164
  """
137
165
  Class to process reports in CSV form.
138
166
 
@@ -149,16 +177,17 @@ class TXTProcessor(ReportProcessor):
149
177
  def process_report(self) -> Result[AgilentReport, AnyStr]:
150
178
  """
151
179
  Method to parse details from CSV report.
152
-
153
- :returns: subset of complete report details, specifically the sample location, solvents in pumps,
154
- and list of peaks at each wavelength channel.
155
-
156
180
  If you want more functionality, use `aghplctools`.
157
181
  `from aghplctools.ingestion.text import pull_hplc_area_from_txt`
158
182
  `signals = pull_hplc_area_from_txt(file_path)`
183
+
184
+ :return: subset of complete report details, specifically the sample location, solvents in pumps,
185
+ and list of peaks at each wavelength channel.
159
186
  """
160
187
 
161
- with open(os.path.join(self.path, "REPORT.TXT"), 'r', encoding='utf-16') as openfile:
188
+ with open(
189
+ os.path.join(self.path, "REPORT.TXT"), "r", encoding="utf-16"
190
+ ) as openfile:
162
191
  text = openfile.read()
163
192
 
164
193
  try:
@@ -166,25 +195,33 @@ class TXTProcessor(ReportProcessor):
166
195
  except ValueError as e:
167
196
  return Err("No peaks found: " + str(e))
168
197
 
169
- signals = {key: signals[key] for key in self.target_wavelength_range if key in signals}
198
+ signals = {
199
+ key: signals[key] for key in self.target_wavelength_range if key in signals
200
+ }
170
201
 
171
202
  parsed_signals = []
172
203
  for wavelength, wavelength_dict in signals.items():
173
- current_wavelength_signals = Signals(wavelength=wavelength, peaks=[], data=None)
204
+ current_wavelength_signals = Signals(
205
+ wavelength=int(wavelength), peaks=[], data=None
206
+ )
174
207
  for ret_time, ret_time_dict in wavelength_dict.items():
175
208
  if self.min_ret_time <= ret_time <= self.max_ret_time:
176
- current_wavelength_signals.peaks.append(AgilentPeak(retention_time=ret_time,
177
- area=ret_time_dict['Area'],
178
- width=ret_time_dict['Width'],
179
- height=ret_time_dict['Height'],
180
- peak_number=None,
181
- peak_type=ret_time_dict['Type'],
182
- height_percent=None))
209
+ current_wavelength_signals.peaks.append(
210
+ AgilentPeak(
211
+ retention_time=ret_time,
212
+ area=ret_time_dict["Area"],
213
+ width=ret_time_dict["Width"],
214
+ height=ret_time_dict["Height"],
215
+ peak_number=None,
216
+ peak_type=ret_time_dict["Type"],
217
+ area_percent=None,
218
+ )
219
+ )
183
220
  parsed_signals.append(current_wavelength_signals)
184
221
 
185
- return Ok(AgilentReport(vial_location=None,
186
- solvents=None,
187
- signals=parsed_signals))
222
+ return Ok(
223
+ AgilentReport(vial_location=None, solvents=None, signals=parsed_signals)
224
+ )
188
225
 
189
226
  def parse_area_report(self, report_text: str) -> Dict:
190
227
  """
@@ -200,7 +237,7 @@ class TXTProcessor(ReportProcessor):
200
237
  should be able to use the `parse_area_report` method of aghplctools v4.8.8
201
238
  """
202
239
  if re.search(_no_peaks_re, report_text): # There are no peaks in Report.txt
203
- raise ValueError(f'No peaks found in Report.txt')
240
+ raise ValueError("No peaks found in Report.txt")
204
241
  blocks = _header_block_re.split(report_text)
205
242
  signals = {} # output dictionary
206
243
  for ind, block in enumerate(blocks):
@@ -213,23 +250,28 @@ class TXTProcessor(ReportProcessor):
213
250
  si = _signal_info_re.match(table)
214
251
  if si is not None:
215
252
  # some error state (e.g. 'not found')
216
- if si.group('error') != '':
253
+ if si.group("error") != "":
217
254
  continue
218
- wavelength = float(si.group('wavelength'))
255
+ wavelength = float(si.group("wavelength"))
219
256
  if wavelength in signals:
220
257
  # placeholder error raise just in case (this probably won't happen)
221
258
  raise KeyError(
222
- f'The wavelength {float(si.group("wavelength"))} is already in the signals dictionary')
259
+ f"The wavelength {float(si.group('wavelength'))} is already in the signals dictionary"
260
+ )
223
261
  signals[wavelength] = {}
224
262
  # build peak regex
225
263
  peak_re = self.build_peak_regex(table)
226
- if peak_re is None: # if there are no columns (empty table), continue
264
+ if (
265
+ peak_re is None
266
+ ): # if there are no columns (empty table), continue
227
267
  continue
228
- for line in table.split('\n'):
268
+ for line in table.split("\n"):
229
269
  peak = peak_re.match(line)
230
270
  if peak is not None:
231
- signals[wavelength][float(peak.group('RetTime'))] = {}
232
- current = signals[wavelength][float(peak.group('RetTime'))]
271
+ signals[wavelength][float(peak.group("RetTime"))] = {}
272
+ current = signals[wavelength][
273
+ float(peak.group("RetTime"))
274
+ ]
233
275
  for key in self._column_re_dictionary:
234
276
  if key in peak.re.groupindex:
235
277
  try: # try float conversion, otherwise continue
@@ -248,30 +290,35 @@ class TXTProcessor(ReportProcessor):
248
290
  :param signal_table: block of lines associated with an area table
249
291
  :return: peak line regex object (<=3.6 _sre.SRE_PATTERN, >=3.7 re.Pattern)
250
292
  """
251
- split_table = signal_table.split('\n')
293
+ split_table = signal_table.split("\n")
252
294
  if len(split_table) <= 4: # catch peak table with no values
253
295
  return None
254
296
  # todo verify that these indicies are always true
255
297
  column_line = split_table[2] # table column line
256
298
  unit_line = split_table[3] # column unit line
257
- length_line = [len(val) + 1 for val in split_table[4].split('|')] # length line
299
+ length_line = [len(val) + 1 for val in split_table[4].split("|")] # length line
258
300
 
259
301
  # iterate over header values and units to build peak table regex
260
302
  peak_re_string = []
261
303
  for header, unit in zip(
262
- chunk_string(column_line, length_line),
263
- chunk_string(unit_line, length_line)
304
+ chunk_string(column_line, length_line), chunk_string(unit_line, length_line)
264
305
  ):
265
- if header == '': # todo create a better catch for an undefined header
306
+ if header == "": # todo create a better catch for an undefined header
266
307
  continue
267
308
  try:
268
309
  peak_re_string.append(
269
- self._column_re_dictionary[header][unit] # append the appropriate regex
310
+ self._column_re_dictionary[header][
311
+ unit
312
+ ] # append the appropriate regex
270
313
  )
271
314
  except KeyError: # catch for undefined regexes (need to be built)
272
- raise KeyError(f'The header/unit combination "{header}" "{unit}" is not defined in the peak regex '
273
- f'dictionary. Let Lars know.')
315
+ raise KeyError(
316
+ f'The header/unit combination "{header}" "{unit}" is not defined in the peak regex '
317
+ f"dictionary. Let Lars know."
318
+ )
274
319
  return re.compile(
275
- '[ ]+'.join(peak_re_string) # constructed string delimited by 1 or more spaces
276
- + '[\s]*' # and any remaining white space
320
+ "[ ]+".join(
321
+ peak_re_string
322
+ ) # constructed string delimited by 1 or more spaces
323
+ + "[\s]*" # and any remaining white space
277
324
  )
@@ -1,28 +1,24 @@
1
- # Examples of usecases
1
+ # Examples of usecases
2
2
 
3
- ## Initialization
4
3
  ```python
5
4
  from pychemstation.control import HPLCController
6
5
 
7
6
  DEFAULT_METHOD_DIR = "C:\\ChemStation\\1\\Methods\\"
8
- DATA_DIR = "C:\\Users\\Public\\Documents\\ChemStation\\3\\Data"
9
7
  SEQUENCE_DIR = "C:\\USERS\\PUBLIC\\DOCUMENTS\\CHEMSTATION\\3\\Sequence"
10
8
  DEFAULT_COMMAND_PATH = "C:\\Users\\User\\Desktop\\Lucy\\"
9
+ DATA_DIR_2 = "C:\\Users\\Public\\Documents\\ChemStation\\2\\Data"
10
+ DATA_DIR_3 = "C:\\Users\\Public\\Documents\\ChemStation\\3\\Data"
11
11
 
12
- hplc_controller = HPLCController(data_dir=DATA_DIR,
12
+ # Initialize HPLC Controller
13
+ hplc_controller = HPLCController(data_dirs=[DATA_DIR_2, DATA_DIR_3],
13
14
  comm_dir=DEFAULT_COMMAND_PATH,
14
15
  method_dir=DEFAULT_METHOD_DIR,
15
16
  sequence_dir=SEQUENCE_DIR)
16
- ```
17
17
 
18
- ## Switching a method
19
- ```python
18
+ # Switching a method
20
19
  hplc_controller.switch_method("General-Poroshell")
21
- ```
22
-
23
- ## Editing a method
24
20
 
25
- ```python
21
+ # Editing a method
26
22
  from pychemstation.utils.method_types import *
27
23
 
28
24
  new_method = MethodDetails(
@@ -45,47 +41,27 @@ new_method = MethodDetails(
45
41
  stop_time=5,
46
42
  post_time=2
47
43
  )
48
-
49
44
  hplc_controller.edit_method(new_method)
50
- ```
51
45
 
52
- ## Running a method and get data from last run method
53
- ```python
46
+ # Run a method and get a report or data from last run method
54
47
  hplc_controller.run_method(experiment_name="test_experiment")
55
48
  chrom = hplc_controller.get_last_run_method_data()
56
49
  channel_a_time = chrom.A.x
57
- ```
50
+ report = hplc_controller.get_last_run_method_report()
51
+ vial_location = report.vial_location
58
52
 
59
- ## Switching a sequence
60
- ```python
53
+ # switch the currently loaded sequence
61
54
  hplc_controller.switch_sequence(sequence_name="hplc_testing")
62
- ```
63
- ## Editing a Sequence Row
64
- ```python
65
- from pychemstation.utils.sequence_types import *
66
- from pychemstation.utils.tray_types import *
67
55
 
68
- hplc_controller.edit_sequence_row(SequenceEntry(
69
- vial_location=FiftyFourVialPlate(plate=Plate.TWO, letter=Letter.A, num=Num.SEVEN).value(),
70
- method="General-Poroshell",
71
- num_inj=3,
72
- inj_vol=4,
73
- sample_name="Blank",
74
- sample_type=SampleType.BLANK,
75
- inj_source=InjectionSource.HIP_ALS
76
- ), 1)
77
- ```
78
-
79
- ## Editing entire Sequence Table
80
- ```python
56
+ # edit the sequence table
81
57
  from pychemstation.utils.tray_types import *
82
58
  from pychemstation.utils.sequence_types import *
83
59
 
84
60
  seq_table = SequenceTable(
85
- name=DEFAULT_SEQUENCE,
61
+ name="hplc_testing",
86
62
  rows=[
87
63
  SequenceEntry(
88
- vial_location=FiftyFourVialPlate(plate=Plate.TWO, letter=Letter.A, num=Num.SEVEN).value(),
64
+ vial_location=FiftyFourVialPlate.from_str("P1-A1"),
89
65
  method="General-Poroshell",
90
66
  num_inj=3,
91
67
  inj_vol=4,
@@ -94,7 +70,7 @@ seq_table = SequenceTable(
94
70
  inj_source=InjectionSource.MANUAL
95
71
  ),
96
72
  SequenceEntry(
97
- vial_location=TenVialColumn.ONE.value,
73
+ vial_location=TenVialColumn.ONE,
98
74
  method="General-Poroshell",
99
75
  num_inj=1,
100
76
  inj_vol=1,
@@ -103,7 +79,7 @@ seq_table = SequenceTable(
103
79
  inj_source=InjectionSource.AS_METHOD
104
80
  ),
105
81
  SequenceEntry(
106
- vial_location=10,
82
+ vial_location=FiftyFourVialPlate.from_str("P2-B4"),
107
83
  method="General-Poroshell",
108
84
  num_inj=3,
109
85
  inj_vol=4,
@@ -114,11 +90,11 @@ seq_table = SequenceTable(
114
90
  ]
115
91
  )
116
92
  hplc_controller.edit_sequence(seq_table)
117
- ```
118
93
 
119
- ## Running a sequence and get data from last run sequence
120
- ```python
121
- hplc_controller.run_sequence(seq_table)
122
- chroms = hplc_controller.get_last_run_sequence_data()
123
- channel_A_time = chroms[0].A.x
94
+ # Run a sequence and get data or report from last run sequence
95
+ hplc_controller.run_sequence()
96
+ chroms = hplc_controller.get_last_run_sequence_data(read_uv=True)
97
+ row_1_channel_A_abs = chroms[0][210].y
98
+ report = hplc_controller.get_last_run_sequence_reports()
99
+ vial_location_row_1 = report[0].vial_location
124
100
  ```
@@ -1,4 +1,9 @@
1
1
  """
2
2
  .. include:: README.md
3
3
  """
4
+
4
5
  from .hplc import HPLCController
6
+
7
+ __all__ = [
8
+ "HPLCController",
9
+ ]
@@ -5,3 +5,5 @@
5
5
  from .comm import CommunicationController
6
6
  from .tables.method import MethodController
7
7
  from .tables.sequence import SequenceController
8
+
9
+ __all__ = ["CommunicationController", "MethodController", "SequenceController"]
@@ -9,13 +9,21 @@ been processed.
9
9
 
10
10
  Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
11
11
  """
12
+
12
13
  import os
13
14
  import time
14
- from typing import Optional
15
+ from typing import Optional, Union
15
16
 
16
- from result import Result, Ok, Err
17
+ from result import Err, Ok, Result
17
18
 
18
- from ...utils.macro import *
19
+ from ...utils.macro import (
20
+ str_to_status,
21
+ HPLCAvailStatus,
22
+ HPLCErrorStatus,
23
+ Command,
24
+ Status,
25
+ Response,
26
+ )
19
27
 
20
28
 
21
29
  class CommunicationController:
@@ -27,11 +35,11 @@ class CommunicationController:
27
35
  MAX_CMD_NO = 255
28
36
 
29
37
  def __init__(
30
- self,
31
- comm_dir: str,
32
- cmd_file: str = "cmd",
33
- reply_file: str = "reply",
34
- debug: bool = False
38
+ self,
39
+ comm_dir: str,
40
+ cmd_file: str = "cmd",
41
+ reply_file: str = "reply",
42
+ debug: bool = False,
35
43
  ):
36
44
  """
37
45
  :param comm_dir:
@@ -55,7 +63,7 @@ class CommunicationController:
55
63
  self.reset_cmd_counter()
56
64
 
57
65
  # Initialize row counter for table operations
58
- self.send('Local Rows')
66
+ self.send("Local Rows")
59
67
 
60
68
  def get_num_val(self, cmd: str) -> Union[int, float]:
61
69
  tries = 5
@@ -138,6 +146,7 @@ class CommunicationController:
138
146
  :return: Potential ChemStation response
139
147
  """
140
148
  err: Optional[Union[OSError, IndexError]] = None
149
+ err_msg = ""
141
150
  for _ in range(num_attempts):
142
151
  time.sleep(1)
143
152
 
@@ -150,7 +159,11 @@ class CommunicationController:
150
159
 
151
160
  try:
152
161
  first_line = response.splitlines()[0]
153
- response_no = int(first_line.split()[0])
162
+ try:
163
+ response_no = int(first_line.split()[0])
164
+ except ValueError as e:
165
+ err = e
166
+ err_msg = f"Caused by {first_line}"
154
167
  except IndexError as e:
155
168
  err = e
156
169
  continue
@@ -161,7 +174,9 @@ class CommunicationController:
161
174
  else:
162
175
  continue
163
176
  else:
164
- return Err(f"Failed to receive reply to command #{cmd_no} due to {err}.")
177
+ return Err(
178
+ f"Failed to receive reply to command #{cmd_no} due to {err} caused by {err_msg}."
179
+ )
165
180
 
166
181
  def sleepy_send(self, cmd: Union[Command, str]):
167
182
  self.send("Sleep 0.1")
@@ -187,20 +202,28 @@ class CommunicationController:
187
202
  def receive(self) -> Result[Response, str]:
188
203
  """Returns messages received in reply file.
189
204
 
190
- :return: ChemStation response
205
+ :return: ChemStation response
191
206
  """
192
207
  num_response_prefix = "Numerical Responses:"
193
208
  str_response_prefix = "String Responses:"
194
209
  possible_response = self._receive(self.cmd_no)
195
210
  if possible_response.is_ok():
196
- lines = possible_response.value.splitlines()
211
+ lines = possible_response.ok_value.splitlines()
197
212
  for line in lines:
198
213
  if str_response_prefix in line and num_response_prefix in line:
199
- string_responses_dirty, _, numerical_responses = line.partition(num_response_prefix)
200
- _, _, string_responses = string_responses_dirty.partition(str_response_prefix)
201
- return Ok(Response(string_response=string_responses.strip(),
202
- num_response=float(numerical_responses.strip())))
203
- return Err(f"Could not retrieve HPLC response")
214
+ string_responses_dirty, _, numerical_responses = line.partition(
215
+ num_response_prefix
216
+ )
217
+ _, _, string_responses = string_responses_dirty.partition(
218
+ str_response_prefix
219
+ )
220
+ return Ok(
221
+ Response(
222
+ string_response=string_responses.strip(),
223
+ num_response=float(numerical_responses.strip()),
224
+ )
225
+ )
226
+ return Err("Could not retrieve HPLC response")
204
227
  else:
205
228
  return Err(f"Could not establish response to HPLC: {possible_response}")
206
229