pytrms 0.2.1__py3-none-any.whl → 0.9.0__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.
pytrms/helpers.py CHANGED
@@ -1,9 +1,6 @@
1
- import pandas as pd
1
+ import requests.exceptions
2
2
 
3
+ class PTRConnectionError(requests.exceptions.ConnectionError):
4
+ pass
3
5
 
4
- def convert_labview_to_posix(ts):
5
- '''Create a `Pandas.Timestamp` from LabView time.'''
6
- posix_time = ts - 2082844800
7
-
8
- return pd.Timestamp(posix_time, unit='s')
9
6
 
pytrms/instrument.py CHANGED
@@ -1,8 +1,11 @@
1
- import time
2
1
  import os.path
2
+ import time
3
+ from abc import abstractmethod, ABC
4
+
5
+ from .measurement import *
3
6
 
4
7
 
5
- class Instrument:
8
+ class Instrument(ABC):
6
9
  '''
7
10
  Class for controlling the PTR instrument remotely.
8
11
 
@@ -11,163 +14,101 @@ class Instrument:
11
14
  instrument can be stopped. But trying to start a measurement twice will raise an
12
15
  exception (RuntimeError).
13
16
 
14
- This is a singleton class, i.e. it is only instanciated once per script.
17
+ Note, that for every client PTR instrument there is only one instance of this class.
18
+ This is to prevent different instances to be in other states than the instrument.
15
19
  '''
16
20
 
17
21
  __instance = None
18
22
 
19
23
  def _new_state(self, newstate):
24
+ # Note: we get ourselves a nifty little state-machine :)
20
25
  self.__class__ = newstate
21
- print(self)
22
26
 
23
- def __new__(cls, client, buffer):
24
- # make this class a singleton
27
+ def __new__(cls, backend):
28
+ # make this class a singleton..
25
29
  if cls._Instrument__instance is not None:
26
30
  # quick reminder: If __new__() does not return an instance of cls, then the
27
31
  # new instance’s __init__() method will *not* be invoked:
28
32
  return cls._Instrument__instance
29
- #raise Exception('the Instrument class can only have one instance')
30
33
 
31
- inst = object.__new__(cls)
32
- cls._Instrument__instance = inst
33
-
34
- # launch the buffer's thread..
35
- if not buffer.is_alive():
36
- buffer.daemon = True
37
- buffer.start()
38
- # ..and synchronize the PTR-instrument state with this Python object:
39
- Instrument._new_state(inst, BusyInstrument)
40
- if buffer.is_idle:
41
- Instrument._new_state(inst, IdleInstrument)
34
+ # ..that is synchronized with the PTR-instrument state:
35
+ if backend.is_running:
36
+ cls = RunningInstrument
42
37
  else:
43
- Instrument._new_state(inst, RunningInstrument)
38
+ cls = IdleInstrument
39
+
40
+ inst = object.__new__(cls)
41
+ Instrument._Instrument__instance = inst
44
42
 
45
43
  return inst
46
44
 
47
- def __init__(self, client, buffer):
45
+ def __init__(self, backend):
48
46
  # dispatch all blocking calls to the client
49
- # and fetch current data from the buffer!
50
- self._client = client
51
- self._buffer = buffer
47
+ self.backend = backend
52
48
 
49
+ @property
53
50
  def is_local(self):
54
51
  """Returns True if files are written to the local machine."""
55
- host = self._client.host
56
- return host == 'localhost' or host == '127.0.0.1'
57
-
58
- def wait(self, seconds, reason=''):
59
- if reason:
60
- print(reason)
61
- time.sleep(seconds)
52
+ host = str(self.backend.host)
53
+ return 'localhost' in host or '127.0.0.1' in host
62
54
 
63
55
  def get(self, varname):
64
56
  """Get the current value of a setting."""
65
- return self._client.get(varname)
66
-
67
- def set(self, varname, value):
68
- """Set a variable to a new value."""
69
- return self._client.set(varname, value)
57
+ # TODO :: this is not an interface implementation
58
+ raw = self.backend.get(varname)
59
+ if not isinstance(self.backend, MqttClient):
60
+ import json
61
+ jobj = json.loads(raw)
70
62
 
71
- def __enter__(self):
72
- # TODO :: implement proper context with dict of settings...
73
- self.start()
74
- return self
63
+ return jobj[0]['Act']['Real']
75
64
 
76
- def __exit__(self, exc_type, exc_val, tb):
77
- self.stop()
65
+ ## how it should be:
66
+ return raw
78
67
 
79
- def start(self, path=''):
80
- """Start a measurement on the PTR server.
81
-
82
- 'path' is the filename of the datafile to write to.
83
- If left blank, start a "quick measurement".
84
-
85
- If pointing to a file and the file exist on the (local) server, this raises an exception.
86
- To create unique filenames, use placeholders for year (%Y), month (%m), and so on,
87
- for example `path=C:/Ionicon/Data/Sauerteig_%Y-%m-%d_%H-%M-%S.h5`.
68
+ def set(self, varname, value, unit='-'):
69
+ """Set a variable to a new value."""
70
+ return self.backend.set(varname, value, unit='-')
88
71
 
89
- see also:
90
- """
72
+ def start_measurement(self, filename=''):
91
73
  # this method must be implemented by each state
92
- raise NotImplementedError()
74
+ raise RuntimeError("can't start %s" % self.__class__)
93
75
 
94
- start.__doc__ += time.strftime.__doc__
76
+ start_measurement.__doc__ = Measurement.start.__doc__
95
77
 
96
- def stop(self):
97
- """Stop the current measurement on the PTR server."""
78
+ def stop_measurement(self):
98
79
  # this method must be implemented by each state
99
- raise NotImplementedError()
80
+ raise RuntimeError("can't stop %s" % self.__class__)
100
81
 
101
- def get_traces(self, kind='raw', index='abs_cycle'):
102
- """Return the timeseries ("traces") of all masses, compounds and settings.
103
-
104
- This will grow with time if a measurement is currently running and stop growing
105
- once the measurement has been stopped. The tracedata is cleared, when a new
106
- measurement is started.
107
-
108
- 'kind' is the type of traces and must be one of 'raw', 'concentration' or 'corrected'.
109
-
110
- 'index' specifies the desired index and must be one of 'abs_cycle', 'rel_cycle',
111
- 'abs_time' or 'rel_time'.
112
- """
113
- # TODO :: this method must be implemented by each state
114
- raise NotImplementedError()
115
-
116
- def follow(self, kind='raw'):
117
- """Returns an iterator over the timeseries ("traces") of all masses, compounds and settings.
118
-
119
- 'kind' is the type of traces and must be one of 'raw', 'concentration' or 'corrected'.
120
- """
121
- # TODO :: this method must be implemented by each state
122
- raise NotImplementedError()
82
+ stop_measurement.__doc__ = Measurement.stop.__doc__
123
83
 
124
84
 
125
85
  class IdleInstrument(Instrument):
126
86
 
127
- def start(self, path=''):
87
+ def start_measurement(self, filename=''):
128
88
  # if we send a filepath to the server that does not exist there, the server will
129
89
  # open a dialog and "hang" (which I'd very much like to avoid).
130
90
  # the safest way is to not send a path at all and start a 'Quick' measurement.
131
91
  # but if the server is the local machine, we do our best to verify the path:
132
- if path and self.is_local():
133
- home = os.path.dirname(path)
92
+ if filename and self.is_local:
93
+ home = os.path.dirname(filename)
134
94
  os.makedirs(home, exist_ok=True)
135
- base = os.path.basename(path)
95
+ base = os.path.basename(filename)
136
96
  if not base:
137
97
  base = '%Y-%m-%d_%H-%M-%S.h5'
138
98
  base = time.strftime(base)
139
- path = os.path.join(home, base)
140
- if os.path.exists(path):
141
- raise RuntimeError(f'path exists and cannot be overwritten')
99
+ filename = os.path.join(home, base)
100
+ if os.path.exists(filename):
101
+ raise RuntimeError(f'filename exists and cannot be overwritten')
142
102
 
143
- self._new_state(BusyInstrument)
144
- self._client.start_measurement(path)
145
- self.start(path)
103
+ self.backend.start_measurement(filename)
104
+ self._new_state(RunningInstrument)
146
105
 
147
- def stop(self):
148
- raise RuntimeError('instrument is not running')
106
+ return RunningMeasurement(self)
149
107
 
150
108
 
151
109
  class RunningInstrument(Instrument):
152
110
 
153
- def start(self, path=''):
154
- raise RuntimeError('instrument is already running')
155
-
156
- def stop(self):
157
- self._new_state(BusyInstrument)
158
- self._client.stop_measurement()
159
- self.stop()
160
-
161
-
162
- class BusyInstrument(Instrument):
163
-
164
- def start(self, path=''):
165
- while self._buffer.is_idle:
166
- time.sleep(0.01)
167
- self._new_state(RunningInstrument)
168
-
169
- def stop(self):
170
- while not self._buffer.is_idle:
171
- time.sleep(0.01)
111
+ def stop_measurement(self):
112
+ self.backend.stop_measurement()
172
113
  self._new_state(IdleInstrument)
173
114
 
pytrms/measurement.py CHANGED
@@ -1,77 +1,160 @@
1
- import os.path
2
- from abc import abstractmethod
3
- from collections.abc import Iterable
1
+ import time
2
+ from glob import glob
3
+ from operator import attrgetter
4
+ from itertools import chain
5
+ from abc import abstractmethod, ABC
4
6
 
7
+ from .readers import IoniTOFReader
5
8
 
6
- class Measurement(Iterable):
7
- """Base class for PTRMS-measurements or batch processing.
9
+ __all__ = ['Measurement', 'PreparingMeasurement', 'RunningMeasurement', 'FinishedMeasurement']
8
10
 
9
- Every instance is associated with exactly one `.filename` to a datafile.
10
- The start time of the measurement is given by `.timezero`.
11
+
12
+ class Measurement(ABC):
13
+ """Class for PTRMS-measurements and batch processing.
14
+
15
+ The start time of the measurement is given by `.time_of_meas`.
11
16
 
12
17
  A measurement is iterable over the 'rows' of its data.
13
- In the online case, this would slowly produce the current trace, one after another.
18
+ In the online case, this would slowly produce the current trace, one
19
+ after another.
14
20
  In the offline case, this would quickly iterate over the traces in the given
15
21
  measurement file.
16
22
  """
17
23
 
18
- def is_local(self):
19
- return os.path.exists(self.filename)
24
+ def _new_state(self, newstate):
25
+ # Note: we get ourselves a nifty little state-machine :)
26
+ self.__class__ = newstate
27
+
28
+ def start(self, filename=''):
29
+ """Start a measurement on the PTR server.
30
+
31
+ 'filename' is the filename of the datafile to write to.
32
+ If left blank, start a "quick measurement".
33
+
34
+ If pointing to a file and the file exist on the (local) server, this raises an exception.
35
+ To create unique filenames, use placeholders for year (%Y), month (%m), and so on,
36
+ for example `filename=C:/Ionicon/Data/Sauerteig_%Y-%m-%d_%H-%M-%S.h5`.
37
+
38
+ see also:
39
+ """
40
+ # this method must be implemented by each state
41
+ raise RuntimeError("can't start %s" % self.__class__)
42
+
43
+ # (see also: this docstring)
44
+ start.__doc__ += time.strftime.__doc__
45
+
46
+ def stop(self):
47
+ """Stop the current measurement on the PTR server."""
48
+ raise RuntimeError("can't stop %s" % self.__class__)
49
+
50
+ @abstractmethod
51
+ def __len__(self):
52
+ pass
53
+
20
54
 
21
- def __init__(self, filename):
22
- self._filename = filename
55
+ class PreparingMeasurement(Measurement):
23
56
 
24
57
  @property
25
- def filename(self):
26
- return self._filename
27
-
58
+ def single_spec_duration_ms(self):
59
+ return self.ptr.get('ACQ_SRV_SpecTime_ms')
60
+
61
+ @single_spec_duration_ms.setter
62
+ def single_spec_duration_ms(self, value):
63
+ self.ptr.set('ACQ_SRV_SpecTime_ms', int(value), unit='ms')
64
+
28
65
  @property
29
- @abstractmethod
30
- def timezero(self):
31
- raise NotImplementedError()
66
+ def extraction_time_us(self):
67
+ return self.ptr.get('ACQ_SRV_ExtractTime')
68
+
69
+ @extraction_time_us.setter
70
+ def extraction_time_us(self, value):
71
+ self.ptr.set('ACQ_SRV_ExtractTime', int(value), unit='us')
32
72
 
33
73
  @property
34
- @abstractmethod
35
- def traces(self):
36
- raise NotImplementedError()
74
+ def max_flight_time_us(self):
75
+ return self.ptr.get('ACQ_SRV_MaxFlighttime')
37
76
 
77
+ @max_flight_time_us.setter
78
+ def max_flight_time_us(self, value):
79
+ self.ptr.set('ACQ_SRV_MaxFlighttime', int(value), unit='us')
80
+
81
+ @property
82
+ def expected_mass_range_amu(self):
83
+ return self.ptr.get('ACQ_SRV_ExpectMRange')
38
84
 
39
- class OnlineMeasurement(Measurement):
85
+ @expected_mass_range_amu.setter
86
+ def expected_mass_range_amu(self, value):
87
+ self.ptr.set('ACQ_SRV_ExpectMRange', int(value), unit='amu')
40
88
 
41
- # TODO :: ausbauen oder abreissen....
42
89
  def __init__(self, instrument):
43
- super().__init__(filename)
44
- self.instrument = instrument
90
+ self.ptr = instrument
45
91
 
46
- @property
47
- def timezero(self):
92
+ def start(self, filename=''):
93
+ self.ptr.start_measurement(filename)
94
+ self._new_state(RunningMeasurement)
95
+
96
+ def __len__(self):
48
97
  return 0
49
98
 
50
- @property
51
- def traces(self):
52
- pass
53
99
 
54
- def __iter__(self):
55
- while issubclass(self.__class__, RunningMeasurement):
56
- yield self.instrument.buffer.queue.get()
100
+ class RunningMeasurement(Measurement):
101
+
102
+ def __init__(self, instrument):
103
+ self.ptr = instrument
104
+
105
+ def stop(self):
106
+ self.ptr.stop_measurement()
107
+ self._new_state(FinishedMeasurement)
108
+
109
+ def __len__(self):
110
+ return -1
57
111
 
58
112
 
59
- class OfflineMeasurement(Measurement):
113
+ class FinishedMeasurement(Measurement):
60
114
 
61
- def __init__(self, h5reader):
62
- super().__init__(h5reader.path)
63
- self.hr = h5reader
115
+ @classmethod
116
+ def _check(cls, sourcefiles):
117
+ _assumptions = ("incompatible files! "
118
+ "sourcefiles must have the same number-of-timebins and "
119
+ "the same instrument-type to be collected as a batch")
120
+
121
+ assert 1 == len(set(sf.inst_type for sf in sourcefiles)), _assumptions
122
+ assert 1 == len(set(sf.number_of_timebins for sf in sourcefiles)), _assumptions
64
123
 
65
124
  @property
66
- def timezero(self):
67
- return self.hr.timezero
125
+ def number_of_timebins(self):
126
+ return next(iter(self.sourcefiles)).number_of_timebins
68
127
 
69
128
  @property
70
- def traces(self):
71
- """shortcut for `.get_traces(kind='concentration')`."""
72
- return self.get_traces(kind='concentration')
129
+ def poisson_deadtime_ns(self):
130
+ return next(iter(self.sourcefiles)).poisson_deadtime_ns
131
+
132
+ @property
133
+ def pulsing_period_ns(self):
134
+ return next(iter(self.sourcefiles)).pulsing_period_ns
135
+
136
+ @property
137
+ def single_spec_duration_ms(self):
138
+ return next(iter(self.sourcefiles)).single_spec_duration_ms
139
+
140
+ @property
141
+ def start_delay_ns(self):
142
+ return next(iter(self.sourcefiles)).start_delay_ns
143
+
144
+ @property
145
+ def timebin_width_ps(self):
146
+ return next(iter(self.sourcefiles)).timebin_width_ps
147
+
148
+ def __init__(self, filenames, _reader=IoniTOFReader):
149
+ if isinstance(filenames, str):
150
+ filenames = glob(filenames)
151
+ if not len(filenames):
152
+ raise ValueError("file not found or empty glob expression")
153
+
154
+ self.sourcefiles = sorted((_reader(f) for f in filenames), key=attrgetter('time_of_file'))
155
+ self._check(self.sourcefiles)
73
156
 
74
- def get_traces(self, kind='raw', index='abs_cycle', force_original=False):
157
+ def iter_traces(self, kind='raw', index='abs_cycle', force_original=False):
75
158
  """Return the timeseries ("traces") of all masses, compounds and settings.
76
159
 
77
160
  'kind' is the type of traces and must be one of 'raw', 'concentration' or
@@ -80,11 +163,9 @@ class OfflineMeasurement(Measurement):
80
163
  'index' specifies the desired index and must be one of 'abs_cycle', 'rel_cycle',
81
164
  'abs_time' or 'rel_time'.
82
165
 
83
- If the traces have been post-processed in the Ionicon Viewer, those will be used,
84
- unless `force_original=True`.
85
166
  """
86
- return self.hr.get_all(kind, index, force_original)
167
+ return chain.from_iterable(sf.get_all(kind, index, force_original) for sf in self.sourcefiles)
87
168
 
88
- def __iter__(self):
89
- return iter(self.hr)
169
+ def __len__(self):
170
+ return sum(len(sf) for sf in self.sourcefiles)
90
171