pytrms 0.9.3__tar.gz → 0.9.5__tar.gz

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 (27) hide show
  1. {pytrms-0.9.3 → pytrms-0.9.5}/PKG-INFO +1 -1
  2. {pytrms-0.9.3 → pytrms-0.9.5}/pyproject.toml +1 -1
  3. pytrms-0.9.5/pytrms/__init__.py +42 -0
  4. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/clients/mqtt.py +27 -7
  5. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/data/ParaIDs.csv +8 -8
  6. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/instrument.py +9 -4
  7. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/measurement.py +77 -25
  8. pytrms-0.9.3/pytrms/__init__.py +0 -38
  9. {pytrms-0.9.3 → pytrms-0.9.5}/LICENSE +0 -0
  10. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/_base/__init__.py +0 -0
  11. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/_base/ioniclient.py +0 -0
  12. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/_base/mqttclient.py +0 -0
  13. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/_version.py +0 -0
  14. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/clients/__init__.py +0 -0
  15. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/clients/db_api.py +0 -0
  16. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/clients/ioniclient.py +0 -0
  17. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/clients/modbus.py +0 -0
  18. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/clients/ssevent.py +0 -0
  19. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/compose/__init__.py +0 -0
  20. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/compose/composition.py +0 -0
  21. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/data/IoniTofPrefs.ini +0 -0
  22. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/helpers.py +0 -0
  23. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/peaktable.py +0 -0
  24. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/plotting/__init__.py +0 -0
  25. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/plotting/plotting.py +0 -0
  26. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/readers/__init__.py +0 -0
  27. {pytrms-0.9.3 → pytrms-0.9.5}/pytrms/readers/ionitof_reader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pytrms
3
- Version: 0.9.3
3
+ Version: 0.9.5
4
4
  Summary: Python bundle for proton-transfer reaction mass-spectrometry (PTR-MS).
5
5
  License: GPL-2.0
6
6
  Author: Moritz Koenemann
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pytrms"
3
- version = "0.9.3"
3
+ version = "0.9.5"
4
4
  description = "Python bundle for proton-transfer reaction mass-spectrometry (PTR-MS)."
5
5
  authors = ["Moritz Koenemann <moritz.koenemann@ionicon.com>"]
6
6
  license = "GPL-2.0"
@@ -0,0 +1,42 @@
1
+ _version = '0.9.5'
2
+
3
+ __all__ = ['load', 'connect']
4
+
5
+
6
+ def load(path):
7
+ '''Open a datafile for post-analysis or batch processing.
8
+
9
+ `path` may be a glob-expression to collect a whole batch.
10
+
11
+ returns a `Measurement` instance.
12
+ '''
13
+ import glob
14
+ from .measurement import FinishedMeasurement
15
+
16
+ files = glob.glob(path)
17
+
18
+ return FinishedMeasurement(*files)
19
+
20
+
21
+ def connect(host='localhost', port=None, method='mqtt'):
22
+ '''Connect a client to a running measurement server.
23
+
24
+ 'method' is the preferred connection, either 'mqtt' (default), 'webapi' or 'modbus'.
25
+
26
+ returns an `Instrument` if connected successfully.
27
+ '''
28
+ from .instrument import Instrument
29
+
30
+ if method.lower() == 'mqtt':
31
+ from .clients.mqtt import MqttClient as _client
32
+ elif method.lower() == 'webapi':
33
+ from .clients.ioniclient import IoniClient as _client
34
+ elif method.lower() == 'modbus':
35
+ from .modbus import IoniconModbus as _client
36
+ else:
37
+ raise NotImplementedError(str(method))
38
+
39
+ backend = _client(host, port) if port is not None else _client(host)
40
+
41
+ return Instrument(backend)
42
+
@@ -326,12 +326,14 @@ def follow_state(client, self, msg):
326
326
  # replace the current state with the new element:
327
327
  self._server_state.append(state)
328
328
  meas_running = (state == "ACQ_Aquire") # yes, there's a typo, plz keep it :)
329
- just_started = (meas_running and not msg.retain)
330
329
  if meas_running:
331
330
  # signal the relevant thread(s) that we need an update:
332
331
  self._calcconzinfo.append(_NOT_INIT)
333
- if just_started:
334
- # invalidate the source-file until we get a new one:
332
+ if not meas_running:
333
+ # Note: the user-interface in `.current_sourcefile` checks for the
334
+ # above _server_state and expects an initialized filename if and
335
+ # only if the server is running! Therefore, we can safely
336
+ # invalidate the source-file until we get a new one:
335
337
  self._sf_filename.append(_NOT_INIT)
336
338
 
337
339
  follow_state.topics = ["DataCollection/Act/ACQ_SRV_CurrentState"]
@@ -359,7 +361,8 @@ def follow_act_set_values(client, self, msg):
359
361
  server, kind, parID = msg.topic.split('/')
360
362
  if server == "DataCollection":
361
363
  # Note: this topic doesn't strictly follow the convention and is handled separately
362
- return
364
+ if kind != "Set":
365
+ return
363
366
 
364
367
  if server == "Sequencer":
365
368
  # Note: this is a separate program and will be ignored (has its own AUTO_-numbers et.c.)
@@ -730,6 +733,7 @@ class MqttClient(MqttClientBase):
730
733
  * Elements will be buffered up to a maximum of `buffer_size` cycles (default: 300).
731
734
  * Cycles recorded prior to calling `next()` on the iterator may be missed,
732
735
  so ideally this should be set up before any measurement is running.
736
+ * Once the measurement stops, this iterator will raise `StopIteration`.
733
737
  * [Important]: When the buffer runs full, a `queue.Full` exception will be raised!
734
738
  Therefore, the caller should consume the iterator as soon as possible while the
735
739
  measurement is running.
@@ -740,7 +744,10 @@ class MqttClient(MqttClientBase):
740
744
 
741
745
  def callback(client, self, msg):
742
746
  try:
743
- q.put_nowait(_parse_fullcycle(msg.payload, need_add_data=True))
747
+ _fc = _parse_fullcycle(msg.payload, need_add_data=True)
748
+ # IoniTOF cycle-indexing starts at 1, while 0 marks idle state:
749
+ if _fc.timecycle.abs_cycle > 0:
750
+ q.put_nowait(_fc)
744
751
  log.debug(f"received fullcycle, buffer at ({q.qsize()}/{q.maxsize})")
745
752
  except queue.Full:
746
753
  # DO NOT FAIL INSIDE THE CALLBACK!
@@ -764,7 +771,20 @@ class MqttClient(MqttClientBase):
764
771
  # if block is true and timeout is None, [the q.get()] operation goes into an
765
772
  # uninterruptible wait on an underlying lock. This means that no exceptions
766
773
  # can occur, and in particular a SIGINT will not trigger a KeyboardInterrupt!
767
- yield q.get(block=True, timeout=timeout_s) # waiting for measurement to run...
774
+ if timeout_s is None and not self.is_running:
775
+ log.warn(f"waiting indefinitely for measurement to run...")
776
+
777
+ yield q.get(block=True, timeout=timeout_s)
778
+
779
+ # make double sure that there's more to come..
780
+ _started_at = time.monotonic()
781
+ while timeout_s is None or time.monotonic() < _started_at + timeout_s:
782
+ if self.is_running:
783
+ break
784
+
785
+ time.sleep(10e-3)
786
+ else:
787
+ raise TimeoutError(f"[{self}] received specdata, but measurement won't start");
768
788
 
769
789
  while self.is_running or not q.empty():
770
790
  if q.full():
@@ -782,7 +802,7 @@ class MqttClient(MqttClientBase):
782
802
 
783
803
  except queue.Empty:
784
804
  assert timeout_s is not None, "this should never happen"
785
- raise TimeoutError("no measurement running after {timeout_s} seconds")
805
+ raise TimeoutError(f"no measurement running after {timeout_s} seconds")
786
806
 
787
807
  finally:
788
808
  # ...also, when using more than one iterator, the first to finish will
@@ -38,9 +38,9 @@ ID Name DataType Access ServerName LVWriteQueueName SharedVariable PrettyName Un
38
38
  36 DPS_IhcOnOff BOOL RW PTR PTR_Write Ihc On/Off
39
39
  37 DPS_Pdrift_Ctrl_OnOff RW PTR PTR_Write p-drift controlled
40
40
  38 DPS_Pdrift_Ctrl_Val RW PTR PTR_Write p-drift ctrl Value
41
- 39 SourceValve RW PTR PTR_Write Source Valve
42
- 40 PrimionIdx RW PTR PTR_Write Prim Ion Idx
43
- 41 E/N DBL R E/N Td
41
+ 39 SourceValve DBL RW PTR PTR_Write Source Valve
42
+ 40 PrimionIdx I32 RW PTR PTR_Write Prim Ion Idx
43
+ 41 E_N DBL R E/N Td
44
44
  42 FC_H2O DBL R PTR PTR_Write FC H2O sccm
45
45
  43 SmartGAS1_Conc DBL R PTR Conz1 SG
46
46
  44 SmartGAS2_Conc DBL R PTR Conz2 SG
@@ -66,16 +66,16 @@ ID Name DataType Access ServerName LVWriteQueueName SharedVariable PrettyName Un
66
66
  64 Spare_64
67
67
  65 Spare_65
68
68
  66 Spare_66
69
- 67 Spare_67
70
- 68 Spare_68
71
- 69 Spare_69
69
+ 67 MPV_Dir_1 I32 PTR PTR_Write MPV Dir
70
+ 68 MPV_Dir_2 I32 PTR PTR_Write MPV Dir2
71
+ 69 MPV_Dir_3 I32 PTR PTR_Write MPV Dir3
72
72
  70 MPV_1 PTR PTR_Write MPValve1
73
73
  71 MPV_2 PTR PTR_Write MPValve2
74
74
  72 MPV_3 PTR PTR_Write MPValve3
75
75
  73 DPS_Uf DBL RW PTR PTR_Write Uf V
76
76
  74 DPS_U1 DBL RW PTR PTR_Write UF1 V
77
77
  75 DPS_U2 DBL RW PTR PTR_Write UF2 V
78
- 76 Spare_76
78
+ 76 DPS_IhcOnOff_Neg BOOL RW PTR PTR_Write Ihc On/Off
79
79
  77 Spare_77
80
80
  78 Spare_78
81
81
  79 Spare_79
@@ -323,7 +323,7 @@ ID Name DataType Access ServerName LVWriteQueueName SharedVariable PrettyName Un
323
323
  321 GC2_ConstantTemperature DBL RW PTR GC2 Temp Set
324
324
  322 GC2_FC DBL RW PTR FC GC
325
325
  323 GC2_FC_Makeup DBL RW PTR FC FastGC Makeup
326
- 324 Spare324
326
+ 324 GC2_FC_Ri DBL RW PTR FC FastGC Ri
327
327
  325 Spare325
328
328
  326 Spare326
329
329
  327 Spare327
@@ -2,7 +2,10 @@ import os.path
2
2
  import time
3
3
  from abc import abstractmethod, ABC
4
4
 
5
- from .measurement import *
5
+ from .measurement import (
6
+ RunningMeasurement,
7
+ FinishedMeasurement,
8
+ )
6
9
 
7
10
 
8
11
  class Instrument(ABC):
@@ -53,15 +56,17 @@ class Instrument(ABC):
53
56
 
54
57
  def get(self, varname):
55
58
  """Get the current value of a setting."""
56
- # TODO :: this is not an interface implementation
59
+ # TODO :: this is not an interface implementation...
57
60
  raw = self.backend.get(varname)
61
+
62
+ from .clients.mqtt import MqttClient
58
63
  if not isinstance(self.backend, MqttClient):
59
64
  import json
60
65
  jobj = json.loads(raw)
61
66
 
62
67
  return jobj[0]['Act']['Real']
63
68
 
64
- ## how it should be:
69
+ ## ...how it should be: just:
65
70
  return raw
66
71
 
67
72
  def set(self, varname, value, unit='-'):
@@ -115,5 +120,5 @@ class RunningInstrument(Instrument):
115
120
  self._new_state(IdleInstrument)
116
121
 
117
122
  # TODO :: this catches only one sourcefile.. it'll do for simple cases:
118
- return FinishedMeasurement(_current_sourcefile)
123
+ return FinishedMeasurement(self._current_sourcefile)
119
124
 
@@ -26,6 +26,10 @@ class Measurement(ABC):
26
26
  # Note: we get ourselves a nifty little state-machine :)
27
27
  self.__class__ = newstate
28
28
 
29
+ @property
30
+ def is_running(self):
31
+ return self.__class__ == RunningMeasurement
32
+
29
33
  def start(self, filename=''):
30
34
  """Start a measurement on the PTR server.
31
35
 
@@ -48,10 +52,6 @@ class Measurement(ABC):
48
52
  """Stop the current measurement on the PTR server."""
49
53
  raise RuntimeError("can't stop %s" % self.__class__)
50
54
 
51
- @abstractmethod
52
- def __len__(self):
53
- pass
54
-
55
55
 
56
56
  class PreparingMeasurement(Measurement):
57
57
 
@@ -94,8 +94,17 @@ class PreparingMeasurement(Measurement):
94
94
  self.ptr.start_measurement(filename)
95
95
  self._new_state(RunningMeasurement)
96
96
 
97
- def __len__(self):
98
- return 0
97
+ def __iter__(self):
98
+ from .clients.mqtt import MqttClient
99
+ if not isinstance(self.ptr.backend, MqttClient):
100
+ raise NotImplementedError("iteration only provided w/ backend 'mqtt'")
101
+
102
+ print("warning: the measurement needs to be started externally")
103
+ while not self.ptr.backend.is_running:
104
+ time.sleep(50e-3)
105
+
106
+ self._new_state(RunningMeasurement)
107
+ yield from iter(self)
99
108
 
100
109
 
101
110
  class RunningMeasurement(Measurement):
@@ -107,51 +116,95 @@ class RunningMeasurement(Measurement):
107
116
  self.ptr.stop_measurement()
108
117
  self._new_state(FinishedMeasurement)
109
118
 
110
- def __len__(self):
111
- return -1
119
+ def __iter__(self):
120
+ from .clients.mqtt import MqttClient
121
+ if not isinstance(self.ptr.backend, MqttClient):
122
+ raise NotImplementedError("iteration only provided w/ backend 'mqtt'")
123
+
124
+ if not self.ptr.backend.is_connected:
125
+ raise Exception("no connection to instrument")
126
+
127
+ timeout_s = 15
128
+ ssd_s = 1e-3 * self.ptr.get('ACQ_SRV_SpecTime_ms')
129
+ last_rel_cycle = -1
130
+ sourcefile = ''
131
+ for specdata in self.ptr.backend.iter_specdata(timeout_s=timeout_s+ssd_s, buffer_size=300):
132
+ if last_rel_cycle == -1 or specdata.timecycle.rel_cycle < last_rel_cycle:
133
+ # the source-file has been switched, so wait for the new path:
134
+ started_at = time.monotonic()
135
+ while time.monotonic() < started_at + timeout_s:
136
+ candidate = self.ptr.backend.current_sourcefile
137
+ if candidate and candidate != sourcefile:
138
+ sourcefile = candidate
139
+ break
140
+
141
+ time.sleep(10e-3)
142
+ else:
143
+ raise TimeoutError(f"no new sourcefile after ({timeout_s = })")
144
+ last_rel_cycle = specdata.timecycle.rel_cycle
145
+
146
+ yield sourcefile, specdata
147
+
148
+ if not self.ptr.backend.is_running:
149
+ self._new_state(FinishedMeasurement)
150
+ ## TODO :: das hier braucht noch seine .sourcefiles !!
151
+ self.sourcefiles = []
112
152
 
113
153
 
114
154
  class FinishedMeasurement(Measurement):
115
155
 
116
156
  @classmethod
117
- def _check(cls, sourcefiles):
157
+ def _check(cls, _readers):
118
158
  _assumptions = ("incompatible files! "
119
- "sourcefiles must have the same number-of-timebins and "
159
+ "_readers must have the same number-of-timebins and "
120
160
  "the same instrument-type to be collected as a batch")
121
161
 
122
- assert 1 == len(set(sf.inst_type for sf in sourcefiles)), _assumptions
123
- assert 1 == len(set(sf.number_of_timebins for sf in sourcefiles)), _assumptions
162
+ assert 1 == len(set(sf.inst_type for sf in _readers)), _assumptions
163
+ assert 1 == len(set(sf.number_of_timebins for sf in _readers)), _assumptions
124
164
 
125
165
  @property
126
166
  def number_of_timebins(self):
127
- return next(iter(self.sourcefiles)).number_of_timebins
167
+ return next(iter(self._readers)).number_of_timebins
128
168
 
129
169
  @property
130
170
  def poisson_deadtime_ns(self):
131
- return next(iter(self.sourcefiles)).poisson_deadtime_ns
171
+ return next(iter(self._readers)).poisson_deadtime_ns
132
172
 
133
173
  @property
134
174
  def pulsing_period_ns(self):
135
- return next(iter(self.sourcefiles)).pulsing_period_ns
175
+ return next(iter(self._readers)).pulsing_period_ns
136
176
 
137
177
  @property
138
178
  def single_spec_duration_ms(self):
139
- return next(iter(self.sourcefiles)).single_spec_duration_ms
179
+ return next(iter(self._readers)).single_spec_duration_ms
140
180
 
141
181
  @property
142
182
  def start_delay_ns(self):
143
- return next(iter(self.sourcefiles)).start_delay_ns
183
+ return next(iter(self._readers)).start_delay_ns
144
184
 
145
185
  @property
146
186
  def timebin_width_ps(self):
147
- return next(iter(self.sourcefiles)).timebin_width_ps
187
+ return next(iter(self._readers)).timebin_width_ps
188
+
189
+ def __new__(cls, *args, **kwargs):
190
+ # TODO :: das hier passt mal **ueberhaupt gar nicht** in das "state-machine"-
191
+ ## schema hinein!! Wie soll man das init-ialisieren, wenn bloss _new_state
192
+ ## ge-called wird ???!?!?!?!?
193
+
194
+ # quick reminder: If __new__() does not return an instance of cls, then the
195
+ # new instance’s __init__() method will *not* be invoked:
196
+ print(*args, **kwargs)
197
+
198
+ inst = object.__new__(cls)
199
+
200
+ return inst
148
201
 
149
202
  def __init__(self, *filenames, _reader=IoniTOFReader):
150
203
  if not len(filenames):
151
204
  raise ValueError("no filename given")
152
205
 
153
- self.sourcefiles = sorted((_reader(f) for f in filenames), key=attrgetter('time_of_file'))
154
- self._check(self.sourcefiles)
206
+ self._readers = sorted((_reader(f) for f in filenames), key=attrgetter('time_of_file'))
207
+ self._check(self._readers)
155
208
 
156
209
  def read_traces(self, kind='conc', index='abs_cycle', force_original=False):
157
210
  """Return the timeseries ("traces") of all masses, compounds and settings.
@@ -163,11 +216,10 @@ class FinishedMeasurement(Measurement):
163
216
  'abs_time' or 'rel_time'.
164
217
 
165
218
  """
166
- return pd.concat(sf.read_all(kind, index, force_original) for sf in self.sourcefiles)
219
+ return pd.concat(sf.read_all(kind, index, force_original) for sf in self._readers)
167
220
 
168
221
  def __iter__(self):
169
- return iter(self.sourcefiles)
170
-
171
- def __len__(self):
172
- return len(self.sourcefiles)
222
+ for reader in self._readers:
223
+ for specdata in reader.iter_specdata():
224
+ yield reader.filename, specdata
173
225
 
@@ -1,38 +0,0 @@
1
- _version = '0.9.3'
2
-
3
- __all__ = ['load', 'connect']
4
-
5
-
6
- def load(path):
7
- '''Open a datafile for post-analysis or batch processing.
8
-
9
- `path` may be a glob-expression to collect a whole batch.
10
-
11
- returns a `Measurement` instance.
12
- '''
13
- import glob
14
- from .measurement import FinishedMeasurement
15
-
16
- files = glob.glob(path)
17
-
18
- return FinishedMeasurement(*files)
19
-
20
- def connect(host=None, method='webapi'):
21
- '''Connect a client to a running measurement server.
22
-
23
- 'method' is the preferred connection, either 'webapi' (default) or 'modbus'.
24
-
25
- returns an `Instrument` if connected successfully.
26
- '''
27
- from .instrument import Instrument
28
-
29
- if method.lower() == 'webapi':
30
- from .clients.ioniclient import IoniClient
31
- return IoniClient(host)
32
-
33
- if method.lower() == 'modbus':
34
- from .modbus import IoniconModbus
35
- return IoniconModbus(host)
36
-
37
- raise NotImplementedError(str(method))
38
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes