pychemstation 0.10.6__py3-none-any.whl → 0.10.7.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.
Files changed (31) hide show
  1. pychemstation/analysis/base_spectrum.py +14 -15
  2. pychemstation/analysis/chromatogram.py +7 -8
  3. pychemstation/analysis/process_report.py +7 -15
  4. pychemstation/control/README.md +2 -2
  5. pychemstation/control/controllers/__init__.py +2 -1
  6. pychemstation/control/controllers/comm.py +40 -13
  7. pychemstation/control/controllers/data_aq/method.py +19 -22
  8. pychemstation/control/controllers/data_aq/sequence.py +129 -111
  9. pychemstation/control/controllers/devices/injector.py +7 -7
  10. pychemstation/control/hplc.py +57 -60
  11. pychemstation/utils/__init__.py +23 -0
  12. pychemstation/utils/{mocking → abc_tables}/abc_comm.py +8 -14
  13. pychemstation/utils/abc_tables/device.py +27 -0
  14. pychemstation/{control/controllers → utils}/abc_tables/run.py +69 -34
  15. pychemstation/{control/controllers → utils}/abc_tables/table.py +29 -22
  16. pychemstation/utils/macro.py +13 -0
  17. pychemstation/utils/method_types.py +12 -13
  18. pychemstation/utils/mocking/mock_comm.py +1 -1
  19. pychemstation/utils/num_utils.py +3 -3
  20. pychemstation/utils/sequence_types.py +30 -12
  21. pychemstation/utils/spec_utils.py +42 -66
  22. pychemstation/utils/table_types.py +13 -2
  23. pychemstation/utils/tray_types.py +28 -16
  24. {pychemstation-0.10.6.dist-info → pychemstation-0.10.7.dev1.dist-info}/METADATA +2 -8
  25. pychemstation-0.10.7.dev1.dist-info/RECORD +41 -0
  26. pychemstation/control/controllers/abc_tables/device.py +0 -15
  27. pychemstation/utils/pump_types.py +0 -7
  28. pychemstation-0.10.6.dist-info/RECORD +0 -42
  29. /pychemstation/{control/controllers → utils}/abc_tables/__init__.py +0 -0
  30. {pychemstation-0.10.6.dist-info → pychemstation-0.10.7.dev1.dist-info}/WHEEL +0 -0
  31. {pychemstation-0.10.6.dist-info → pychemstation-0.10.7.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,7 @@ import logging
2
2
  import os
3
3
  import pickle
4
4
  from abc import ABC, abstractmethod
5
+ from typing import Optional, Union
5
6
 
6
7
  import matplotlib.pyplot as plt
7
8
  import numpy as np
@@ -43,16 +44,15 @@ class AbstractSpectrum(ABC):
43
44
  "baseline",
44
45
  }
45
46
 
46
- def __init__(self, path=None, autosaving=True):
47
+ def __init__(
48
+ self, path: Optional[Union[str, bool]] = None, autosaving: Optional[bool] = True
49
+ ):
47
50
  """Default constructor, loads properties into instance namespace.
48
51
 
49
52
  Can be redefined in ancestor classes.
50
53
 
51
- Args:
52
- path (Union[str, bool], optional): Valid path to save data to.
53
- If omitted, uses ".//spectrum". If False - no folder created.
54
- autosaving (bool, optional): If the True (default) will save the
55
- spectrum when the new one is loaded. Will drop otherwise.
54
+ :param path: Valid path to save data to. If omitted, uses ".//spectrum". If False - no folder created.
55
+ :param autosaving: If the True (default) will save the spectrum when the new one is loaded. Will drop otherwise.
56
56
  """
57
57
 
58
58
  self.autosaving = autosaving
@@ -70,10 +70,10 @@ class AbstractSpectrum(ABC):
70
70
  self.path = os.path.join(".", "spectrum")
71
71
  os.makedirs(self.path, exist_ok=True)
72
72
  else:
73
- try:
73
+ if isinstance(path, str):
74
74
  os.makedirs(path, exist_ok=True)
75
75
  self.path = path
76
- except TypeError: # type(path) -> bool
76
+ else:
77
77
  self.path = "."
78
78
 
79
79
  # creating logger
@@ -86,15 +86,14 @@ class AbstractSpectrum(ABC):
86
86
  self.__init__(path=self.path, autosaving=self.autosaving)
87
87
 
88
88
  @abstractmethod
89
- def load_spectrum(self, x, y, timestamp):
89
+ def load_spectrum(self, x: np.ndarray, y: np.ndarray, timestamp: float):
90
90
  """Loads the spectral data.
91
91
 
92
92
  This method must be redefined in ancestor classes.
93
93
 
94
- Args:
95
- x (:obj: np.array): An array with data to be plotted as "x" axis.
96
- y (:obj: np.array): An array with data to be plotted as "y" axis.
97
- timestamp (float): Timestamp to the corresponding spectrum.
94
+ :param x: An array with data to be plotted as "x" axis.
95
+ :param y: An array with data to be plotted as "y" axis.
96
+ :param timestamp: Timestamp to the corresponding spectrum.
98
97
  """
99
98
 
100
99
  try:
@@ -107,8 +106,8 @@ class AbstractSpectrum(ABC):
107
106
  self.save_data()
108
107
  self._dump()
109
108
 
110
- self.x = x
111
- self.y = y
109
+ self.x: np.ndarray = x
110
+ self.y: np.ndarray = y
112
111
  self.timestamp = timestamp
113
112
 
114
113
  def save_data(self, filename=None, verbose=False):
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  import time
5
5
  from dataclasses import dataclass
6
- from typing import Dict
6
+ from typing import Dict, Tuple
7
7
 
8
8
  import numpy as np
9
9
 
@@ -80,15 +80,14 @@ class AgilentHPLCChromatogram(AbstractSpectrum):
80
80
 
81
81
  ### PUBLIC METHODS TO LOAD RAW DATA ###
82
82
 
83
- def extract_rawdata(self, experiment_dir: str, channel: str):
84
- """
85
- Reads raw data from Chemstation .CH files.
83
+ def extract_rawdata(
84
+ self, experiment_dir: str, channel: str
85
+ ) -> Tuple[np.ndarray, np.ndarray]:
86
+ """Reads raw data from Chemstation .CH files.
86
87
 
87
- Args:
88
- experiment_dir: .D directory with the .CH files
88
+ :param experiment_dir: .D directory with the .CH files
89
89
 
90
- Returns:
91
- np.array(times), np.array(values) Raw chromatogram data
90
+ :returns: np.array(times), np.array(values) Raw chromatogram data
92
91
  """
93
92
  filename = os.path.join(experiment_dir, f"DAD1{channel}")
94
93
  npz_file = filename + ".npz"
@@ -65,8 +65,7 @@ class ReportProcessor(abc.ABC):
65
65
 
66
66
  class CSVProcessor(ReportProcessor):
67
67
  def __init__(self, path: str):
68
- """
69
- Class to process reports in CSV form.
68
+ """Class to process reports in CSV form.
70
69
 
71
70
  :param path: the parent folder that contains the CSV report(s) to parse.
72
71
  """
@@ -104,8 +103,7 @@ class CSVProcessor(ReportProcessor):
104
103
  return all_labels_seen
105
104
 
106
105
  def process_report(self) -> Result[AgilentReport, AnyStr]:
107
- """
108
- Method to parse details from CSV report.
106
+ """Method to parse details from CSV report.
109
107
 
110
108
  :return: subset of complete report details, specifically the sample location, solvents in pumps,
111
109
  and list of peaks at each wavelength channel.
@@ -176,9 +174,7 @@ class CSVProcessor(ReportProcessor):
176
174
 
177
175
 
178
176
  class TXTProcessor(ReportProcessor):
179
- """
180
- Regex matches for column and unit combinations, courtesy of Veronica Lai.
181
- """
177
+ """Regex matches for column and unit combinations, courtesy of Veronica Lai."""
182
178
 
183
179
  _column_re_dictionary = {
184
180
  "Peak": { # peak index
@@ -212,8 +208,7 @@ class TXTProcessor(ReportProcessor):
212
208
  max_ret_time: int = 999,
213
209
  target_wavelength_range=None,
214
210
  ):
215
- """
216
- Class to process reports in CSV form.
211
+ """Class to process reports in CSV form.
217
212
 
218
213
  :param path: the parent folder that contains the CSV report(s) to parse.
219
214
  :param min_ret_time: peaks after this value (min) will be returned
@@ -228,8 +223,7 @@ class TXTProcessor(ReportProcessor):
228
223
  super().__init__(path)
229
224
 
230
225
  def process_report(self) -> Result[AgilentReport, Union[AnyStr, Exception]]:
231
- """
232
- Method to parse details from CSV report.
226
+ """Method to parse details from CSV report.
233
227
  If you want more functionality, use `aghplctools`.
234
228
  `from aghplctools.ingestion.text import pull_hplc_area_from_txt`
235
229
  `signals = pull_hplc_area_from_txt(file_path)`
@@ -281,8 +275,7 @@ class TXTProcessor(ReportProcessor):
281
275
  return Err(e)
282
276
 
283
277
  def parse_area_report(self, report_text: str) -> Dict:
284
- """
285
- Interprets report text and parses the area report section, converting it to dictionary.
278
+ """Interprets report text and parses the area report section, converting it to dictionary.
286
279
  Courtesy of Veronica Lai.
287
280
 
288
281
  :param report_text: plain text version of the report.
@@ -340,8 +333,7 @@ class TXTProcessor(ReportProcessor):
340
333
  return signals
341
334
 
342
335
  def build_peak_regex(self, signal_table: str) -> Pattern[str] | None:
343
- """
344
- Builds a peak regex from a signal table. Courtesy of Veronica Lai.
336
+ """Builds a peak regex from a signal table. Courtesy of Veronica Lai.
345
337
 
346
338
  :param signal_table: block of lines associated with an area table
347
339
  :return: peak line regex object (<=3.6 _sre.SRE_PATTERN, >=3.7 re.Pattern)
@@ -10,7 +10,7 @@ DATA_DIR_2 = "C:\\Users\\Public\\Documents\\ChemStation\\2\\Data"
10
10
  DATA_DIR_3 = "C:\\Users\\Public\\Documents\\ChemStation\\3\\Data"
11
11
 
12
12
  # Initialize HPLC Controller
13
- hplc_controller = HPLCController(data_dirs=[DATA_DIR_2, DATA_DIR_3],
13
+ hplc_controller = HPLCController(extra_data_dirs=[DATA_DIR_2, DATA_DIR_3],
14
14
  comm_dir=DEFAULT_COMMAND_PATH,
15
15
  method_dir=DEFAULT_METHOD_DIR,
16
16
  sequence_dir=SEQUENCE_DIR)
@@ -70,7 +70,7 @@ seq_table = SequenceTable(
70
70
  inj_source=InjectionSource.MANUAL
71
71
  ),
72
72
  SequenceEntry(
73
- vial_location=TenVialColumn.ONE,
73
+ vial_location=VialBar.ONE,
74
74
  method="General-Poroshell",
75
75
  num_inj=1,
76
76
  inj_vol=1,
@@ -4,5 +4,6 @@
4
4
 
5
5
  from .comm import CommunicationController
6
6
  from . import data_aq
7
+ from . import devices
7
8
 
8
- __all__ = ["CommunicationController", "data_aq"]
9
+ __all__ = ["CommunicationController", "data_aq", "devices"]
@@ -11,22 +11,27 @@ Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
11
11
  """
12
12
 
13
13
  import time
14
- from typing import Optional, Union
14
+ from typing import Optional, Union, Tuple, List
15
15
 
16
16
  from result import Err, Ok, Result
17
17
 
18
+ from ...utils.abc_tables.abc_comm import ABCCommunicationController
18
19
  from ...utils.macro import (
19
- str_to_status,
20
- HPLCErrorStatus,
21
20
  Command,
21
+ HPLCErrorStatus,
22
22
  Status,
23
+ str_to_status,
23
24
  )
24
- from ...utils.mocking.abc_comm import ABCCommunicationController
25
25
 
26
26
 
27
27
  class CommunicationController(ABCCommunicationController):
28
- """
29
- Class that communicates with Agilent using Macros
28
+ """Class that communicates with Agilent using Macros
29
+
30
+ :param comm_dir: the complete directory path that was used in the MACRO file, common file that pychemstation and Chemstation use to communicate.
31
+ :param cmd_file: name of the write file that pychemstation writes MACROs to, in `comm_dir`
32
+ :param reply_file: name of the read file that Chemstation replies to, in `comm_dir
33
+ :param offline: whether or not communication with Chemstation is to be established
34
+ :param debug: if True, prints all send MACROs to an out.txt file
30
35
  """
31
36
 
32
37
  def __init__(
@@ -37,12 +42,6 @@ class CommunicationController(ABCCommunicationController):
37
42
  offline: bool = False,
38
43
  debug: bool = False,
39
44
  ):
40
- """
41
- :param comm_dir:
42
- :param cmd_file: Name of command file
43
- :param reply_file: Name of reply file
44
- :param debug: whether to save log of sent commands
45
- """
46
45
  super().__init__(comm_dir, cmd_file, reply_file, offline, debug)
47
46
 
48
47
  def get_num_val(self, cmd: str) -> Union[int, float]:
@@ -76,7 +75,7 @@ class CommunicationController(ABCCommunicationController):
76
75
  if res.is_err():
77
76
  return HPLCErrorStatus.NORESPONSE
78
77
  if res.is_ok():
79
- parsed_response = self.receive().value.string_response
78
+ parsed_response = self.receive().ok_value.string_response
80
79
  self._most_recent_hplc_status = str_to_status(parsed_response)
81
80
  return self._most_recent_hplc_status
82
81
  else:
@@ -148,3 +147,31 @@ class CommunicationController(ABCCommunicationController):
148
147
  return Err(
149
148
  f"Failed to receive reply to command #{cmd_no} due to {err} caused by {err_msg}."
150
149
  )
150
+
151
+ def get_chemstation_dirs(self) -> Tuple[str, str, List[str]]:
152
+ method_dir, sequence_dir, data_dirs = None, None, None
153
+ for _ in range(10):
154
+ self.send(Command.GET_METHOD_DIR)
155
+ res = self.receive()
156
+ if res.is_ok():
157
+ method_dir = res.ok_value.string_response
158
+ self.send(Command.GET_SEQUENCE_DIR)
159
+ res = self.receive()
160
+ if res.is_ok():
161
+ sequence_dir = res.ok_value.string_response
162
+ self.send(Command.GET_DATA_DIRS)
163
+ res = self.receive()
164
+ if res.is_ok():
165
+ data_dirs = res.ok().string_response.split("|")
166
+ if method_dir and sequence_dir and data_dirs:
167
+ if not sequence_dir[0].isalpha():
168
+ sequence_dir = "C:" + sequence_dir
169
+ if not method_dir[0].isalpha():
170
+ method_dir = "C:" + method_dir
171
+ for i, data_dir in enumerate(data_dirs):
172
+ if not data_dir[0].isalpha():
173
+ data_dirs[i] = "C:" + data_dir
174
+ return method_dir, sequence_dir, data_dirs
175
+ raise ValueError(
176
+ f"One of the method: {method_dir}, sequence: {sequence_dir} or data directories: {data_dirs} could not be found, please provide your own."
177
+ )
@@ -3,11 +3,10 @@ from __future__ import annotations
3
3
  import os
4
4
  import time
5
5
  import warnings
6
- from typing import List, Optional, Union, Dict
6
+ from typing import List, Optional, Union, Dict, Set
7
7
 
8
8
  from result import Err, Ok, Result
9
9
 
10
- from ..abc_tables.run import RunController
11
10
  from ....analysis.process_report import AgilentReport, ReportType
12
11
  from ....control.controllers import CommunicationController
13
12
  from pychemstation.analysis.chromatogram import (
@@ -15,6 +14,7 @@ from pychemstation.analysis.chromatogram import (
15
14
  AgilentChannelChromatogramData,
16
15
  AgilentHPLCChromatogram,
17
16
  )
17
+ from ....utils.abc_tables.run import RunController
18
18
  from ....utils.macro import Command
19
19
  from ....utils.method_types import (
20
20
  HPLCMethodParams,
@@ -28,15 +28,13 @@ from ..devices.injector import InjectorController
28
28
 
29
29
 
30
30
  class MethodController(RunController):
31
- """
32
- Class containing method related logic
33
- """
31
+ """Class containing method related logic."""
34
32
 
35
33
  def __init__(
36
34
  self,
37
- controller: CommunicationController,
38
- src: str,
39
- data_dirs: List[str],
35
+ controller: Optional[CommunicationController],
36
+ src: Optional[str],
37
+ data_dirs: Optional[List[str]],
40
38
  table: Table,
41
39
  offline: bool,
42
40
  injector_controller: InjectorController,
@@ -51,7 +49,7 @@ class MethodController(RunController):
51
49
  offline=offline,
52
50
  )
53
51
 
54
- def check(self) -> str:
52
+ def get_current_method_name(self) -> str:
55
53
  time.sleep(2)
56
54
  self.send(Command.GET_METHOD_CMD)
57
55
  time.sleep(2)
@@ -413,8 +411,6 @@ class MethodController(RunController):
413
411
  :param stall_while_running: whether to stall or immediately return
414
412
  :param add_timestamp: if should add timestamp to experiment name
415
413
  """
416
-
417
- folder_name = ""
418
414
  hplc_is_running = False
419
415
  tries = 0
420
416
  while tries < 10 and not hplc_is_running:
@@ -427,18 +423,15 @@ class MethodController(RunController):
427
423
  else experiment_name,
428
424
  )
429
425
  )
430
- folder_name = (
431
- f"{experiment_name}_{timestamp}.D"
432
- if add_timestamp
433
- else f"{experiment_name}.D"
434
- )
426
+
435
427
  hplc_is_running = self.check_hplc_is_running()
436
428
  tries += 1
437
429
 
430
+ data_dir, data_file = self.get_current_run_data_dir_file()
438
431
  if not hplc_is_running:
439
432
  raise RuntimeError("Method failed to start.")
440
433
 
441
- self.data_files.append(os.path.join(self.data_dirs[0], folder_name))
434
+ self.data_files.append(os.path.join(os.path.normpath(data_dir), data_file))
442
435
  self.timeout = (self.get_total_runtime()) * 60
443
436
 
444
437
  if stall_while_running:
@@ -446,18 +439,22 @@ class MethodController(RunController):
446
439
  if run_completed.is_ok():
447
440
  self.data_files[-1] = run_completed.ok_value
448
441
  else:
449
- raise RuntimeError("Run error has occurred.")
442
+ warnings.warn(run_completed.err_value)
450
443
  else:
451
- folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
452
- while folder.is_err():
453
- folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
444
+ folder = self._fuzzy_match_most_recent_folder(self.data_files[-1], None)
445
+ i = 0
446
+ while folder.is_err() and i < 10:
447
+ folder = self._fuzzy_match_most_recent_folder(self.data_files[-1], None)
448
+ i+=1
454
449
  if folder.is_ok():
455
450
  self.data_files[-1] = folder.ok_value
456
451
  else:
457
452
  warning = f"Data folder {self.data_files[-1]} may not exist, returning and will check again after run is done."
458
453
  warnings.warn(warning)
459
454
 
460
- def fuzzy_match_most_recent_folder(self, most_recent_folder: T) -> Result[str, str]:
455
+ def _fuzzy_match_most_recent_folder(
456
+ self, most_recent_folder: T, child_dirs: Optional[Set[str]]
457
+ ) -> Result[str, str]:
461
458
  if isinstance(most_recent_folder, str) or isinstance(most_recent_folder, bytes):
462
459
  if os.path.exists(most_recent_folder):
463
460
  return Ok(most_recent_folder)