pytrms 0.9.0__py3-none-any.whl → 0.9.2__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/clients/ssevent.py CHANGED
@@ -1,82 +1,82 @@
1
1
  import re
2
- import logging
2
+ from collections import namedtuple
3
3
  from collections.abc import Iterable
4
4
 
5
5
  import requests
6
6
 
7
- from . import ionitof_url
7
+ from . import _logging
8
8
 
9
- log = logging.getLogger()
9
+ log = _logging.getLogger(__name__)
10
10
 
11
+ _event_rv = namedtuple('ssevent', ['event', 'data'])
11
12
 
12
13
  class SSEventListener(Iterable):
13
14
 
14
- def __init__(self, endpoint=''):
15
- if not endpoint:
16
- endpoint = ionitof_url + '/api/timing/stream'
17
- self.endpoint = endpoint
18
- self._response = None
19
- self.subscriptions = set()
20
-
21
- def subscribe(self, event='cycle'):
22
- """Listen for events matching the given string or regular expression.
23
-
24
- This will already match if the string is contained in the event-topic.
25
- """
26
- if self._response is None:
27
- r = requests.get(self.endpoint, headers={'accept': 'text/event-stream'}, stream=True)
28
- if not r.ok:
29
- log.error(f"no connection to {self.endpoint} (got [{r.status_code}])")
30
- r.raise_for_status()
31
-
32
- self._response = r
33
-
34
- regex = re.compile('.*' + event + '.*') # be rather loose with matching..
35
- self.subscriptions.add(regex)
36
-
37
- def unsubscribe(self, event='cycle'):
38
- self.subscriptions.remove(event)
39
- if not len(self.subscriptions):
40
- log.debug(f"closing connection to {self.endpoint}")
41
- self._response.close()
42
- self._response = None
43
-
44
15
  @staticmethod
45
- def line_stream(response):
16
+ def _line_stream(response):
46
17
  # Note: using .iter_content() seems to yield results faster than .iter_lines()
47
18
  line = ''
48
19
  for bite in response.iter_content(chunk_size=1, decode_unicode=True):
49
20
  line += bite
50
21
  if bite == '\n':
51
- yield line
22
+ yield line.strip()
52
23
  line = ''
53
24
 
54
- def items(self):
55
- for msg in self:
56
- yield self.event, msg
25
+ def __init__(self, event_re=None, host_url='http://127.0.0.1:5066',
26
+ endpoint='/api/events', session=None):
27
+ self.uri = host_url + endpoint
28
+ if session is not None:
29
+ self._get = session.get
30
+ else:
31
+ self._get = requests.get
32
+ self._connect_response = None
33
+ self.subscriptions = set()
34
+ if event_re is not None:
35
+ self.subscribe(event_re)
36
+
37
+ def subscribe(self, event_re):
38
+ """Listen for events matching the given string or regular expression."""
39
+ self.subscriptions.add(re.compile(event_re))
40
+ if self._connect_response is None:
41
+ r = self._get(self.uri, headers={'accept': 'text/event-stream'}, stream=True)
42
+ if not r.ok:
43
+ log.error(f"no connection to {self.uri} (got [{r.status_code}])")
44
+ r.raise_for_status()
45
+
46
+ self._connect_response = r
47
+
48
+ def unsubscribe(self, event_re):
49
+ """Stop listening for certain events."""
50
+ self.subscriptions.remove(re.compile(event_re))
51
+ if not len(self.subscriptions):
52
+ log.debug(f"closing connection to {self.uri}")
53
+ self._connect_response.close()
54
+ self._connect_response = None
57
55
 
58
56
  def __iter__(self):
59
- if self._response is None:
57
+ if self._connect_response is None:
60
58
  raise Exception("call .subscribe() first to listen for events")
61
59
 
62
- for line in self.line_stream(self._response): # blocks...
63
- if not line.strip():
64
- continue
60
+ event = msg = ''
61
+ for line in self._line_stream(self._connect_response): # blocks...
62
+ if not line:
63
+ # an empty line concludes an event
64
+ if event and any(re.match(sub, event) for sub in self.subscriptions):
65
+ yield _event_rv(event, msg)
65
66
 
66
- key, msg = line.split(':', maxsplit=1)
67
- msg = msg.strip()
68
- if key == 'event':
69
- self.event = msg
70
-
71
- if not any(sub.match(self.event) for sub in self.subscriptions):
72
- log.debug(f"skipping event <{msg}>")
73
- continue
67
+ # Note: any further empty lines are ignored (may be used as keep-alive),
68
+ # but in either case clear event and msg to rearm for the next event:
69
+ event = msg = ''
70
+ continue
74
71
 
72
+ key, val = line.split(':', maxsplit=1)
73
+ if not key:
74
+ # this is a comment, starting with a colon ':' ...
75
+ log.log(_logging.TRACE, "sse:" + val)
76
+ elif key == 'event':
77
+ event = val.lstrip()
75
78
  elif key == 'data':
76
- yield msg
77
-
79
+ msg += val.lstrip()
78
80
  else:
79
- log.warning(f"skipping unknown key <{key}> in stream")
80
- continue
81
-
81
+ log.warning(f"unknown SSE-key <{key}> in stream")
82
82
 
@@ -4,6 +4,7 @@ import itertools
4
4
  import logging
5
5
  from collections.abc import Iterable
6
6
  from collections import namedtuple
7
+ from itertools import tee
7
8
  from functools import wraps
8
9
 
9
10
  __all__ = ['Step', 'Composition']
@@ -30,14 +31,19 @@ class Step:
30
31
  >>> step.set_values
31
32
  {'DPS_Udrift': 500}
32
33
 
33
- Note, that no Automation-numbers can be defined in the step:
34
+ Note, that no Automation-numbers can be defined in the step..
34
35
  >>> Step("H50", {'AUTO_UseMean': 0}, 10, start_delay=2)
35
36
  Traceback (most recent call last):
36
37
  ...
37
38
  AssertionError: Automation numbers cannot be defined
38
39
 
39
- '''
40
+ ..and neither can a 'OP_Mode' alongside anything else:
41
+ >>> Step("Odd2", {'DPS_Udrift': 345, 'OP_Mode': 2}, 10, start_delay=2)
42
+ Traceback (most recent call last):
43
+ ...
44
+ AssertionError: if 'OP_Mode' is specified, nothing else can be
40
45
 
46
+ '''
41
47
  protected_keys = ['AME_RunNumber', 'AME_StepNumber', 'AUTO_UseMean']
42
48
 
43
49
  def __init__(self, name, set_values, duration, start_delay):
@@ -51,8 +57,13 @@ class Step:
51
57
  assert self.start_delay >= 0
52
58
  assert self.start_delay < self.duration
53
59
 
54
- for key in self.set_values.keys():
60
+ for key in self.set_values:
55
61
  assert key not in Step.protected_keys, "Automation numbers cannot be defined"
62
+ if 'OP_Mode' in self.set_values:
63
+ assert len(self.set_values) == 1, "if 'OP_Mode' is specified, nothing else can be"
64
+
65
+ def __repr__(self):
66
+ return f"{self.name}: ({self.start_delay}/{self.duration}) sec ~> {self.set_values}"
56
67
 
57
68
 
58
69
  class Composition(Iterable):
@@ -77,15 +88,34 @@ class Composition(Iterable):
77
88
  >>> list(co.sequence())
78
89
  [(8, {'Eins': 1}), (18, {'Zwei': 2})]
79
90
 
80
- ...with an action-number at the start...
91
+ ...with an action-number at the start (note, that AME-numbers are 1 cycle ahead)...
81
92
  >>> co.start_action = 7
82
93
  >>> list(co.sequence())
83
- [(8, {'AME_ActionNumber': 7}), (8, {'Eins': 1}), (18, {'Zwei': 2})]
94
+ [(9, {'AME_ActionNumber': 7}), (8, {'Eins': 1}), (18, {'Zwei': 2})]
84
95
 
85
96
  ...or with automation numbers, where the 'start_delay' comes into play:
86
97
  >>> co.generate_automation = True
87
- >>> list(co.sequence())
88
- [(8, {'AME_ActionNumber': 7}), (8, {'Eins': 1, 'AME_StepNumber': 1, 'AME_RunNumber': 1}), (8, {'AUTO_UseMean': 0}), (10, {'AUTO_UseMean': 1}), (18, {'Zwei': 2, 'AME_StepNumber': 2}), (18, {'AUTO_UseMean': 0}), (21, {'AUTO_UseMean': 1})]
98
+ >>> seq = co.sequence()
99
+ >>> next(seq)
100
+ (9, {'AME_ActionNumber': 7})
101
+
102
+ >>> next(seq)
103
+ (8, {'Eins': 1})
104
+
105
+ >>> next(seq)
106
+ (9, {'AME_StepNumber': 1, 'AME_RunNumber': 1, 'AUTO_UseMean': 0})
107
+
108
+ >>> next(seq)
109
+ (11, {'AUTO_UseMean': 1})
110
+
111
+ >>> next(seq)
112
+ (18, {'Zwei': 2})
113
+
114
+ >>> next(seq)
115
+ (19, {'AME_StepNumber': 2, 'AUTO_UseMean': 0})
116
+
117
+ >>> next(seq)
118
+ (22, {'AUTO_UseMean': 1})
89
119
 
90
120
  '''
91
121
 
@@ -122,6 +152,67 @@ class Composition(Iterable):
122
152
  def dump(self, ofstream):
123
153
  json.dump(self, ofstream, indent=2, default=vars)
124
154
 
155
+ def translate_op_modes(self, preset_items, check=True):
156
+ '''Given the `preset_items` (from a presets-file), compile a list of set_values.
157
+
158
+ >>> presets = {}
159
+ >>> presets[0] = ('H3O+', {('Drift', 'Global_System.DriftPressureSet', 'FLOAT'): 2.6})
160
+ >>> presets[2] = ('O3+', {('T-Drift[°C]', 'Global_Temperatures.TempsSet[0]', 'FLOAT'): 75.0})
161
+
162
+ next, define a couple of Steps that use the presets (a.k.a. 'OP_Mode'):
163
+ >>> steps = []
164
+ >>> steps.append(Step('uno', {'OP_Mode': 0}, 10, 2)) # set p-Drift by OP_Mode
165
+ >>> steps.append(Step('due', {'Udrift': 420.0, 'T-Drift': 81.0}, 10, 2))
166
+ >>> steps.append(Step('tre', {'OP_Mode': 2}, 10, 2)) # set T-Drift by OP_Mode
167
+
168
+ the Composition of these steps will translate to the output underneath.
169
+ note, that the set-value for Pdrift_Ctrl is carried along with each step:
170
+ >>> co = Composition(steps)
171
+ >>> co.translate_op_modes(presets, check=False)
172
+ [{'DPS_Pdrift_Ctrl_Val': 2.6}, {'Udrift': 420.0, 'T-Drift': 81.0, 'DPS_Pdrift_Ctrl_Val': 2.6}, {'T-Drift': 75.0, 'DPS_Pdrift_Ctrl_Val': 2.6, 'Udrift': 420.0}]
173
+
174
+ Since we didn't specify the full set of reaction-parameters, the self-check will fail:
175
+ >>> co.translate_op_modes(presets, check=True)
176
+ Traceback (most recent call last):
177
+ ...
178
+ AssertionError: reaction-data missing in presets
179
+
180
+ '''
181
+ if preset_items is None:
182
+ raise ValueError('preset_items is None')
183
+
184
+ # Note: the `preset_items` is a dict[step_index] ~> (name, preset_items)
185
+ # and in the items one would expect these keys:
186
+ preset_keys = {
187
+ 'PrimionIdx': ('PrimionIdx', '', 'INT'),
188
+ 'TransmissionIdx': ('TransmissionIdx', '', 'INT'),
189
+ 'DPS_Udrift': ('UDrift', 'Global_DTS500.TR_DTS500_Set[0].SetU_Udrift', 'FLOAT'),
190
+ 'DPS_Pdrift_Ctrl_Val': ('Drift', 'Global_System.DriftPressureSet', 'FLOAT'),
191
+ 'T-Drift': ('T-Drift[°C]', 'Global_Temperatures.TempsSet[0]', 'FLOAT'),
192
+ }
193
+ # make a deep copy of the `set_values`:
194
+ set_values = [dict(step.set_values) for step in self.steps]
195
+ carry = dict()
196
+ for entry in set_values:
197
+ # replace OP_Mode with the stuff found in preset_items
198
+ if 'OP_Mode' in entry:
199
+ index = entry['OP_Mode']
200
+ name, items = preset_items[index]
201
+ for parID, key in preset_keys.items():
202
+ if key in items:
203
+ entry[parID] = items[key]
204
+ del entry['OP_Mode']
205
+
206
+ # Note: each preset is only an update of set-values over what has already
207
+ # been set. thus, when following the sequence of OP_Modes, each one must
208
+ # carry with it the set-values of all its predecessors:
209
+ carry.update(entry)
210
+ entry.update(carry)
211
+ if check:
212
+ assert all(key in entry for key in preset_keys), "reaction-data missing in presets"
213
+
214
+ return set_values
215
+
125
216
  def sequence(self):
126
217
  '''A (possibly infinite) iterator over this Composition's future_cycles and steps.
127
218
 
@@ -130,27 +221,31 @@ class Composition(Iterable):
130
221
  The first 'future_cycle' is 0 unless otherwise specified with class-parameter 'start_cycle'.
131
222
  This generates AME_Run/Step-Number and AUTO_UseMean unless otherwise specified.
132
223
  '''
224
+ _offset_ame = True # whether ame-numbers should mark the *next* cycle, see [#2897]
225
+
133
226
  future_cycle = self.start_cycle
134
227
  if self.start_action is not None:
135
- yield future_cycle, dict([(self.ACTION_MARKER, int(self.start_action))])
228
+ yield future_cycle + int(_offset_ame), dict([(self.ACTION_MARKER, int(self.start_action))])
136
229
 
137
- for run, step, current in self:
138
- automation = {self.STEP_MARKER: step}
139
- if step == 1:
140
- automation[self.RUN_MARKER] = run
230
+ for run, step, step_info in self:
231
+ yield future_cycle, dict(step_info.set_values)
141
232
 
142
- set_values = dict(current.set_values)
143
233
  if self.generate_automation:
144
- set_values = dict(**set_values, **automation)
145
-
146
- yield future_cycle, set_values
147
-
148
- # insert two updates for AUTO_UseMean flag:
149
- if self.generate_automation and current.start_delay > 0:
150
- yield future_cycle, {self.USE_MARKER: 0}
151
- yield future_cycle + current.start_delay, {self.USE_MARKER: 1}
152
-
153
- future_cycle = future_cycle + current.duration
234
+ automation = {self.STEP_MARKER: step}
235
+ if step == 1:
236
+ automation[self.RUN_MARKER] = run
237
+
238
+ if step_info.start_delay == 0:
239
+ # all cycles get the AUTO_UseMean flag set to True:
240
+ automation[self.USE_MARKER] = 1
241
+ yield future_cycle + int(_offset_ame), automation
242
+ else:
243
+ # split into two updates for AUTO_UseMean flag:
244
+ automation[self.USE_MARKER] = 0
245
+ yield future_cycle + int(_offset_ame), automation
246
+ yield future_cycle + int(_offset_ame) + step_info.start_delay, {self.USE_MARKER: 1}
247
+
248
+ future_cycle = future_cycle + step_info.duration
154
249
 
155
250
  @coroutine
156
251
  def schedule_routine(self, schedule_fun):
pytrms/helpers.py CHANGED
@@ -1,6 +1,120 @@
1
- import requests.exceptions
1
+ """@file helpers.py
2
2
 
3
- class PTRConnectionError(requests.exceptions.ConnectionError):
4
- pass
3
+ common helper functions.
4
+ """
5
5
 
6
+ def convert_labview_to_posix(lv_time_utc, utc_offset_sec):
7
+ '''Create a `pandas.Timestamp` from LabView time.'''
8
+ from pandas import Timestamp
9
+
10
+ # change epoch from 01.01.1904 to 01.01.1970:
11
+ posix_time = lv_time_utc - 2082844800
12
+ # the tz must be specified in isoformat like '+02:30'..
13
+ tz_sec = int(utc_offset_sec)
14
+ tz_designator = '{0}{1:02d}:{2:02d}'.format(
15
+ '+' if tz_sec >= 0 else '-', tz_sec // 3600, tz_sec % 3600 // 60)
16
+
17
+ return Timestamp(posix_time, unit='s', tz=tz_designator)
18
+
19
+
20
+ def parse_presets_file(presets_file):
21
+ '''Load a `presets_file` as XML-tree and interpret the "OP_Mode" of this `Composition`.
22
+
23
+ The tricky thing is, that any OP_Mode may or may not override previous settings!
24
+ Therefore, it depends on the order of modes in this Composition to be able to assign
25
+ each OP_Mode its actual dictionary of set_values.
26
+
27
+ Note, that the preset file uses its own naming convention that cannot neccessarily be
28
+ translated into standard parID-names. You may choose whatever you like to do with it.
29
+ '''
30
+ import xml.etree.ElementTree as ET
31
+ from collections import namedtuple, defaultdict
32
+
33
+ _key = namedtuple('preset_item', ['name', 'ads_path', 'dtype'])
34
+ _parse_value = {
35
+ "FLOAT": float,
36
+ "BOOL": bool,
37
+ "BYTE": int,
38
+ "ENUM": int,
39
+ }
40
+ tree = ET.parse(presets_file)
41
+ root = tree.getroot()
42
+
43
+ preset_names = {}
44
+ preset_items = defaultdict(dict)
45
+ for index, preset in enumerate(root.iterfind('preset')):
46
+ preset_names[index] = preset.find('name').text.strip()
47
+
48
+ if preset.find('WritePrimIon').text.upper() == "TRUE":
49
+ val = preset.find('IndexPrimIon').text
50
+ preset_items[index][_key('PrimionIdx', '', 'INT')] = int(val)
51
+
52
+ if preset.find('WriteTransmission').text.upper() == "TRUE":
53
+ val = preset.find('IndexTransmission').text
54
+ preset_items[index][_key('TransmissionIdx', '', 'INT')] = int(val)
55
+
56
+ for item in preset.iterfind('item'):
57
+ if item.find('Write').text.upper() == "TRUE":
58
+ # device_index = item.find('DeviceIndex').text
59
+ ads_path = item.find('AdsPath').text
60
+ data_type = item.find('DataType').text
61
+ # page_name = item.find('PageName').text
62
+ name = item.find('Name').text
63
+ value_text = item.find('Value').text
64
+
65
+ key = _key(name, ads_path, data_type)
66
+ val = _parse_value[data_type](value_text)
67
+ preset_items[index][key] = val
68
+
69
+ return {index: (preset_names[index], preset_items[index]) for index in preset_names.keys()}
70
+
71
+
72
+ def setup_measurement_dir(config_dir=None, data_root_dir='D:/Data', suffix='',
73
+ date_fmt = "%Y_%m_%d__%H_%M_%S"):
74
+ """Create a new directory for saving the measurement and set it up.
75
+
76
+ Optional: copy all files from the given config-directory.
77
+
78
+ data_root_dir: the base folder for storing new measurement-directories
79
+ suffix: will be appended to directory and data-file
80
+ date_fmt: format for the source-folder and -file to be timestamped
81
+ """
82
+ import os
83
+ import glob
84
+ import shutil
85
+ from collections import namedtuple
86
+ from datetime import datetime
87
+ from itertools import chain
88
+
89
+ recipe = namedtuple('recipe', ['dirname', 'h5_file', 'pt_file', 'alarms_file'])
90
+ _pt_formats = ['*.ionipt']
91
+ _al_formats = ['*.alm']
92
+ # make directory with current timestamp:
93
+ now = datetime.now()
94
+ new_h5_file = os.path.abspath(os.path.join(
95
+ data_root_dir,
96
+ now.strftime(date_fmt) + suffix,
97
+ now.strftime(date_fmt) + suffix + '.h5',
98
+ ))
99
+ new_recipe_dir = os.path.dirname(new_h5_file)
100
+ os.makedirs(new_recipe_dir, exist_ok=False) # may throw!
101
+ if not config_dir:
102
+ # we're done here..
103
+ return recipe(new_recipe_dir, new_h5_file, '', '')
104
+
105
+ # find the *first* matching file or an empty string if no match...
106
+ new_pt_file = next(chain.from_iterable(glob.iglob(config_dir + "/" + g) for g in _pt_formats), '')
107
+ new_al_file = next(chain.from_iterable(glob.iglob(config_dir + "/" + g) for g in _al_formats), '')
108
+ # ...and copy all files from the master-recipe-dir:
109
+ files2copy = glob.glob(config_dir + "/*")
110
+ for file in files2copy:
111
+ new_file = shutil.copy(file, new_recipe_dir)
112
+ try: # remove write permission (a.k.a. make files read-only)
113
+ mode = os.stat(file).st_mode
114
+ os.chmod(new_file, mode & ~stat.S_IWRITE)
115
+ except Exception as exc:
116
+ # well, we can't set write permission
117
+ pass
118
+
119
+ return recipe(new_recipe_dir, new_h5_file, new_pt_file, new_al_file)
6
120
 
pytrms/instrument.py CHANGED
@@ -17,7 +17,6 @@ class Instrument(ABC):
17
17
  Note, that for every client PTR instrument there is only one instance of this class.
18
18
  This is to prevent different instances to be in other states than the instrument.
19
19
  '''
20
-
21
20
  __instance = None
22
21
 
23
22
  def _new_state(self, newstate):
@@ -69,38 +68,41 @@ class Instrument(ABC):
69
68
  """Set a variable to a new value."""
70
69
  return self.backend.set(varname, value, unit='-')
71
70
 
71
+ _current_sourcefile = ''
72
+
72
73
  def start_measurement(self, filename=''):
73
74
  # this method must be implemented by each state
74
75
  raise RuntimeError("can't start %s" % self.__class__)
75
76
 
76
- start_measurement.__doc__ = Measurement.start.__doc__
77
-
78
77
  def stop_measurement(self):
79
78
  # this method must be implemented by each state
80
79
  raise RuntimeError("can't stop %s" % self.__class__)
81
80
 
82
- stop_measurement.__doc__ = Measurement.stop.__doc__
83
-
84
81
 
85
82
  class IdleInstrument(Instrument):
86
83
 
87
84
  def start_measurement(self, filename=''):
88
- # if we send a filepath to the server that does not exist there, the server will
89
- # open a dialog and "hang" (which I'd very much like to avoid).
90
- # the safest way is to not send a path at all and start a 'Quick' measurement.
91
- # but if the server is the local machine, we do our best to verify the path:
92
- if filename and self.is_local:
93
- home = os.path.dirname(filename)
94
- os.makedirs(home, exist_ok=True)
95
- base = os.path.basename(filename)
96
- if not base:
97
- base = '%Y-%m-%d_%H-%M-%S.h5'
98
- base = time.strftime(base)
99
- filename = os.path.join(home, base)
85
+ dirname = os.path.dirname(filename)
86
+ if dirname and self.is_local:
87
+ # Note: if we send a filepath to the server that does not exist there, the
88
+ # server will open a dialog and "hang" (which I'd very much like to avoid).
89
+ # the safest way is to not send a path at all and start a 'Quick' measurement.
90
+ # but if the server is the local machine, we do our best to verify the path:
91
+ os.makedirs(dirname, exist_ok=True)
92
+
93
+ if filename:
94
+ basename = os.path.basename(filename)
95
+ # this may very well be a directory to record a filename into:
96
+ if not basename:
97
+ basename = '%Y-%m-%d_%H-%M-%S.h5'
98
+ filename = os.path.join(dirname, basename)
99
+ # finally, pass everything through strftime...
100
+ filename = time.strftime(filename)
100
101
  if os.path.exists(filename):
101
102
  raise RuntimeError(f'filename exists and cannot be overwritten')
102
103
 
103
104
  self.backend.start_measurement(filename)
105
+ self._current_sourcefile = filename
104
106
  self._new_state(RunningInstrument)
105
107
 
106
108
  return RunningMeasurement(self)
@@ -112,3 +114,6 @@ class RunningInstrument(Instrument):
112
114
  self.backend.stop_measurement()
113
115
  self._new_state(IdleInstrument)
114
116
 
117
+ # TODO :: this catches only one sourcefile.. it'll do for simple cases:
118
+ return FinishedMeasurement(_current_sourcefile)
119
+
pytrms/measurement.py CHANGED
@@ -1,9 +1,10 @@
1
1
  import time
2
- from glob import glob
3
2
  from operator import attrgetter
4
3
  from itertools import chain
5
4
  from abc import abstractmethod, ABC
6
5
 
6
+ import pandas as pd
7
+
7
8
  from .readers import IoniTOFReader
8
9
 
9
10
  __all__ = ['Measurement', 'PreparingMeasurement', 'RunningMeasurement', 'FinishedMeasurement']
@@ -145,16 +146,14 @@ class FinishedMeasurement(Measurement):
145
146
  def timebin_width_ps(self):
146
147
  return next(iter(self.sourcefiles)).timebin_width_ps
147
148
 
148
- def __init__(self, filenames, _reader=IoniTOFReader):
149
- if isinstance(filenames, str):
150
- filenames = glob(filenames)
149
+ def __init__(self, *filenames, _reader=IoniTOFReader):
151
150
  if not len(filenames):
152
- raise ValueError("file not found or empty glob expression")
151
+ raise ValueError("no filename given")
153
152
 
154
153
  self.sourcefiles = sorted((_reader(f) for f in filenames), key=attrgetter('time_of_file'))
155
154
  self._check(self.sourcefiles)
156
155
 
157
- def iter_traces(self, kind='raw', index='abs_cycle', force_original=False):
156
+ def read_traces(self, kind='conc', index='abs_cycle', force_original=False):
158
157
  """Return the timeseries ("traces") of all masses, compounds and settings.
159
158
 
160
159
  'kind' is the type of traces and must be one of 'raw', 'concentration' or
@@ -164,8 +163,11 @@ class FinishedMeasurement(Measurement):
164
163
  'abs_time' or 'rel_time'.
165
164
 
166
165
  """
167
- return chain.from_iterable(sf.get_all(kind, index, force_original) for sf in self.sourcefiles)
166
+ return pd.concat(sf.read_all(kind, index, force_original) for sf in self.sourcefiles)
167
+
168
+ def __iter__(self):
169
+ return iter(self.sourcefiles)
168
170
 
169
171
  def __len__(self):
170
- return sum(len(sf) for sf in self.sourcefiles)
172
+ return len(self.sourcefiles)
171
173
 
pytrms/peaktable.py CHANGED
@@ -35,7 +35,7 @@ other custom formats.
35
35
  >>> pt
36
36
  <PeakTable (1) [21.0u]>
37
37
  >>> pt[0]
38
- <Peak [H3O+] @ 21.0219>
38
+ <Peak @ 21.0219+0.0000 [H3O+]>
39
39
 
40
40
  Peaks may be modified and the PeakTable exported in the same format:
41
41
  >>> pt[0].formula = 'H3O'
@@ -56,7 +56,9 @@ Peaks may be modified and the PeakTable exported in the same format:
56
56
  "parent": "?",
57
57
  "isotopic_abundance": 0.678,
58
58
  "k_rate": 2.1,
59
- "multiplier": 488
59
+ "multiplier": 488.0,
60
+ "resolution": 1000.0,
61
+ "shift": 0.0
60
62
  }
61
63
  ]
62
64
  }
@@ -144,14 +146,14 @@ class Peak:
144
146
  return self.center == round(float(other), Peak._exact_decimals)
145
147
 
146
148
  def __hash__(self):
147
- return hash(str(self.center)) # + self.label)
149
+ return hash(str(self.center) + self.label)
148
150
 
149
151
  def __float__(self):
150
152
  return self.center
151
153
 
152
154
  def __repr__(self):
153
- return '<%s [%s <~ %s] @ %.4f+%.4f>' % (self.__class__.__name__,
154
- self.label, self.parent, self.center, self.shift)
155
+ return '<%s @ %.4f+%.4f [%s]>' % (self.__class__.__name__,
156
+ self.center, self.shift, self.label)
155
157
 
156
158
 
157
159
  class PeakTable: