pychemstation 0.4.7.dev1__py3-none-any.whl → 0.4.7.dev2__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 (104) hide show
  1. pychemstation/control/__init__.py +3 -2
  2. hein-analytical-control/devices/Agilent/hplc.py → pychemstation/control/comm.py +21 -181
  3. pychemstation/control/method.py +232 -0
  4. pychemstation/control/sequence.py +140 -0
  5. pychemstation/control/table_controller.py +75 -0
  6. pychemstation/utils/__init__.py +0 -2
  7. {ag_hplc_macro/control → pychemstation/utils}/chromatogram.py +2 -1
  8. pychemstation/utils/constants.py +1 -1
  9. hein_analytical_control/devices/Agilent/hplc_param_types.py → pychemstation/utils/macro.py +5 -69
  10. pychemstation/utils/method_types.py +44 -0
  11. pychemstation/utils/sequence_types.py +33 -0
  12. pychemstation/utils/table_types.py +60 -0
  13. {pychemstation-0.4.7.dev1.dist-info → pychemstation-0.4.7.dev2.dist-info}/METADATA +13 -12
  14. pychemstation-0.4.7.dev2.dist-info/RECORD +30 -0
  15. ag_hplc_macro/__init__.py +0 -3
  16. ag_hplc_macro/analysis/__init__.py +0 -1
  17. ag_hplc_macro/analysis/base_spectrum.py +0 -509
  18. ag_hplc_macro/analysis/spec_utils.py +0 -304
  19. ag_hplc_macro/analysis/utils.py +0 -63
  20. ag_hplc_macro/control/__init__.py +0 -5
  21. ag_hplc_macro/control/hplc.py +0 -673
  22. ag_hplc_macro/generated/__init__.py +0 -56
  23. ag_hplc_macro/generated/dad_method.py +0 -367
  24. ag_hplc_macro/generated/pump_method.py +0 -519
  25. ag_hplc_macro/utils/__init__.py +0 -2
  26. ag_hplc_macro/utils/constants.py +0 -15
  27. ag_hplc_macro/utils/hplc_param_types.py +0 -185
  28. hein-analytical-control/__init__.py +0 -3
  29. hein-analytical-control/analysis/__init__.py +0 -1
  30. hein-analytical-control/analysis/base_spectrum.py +0 -509
  31. hein-analytical-control/analysis/spec_utils.py +0 -304
  32. hein-analytical-control/analysis/utils.py +0 -63
  33. hein-analytical-control/devices/Agilent/__init__.py +0 -3
  34. hein-analytical-control/devices/Agilent/chemstation.py +0 -290
  35. hein-analytical-control/devices/Agilent/chromatogram.py +0 -129
  36. hein-analytical-control/devices/Agilent/hplc_param_types.py +0 -141
  37. hein-analytical-control/devices/Magritek/Spinsolve/__init__.py +0 -0
  38. hein-analytical-control/devices/Magritek/Spinsolve/commands.py +0 -495
  39. hein-analytical-control/devices/Magritek/Spinsolve/spectrum.py +0 -822
  40. hein-analytical-control/devices/Magritek/Spinsolve/spinsolve.py +0 -425
  41. hein-analytical-control/devices/Magritek/Spinsolve/utils/__init__.py +0 -5
  42. hein-analytical-control/devices/Magritek/Spinsolve/utils/connection.py +0 -168
  43. hein-analytical-control/devices/Magritek/Spinsolve/utils/constants.py +0 -8
  44. hein-analytical-control/devices/Magritek/Spinsolve/utils/exceptions.py +0 -25
  45. hein-analytical-control/devices/Magritek/Spinsolve/utils/parser.py +0 -340
  46. hein-analytical-control/devices/Magritek/Spinsolve/utils/shimming.py +0 -55
  47. hein-analytical-control/devices/Magritek/Spinsolve/utils/spinsolve_logging.py +0 -43
  48. hein-analytical-control/devices/Magritek/__init__.py +0 -0
  49. hein-analytical-control/devices/OceanOptics/IR/NIRQuest512.py +0 -90
  50. hein-analytical-control/devices/OceanOptics/IR/__init__.py +0 -0
  51. hein-analytical-control/devices/OceanOptics/IR/ir_spectrum.py +0 -191
  52. hein-analytical-control/devices/OceanOptics/Raman/__init__.py +0 -0
  53. hein-analytical-control/devices/OceanOptics/Raman/raman_control.py +0 -46
  54. hein-analytical-control/devices/OceanOptics/Raman/raman_spectrum.py +0 -148
  55. hein-analytical-control/devices/OceanOptics/UV/QEPro2192.py +0 -90
  56. hein-analytical-control/devices/OceanOptics/UV/__init__.py +0 -0
  57. hein-analytical-control/devices/OceanOptics/UV/uv_spectrum.py +0 -227
  58. hein-analytical-control/devices/OceanOptics/__init__.py +0 -0
  59. hein-analytical-control/devices/OceanOptics/oceanoptics.py +0 -115
  60. hein-analytical-control/devices/__init__.py +0 -15
  61. hein-analytical-control/generated/__init__.py +0 -56
  62. hein-analytical-control/generated/dad_method.py +0 -367
  63. hein-analytical-control/generated/pump_method.py +0 -519
  64. hein_analytical_control/__init__.py +0 -3
  65. hein_analytical_control/analysis/__init__.py +0 -1
  66. hein_analytical_control/analysis/base_spectrum.py +0 -509
  67. hein_analytical_control/analysis/spec_utils.py +0 -304
  68. hein_analytical_control/analysis/utils.py +0 -63
  69. hein_analytical_control/devices/Agilent/__init__.py +0 -3
  70. hein_analytical_control/devices/Agilent/chemstation.py +0 -290
  71. hein_analytical_control/devices/Agilent/chromatogram.py +0 -129
  72. hein_analytical_control/devices/Agilent/hplc.py +0 -436
  73. hein_analytical_control/devices/Magritek/Spinsolve/__init__.py +0 -0
  74. hein_analytical_control/devices/Magritek/Spinsolve/commands.py +0 -495
  75. hein_analytical_control/devices/Magritek/Spinsolve/spectrum.py +0 -822
  76. hein_analytical_control/devices/Magritek/Spinsolve/spinsolve.py +0 -425
  77. hein_analytical_control/devices/Magritek/Spinsolve/utils/__init__.py +0 -5
  78. hein_analytical_control/devices/Magritek/Spinsolve/utils/connection.py +0 -168
  79. hein_analytical_control/devices/Magritek/Spinsolve/utils/constants.py +0 -8
  80. hein_analytical_control/devices/Magritek/Spinsolve/utils/exceptions.py +0 -25
  81. hein_analytical_control/devices/Magritek/Spinsolve/utils/parser.py +0 -340
  82. hein_analytical_control/devices/Magritek/Spinsolve/utils/shimming.py +0 -55
  83. hein_analytical_control/devices/Magritek/Spinsolve/utils/spinsolve_logging.py +0 -43
  84. hein_analytical_control/devices/Magritek/__init__.py +0 -0
  85. hein_analytical_control/devices/OceanOptics/IR/NIRQuest512.py +0 -90
  86. hein_analytical_control/devices/OceanOptics/IR/__init__.py +0 -0
  87. hein_analytical_control/devices/OceanOptics/IR/ir_spectrum.py +0 -191
  88. hein_analytical_control/devices/OceanOptics/Raman/__init__.py +0 -0
  89. hein_analytical_control/devices/OceanOptics/Raman/raman_control.py +0 -46
  90. hein_analytical_control/devices/OceanOptics/Raman/raman_spectrum.py +0 -148
  91. hein_analytical_control/devices/OceanOptics/UV/QEPro2192.py +0 -90
  92. hein_analytical_control/devices/OceanOptics/UV/__init__.py +0 -0
  93. hein_analytical_control/devices/OceanOptics/UV/uv_spectrum.py +0 -227
  94. hein_analytical_control/devices/OceanOptics/__init__.py +0 -0
  95. hein_analytical_control/devices/OceanOptics/oceanoptics.py +0 -115
  96. hein_analytical_control/devices/__init__.py +0 -15
  97. hein_analytical_control/generated/__init__.py +0 -56
  98. hein_analytical_control/generated/dad_method.py +0 -367
  99. hein_analytical_control/generated/pump_method.py +0 -519
  100. pychemstation-0.4.7.dev1.dist-info/RECORD +0 -109
  101. /ag_hplc_macro/utils/chemstation.py → /pychemstation/utils/parsing.py +0 -0
  102. {pychemstation-0.4.7.dev1.dist-info → pychemstation-0.4.7.dev2.dist-info}/LICENSE +0 -0
  103. {pychemstation-0.4.7.dev1.dist-info → pychemstation-0.4.7.dev2.dist-info}/WHEEL +0 -0
  104. {pychemstation-0.4.7.dev1.dist-info → pychemstation-0.4.7.dev2.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  """
2
2
  .. include:: README.md
3
3
  """
4
- from .hplc import HPLCController
5
- from .chromatogram import AgilentHPLCChromatogram
4
+ from .comm import HPLCController
5
+ from .method import MethodController
6
+ from .sequence import SequenceController
@@ -13,18 +13,13 @@ Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
13
13
  import logging
14
14
  import os
15
15
  import time
16
- from typing import Union
17
16
 
18
17
  import polling
19
- from xsdata.formats.dataclass.parsers import XmlParser
20
18
 
21
- from .chromatogram import AgilentHPLCChromatogram, TIME_FORMAT
22
- from .hplc_param_types import Command, HPLCAvailStatus, HPLCMethodParams, HPLCRunningStatus, HPLCErrorStatus, Param, \
23
- str_to_status, PType, MethodTimetable, Entry, RegisterFlag
24
- from ...generated import PumpMethod, DadMethod
25
-
26
- # maximum command number
27
- MAX_CMD_NO = 255
19
+ from ..utils.chromatogram import AgilentHPLCChromatogram
20
+ from ..utils.constants import MAX_CMD_NO
21
+ from ..utils.macro import *
22
+ from ..utils.method_types import *
28
23
 
29
24
 
30
25
  class HPLCController:
@@ -36,7 +31,6 @@ class HPLCController:
36
31
  self,
37
32
  comm_dir: str,
38
33
  data_dir: str,
39
- method_dir: str,
40
34
  cmd_file: str = "cmd",
41
35
  reply_file: str = "reply",
42
36
  ):
@@ -62,11 +56,6 @@ class HPLCController:
62
56
  else:
63
57
  raise FileNotFoundError(f"data_dir: {data_dir} not found.")
64
58
 
65
- if os.path.isdir(method_dir):
66
- self.method_dir = method_dir
67
- else:
68
- raise FileNotFoundError(f"method_dir: {method_dir} not found.")
69
-
70
59
  self.spectra = {
71
60
  "A": AgilentHPLCChromatogram(self.data_dir),
72
61
  "B": AgilentHPLCChromatogram(self.data_dir),
@@ -75,6 +64,7 @@ class HPLCController:
75
64
  }
76
65
 
77
66
  self.data_files: list[str] = []
67
+ self.internal_variables: list[dict[str, str]] = []
78
68
 
79
69
  # Create files for Chemstation to communicate with Python
80
70
  open(self.cmd_file, "a").close()
@@ -120,6 +110,17 @@ class HPLCController:
120
110
 
121
111
  return hplc_run_done
122
112
 
113
+ def get_spectrum(self):
114
+ """ Load last chromatogram for any channel in spectra dictionary."""
115
+ last_file = self.data_files[-1] if len(self.data_files) > 0 else None
116
+
117
+ if last_file is None:
118
+ raise IndexError
119
+
120
+ for channel, spec in self.controller.spectra.items():
121
+ spec.load_spectrum(data_path=last_file, channel=channel)
122
+ self.logger.info("%s chromatogram loaded.", channel)
123
+
123
124
  def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
124
125
  """Low-level execution primitive. Sends a command string to HPLC.
125
126
 
@@ -186,6 +187,11 @@ class HPLCController:
186
187
  else:
187
188
  raise IOError(f"Failed to receive reply to command #{cmd_no}.") from err
188
189
 
190
+ def sleepy_send(self, cmd: Union[Command, str]):
191
+ self.send("Sleep 0.1")
192
+ self.send(cmd)
193
+ self.send("Sleep 0.1")
194
+
189
195
  def send(self, cmd: Union[Command, str]):
190
196
  """Sends a command to Chemstation.
191
197
 
@@ -253,132 +259,6 @@ class HPLCController:
253
259
  """Stops Macro execution. Connection will be lost."""
254
260
  self.send(Command.STOP_MACRO_CMD)
255
261
 
256
- def update_method(self):
257
- pass
258
-
259
- def edit_method_timetable(self):
260
- pass
261
-
262
- def _update_method_timetable(self):
263
- pass
264
-
265
- def _update_method_param(self, method_param: Param):
266
- """Change a method parameter.
267
-
268
- :param method_param: a parameter to update for currently loaded method
269
- """
270
-
271
- setting_command = "SetObjHdrVal" if method_param.ptype == PType.NUM else "SetObjHdrText"
272
- if isinstance(method_param.chemstation_key, list):
273
- for register in method_param.chemstation_key:
274
- self.send(
275
- f'{setting_command} RCPMP1Method[1], {register}, {method_param.val}'
276
- )
277
- else:
278
- self.send(
279
- f'{setting_command} RCPMP1Method[1], {method_param.chemstation_key}, {method_param.val}'
280
- )
281
- time.sleep(2)
282
-
283
- def desired_method_already_loaded(self, method_name: str) -> bool:
284
- """Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
285
-
286
- :param method_name: a Chemstation method
287
- :return: True if method is already loaded
288
- """
289
- self.send(Command.GET_METHOD_CMD)
290
- parsed_response = self.receive().splitlines()[1].split()[1:][0]
291
- return method_name in parsed_response
292
-
293
- def switch_method(self, method_name: str):
294
- """Allows the user to switch between pre-programmed methods. No need to append '.M'
295
- to the end of the method name. For example. for the method named 'General-Poroshell.M',
296
- only 'General-Poroshell' is needed.
297
-
298
- :param method_name: any available method in Chemstation method directory
299
- :raise IndexError: Response did not have expected format. Try again.
300
- :raise AssertionError: The desired method is not selected. Try again.
301
- """
302
- self.send(
303
- Command.SWITCH_METHOD_CMD.value.format(method_dir=self.method_dir, method_name=method_name)
304
- )
305
-
306
- time.sleep(2)
307
- self.send(Command.GET_METHOD_CMD)
308
- time.sleep(2)
309
- # check that method switched
310
- for _ in range(10):
311
- try:
312
- parsed_response = self.receive().splitlines()[1].split()[1:][0]
313
- break
314
- except IndexError:
315
- self.logger.debug("Malformed response. Trying again.")
316
- continue
317
-
318
- assert parsed_response == f"{method_name}.M", "Switching Methods failed."
319
-
320
- def load_method_details(self, method_name: str) -> MethodTimetable:
321
- """Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
322
- organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
323
- only two solvents are being used.
324
-
325
- :param method_name: name of method to load details of
326
- :raises FileNotFoundError: Method does not exist
327
- :return: method details
328
- """
329
- method_path = os.path.join(self.method_dir, "AgilentPumpDriver1.RapidControl.MethodXML.xml")
330
- dad_path = os.path.join(self.method_dir, "Agilent1200erDadDriver1.RapidControl.MethodXML.xml")
331
-
332
- if os.path.exists(os.path.join(self.method_dir, f"{method_name}.M")):
333
- parser = XmlParser()
334
- method = parser.parse(method_path, PumpMethod)
335
- dad = parser.parse(dad_path, DadMethod)
336
-
337
- organic_modifier = None
338
- aq_modifier = None
339
-
340
- if len(method.solvent_composition.solvent_element) == 2:
341
- for solvent in method.solvent_composition.solvent_element:
342
- if solvent.channel == "Channel_A":
343
- aq_modifier = solvent
344
- elif solvent.channel == "Channel_B":
345
- organic_modifier = solvent
346
-
347
- return MethodTimetable(
348
- first_row=HPLCMethodParams(
349
- organic_modifier=Param(val=organic_modifier.percentage,
350
- chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
351
- ptype=PType.NUM),
352
- flow=Param(val=method.flow,
353
- chemstation_key=RegisterFlag.FLOW,
354
- ptype=PType.NUM),
355
- maximum_run_time=Param(val=method,
356
- chemstation_key=RegisterFlag.MAX_TIME,
357
- ptype=PType.NUM),
358
- temperature=Param(val=None,
359
- chemstation_key=[RegisterFlag.COLUMN_OVEN_TEMP1,
360
- RegisterFlag.COLUMN_OVEN_TEMP2],
361
- ptype=PType.NUM),
362
- inj_vol=Param(val=None,
363
- chemstation_key=None,
364
- ptype=PType.NUM),
365
- equ_time=Param(val=None,
366
- chemstation_key=None,
367
- ptype=PType.NUM)),
368
- subsequent_rows=[
369
- Entry(
370
- start_time=tte.time,
371
- organic_modifer=tte.percent_b,
372
- flow=method.flow
373
- ) for tte in method.timetable.timetable_entry
374
- ],
375
- dad_wavelengthes=dad.signals.signal,
376
- organic_modifier=organic_modifier,
377
- modifier_a=aq_modifier
378
- )
379
- else:
380
- raise FileNotFoundError
381
-
382
262
  def lamp_on(self):
383
263
  """Turns the UV lamp on."""
384
264
  self.send(Command.LAMP_ON_CMD)
@@ -394,43 +274,3 @@ class HPLCController:
394
274
  def pump_off(self):
395
275
  """Turns the pump off."""
396
276
  self.send(Command.PUMP_OFF_CMD)
397
-
398
- def start_method(self):
399
- """Starts and executes currently loaded method to run according to Run Time Checklist. Device must be ready."""
400
- self.send(Command.START_METHOD_CMD)
401
-
402
- def run_method(self, experiment_name: str):
403
- """This is the preferred method to trigger a run.
404
- Starts the currently selected method, storing data
405
- under the <data_dir>/<experiment_name>.D folder.
406
- The should <experiment_name> end with a timestamp in the '%Y-%m-%d-%H-%M' format.
407
- Device must be ready.
408
-
409
- :param experiment_name: Name of the experiment
410
- """
411
- timestamp = time.strftime(TIME_FORMAT)
412
-
413
- self.send(
414
- Command.RUN_METHOD_CMD.value.format(
415
- data_dir=self.data_dir, experiment_name=experiment_name, timestamp=timestamp
416
- )
417
- )
418
-
419
- folder_name = f"{experiment_name}_{timestamp}.D"
420
- self.data_files.append(os.path.join(self.data_dir, folder_name))
421
- self.logger.info("Started HPLC run: %s.", folder_name)
422
-
423
- def stop_method(self):
424
- """Stops the run. A dialog window will pop up and manual intervention may be required."""
425
- self.send(Command.STOP_METHOD_CMD)
426
-
427
- def get_spectrum(self):
428
- """ Load last chromatogram for any channel in spectra dictionary."""
429
- last_file = self.data_files[-1] if len(self.data_files) > 0 else None
430
-
431
- if last_file is None:
432
- raise IndexError
433
-
434
- for channel, spec in self.spectra.items():
435
- spec.load_spectrum(data_path=last_file, channel=channel)
436
- self.logger.info("%s chromatogram loaded.", channel)
@@ -0,0 +1,232 @@
1
+ import os
2
+ import time
3
+
4
+ from xsdata.formats.dataclass.parsers import XmlParser
5
+
6
+ from ..control.table_controller import TableController, HPLCController
7
+ from ..generated import DadMethod, PumpMethod
8
+ from ..utils.chromatogram import TIME_FORMAT
9
+ from ..utils.constants import METHOD_TIMETABLE
10
+ from ..utils.macro import Command
11
+ from ..utils.method_types import MethodTimetable, HPLCMethodParams, Param, PType, TimeTableEntry
12
+ from ..utils.table_types import RegisterFlag, TableOperation
13
+
14
+
15
+ class MethodController(TableController):
16
+ """
17
+ Class containing method related logic
18
+ """
19
+
20
+ def __init__(self, controller: HPLCController, src: str):
21
+ super().__init__(controller, src)
22
+
23
+ def is_loaded(self, method_name: str):
24
+ """Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
25
+
26
+ :param method_name: a Chemstation method
27
+ :return: True if method is already loaded
28
+ """
29
+ self.send(Command.GET_METHOD_CMD)
30
+ parsed_response = self.receive().splitlines()[1].split()[1:][0]
31
+ return method_name in parsed_response
32
+
33
+ def switch(self, method_name: str):
34
+ """Allows the user to switch between pre-programmed methods. No need to append '.M'
35
+ to the end of the method name. For example. for the method named 'General-Poroshell.M',
36
+ only 'General-Poroshell' is needed.
37
+
38
+ :param method_name: any available method in Chemstation method directory
39
+ :raise IndexError: Response did not have expected format. Try again.
40
+ :raise AssertionError: The desired method is not selected. Try again.
41
+ """
42
+ self.send(Command.SWITCH_METHOD_CMD.value.format(method_dir=self.src,
43
+ method_name=method_name))
44
+
45
+ time.sleep(2)
46
+ self.send(Command.GET_METHOD_CMD)
47
+ time.sleep(2)
48
+
49
+ # check that method switched
50
+ for _ in range(10):
51
+ try:
52
+ parsed_response = self.receive().splitlines()[1].split()[1:][0]
53
+ break
54
+ except IndexError:
55
+ continue
56
+
57
+ assert parsed_response == f"{method_name}.M", "Switching Methods failed."
58
+
59
+ def run(self, experiment_name: str):
60
+ """This is the preferred method to trigger a run.
61
+ Starts the currently selected method, storing data
62
+ under the <data_dir>/<experiment_name>.D folder.
63
+ The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
64
+ Device must be ready.
65
+
66
+ :param experiment_name: Name of the experiment
67
+ """
68
+ timestamp = time.strftime(TIME_FORMAT)
69
+
70
+ self.send(Command.RUN_METHOD_CMD.value.format(data_dir=self.src,
71
+ experiment_name=experiment_name,
72
+ timestamp=timestamp))
73
+
74
+ folder_name = f"{experiment_name}_{timestamp}.D"
75
+ self.controller.data_files.append(os.path.join(self.src, folder_name))
76
+
77
+ def load(self, method_name: str):
78
+ """Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
79
+ organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
80
+ only two solvents are being used.
81
+
82
+ :param method_name: name of method to load details of
83
+ :raises FileNotFoundError: Method does not exist
84
+ :return: method details
85
+ """
86
+ method_folder = f"{method_name}.M"
87
+ method_path = os.path.join(self.src, method_folder, "AgilentPumpDriver1.RapidControl.MethodXML.xml")
88
+ dad_path = os.path.join(self.src, method_folder, "Agilent1200erDadDriver1.RapidControl.MethodXML.xml")
89
+
90
+ if os.path.exists(os.path.join(self.src, f"{method_name}.M")):
91
+ parser = XmlParser()
92
+ method = parser.parse(method_path, PumpMethod)
93
+ dad = parser.parse(dad_path, DadMethod)
94
+
95
+ organic_modifier = None
96
+ aq_modifier = None
97
+
98
+ if len(method.solvent_composition.solvent_element) == 2:
99
+ for solvent in method.solvent_composition.solvent_element:
100
+ if solvent.channel == "Channel_A":
101
+ aq_modifier = solvent
102
+ elif solvent.channel == "Channel_B":
103
+ organic_modifier = solvent
104
+
105
+ return MethodTimetable(
106
+ first_row=HPLCMethodParams(
107
+ organic_modifier=Param(val=organic_modifier.percentage,
108
+ chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
109
+ ptype=PType.NUM),
110
+ flow=Param(val=method.flow,
111
+ chemstation_key=RegisterFlag.FLOW,
112
+ ptype=PType.NUM),
113
+ maximum_run_time=Param(val=method.stop_time,
114
+ chemstation_key=RegisterFlag.MAX_TIME,
115
+ ptype=PType.NUM),
116
+ temperature=Param(val=None,
117
+ chemstation_key=[RegisterFlag.COLUMN_OVEN_TEMP1,
118
+ RegisterFlag.COLUMN_OVEN_TEMP2],
119
+ ptype=PType.NUM),
120
+ inj_vol=Param(val=None,
121
+ chemstation_key=None,
122
+ ptype=PType.NUM),
123
+ equ_time=Param(val=None,
124
+ chemstation_key=None,
125
+ ptype=PType.NUM)),
126
+ subsequent_rows=[
127
+ TimeTableEntry(
128
+ start_time=tte.time,
129
+ organic_modifer=tte.percent_b,
130
+ flow=method.flow
131
+ ) for tte in method.timetable.timetable_entry
132
+ ],
133
+ dad_wavelengthes=dad.signals.signal,
134
+ organic_modifier=organic_modifier,
135
+ modifier_a=aq_modifier
136
+ )
137
+ else:
138
+ raise FileNotFoundError
139
+
140
+ def edit(self, updated_method: MethodTimetable):
141
+ """Updated the currently loaded method in ChemStation with provided values.
142
+
143
+ :param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
144
+ """
145
+ initial_organic_modifier: Param = updated_method.first_row.organic_modifier
146
+ max_time: Param = updated_method.first_row.maximum_run_time
147
+ temp: Param = updated_method.first_row.temperature
148
+ injvol: Param = updated_method.first_row.inj_vol
149
+ equalib_time: Param = updated_method.first_row.equ_time
150
+ flow: Param = updated_method.first_row.flow
151
+
152
+ # Method settings required for all runs
153
+ self.send(TableOperation.DELETE_TABLE.value.format(register=METHOD_TIMETABLE.register,
154
+ table_name=METHOD_TIMETABLE.name, ))
155
+ self._update_param(initial_organic_modifier)
156
+ self._update_param(flow)
157
+ self._update_param(Param(val="Set",
158
+ chemstation_key=RegisterFlag.STOPTIME_MODE,
159
+ ptype=PType.STR))
160
+ self._update_param(max_time)
161
+ self._update_param(Param(val="Off",
162
+ chemstation_key=RegisterFlag.POSTIME_MODE,
163
+ ptype=PType.STR))
164
+
165
+ self.send("DownloadRCMethod PMP1")
166
+
167
+ self._update_method_timetable(updated_method.subsequent_rows)
168
+
169
+ def _update_method_timetable(self, timetable_rows: list[TimeTableEntry]):
170
+ self.sleepy_send('Local Rows')
171
+ self._get_table_rows(METHOD_TIMETABLE)
172
+
173
+ self.sleepy_send('DelTab RCPMP1Method[1], "Timetable"')
174
+ res = self._get_table_rows(METHOD_TIMETABLE)
175
+ while "ERROR" not in res:
176
+ self.sleepy_send('DelTab RCPMP1Method[1], "Timetable"')
177
+ res = self._get_table_rows(METHOD_TIMETABLE)
178
+
179
+ self.sleepy_send('NewTab RCPMP1Method[1], "Timetable"')
180
+ self._get_table_rows(METHOD_TIMETABLE)
181
+
182
+ for i, row in enumerate(timetable_rows):
183
+ if i == 0:
184
+ self.send('Sleep 1')
185
+ self.sleepy_send('InsTabRow RCPMP1Method[1], "Timetable"')
186
+ self.send('Sleep 1')
187
+
188
+ self.sleepy_send('NewColText RCPMP1Method[1], "Timetable", "Function", "SolventComposition"')
189
+ self.sleepy_send(f'NewColVal RCPMP1Method[1], "Timetable", "Time", {row.start_time}')
190
+ self.sleepy_send(
191
+ f'NewColVal RCPMP1Method[1], "Timetable", "SolventCompositionPumpChannel2_Percentage", {row.organic_modifer}')
192
+
193
+ self.send('Sleep 1')
194
+ self.sleepy_send("DownloadRCMethod PMP1")
195
+ self.send('Sleep 1')
196
+ else:
197
+ self.sleepy_send('InsTabRow RCPMP1Method[1], "Timetable"')
198
+ self._get_table_rows(METHOD_TIMETABLE)
199
+
200
+ self.sleepy_send(
201
+ f'SetTabText RCPMP1Method[1], "Timetable", Rows, "Function", "SolventComposition"')
202
+ self.sleepy_send(
203
+ f'SetTabVal RCPMP1Method[1], "Timetable", Rows, "Time", {row.start_time}')
204
+ self.sleepy_send(
205
+ f'SetTabVal RCPMP1Method[1], "Timetable", Rows, "SolventCompositionPumpChannel2_Percentage", {row.organic_modifer}')
206
+
207
+ self.send("Sleep 1")
208
+ self.sleepy_send("DownloadRCMethod PMP1")
209
+ self.send("Sleep 1")
210
+ self._get_table_rows(METHOD_TIMETABLE)
211
+
212
+ def _update_param(self, method_param: Param):
213
+ """Change a method parameter, changes what is visibly seen in Chemstation GUI. (changes the first row in the timetable)
214
+
215
+ :param method_param: a parameter to update for currently loaded method.
216
+ """
217
+ register = METHOD_TIMETABLE.register
218
+ setting_command = TableOperation.UPDATE_OBJ_HDR_VAL if method_param.ptype == PType.NUM else TableOperation.UPDATE_OBJ_HDR_TEXT
219
+ if isinstance(method_param.chemstation_key, list):
220
+ for register_flag in method_param.chemstation_key:
221
+ self.send(setting_command.value.format(register=register,
222
+ register_flag=register_flag,
223
+ val=method_param.val))
224
+ else:
225
+ self.send(setting_command.value.format(register=register,
226
+ register_flag=method_param.chemstation_key,
227
+ val=method_param.val))
228
+ time.sleep(2)
229
+
230
+ def stop(self):
231
+ """Stops the run. A dialog window will pop up and manual intervention may be required."""
232
+ self.send(Command.STOP_METHOD_CMD)
@@ -0,0 +1,140 @@
1
+ import os
2
+ import time
3
+
4
+ from ..control.table_controller import TableController, HPLCController
5
+ from ..utils.chromatogram import SEQUENCE_TIME_FORMAT
6
+ from ..utils.constants import SEQUENCE_TABLE
7
+ from ..utils.macro import Command
8
+ from ..utils.sequence_types import SequenceTable, SequenceEntry
9
+ from ..utils.table_types import RegisterFlag, TableOperation
10
+
11
+
12
+ class SequenceController(TableController):
13
+ """
14
+ Class containing sequence related logic
15
+ """
16
+
17
+ def __init__(self, controller: HPLCController, src: str):
18
+ super().__init__(controller, src)
19
+
20
+ def switch(self, seq_name: str):
21
+ """Switch to the specified sequence. The sequence name does not need the '.S' extension.
22
+
23
+ :param seq_name: The name of the sequence file
24
+ """
25
+ self.send(f'_SeqFile$ = "{seq_name}.S"')
26
+ self.send(f'_SeqPath$ = "{self.src}"')
27
+ self.send(Command.SWITCH_SEQUENCE_CMD)
28
+ time.sleep(2)
29
+ self.send(Command.GET_SEQUENCE_CMD)
30
+ time.sleep(2)
31
+ # check that method switched
32
+ for _ in range(10):
33
+ try:
34
+ parsed_response = self.receive().splitlines()[1].split()[1:][0]
35
+ break
36
+ except IndexError:
37
+ continue
38
+
39
+ assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
40
+
41
+ def run(self, sequence_table: SequenceTable):
42
+ """Starts the currently loaded sequence, storing data
43
+ under the <data_dir>/<sequence table name> folder.
44
+ The <sequence table name> will be appended with a timestamp in the "%Y %m %d %H %m %s" format.
45
+ Device must be ready.
46
+
47
+ :param sequence_table:
48
+ """
49
+ timestamp = time.strftime(SEQUENCE_TIME_FORMAT)
50
+ self.send(Command.RUN_SEQUENCE_CMD.value)
51
+ self.send(Command.RUN_SEQUENCE_CMD.value)
52
+ folder_name = f"{sequence_table.name} {timestamp}"
53
+ subdirs = [x[0] for x in os.walk(self.src)]
54
+ time.sleep(60)
55
+ potential_folders = sorted(list(filter(lambda d: folder_name in d, subdirs)))
56
+ self.controller.data_files.append(potential_folders[0])
57
+
58
+ def edit(self, sequence_table: SequenceTable):
59
+ """Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
60
+ If you would only like to edit a single row of a sequence table, use `edit_sequence_table_row` instead.
61
+
62
+ :param sequence_table:
63
+ """
64
+ self.send("Local Rows")
65
+ self.sleep(1)
66
+ self.delete_table(SEQUENCE_TABLE)
67
+ self.sleep(1)
68
+ self.new_table(SEQUENCE_TABLE)
69
+ self.sleep(1)
70
+ self._get_table_rows(SEQUENCE_TABLE)
71
+ for _ in sequence_table.rows:
72
+ self.add_table_row(SEQUENCE_TABLE)
73
+ self.sleep(1)
74
+ self.send(Command.SAVE_SEQUENCE_CMD)
75
+ self.sleep(1)
76
+ self._get_table_rows(SEQUENCE_TABLE)
77
+ self.send(Command.SWITCH_SEQUENCE_CMD)
78
+ for i, row in enumerate(sequence_table.rows):
79
+ self.edit(row=row, row_num=i + 0)
80
+ self.sleep(1)
81
+ self.send(Command.SAVE_SEQUENCE_CMD)
82
+ self.sleep(1)
83
+
84
+ def edit_row(self, row: SequenceEntry, row_num: int):
85
+ """Edits a row in the sequence table. Assumes the row already exists.
86
+
87
+ :param row: sequence row entry with updated information
88
+ :param row_num: the row to edit, based on -1-based indexing
89
+ """
90
+ if row.vial_location:
91
+ self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=SEQUENCE_TABLE.register,
92
+ table_name=SEQUENCE_TABLE.name,
93
+ row=row_num,
94
+ col_name=RegisterFlag.VIAL_LOCATION,
95
+ val=row.vial_location))
96
+ if row.method:
97
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
98
+ table_name=SEQUENCE_TABLE.name,
99
+ row=row_num,
100
+ col_name=RegisterFlag.METHOD,
101
+ val=row.method))
102
+
103
+ if row.num_inj:
104
+ self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=SEQUENCE_TABLE.register,
105
+ table_name=SEQUENCE_TABLE.name,
106
+ row=row_num,
107
+ col_name=RegisterFlag.NUM_INJ,
108
+ val=row.num_inj))
109
+
110
+ if row.inj_vol:
111
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
112
+ table_name=SEQUENCE_TABLE.name,
113
+ row=row_num,
114
+ col_name=RegisterFlag.INJ_VOL,
115
+ val=row.inj_vol))
116
+
117
+ if row.inj_source:
118
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
119
+ table_name=SEQUENCE_TABLE.name,
120
+ row=row_num,
121
+ col_name=RegisterFlag.INJ_SOR,
122
+ val=row.inj_source))
123
+
124
+ if row.sample_name:
125
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
126
+ table_name=SEQUENCE_TABLE.name,
127
+ row=row_num,
128
+ col_name=RegisterFlag.NAME,
129
+ val=row.sample_name))
130
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
131
+ table_name=SEQUENCE_TABLE.name,
132
+ row=row_num,
133
+ col_name=RegisterFlag.DATA_FILE,
134
+ val=row.sample_name))
135
+ if row.sample_type:
136
+ self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=SEQUENCE_TABLE.register,
137
+ table_name=SEQUENCE_TABLE.name,
138
+ row=row_num,
139
+ col_name=RegisterFlag.SAMPLE_TYPE,
140
+ val=row.sample_type))