1 from __future__
import with_statement
4 from oscopy
import Signal
5 from reader
import Reader
, ReadError
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
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}
34 """ Search for the option line (starting with '#') and returns result
35 of processing this line.
40 Path to the file to test
45 True if the file can be handled by this reader
53 line
= f
.readline().strip()
54 if not line
or line
.startswith('!'):
56 elif line
.startswith('#'):
57 return (self
._process
_option
(line
) is not None)
58 elif line
.startswith('['):
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.
81 The list of Signals read from the file
86 In case of invalid path or unsupported file format
88 options
= self
._process
_option
('#')
90 with
open(self
._fn
) as f
:
93 line
= f
.readline().strip()
96 if line
.startswith('!'):
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():
107 return self
._read
_signals
_v
1(f
, options
, len(line
.split()))
109 return self
._read
_signals
_v
2(f
, options
)
111 raise NotImplementedError(_('touchstone_reader: format version %s not supported' % version
))
113 def _process_option(self
, line
):
114 """ Parse the option line
119 the line containing the options
124 The list of parameters extracted from option line
126 The frequency multiplier, e.g. 1e9 for GHz
128 The parameter measured, e.g. 'S' for S-parameter
130 The file format, e.g. 'MA' for Magnitude-Angle
132 The value of reference resistance
134 options
= {'freq_mult': 1e9
, 'param': 'S', 'format': 'MA', 'ref': 50}
135 opts
= line
.lstrip('#').upper().split()
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
147 options
['ref'] = float(opt
)
149 return None #Keyword not recognized
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']
160 The file object (already opened) to read data from
163 Parameters from option line. Used here: 'format', 'param', 'freq_mult'
166 Number of word on first line found with first character being a digit
167 assumed to be the first data line
171 self._signals: dict of Signals
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)
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)
186 raise ReadError(_('touchstone_reader: unknown number of ports'))
188 # Instanciate signals
189 (ref
, signals
, names
) = self
._instanciate
_signals
(nports
, options
)
192 nparams
= len(signals
)
193 data
= [[] for x
in xrange(len(signals
))]
194 append
= [x
.append
for x
in data
]
198 np_append
= noise_param
.append
200 if not len(line
) or line
.startswith('!') or line
.startswith('#'):
202 elif len(line
.split()) == 5:
203 # A line of noise parameter data
204 np_append([float(x
) for x
in line
.split()])
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
211 append
[offset
+ i
](float(val
))
214 # line is not finished, remaining parameters on next line
221 ref
.data
= ref
.data
* options
['freq_mult']
222 for i
, s
in enumerate(signals
[1:]):
225 self
._signals
= dict(zip(names
[1:], signals
[1:]))
227 self
._info
['noise_param'] = self
._process
_noise
_param
(noise_param
, 1)
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.
242 The file object (already opened) to read data from
245 Parameters from option line. Used here: 'format', 'param', 'freq_mult'
248 Number of word on first line found with first character being a digit
249 assumed to be the first data line
253 self._signals: dict of Signals
258 unkown string, unknown keyword or argument, excess of reference values
264 np_append
= noise_param
.append
266 if not len(line
) or line
.startswith('!') or line
.startswith('#'):
267 # Blank line or comment
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())
275 raise ReadError(_('touchstone_reader: unrecognized \'%s\'') % line
)
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
285 (ref
, signals
, names
) = self
._instanciate
_signals
(nports
,
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()
292 elif kw
.lower() == 'noise data':
293 # Next line will be noise data
296 elif kw
.lower() == 'end':
297 # No more data to read
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
)
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'
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
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
330 if len(tmp
) > len(self
._info
['reference']):
331 raise ReadError(_('touchstone_reader: excess references found \'%s\'') % line
)
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]
347 ref
.data
= ref
.data
* options
['freq_mult']
348 for i
, s
in enumerate(signals
[1:]):
351 self
._signals
= dict(zip(names
[1:], signals
[1:]))
353 self
._info
['noise_param'] = self
._process
_noise
_param
(noise_param
, 2)
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'.
368 The parameter stored in file (e.g. S, Z, H...)
376 signals: list of Signals
379 names: list of strings
380 Names of the Signals instanciated, same order as 'signals'
382 ref
= Signal('Frequency', 'Hz')
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
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
409 The number of ports, i.e. matrix rank
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],
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
430 np: list of list of float
431 The noise parameter data as read
434 Format version of the file
439 'minnf': Minimum noise figure
441 'Refl_coef_b': Source reflection coefficient to realize minimum
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
):
460 freq
.data
= noise_param
[0]
461 for i
, s
in enumerate(sigs
[1:]):
463 s
.data
= noise_param
[i
+ 1]
465 ret
= {'Frequency': freq
, 'minnf': minnf
,
466 'Refl_coef_a': reflcoefa
, 'Refl_coef_b': reflcoefb
,