stdf-reader 0.10.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.

Potentially problematic release.


This version of stdf-reader might be problematic. Click here for more details.

@@ -0,0 +1,148 @@
1
+ #
2
+ # PySTDF - The Pythonic STDF Parser
3
+ # Copyright (C) 2006 Casey Marshall
4
+ #
5
+ # This program is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU General Public License
7
+ # as published by the Free Software Foundation; either version 2
8
+ # of the License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
+ #
19
+
20
+ from pystdf.Pipeline import EventSource
21
+ from pystdf.SummaryStatistics import SummaryStatistics
22
+ from pystdf.V4 import prr, hbr, sbr
23
+
24
+ def ifElse(cond, trueVal, falseVal):
25
+ if cond:
26
+ return trueVal
27
+ else:
28
+ return falseVal
29
+
30
+ class BinSummarizer(EventSource):
31
+
32
+ FLAG_SYNTH = 0x80
33
+ FLAG_FAIL = 0x08
34
+ FLAG_UNKNOWN = 0x02
35
+ FLAG_OVERALL = 0x01
36
+
37
+ def __init__(self):
38
+ EventSource.__init__(self, ['binSummaryReady'])
39
+
40
+ def binSummaryReady(self, dataSource): pass
41
+
42
+ def getHPfFlags(self, row):
43
+ flag = 0
44
+ if row[hbr.HBIN_PF] == 'F':
45
+ flag |= self.FLAG_FAIL
46
+ elif row[hbr.HBIN_PF] != 'P':
47
+ flag |= self.FLAG_UNKNOWN
48
+ return flag
49
+
50
+ def getSPfFlags(self, row):
51
+ flag = 0
52
+ if row[sbr.SBIN_PF] == 'F':
53
+ flag |= self.FLAG_FAIL
54
+ elif row[sbr.SBIN_PF] != 'P':
55
+ flag |= self.FLAG_UNKNOWN
56
+ return flag
57
+
58
+ def getOverallHbins(self):
59
+ return self.overallHbrs.values()
60
+
61
+ def getSiteHbins(self):
62
+ return self.summaryHbrs.values()
63
+
64
+ def getSiteSynthHbins(self):
65
+ for siteBin, info in self.hbinParts.iteritems():
66
+ site, bin = siteBin
67
+ partCount, isPass = info
68
+ if isPass[0]:
69
+ pf = 'P'
70
+ else:
71
+ pf = 'F'
72
+ row = [0, site, bin, partCount[0], pf, None]
73
+ yield row
74
+
75
+ def getOverallSbins(self):
76
+ return self.overallSbrs.values()
77
+
78
+ def getSiteSbins(self):
79
+ return self.summarySbrs.values()
80
+
81
+ def getSiteSynthSbins(self):
82
+ for siteBin, info in self.sbinParts.iteritems():
83
+ site, bin = siteBin
84
+ partCount, isPass = info
85
+ if isPass[0]:
86
+ pf = 'P'
87
+ else:
88
+ pf = 'F'
89
+ row = [0, site, bin, partCount[0], pf, None]
90
+ yield row
91
+
92
+ def before_begin(self, dataSource):
93
+ self.hbinParts = dict()
94
+ self.sbinParts = dict()
95
+ self.summaryHbrs = dict()
96
+ self.summarySbrs = dict()
97
+ self.overallHbrs = dict()
98
+ self.overallSbrs = dict()
99
+
100
+ def before_complete(self, dataSource):
101
+ self.binSummaryReady(dataSource)
102
+
103
+ def before_send(self, dataSource, data):
104
+ table, row = data
105
+ if table.name == prr.name:
106
+ self.onPrr(row)
107
+ elif table.name == hbr.name:
108
+ self.onHbr(row)
109
+ elif table.name == sbr.name:
110
+ self.onSbr(row)
111
+
112
+ def ifElse(cond, trueVal, falseVal):
113
+ if cond:
114
+ return trueVal
115
+ else:
116
+ return falseVal
117
+
118
+ def onPrr(self, row):
119
+ countList, passList = self.hbinParts.setdefault(
120
+ (row[prr.SITE_NUM], row[prr.HARD_BIN]), ([0], [None]))
121
+ countList[0] += 1
122
+ if passList[0] is None:
123
+ passList[0] = ifElse(row[prr.PART_FLG] & 0x08 == 0, 'P', 'F')
124
+ elif passList[0] != ' ':
125
+ if passList[0] != ifElse(row[prr.PART_FLG] & 0x08 == 0, 'P', 'F'):
126
+ passList[0] = ' '
127
+
128
+ countList, passList = self.sbinParts.setdefault(
129
+ (row[prr.SITE_NUM], row[prr.SOFT_BIN]), ([0], [False]))
130
+ countList[0] += 1
131
+ if passList[0] is None:
132
+ passList[0] = ifElse(row[prr.PART_FLG] & 0x08 == 0, 'P', 'F')
133
+ elif passList[0] != ' ':
134
+ if passList[0] != ifElse(row[prr.PART_FLG] & 0x08 == 0, 'P', 'F'):
135
+ passList[0] = ' '
136
+
137
+ def onHbr(self, row):
138
+ if row[hbr.HEAD_NUM] == 255:
139
+ self.overallHbrs[row[hbr.HBIN_NUM]] = row
140
+ else:
141
+ self.summaryHbrs[(row[hbr.SITE_NUM], row[hbr.HBIN_NUM])] = row
142
+
143
+ def onSbr(self, row):
144
+ if row[sbr.HEAD_NUM] == 255:
145
+ self.overallSbrs[row[sbr.SBIN_NUM]] = row
146
+ else:
147
+ self.summarySbrs[(row[sbr.SITE_NUM], row[sbr.SBIN_NUM])] = row
148
+
pystdf/IO.py ADDED
@@ -0,0 +1,382 @@
1
+ #
2
+ # PySTDF - The Pythonic STDF Parser
3
+ # Copyright (C) 2006 Casey Marshall
4
+ #
5
+ # This program is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU General Public License
7
+ # as published by the Free Software Foundation; either version 2
8
+ # of the License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
+ #
19
+ import io
20
+ import sys
21
+
22
+ import struct
23
+ import re
24
+ import chardet
25
+
26
+ from pystdf.Types import *
27
+ from pystdf import V4
28
+
29
+ from pystdf.Pipeline import DataSource
30
+
31
+ def appendFieldParser(fn, action):
32
+ """Append a field parsing function to a record parsing function.
33
+ This is used to build record parsing functions based on the record type specification."""
34
+ def newRecordParser(*args):
35
+ fields = fn(*args)
36
+ try:
37
+ fields.append(action(*args))
38
+ except EndOfRecordException: pass
39
+ return fields
40
+ return newRecordParser
41
+
42
+ class Parser(DataSource):
43
+ def readAndUnpack(self, header, fmt):
44
+ size = struct.calcsize(fmt)
45
+ if (size > header.len):
46
+ self.inp.read(header.len)
47
+ header.len = 0
48
+ raise EndOfRecordException()
49
+ buf = self.inp.read(size)
50
+ if len(buf) == 0:
51
+ self.eof = 1
52
+ raise EofException()
53
+ header.len -= len(buf)
54
+ val,=struct.unpack(self.endian + fmt, buf)
55
+ if isinstance(val,bytes):
56
+ return val.decode("ascii")
57
+ else:
58
+ return val
59
+
60
+ def readAndUnpackDirect(self, fmt):
61
+ size = struct.calcsize(fmt)
62
+ buf = self.inp.read(size)
63
+ if len(buf) == 0:
64
+ self.eof = 1
65
+ raise EofException()
66
+ val,=struct.unpack(self.endian + fmt, buf)
67
+ return val
68
+
69
+ def readField(self, header, stdfFmt):
70
+ return self.readAndUnpack(header, packFormatMap[stdfFmt])
71
+
72
+ def readFieldDirect(self, stdfFmt):
73
+ return self.readAndUnpackDirect(packFormatMap[stdfFmt])
74
+
75
+ def readCn(self, header):
76
+ if header.len == 0:
77
+ raise EndOfRecordException()
78
+ slen = self.readField(header, "U1")
79
+ if slen > header.len:
80
+ self.inp.read(header.len)
81
+ header.len = 0
82
+ raise EndOfRecordException()
83
+ if slen == 0:
84
+ return ""
85
+ buf = self.inp.read(slen);
86
+ if len(buf) == 0:
87
+ self.eof = 1
88
+ raise EofException()
89
+ header.len -= len(buf)
90
+ val,=struct.unpack(str(slen) + "s", buf)
91
+ try:
92
+ return val.decode("ascii")
93
+ except Exception as e:
94
+ # to process when UnicodeDecodeError
95
+ print(e)
96
+ result = chardet.detect(val)
97
+ encoding = result['encoding']
98
+ print("The unknow format is: " + encoding)
99
+ try:
100
+ return val.decode(encoding)
101
+ except UnicodeDecodeError:
102
+ return val.decode("utf-8", errors="replace when UnicodeDecodeError")
103
+
104
+ def readBn(self, header):
105
+ blen = self.readField(header, "U1")
106
+ bn = []
107
+ for i in range(0, blen):
108
+ bn.append(self.readField(header, "B1"))
109
+ return bn
110
+
111
+ def readDn(self, header):
112
+ dbitlen = self.readField(header, "U2")
113
+ dlen = dbitlen / 8
114
+ if dbitlen % 8 > 0:
115
+ dlen+=1
116
+ dn = []
117
+ for i in range(0, int(dlen)):
118
+ dn.append(self.readField(header, "B1"))
119
+ return dn
120
+
121
+ def readVn(self, header):
122
+ vlen = self.readField(header, "U2")
123
+ vn = []
124
+ for i in range(0, vlen):
125
+ fldtype = self.readField(header, "B1")
126
+ if fldtype in self.vnMap:
127
+ vn.append(self.vnMap[fldtype](header))
128
+ return vn
129
+
130
+ def readArray(self, header, indexValue, stdfFmt):
131
+ if (stdfFmt == 'N1'):
132
+ self.readArray(header, indexValue/2+indexValue%2, 'U1')
133
+ return
134
+ arr = []
135
+ for i in range(int(indexValue)):
136
+ arr.append(self.unpackMap[stdfFmt](header, stdfFmt))
137
+ return arr
138
+
139
+ def readHeader(self):
140
+ hdr = RecordHeader()
141
+ hdr.len = self.readFieldDirect("U2")
142
+ hdr.typ = self.readFieldDirect("U1")
143
+ hdr.sub = self.readFieldDirect("U1")
144
+ return hdr
145
+
146
+ def __detectEndian(self):
147
+ self.eof = 0
148
+ header = self.readHeader()
149
+ if header.typ != 0 and header.sub != 10:
150
+ raise InitialSequenceException()
151
+ cpuType = self.readFieldDirect("U1")
152
+ if self.reopen_fn:
153
+ self.inp = self.reopen_fn()
154
+ else:
155
+ self.inp.seek(0)
156
+ if cpuType == 2:
157
+ return '<' #'<'
158
+ else:
159
+ return '>'
160
+
161
+ def header(self, header): pass
162
+
163
+ def parse_records(self, count=0, skipType=""):
164
+ i = 0
165
+ self.eof = 0
166
+ fields = None
167
+
168
+ # Convert skipType to a set for efficient O(1) lookup
169
+ # Support both single string and iterable of strings
170
+ if isinstance(skipType, str):
171
+ skip_types = {skipType} if skipType else set()
172
+ else:
173
+ skip_types = set(skipType) if skipType else set()
174
+
175
+ try:
176
+ while self.eof==0:
177
+ header = self.readHeader()
178
+ self.header(header)
179
+ curRec = self.inp.read(header.len)
180
+ bakup = self.inp # backup current position
181
+ self.inp = io.BytesIO(curRec) # make current position to the beginning of type
182
+
183
+ if (header.typ, header.sub) in self.recordMap:
184
+ recType = self.recordMap[(header.typ, header.sub)]
185
+ recParser = self.recordParsers[(header.typ, header.sub)]
186
+ # add skipType to bypass parse some certain recType
187
+ if recType.name in skip_types:
188
+ self.inp = bakup # restore file position
189
+ continue
190
+
191
+ fields = recParser(self, header, [])
192
+ if len(fields) < len(recType.columnNames):
193
+ fields += [None] * (len(recType.columnNames) - len(fields))
194
+ self.send((recType, fields))
195
+ if header.len > 0:
196
+ print(
197
+ "Warning: Broken header. Unprocessed data left in record of type '%s'. Working around it." % recType.__class__.__name__,
198
+ file=sys.stderr,
199
+ )
200
+ self.inp.read(header.len)
201
+ header.len = 0
202
+ else:
203
+ self.inp.read(header.len)
204
+ self.inp = bakup # restore file position
205
+ if count:
206
+ i += 1
207
+ if i >= count: break
208
+ except EofException: pass
209
+
210
+ def auto_detect_endian(self):
211
+ if self.inp.tell() == 0:
212
+ self.endian = '@'
213
+ self.endian = self.__detectEndian()
214
+
215
+ def parse(self, count=0, skipType=""):
216
+ self.begin()
217
+
218
+ try:
219
+ self.auto_detect_endian()
220
+ self.parse_records(count, skipType)
221
+ self.complete()
222
+ except Exception as exception:
223
+ self.cancel(exception)
224
+ raise
225
+
226
+ def getFieldParser(self, fieldType):
227
+ if (fieldType.startswith("k")):
228
+ fieldIndex, arrayFmt = re.match('k(\d+)([A-Z][a-z0-9]+)', fieldType).groups()
229
+ return lambda self, header, fields: self.readArray(header, fields[int(fieldIndex)], arrayFmt)
230
+ else:
231
+ parseFn = self.unpackMap[fieldType]
232
+ return lambda self, header, fields: parseFn(header, fieldType)
233
+
234
+ def createRecordParser(self, recType):
235
+ # Special handling for STR record with conditional fields
236
+ if hasattr(recType, 'typ') and hasattr(recType, 'sub') and recType.typ == 15 and recType.sub == 30:
237
+ return self.createStrRecordParser(recType)
238
+
239
+ fn = lambda self, header, fields: fields
240
+ for stdfType in recType.fieldStdfTypes:
241
+ fn = appendFieldParser(fn, self.getFieldParser(stdfType))
242
+ return fn
243
+
244
+ def createStrRecordParser(self, recType):
245
+ """
246
+ Custom parser for STR (Scan Test Record) that handles conditional fields.
247
+ MASK_MAP and FAL_MAP fields are only present when FMU_FLG > 0.
248
+ """
249
+ def strParser(self, header, fields):
250
+ # Field indices for STR record (from V4.py)
251
+ # 0-12: Fixed fields up to FMU_FLG
252
+ fixed_field_indices = [
253
+ 0, # CONT_FLG
254
+ 1, # TEST_NUM
255
+ 2, # HEAD_NUM
256
+ 3, # SITE_NUM
257
+ 4, # PSR_REF
258
+ 5, # TEST_FLG
259
+ 6, # LOG_TYP
260
+ 7, # TEST_TXT
261
+ 8, # ALARM_ID
262
+ 9, # PROG_TXT
263
+ 10, # RSLT_TXT
264
+ 11, # Z_VAL
265
+ 12 # FMU_FLG
266
+ ]
267
+
268
+ # Read fixed fields (0-12)
269
+ for i in fixed_field_indices:
270
+ try:
271
+ field_value = self.unpackMap[recType.fieldStdfTypes[i]](header, recType.fieldStdfTypes[i])
272
+ fields.append(field_value)
273
+ except EndOfRecordException:
274
+ break
275
+
276
+ # Check FMU_FLG value (field 12)
277
+ fmu_flg = fields[12] if len(fields) > 12 else 0
278
+
279
+ # Fields 13-14: MASK_MAP and FAL_MAP (conditional, only if FMU_FLG > 0)
280
+ if fmu_flg > 0:
281
+ # Read MASK_MAP (field 13)
282
+ try:
283
+ mask_map = self.readDn(header)
284
+ fields.append(mask_map)
285
+ except EndOfRecordException:
286
+ fields.append(None)
287
+
288
+ # Read FAL_MAP (field 14)
289
+ try:
290
+ fal_map = self.readDn(header)
291
+ fields.append(fal_map)
292
+ except EndOfRecordException:
293
+ fields.append(None)
294
+ else:
295
+ # FMU_FLG = 0, these fields don't exist in data
296
+ # Insert None placeholders to maintain field positions
297
+ fields.append(None) # MASK_MAP
298
+ fields.append(None) # FAL_MAP
299
+
300
+ # Read remaining fields (15 onwards)
301
+ # These are fixed fields, need to handle array fields specially
302
+ for i in range(15, len(recType.fieldStdfTypes)):
303
+ try:
304
+ field_type = recType.fieldStdfTypes[i]
305
+
306
+ # Check if it's an array field (starts with 'k')
307
+ if field_type.startswith('k'):
308
+ match = re.match('k(\d+)([A-Z][a-z0-9]+)', field_type)
309
+ if match:
310
+ field_index = int(match.group(1))
311
+ array_fmt = match.group(2)
312
+
313
+ # For array fields, the index refers to the field position
314
+ # When FMU_FLG = 0, fields 13-14 are None, but indices are preserved
315
+ # So we can directly access fields[field_index]
316
+ try:
317
+ array_value = self.readArray(header, fields[field_index], array_fmt)
318
+ fields.append(array_value)
319
+ except (IndexError, EndOfRecordException):
320
+ fields.append(None)
321
+ else:
322
+ # Regular field
323
+ field_value = self.unpackMap[field_type](header, field_type)
324
+ fields.append(field_value)
325
+ except EndOfRecordException:
326
+ # If we run out of data, append None for remaining fields
327
+ fields.append(None)
328
+
329
+ return fields
330
+
331
+ return strParser
332
+
333
+ def __init__(self, recTypes=V4.records, inp=sys.stdin, reopen_fn=None, endian=None):
334
+ DataSource.__init__(self, ['header']);
335
+ self.eof = 1
336
+ self.recTypes = set(recTypes)
337
+ self.inp = inp
338
+ self.reopen_fn = reopen_fn
339
+ self.endian = endian
340
+
341
+ self.recordMap = dict(
342
+ [ ( (recType.typ, recType.sub), recType )
343
+ for recType in recTypes ])
344
+
345
+ self.unpackMap = {
346
+ "C1": self.readField,
347
+ "B1": self.readField,
348
+ "U1": self.readField,
349
+ "U2": self.readField,
350
+ "U4": self.readField,
351
+ "U8": self.readField,
352
+ "I1": self.readField,
353
+ "I2": self.readField,
354
+ "I4": self.readField,
355
+ "I8": self.readField,
356
+ "R4": self.readField,
357
+ "R8": self.readField,
358
+ "Cn": lambda header, fmt: self.readCn(header),
359
+ "Bn": lambda header, fmt: self.readBn(header),
360
+ "Dn": lambda header, fmt: self.readDn(header),
361
+ "Vn": lambda header, fmt: self.readVn(header)
362
+ }
363
+
364
+ self.recordParsers = dict(
365
+ [ ( (recType.typ, recType.sub), self.createRecordParser(recType) )
366
+ for recType in recTypes ])
367
+
368
+ self.vnMap = {
369
+ 0: lambda header: self.inp.read(header, 1),
370
+ 1: lambda header: self.readField(header, "U1"),
371
+ 2: lambda header: self.readField(header, "U2"),
372
+ 3: lambda header: self.readField(header, "U4"),
373
+ 4: lambda header: self.readField(header, "I1"),
374
+ 5: lambda header: self.readField(header, "I2"),
375
+ 6: lambda header: self.readField(header, "I4"),
376
+ 7: lambda header: self.readField(header, "R4"),
377
+ 8: lambda header: self.readField(header, "R8"),
378
+ 10: lambda header: self.readCn(header),
379
+ 11: lambda header: self.readBn(header),
380
+ 12: lambda header: self.readDn(header),
381
+ 13: lambda header: self.readField(header, "U1")
382
+ }
pystdf/Importer.py ADDED
@@ -0,0 +1,86 @@
1
+ #
2
+ # PySTDF - The Pythonic STDF Parser
3
+ # Copyright (C) 2006 Casey Marshall
4
+ #
5
+ # This program is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU General Public License
7
+ # as published by the Free Software Foundation; either version 2
8
+ # of the License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
+ #
19
+ # Modified: 2017 Minh-Hai Nguyen
20
+ #
21
+
22
+ import numpy as np
23
+ import pandas as pd
24
+ from pystdf.IO import Parser
25
+ from pystdf.Writers import TextWriter
26
+
27
+ class MemoryWriter:
28
+ def __init__(self):
29
+ self.data = []
30
+ def after_send(self, dataSource, data):
31
+ self.data.append(data)
32
+ def write(self,line):
33
+ self.data.append(line)
34
+ def flush(self):
35
+ pass # Do nothing
36
+
37
+ def ImportSTDF(fname):
38
+ with open(fname,'rb') as fin:
39
+ p = Parser(inp=fin)
40
+ storage = MemoryWriter()
41
+ p.addSink(storage)
42
+ p.parse()
43
+ return storage.data
44
+
45
+ def STDF2Text(fname,delimiter='|'):
46
+ """ Convert STDF to a list of text representation
47
+ """
48
+ with open(fname,'rb') as fin:
49
+ p = Parser(inp=fin)
50
+ storage = MemoryWriter()
51
+ p.addSink(TextWriter(storage,delimiter=delimiter))
52
+ p.parse()
53
+ return storage.data
54
+ return None
55
+
56
+ def STDF2Dict(fname):
57
+ """ Convert STDF to a list of dictionary objects
58
+ """
59
+ data = ImportSTDF(fname)
60
+ data_out = []
61
+ for datum in data:
62
+ datum_out = {}
63
+ RecType = datum[0].__class__.__name__.upper()
64
+ datum_out['RecType'] = RecType
65
+ for k,v in zip(datum[0].fieldMap,datum[1]):
66
+ datum_out[k[0]] = v
67
+ data_out.append(datum_out)
68
+ return data_out
69
+
70
+ def STDF2DataFrame(fname):
71
+ """ Convert STDF to a dictionary of DataFrame objects
72
+ """
73
+ data = ImportSTDF(fname)
74
+ BigTable = {}
75
+ for datum in data:
76
+ RecType = datum[0].__class__.__name__.upper()
77
+ if RecType not in BigTable:
78
+ BigTable[RecType] = {}
79
+ Rec = BigTable[RecType]
80
+ for k,v in zip(datum[0].fieldMap,datum[1]):
81
+ if k[0] not in Rec:
82
+ Rec[k[0]] = []
83
+ Rec[k[0]].append(v)
84
+ for k,v in BigTable.items():
85
+ BigTable[k] = pd.DataFrame(v)
86
+ return BigTable