Separate the application (oscopy_ipython) from the framework (oscopy)
[oscopy.git] / src / oscopy / readers / touchstone_reader.py
blob7388c48dc432857cf72d678b5c657266a6fe783b
1 from __future__ import with_statement
3 import re
4 from oscopy import Signal
5 from reader import Reader, ReadError
6 import struct, os
8 class TouchstoneReader(Reader):
9 """ Read Touchstone(r) or snp file format, version 1 and 2.0
11 Noise parameter data is read and stored in self.info['noise_param'].
13 Mixed mode parameters of version 2.0 not supported.
15 Note: this Reader uses the file extension to determine the number of ports
16 for version 1 of this format specification ('.snp' where n is 1-4)
18 For more details, see http://www.eda.org/ibis/touchstone_ver2.0/touchstone_ver2_0.pdf
19 """
21 FREQ_UNIT_VALUES = {'HZ': 1, 'KHZ': 1e3, 'MHZ': 1e6, 'GHZ': 1e9}
22 PARAM_VALUES = ['S', 'Y', 'Z', 'H', 'G']
23 FORMAT_VALUES = {'DB': ('dB', 'degrees'), 'MA': ('a.u.', 'degrees'),
24 'RI': ('a.u.', 'a.u')}
25 KEYWORDS = ['version', 'number of ports', 'two-port data order',
26 'number of frequencies', 'number of noise frequencies',
27 'reference', 'matrix format', 'mixed-mode order',
28 'begin information', 'end information',
29 'network data', 'noise data', 'end']
30 MATRIX_FORMAT = ['full', 'lower', 'upper']
31 EXT_PORTS = {'s1p': 1, 's2p': 2, 's3p': 3, 's4p': 4}
33 def detect(self, fn):
34 """ Search for the option line (starting with '#') and returns result
35 of processing this line.
37 Parameter
38 ---------
39 fn: string
40 Path to the file to test
42 Returns
43 -------
44 bool
45 True if the file can be handled by this reader
46 """
47 self._check(fn)
48 try:
49 f = open(fn)
50 except IOError, e:
51 return False
52 while f:
53 line = f.readline().strip()
54 if not line or line.startswith('!'):
55 continue
56 elif line.startswith('#'):
57 return (self._process_option(line) is not None)
58 elif line.startswith('['):
59 continue
60 else:
61 f.close()
62 return False
63 f.close()
64 return True
66 def _read_signals(self):
67 """ Read the signals from the file
69 Check whether file format is version 1 or 2 (detection of '[')
70 and parse the option line when met then call the relevant
71 function to read the data.
73 Parameter
74 ---------
75 fn: string
76 The filename
78 Returns
79 -------
80 Dict of Signals
81 The list of Signals read from the file
83 Raises
84 ------
85 ReaderError
86 In case of invalid path or unsupported file format
87 """
88 options = self._process_option('#')
89 version = 1
90 with open(self._fn) as f:
91 data_start = False
92 while not data_start:
93 line = f.readline().strip()
94 if not line:
95 continue
96 if line.startswith('!'):
97 continue
98 elif line.startswith('#'):
99 options = self._process_option(line)
100 elif line.startswith('['):
101 if line.split()[0].lower() == '[' + self.KEYWORDS[0] + ']':
102 version = float(line.split()[1])
103 elif line.split()[0][0].isdigit():
104 break
105 f.seek(0)
106 if version == 1:
107 return self._read_signals_v1(f, options, len(line.split()))
108 elif version == 2:
109 return self._read_signals_v2(f, options)
110 else:
111 raise NotImplementedError(_('touchstone_reader: format version %s not supported' % version))
113 def _process_option(self, line):
114 """ Parse the option line
116 Parameter
117 ---------
118 line: string
119 the line containing the options
121 Returns
122 -------
123 options: dict
124 The list of parameters extracted from option line
125 'freq_mult': float
126 The frequency multiplier, e.g. 1e9 for GHz
127 'param': string
128 The parameter measured, e.g. 'S' for S-parameter
129 'format': string
130 The file format, e.g. 'MA' for Magnitude-Angle
131 'ref': float
132 The value of reference resistance
134 options = {'freq_mult': 1e9, 'param': 'S', 'format': 'MA', 'ref': 50}
135 opts = line.lstrip('#').upper().split()
136 r_found = False
137 for opt in opts:
138 if opt in self.FREQ_UNIT_VALUES.keys():
139 options['freq_mult'] = self.FREQ_UNIT_VALUES[opt]
140 elif opt in self.PARAM_VALUES:
141 options['param'] = opt
142 elif opt in self.FORMAT_VALUES.keys():
143 options['format'] = opt
144 elif opt == 'R':
145 r_found = True
146 elif r_found:
147 options['ref'] = float(opt)
148 else:
149 return None #Keyword not recognized
150 return options
152 def _read_signals_v1(self, f, options, n = 3):
153 """ Read file using Touchstone 1.0 format specification
155 Noise parameters stored in self._info['noise_param']
157 Parameters
158 ----------
159 f: file
160 The file object (already opened) to read data from
162 options: dict
163 Parameters from option line. Used here: 'format', 'param', 'freq_mult'
165 n: int
166 Number of word on first line found with first character being a digit
167 assumed to be the first data line
169 Returns
170 -------
171 self._signals: dict of Signals
173 Raises
174 ------
175 ReadError
176 Unknown number of ports
178 # Guess number of ports, inspired from W. hoch's dataplot
179 extension = self._fn.split('.')[-1].lower()
180 nports = self.EXT_PORTS.get(extension, None)
181 if nports is None:
182 # Not found in extension, guess from number of data on first line,
183 # but works only for 1-port and 2-port
184 nports = {3: 1, 9: 2}.get(n, None)
185 if nports is None:
186 raise ReadError(_('touchstone_reader: unknown number of ports'))
188 # Instanciate signals
189 (ref, signals, names) = self._instanciate_signals(nports, options)
191 # Read data
192 nparams = len(signals)
193 data = [[] for x in xrange(len(signals))]
194 append = [x.append for x in data]
195 cpt = 0
196 offset = 0
197 noise_param = []
198 np_append = noise_param.append
199 for line in f:
200 if not len(line) or line.startswith('!') or line.startswith('#'):
201 continue
202 elif len(line.split()) == 5:
203 # A line of noise parameter data
204 np_append([float(x) for x in line.split()])
205 else:
206 # Network parameter data
207 for i, val in enumerate(line.split()):
208 if val.startswith('!'):
209 # Comments at end of line so next line
210 break
211 append[offset + i](float(val))
212 cpt = cpt + 1
213 if cpt < nparams:
214 # line is not finished, remaining parameters on next line
215 offset = cpt
216 else:
217 cpt = offset = 0
219 # Gather data
220 ref.data = data[0]
221 ref.data = ref.data * options['freq_mult']
222 for i, s in enumerate(signals[1:]):
223 s.ref = ref
224 s.data = data[i + 1]
225 self._signals = dict(zip(names[1:], signals[1:]))
227 self._info['noise_param'] = self._process_noise_param(noise_param, 1)
228 return self._signals
230 def _read_signals_v2(self, f, options):
231 """ Read file using Touchstone 2.0 format specification
233 Noise parameters stored in self._info['noise_param'] unprocessed
234 Keywords met stored in self._info
236 Parse the file line by line, and when relevant keyowrds are met, store
237 network data or noise parameter data.
239 Parameters
240 ----------
241 f: file
242 The file object (already opened) to read data from
244 options: dict
245 Parameters from option line. Used here: 'format', 'param', 'freq_mult'
247 n: int
248 Number of word on first line found with first character being a digit
249 assumed to be the first data line
251 Returns
252 -------
253 self._signals: dict of Signals
255 Raises
256 ------
257 ReadError
258 unkown string, unknown keyword or argument, excess of reference values
260 data_start = False
261 noise_start = False
262 end = False
263 noise_param = []
264 np_append = noise_param.append
265 for line in f:
266 if not len(line) or line.startswith('!') or line.startswith('#'):
267 # Blank line or comment
268 continue
270 elif line.strip().startswith('['):
271 # Keyword. Extract keyword and argument, check if kw is valid,
272 # store it in self._info, then process the keyword if needed
273 tmp = re.findall('\[([\w\s-]+)\]\s*(\S*)', line.strip())
274 if not tmp:
275 raise ReadError(_('touchstone_reader: unrecognized \'%s\'') % line)
276 kw = tmp[0][0]
277 arg = tmp[0][1] if len(tmp[0]) > 1 else ''
278 if kw.lower() not in self.KEYWORDS:
279 raise ReadError(_('touchstone_reader: unrecognized keyword \'%s\'') % kw)
280 self._info[kw.lower()] = arg
281 # Keyword neededs addtionnal process
282 if kw.lower() == 'network data':
283 # Next line will be data, prepare the signals and variables
284 # used for reading
285 (ref, signals, names) = self._instanciate_signals(nports,
286 options)
287 data = [[] for x in xrange(len(signals))]
288 append = [x.append for x in data]
289 mxfmt = self._info.get('matrix format', 'full').lower()
290 row = 0
291 data_start = True
292 elif kw.lower() == 'noise data':
293 # Next line will be noise data
294 noise_start = True
295 data_start = False
296 elif kw.lower() == 'end':
297 # No more data to read
298 break
299 elif kw.lower() == 'reference':
300 # Assuming [Reference] keyword is without spaces
301 self._info['reference'] = [float(x.strip()) for x in line.split()[1:]]
302 elif kw.lower() == 'number of ports':
303 nports = int(self._info['number of ports'])
304 elif kw.lower() == 'number of frequencies':
305 nfreq = int(self._info['number of frequencies'])
306 elif kw.lower() == ['matrix format']:
307 # Validate the argument
308 if arg.lowrer() not in self.MATRIX_FORMATS:
309 raise ReadError(_('touchstone_reader: unrecognized matrix format \'%s\'') % mxfmt)
311 elif data_start:
312 # Data, assume on row per line for matrices
313 tmp = line.partition('!')[0] # Remove any comment
314 if mxfmt in ['full', 'lower']:
315 offset = 0 if not row else nports * 2 * row + 1
316 else: # mxfmt == 'upper'
317 # Not tested
318 offset = 0 if not row else nports * 2 * row + 1 + row * 2
319 for i, val in enumerate(tmp.split()):
320 append[i + offset](float(val))
321 row = (row + 1) if row < nports and nports > 2 else 0
323 elif noise_start:
324 # Noise
325 np_append([float(x) for x in line.split()])
327 elif len(self._info.get('reference', nports)) < nports:
328 # Reference on more than a line
329 tmp = line.split()
330 if len(tmp) > len(self._info['reference']):
331 raise ReadError(_('touchstone_reader: excess references found \'%s\'') % line)
332 for x in tmp:
333 self.info['reference'].append(float(x.strip()))
334 if mxfmt in ['lower', 'upper']:
335 # Expand matrices in case of 'upper' or 'lower'
336 for i in xrange(nports):
337 for j in xrange(nports):
338 if mxfmt == 'lower' and i < j:
339 data[i * nports * 2 + j * 2 + 1] = data[j * nports * 2 + i * 2 + 1]
340 data[i * nports * 2 + j * 2 + 2] = data[j * nports * 2 + i * 2 + 2]
341 if mxfmt == 'upper' and i > j:
342 data[i * nports * 2 + j * 2 + 1] = data[j * nports * 2 + i * 2 + 1]
343 data[i * nports * 2 + j * 2 + 2] = data[j * nports * 2 + i * 2 + 2]
345 # Gather data
346 ref.data = data[0]
347 ref.data = ref.data * options['freq_mult']
348 for i, s in enumerate(signals[1:]):
349 s.ref = ref
350 s.data = data[i + 1]
351 self._signals = dict(zip(names[1:], signals[1:]))
353 self._info['noise_param'] = self._process_noise_param(noise_param, 2)
354 return self._signals
356 def _instanciate_signals(self, nports, options):
357 """ Instanciate the signals depending on number of port and type
358 of measurement in the form of Xij_n where X is the parameter, i and
359 j the indices and n is either 'a' or 'b'.
361 Parameters
362 ----------
363 nports: int
364 Number of ports
366 options: dict
367 'param': string
368 The parameter stored in file (e.g. S, Z, H...)
370 Returns
371 -------
372 tuple:
373 ref: Signal
374 The reference signal
376 signals: list of Signals
377 Signals instanciated
379 names: list of strings
380 Names of the Signals instanciated, same order as 'signals'
382 ref = Signal('Frequency', 'Hz')
383 signals = [ref]
384 names = [ref.name]
385 for p1 in xrange(nports):
386 for p2 in xrange(nports):
387 name = options['param'] + '%1d' % (p1 + 1) + '%1d' % (p2 + 1)
388 (unit_a, unit_b) = self.FORMAT_VALUES[options['format']]
389 signal_a = Signal(name + '_a', unit_a)
390 signal_b = Signal(name + '_b', unit_b)
391 signals.append(signal_a)
392 signals.append(signal_b)
393 names.append(name + '_a')
394 names.append(name + '_b')
395 return (ref, signals, names)
397 def _view_matrix(self, data, names, nports):
398 """ View X parameter matrix, debug only
400 Parameters
401 ----------
402 data: list of list of float
403 The matrix to be viewed
405 names: list of strings
406 The names of the Signals that will be instanciated
408 nports: int
409 The number of ports, i.e. matrix rank
411 Returns
412 -------
413 Nothing
415 for i in xrange(nports):
416 for j in xrange(nports):
417 print names[i * nports * 2 + j * 2 + 1], names[i * nports * 2 + j * 2 + 2],
418 print data[i * nports * 2 + j * 2 + 1], data[i * nports * 2 + j * 2 + 2],
419 print
422 def _process_noise_param(self, np, version):
423 """ Process noise parameter data and returns a dict of Signals
425 Version of format is requested as in version 1 the effective noise
426 resistance is normalized while in version 2 it is not
428 Parameters
429 ----------
430 np: list of list of float
431 The noise parameter data as read
433 version: int
434 Format version of the file
436 Returns
437 -------
438 ret: dict of Signals
439 'minnf': Minimum noise figure
440 'Refl_coef_a'
441 'Refl_coef_b': Source reflection coefficient to realize minimum
442 noise figure
443 'R_neff': Effective noise resistance
445 freq = Signal('Frequency', 'Hz')
446 minnf = Signal('minnf', 'dB')
447 reflcoefa = Signal('Refl_coef_a', 'a.u.')
448 reflcoefb = Signal('Refl_coef_b', 'degrees')
449 effnr = Signal('R_neff', 'a.u.' if version == 1 else 'Ohm')
451 sigs = [freq, minnf, reflcoefa, reflcoefb, effnr]
453 noise_param = [[] for x in xrange(5)]
454 append = [x.append for x in noise_param]
456 for p in xrange(len(np)):
457 for i, a in enumerate(append):
458 a(np[p][i])
460 freq.data = noise_param[0]
461 for i, s in enumerate(sigs[1:]):
462 s.ref = freq
463 s.data = noise_param[i + 1]
465 ret = {'Frequency': freq, 'minnf': minnf,
466 'Refl_coef_a': reflcoefa, 'Refl_coef_b': reflcoefb,
467 'R_neff': effnr}
468 return ret