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/__init__.py +17 -46
- pytrms/_base/__init__.py +24 -0
- pytrms/_base/ioniclient.py +32 -0
- pytrms/_base/mqttclient.py +106 -0
- pytrms/_version.py +26 -0
- pytrms/clients/__init__.py +29 -3
- pytrms/clients/db_api.py +186 -0
- pytrms/clients/dirigent.py +169 -0
- pytrms/clients/modbus.py +495 -0
- pytrms/clients/mqtt.py +773 -0
- pytrms/clients/ssevent.py +82 -0
- pytrms/compose/__init__.py +2 -0
- pytrms/compose/composition.py +207 -0
- pytrms/data/IoniTofPrefs.ini +113 -0
- pytrms/data/ParaIDs.csv +731 -0
- pytrms/helpers.py +3 -6
- pytrms/instrument.py +51 -110
- pytrms/measurement.py +129 -48
- pytrms/peaktable.py +499 -0
- pytrms/plotting/__init__.py +4 -0
- pytrms/plotting/plotting.py +27 -0
- pytrms/readers/__init__.py +4 -0
- pytrms/readers/ionitof_reader.py +481 -0
- pytrms/tracebuffer.py +33 -8
- pytrms-0.9.0.dist-info/METADATA +19 -0
- pytrms-0.9.0.dist-info/RECORD +29 -0
- {pytrms-0.2.1.dist-info → pytrms-0.9.0.dist-info}/WHEEL +1 -1
- pytrms/clients/mockclient.py +0 -612
- pytrms/reader.py +0 -173
- pytrms/testing/__init__.py +0 -14
- pytrms/testing/response_TRACES_webAPI.txt +0 -556
- pytrms-0.2.1.dist-info/METADATA +0 -16
- pytrms-0.2.1.dist-info/RECORD +0 -15
- {pytrms-0.2.1.dist-info → pytrms-0.9.0.dist-info}/LICENSE +0 -0
pytrms/helpers.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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,
|
|
45
|
+
def __init__(self, backend):
|
|
48
46
|
# dispatch all blocking calls to the client
|
|
49
|
-
|
|
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.
|
|
56
|
-
return
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
# TODO :: implement proper context with dict of settings...
|
|
73
|
-
self.start()
|
|
74
|
-
return self
|
|
63
|
+
return jobj[0]['Act']['Real']
|
|
75
64
|
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
## how it should be:
|
|
66
|
+
return raw
|
|
78
67
|
|
|
79
|
-
def
|
|
80
|
-
"""
|
|
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
|
-
|
|
90
|
-
"""
|
|
72
|
+
def start_measurement(self, filename=''):
|
|
91
73
|
# this method must be implemented by each state
|
|
92
|
-
raise
|
|
74
|
+
raise RuntimeError("can't start %s" % self.__class__)
|
|
93
75
|
|
|
94
|
-
|
|
76
|
+
start_measurement.__doc__ = Measurement.start.__doc__
|
|
95
77
|
|
|
96
|
-
def
|
|
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
|
|
80
|
+
raise RuntimeError("can't stop %s" % self.__class__)
|
|
100
81
|
|
|
101
|
-
|
|
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
|
|
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
|
|
133
|
-
home = os.path.dirname(
|
|
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(
|
|
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
|
-
|
|
140
|
-
if os.path.exists(
|
|
141
|
-
raise RuntimeError(f'
|
|
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.
|
|
144
|
-
self.
|
|
145
|
-
self.start(path)
|
|
103
|
+
self.backend.start_measurement(filename)
|
|
104
|
+
self._new_state(RunningInstrument)
|
|
146
105
|
|
|
147
|
-
|
|
148
|
-
raise RuntimeError('instrument is not running')
|
|
106
|
+
return RunningMeasurement(self)
|
|
149
107
|
|
|
150
108
|
|
|
151
109
|
class RunningInstrument(Instrument):
|
|
152
110
|
|
|
153
|
-
def
|
|
154
|
-
|
|
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
|
|
2
|
-
from
|
|
3
|
-
from
|
|
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
|
-
|
|
7
|
-
"""Base class for PTRMS-measurements or batch processing.
|
|
9
|
+
__all__ = ['Measurement', 'PreparingMeasurement', 'RunningMeasurement', 'FinishedMeasurement']
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
self._filename = filename
|
|
55
|
+
class PreparingMeasurement(Measurement):
|
|
23
56
|
|
|
24
57
|
@property
|
|
25
|
-
def
|
|
26
|
-
return self.
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
self.instrument = instrument
|
|
90
|
+
self.ptr = instrument
|
|
45
91
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
113
|
+
class FinishedMeasurement(Measurement):
|
|
60
114
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
67
|
-
return self.
|
|
125
|
+
def number_of_timebins(self):
|
|
126
|
+
return next(iter(self.sourcefiles)).number_of_timebins
|
|
68
127
|
|
|
69
128
|
@property
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
167
|
+
return chain.from_iterable(sf.get_all(kind, index, force_original) for sf in self.sourcefiles)
|
|
87
168
|
|
|
88
|
-
def
|
|
89
|
-
return
|
|
169
|
+
def __len__(self):
|
|
170
|
+
return sum(len(sf) for sf in self.sourcefiles)
|
|
90
171
|
|