pyibis-ami 7.2.3__py3-none-any.whl → 7.2.5__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.
- {pyibis_ami-7.2.3.dist-info → pyibis_ami-7.2.5.dist-info}/METADATA +133 -134
- pyibis_ami-7.2.5.dist-info/RECORD +27 -0
- {pyibis_ami-7.2.3.dist-info → pyibis_ami-7.2.5.dist-info}/WHEEL +1 -1
- {pyibis_ami-7.2.3.dist-info → pyibis_ami-7.2.5.dist-info}/licenses/LICENSE +8 -8
- pyibisami/IBIS_AMI_Checker.ipynb +1693 -1693
- pyibisami/IBIS_AMI_Tester.ipynb +1457 -1457
- pyibisami/__init__.py +22 -22
- pyibisami/__main__.py +7 -7
- pyibisami/ami/config.py +297 -297
- pyibisami/ami/generic.ami.em +20 -20
- pyibisami/ami/generic.ibs.em +176 -139
- pyibisami/ami/model.py +631 -631
- pyibisami/ami/parameter.py +325 -324
- pyibisami/ami/parser.py +680 -680
- pyibisami/ami/reserved_parameter_names.py +84 -84
- pyibisami/common.py +42 -42
- pyibisami/ibis/file.py +325 -325
- pyibisami/ibis/model.py +399 -399
- pyibisami/ibis/parser.py +525 -525
- pyibisami/tools/run_notebook.py +144 -144
- pyibisami/tools/run_tests.py +273 -273
- pyibisami/tools/test_results.xsl +42 -42
- pyibis_ami-7.2.3.dist-info/RECORD +0 -27
- {pyibis_ami-7.2.3.dist-info → pyibis_ami-7.2.5.dist-info}/entry_points.txt +0 -0
- {pyibis_ami-7.2.3.dist-info → pyibis_ami-7.2.5.dist-info}/top_level.txt +0 -0
pyibisami/ibis/parser.py
CHANGED
@@ -1,525 +1,525 @@
|
|
1
|
-
"""Parse an IBIS model file.
|
2
|
-
|
3
|
-
Original Author: David Banas <capn.freako@gmail.com>
|
4
|
-
|
5
|
-
Original Date: November 1, 2019
|
6
|
-
|
7
|
-
For information regarding the IBIS modeling standard, visit:
|
8
|
-
https://ibis.org/
|
9
|
-
|
10
|
-
Copyright (c) 2019 by David Banas; All rights reserved World wide.
|
11
|
-
"""
|
12
|
-
|
13
|
-
import re
|
14
|
-
|
15
|
-
from parsec import (
|
16
|
-
ParseError,
|
17
|
-
Parser,
|
18
|
-
count,
|
19
|
-
eof,
|
20
|
-
fail_with,
|
21
|
-
generate,
|
22
|
-
letter,
|
23
|
-
many,
|
24
|
-
many1,
|
25
|
-
none_of,
|
26
|
-
one_of,
|
27
|
-
optional,
|
28
|
-
regex,
|
29
|
-
separated,
|
30
|
-
sepBy1,
|
31
|
-
string,
|
32
|
-
times,
|
33
|
-
)
|
34
|
-
|
35
|
-
from pyibisami.ibis.model import Component, Model
|
36
|
-
|
37
|
-
DEBUG = False
|
38
|
-
|
39
|
-
# Parser Definitions
|
40
|
-
|
41
|
-
whitespace = regex(r"\s+", re.MULTILINE)
|
42
|
-
comment = regex(r"\|.*") # To end of line only.
|
43
|
-
ignore = many(whitespace | comment) # None is okay; so, can be used completely safely.
|
44
|
-
|
45
|
-
|
46
|
-
def logf(p: Parser, preStr: str = "") -> Parser:
|
47
|
-
"""
|
48
|
-
Returns parser ``p`` wrapped in a thin shell, which logs any failure at the point of occurence.
|
49
|
-
|
50
|
-
Args:
|
51
|
-
p: The original parser.
|
52
|
-
|
53
|
-
Keyword Args:
|
54
|
-
preStr: A prefix string to use in failure message.
|
55
|
-
(Default = <empty string>)
|
56
|
-
|
57
|
-
Returns:
|
58
|
-
p': The original parser wrapped in a thin failure location shell.
|
59
|
-
"""
|
60
|
-
|
61
|
-
@Parser
|
62
|
-
def fn(txt, ix):
|
63
|
-
res = p(txt, ix)
|
64
|
-
if not res.status:
|
65
|
-
print(
|
66
|
-
f"{preStr}: Expected `{res.expected}` in `{txt[res.index: res.index + 5]}` at {ParseError.loc_info(txt, res.index)}.",
|
67
|
-
flush=True
|
68
|
-
)
|
69
|
-
return res
|
70
|
-
|
71
|
-
return fn
|
72
|
-
|
73
|
-
|
74
|
-
def lexeme(p):
|
75
|
-
"""Lexer for words.
|
76
|
-
|
77
|
-
Skips all ignored characters after word, including newlines.
|
78
|
-
"""
|
79
|
-
return p << ignore
|
80
|
-
|
81
|
-
|
82
|
-
def word(p):
|
83
|
-
"""Line limited word lexer.
|
84
|
-
|
85
|
-
Only skips space after words; dosen't skip comments or newlines.
|
86
|
-
Requires, at least, one white space character after word.
|
87
|
-
"""
|
88
|
-
return p << regex(r"\s+")
|
89
|
-
|
90
|
-
|
91
|
-
@generate("remainder of line")
|
92
|
-
def rest_line():
|
93
|
-
"Parse remainder of line."
|
94
|
-
chars = yield many(none_of("\n\r")) << ignore # So that we still function as a lexeme.
|
95
|
-
return "".join(chars)
|
96
|
-
|
97
|
-
|
98
|
-
skip_line = lexeme(rest_line).result("(Skipped.)")
|
99
|
-
name_only = regex(r"[_a-zA-Z0-9/\.()#-]+")
|
100
|
-
name = word(name_only)
|
101
|
-
symbol = lexeme(regex(r"[a-zA-Z_][^\s()\[\]]*"))
|
102
|
-
true = lexeme(string("True")).result(True)
|
103
|
-
false = lexeme(string("False")).result(False)
|
104
|
-
quoted_string = lexeme(regex(r'"[^"]*"'))
|
105
|
-
skip_keyword = (skip_line >> many(none_of("[") >> skip_line)).result(
|
106
|
-
"(Skipped.)"
|
107
|
-
) # Skip over everything until the next keyword begins.
|
108
|
-
|
109
|
-
IBIS_num_suf = {
|
110
|
-
"T": "e12",
|
111
|
-
"k": "e3",
|
112
|
-
"n": "e-9",
|
113
|
-
"G": "e9",
|
114
|
-
"m": "e-3",
|
115
|
-
"p": "e-12",
|
116
|
-
"M": "e6",
|
117
|
-
"u": "e-6",
|
118
|
-
"f": "e-15",
|
119
|
-
}
|
120
|
-
|
121
|
-
|
122
|
-
@generate("number")
|
123
|
-
def number():
|
124
|
-
"Parse an IBIS numerical value."
|
125
|
-
s = yield (regex(r"[-+]?[0-9]*\.?[0-9]+(([eE][-+]?[0-9]+)|([TknGmpMuf][a-zA-Z]*))?") << many(letter()) << ignore)
|
126
|
-
m = re.search(r"[^\d]+$", s)
|
127
|
-
if m:
|
128
|
-
ix = m.start()
|
129
|
-
c = s[ix]
|
130
|
-
if c in IBIS_num_suf:
|
131
|
-
res = float(s[:ix] + IBIS_num_suf[c])
|
132
|
-
else:
|
133
|
-
raise ParseError("IBIS numerical suffix", s[ix:], ix)
|
134
|
-
else:
|
135
|
-
res = float(s)
|
136
|
-
return res
|
137
|
-
|
138
|
-
|
139
|
-
na = word(string("NA") | string("na")).result(None)
|
140
|
-
|
141
|
-
|
142
|
-
@generate("typminmax")
|
143
|
-
def typminmax():
|
144
|
-
"Parse Typ/Min/Max values."
|
145
|
-
typ = yield number
|
146
|
-
minmax = yield optional(count(number, 2) | count(na, 2).result([]), [])
|
147
|
-
yield ignore # So that ``typminmax`` behaves as a lexeme.
|
148
|
-
res = [typ]
|
149
|
-
res.extend(minmax)
|
150
|
-
return res
|
151
|
-
|
152
|
-
|
153
|
-
vi_line = (number + typminmax) << ignore
|
154
|
-
|
155
|
-
|
156
|
-
@generate("ratio")
|
157
|
-
def ratio():
|
158
|
-
"Parse ratio."
|
159
|
-
[num, den] = yield (separated(number, string("/"), 2, maxt=2, end=False) | na.result([0, 0]))
|
160
|
-
if den:
|
161
|
-
return num / den
|
162
|
-
return None
|
163
|
-
|
164
|
-
|
165
|
-
ramp_line = string("dV/dt_") >> ((string("r").result("rising") | string("f").result("falling")) << ignore) + times(
|
166
|
-
ratio, 1, 3
|
167
|
-
)
|
168
|
-
ex_line = (
|
169
|
-
word(string("Executable"))
|
170
|
-
>> ( # noqa: W503
|
171
|
-
(
|
172
|
-
((string("L") | string("l")) >> string("inux")).result("linux")
|
173
|
-
| ((string("W") | string("w")) >> string("indows")).result("windows") # noqa: W503
|
174
|
-
)
|
175
|
-
<< string("_") # noqa: W503
|
176
|
-
<< many(none_of("_")) # noqa: W503
|
177
|
-
<< string("_") # noqa: W503
|
178
|
-
)
|
179
|
-
+ lexeme(string("32") | string("64")) # noqa: W503
|
180
|
-
+ count(name, 2) # noqa: W503
|
181
|
-
<< ignore # noqa: W503
|
182
|
-
)
|
183
|
-
|
184
|
-
|
185
|
-
def manyTrue(p):
|
186
|
-
"Run a parser multiple times, filtering ``False`` results."
|
187
|
-
|
188
|
-
@generate("manyTrue")
|
189
|
-
def fn():
|
190
|
-
"many(p) >> filter(True)"
|
191
|
-
nodes = yield many(p)
|
192
|
-
res = list(filter(None, nodes))
|
193
|
-
return res
|
194
|
-
|
195
|
-
return fn
|
196
|
-
|
197
|
-
|
198
|
-
def many1True(p):
|
199
|
-
"Run a parser at least once, filtering ``False`` results."
|
200
|
-
|
201
|
-
@generate("many1True")
|
202
|
-
def fn():
|
203
|
-
"many1(p) >> filter(True)"
|
204
|
-
nodes = yield many1(p)
|
205
|
-
res = list(filter(None, nodes))
|
206
|
-
return res
|
207
|
-
|
208
|
-
return fn
|
209
|
-
|
210
|
-
|
211
|
-
# IBIS file parser:
|
212
|
-
|
213
|
-
|
214
|
-
def keyword(kywrd=""):
|
215
|
-
"""Parse an IBIS keyword.
|
216
|
-
|
217
|
-
Keyword Args:
|
218
|
-
kywrd (str): The particular keyword to match; null for any keyword.
|
219
|
-
If provided, *must* be in canonicalized form (i.e. - underscores,
|
220
|
-
no spaces)!
|
221
|
-
|
222
|
-
Returns:
|
223
|
-
Parser: A keyword parser.
|
224
|
-
"""
|
225
|
-
|
226
|
-
@generate("IBIS keyword")
|
227
|
-
def fn():
|
228
|
-
"Parse IBIS keyword."
|
229
|
-
yield regex(r"^\[", re.MULTILINE)
|
230
|
-
wordlets = yield sepBy1(name_only, one_of(" _")) # ``name`` gobbles up trailing space, which we don't want.
|
231
|
-
yield string("]")
|
232
|
-
yield ignore # So that ``keyword`` functions as a lexeme.
|
233
|
-
res = "_".join(wordlets) # Canonicalize to: "<wordlet1>_<wordlet2>_...".
|
234
|
-
if kywrd:
|
235
|
-
if res.lower() == kywrd.lower():
|
236
|
-
return res
|
237
|
-
return fail_with(f"Expecting: {kywrd}; got: {res}.")
|
238
|
-
return res
|
239
|
-
|
240
|
-
return fn
|
241
|
-
|
242
|
-
|
243
|
-
@generate("IBIS parameter")
|
244
|
-
def param():
|
245
|
-
"Parse IBIS parameter."
|
246
|
-
# Parameters must begin with a letter in column 1.
|
247
|
-
pname = yield word(regex(r"^[a-zA-Z]\w*", re.MULTILINE))
|
248
|
-
if DEBUG:
|
249
|
-
print(f"Parsing parameter {pname}...", end="", flush=True)
|
250
|
-
res = yield ((word(string("=")) >> (number | rest_line)) | typminmax | name | rest_line)
|
251
|
-
if DEBUG:
|
252
|
-
print(res, flush=True)
|
253
|
-
yield ignore # So that ``param`` functions as a lexeme.
|
254
|
-
return (pname.lower(), res)
|
255
|
-
|
256
|
-
|
257
|
-
def node(valid_keywords, stop_keywords, debug=False):
|
258
|
-
"""Build a node-specific parser.
|
259
|
-
|
260
|
-
Args:
|
261
|
-
valid_keywords (dict): A dictionary with keys matching those
|
262
|
-
keywords we want parsed. The values are the parsers for
|
263
|
-
those keywords.
|
264
|
-
stop_keywords: Any iterable with primary values (i.e. - those
|
265
|
-
tested by the ``in`` function) matching those keywords we want
|
266
|
-
to stop the parsing of this node and pop us back up the
|
267
|
-
parsing stack.
|
268
|
-
|
269
|
-
Returns:
|
270
|
-
Parser: A parser for this node.
|
271
|
-
|
272
|
-
Notes:
|
273
|
-
1: Any keywords encountered that are _not_ found (via ``in``) in
|
274
|
-
either ``valid_keywords`` or ``stop_keywords`` are ignored.
|
275
|
-
"""
|
276
|
-
|
277
|
-
@generate("kywrd")
|
278
|
-
def kywrd():
|
279
|
-
"Parse keyword syntax."
|
280
|
-
nm = yield keyword()
|
281
|
-
nmL = nm.lower()
|
282
|
-
if debug:
|
283
|
-
print(f"Parsing keyword: [{nm}]...", flush=True)
|
284
|
-
if nmL in valid_keywords:
|
285
|
-
if nmL == "end": # Because ``ibis_file`` expects this to be the last thing it sees,
|
286
|
-
return fail_with("") # we can't consume it here.
|
287
|
-
res = yield logf(valid_keywords[nmL], f"[{nm}]") # Parse the sub-keyword.
|
288
|
-
elif nmL in stop_keywords:
|
289
|
-
return fail_with("") # Stop parsing.
|
290
|
-
else:
|
291
|
-
res = yield skip_keyword
|
292
|
-
yield ignore # So that ``kywrd`` behaves as a lexeme.
|
293
|
-
if debug:
|
294
|
-
print(f"Finished parsing keyword: [{nm}].", flush=True)
|
295
|
-
return (nmL, res)
|
296
|
-
|
297
|
-
return kywrd | param
|
298
|
-
|
299
|
-
|
300
|
-
# Individual IBIS keyword (i.e. - "node") parsers:
|
301
|
-
|
302
|
-
|
303
|
-
# [End]
|
304
|
-
@generate("[End]")
|
305
|
-
def end():
|
306
|
-
"Parse [End]."
|
307
|
-
yield keyword("End")
|
308
|
-
return eof
|
309
|
-
|
310
|
-
|
311
|
-
# [Model]
|
312
|
-
@generate("[Ramp]")
|
313
|
-
def ramp():
|
314
|
-
"Parse [Ramp]."
|
315
|
-
lines = yield count(ramp_line, 2).desc("Two ramp_lines")
|
316
|
-
return dict(lines) # .update(dict(params))
|
317
|
-
|
318
|
-
|
319
|
-
Model_keywords = {
|
320
|
-
"pulldown": many1(vi_line),
|
321
|
-
"pullup": many1(vi_line),
|
322
|
-
"ramp": ramp,
|
323
|
-
"algorithmic_model": many1(ex_line) << keyword("end_algorithmic_model"),
|
324
|
-
"voltage_range": typminmax,
|
325
|
-
"temperature_range": typminmax,
|
326
|
-
"gnd_clamp": many1(vi_line),
|
327
|
-
"power_clamp": many1(vi_line),
|
328
|
-
}
|
329
|
-
|
330
|
-
|
331
|
-
@generate("[Model]")
|
332
|
-
def model():
|
333
|
-
"Parse [Model]."
|
334
|
-
nm = yield name << ignore
|
335
|
-
if DEBUG:
|
336
|
-
print(f"Parsing model: {nm}...", flush=True)
|
337
|
-
res = yield many1(node(Model_keywords, IBIS_keywords, debug=DEBUG))
|
338
|
-
if DEBUG:
|
339
|
-
print(f"[Model] {nm} contains: {dict(res).keys()}", flush=True)
|
340
|
-
try:
|
341
|
-
theModel = Model(dict(res))
|
342
|
-
except LookupError as le:
|
343
|
-
return fail_with(f"[Model] {nm}: {str(le)}")
|
344
|
-
return {nm: theModel}
|
345
|
-
|
346
|
-
|
347
|
-
# [Component]
|
348
|
-
rlc = lexeme(string("R_pin") | string("L_pin") | string("C_pin"))
|
349
|
-
|
350
|
-
|
351
|
-
@generate("[Package]")
|
352
|
-
def package():
|
353
|
-
"Parse package RLC values."
|
354
|
-
rlcs = yield many1(param)
|
355
|
-
if DEBUG:
|
356
|
-
print(f"rlcs: {rlcs}", flush=True)
|
357
|
-
return dict(rlcs)
|
358
|
-
|
359
|
-
|
360
|
-
def pin(rlcs):
|
361
|
-
"Parse indiviual component pin."
|
362
|
-
|
363
|
-
@generate("Component Pin")
|
364
|
-
def fn():
|
365
|
-
"Parse an individual component pin."
|
366
|
-
[nm, sig] = yield count(name, 2)
|
367
|
-
mod = yield name_only
|
368
|
-
rem_line = yield rest_line
|
369
|
-
rlc_vals = optional(count(number, 3), []).parse(rem_line)
|
370
|
-
rlc_dict = {}
|
371
|
-
if rlcs:
|
372
|
-
rlc_dict.update(dict(zip(rlcs, rlc_vals)))
|
373
|
-
return ((nm + "(" + sig + ")"), (mod, rlc_dict))
|
374
|
-
|
375
|
-
return fn
|
376
|
-
|
377
|
-
|
378
|
-
@generate("[Component].[Pin]")
|
379
|
-
def pins():
|
380
|
-
"Parse [Component].[Pin]."
|
381
|
-
|
382
|
-
def filt(x):
|
383
|
-
(_, (mod, _)) = x
|
384
|
-
m = mod.upper()
|
385
|
-
return m not in ("POWER", "GND", "NC")
|
386
|
-
|
387
|
-
yield (lexeme(string("signal_name")) << lexeme(string("model_name")))
|
388
|
-
rlcs = yield optional(count(rlc, 3), [])
|
389
|
-
prs = yield many1(pin(rlcs))
|
390
|
-
prs_filt = list(filter(filt, prs))
|
391
|
-
return dict(prs_filt)
|
392
|
-
|
393
|
-
|
394
|
-
Component_keywords = {
|
395
|
-
"manufacturer": rest_line,
|
396
|
-
"package": package,
|
397
|
-
"pin": pins,
|
398
|
-
"diff_pin": skip_keyword,
|
399
|
-
}
|
400
|
-
|
401
|
-
|
402
|
-
@generate("[Component]")
|
403
|
-
def comp():
|
404
|
-
"Parse [Component]."
|
405
|
-
nm = yield lexeme(name)
|
406
|
-
if DEBUG:
|
407
|
-
print(f"Parsing component: {nm}", flush=True)
|
408
|
-
res = yield many1(node(Component_keywords, IBIS_keywords, debug=DEBUG))
|
409
|
-
try:
|
410
|
-
Component(dict(res))
|
411
|
-
except LookupError as le:
|
412
|
-
return fail_with(f"[Component] {nm}: {str(le)}")
|
413
|
-
except Exception as err: # pylint: disable=broad-exception-caught
|
414
|
-
return fail_with(f"[Component] {nm}: {str(err)}")
|
415
|
-
return {nm: Component(dict(res))}
|
416
|
-
|
417
|
-
|
418
|
-
@generate("[Model Selector]")
|
419
|
-
def modsel():
|
420
|
-
"Parse [Model Selector]."
|
421
|
-
nm = yield name
|
422
|
-
res = yield ignore >> many1(name + rest_line)
|
423
|
-
return {nm: res}
|
424
|
-
|
425
|
-
|
426
|
-
# Note: The following list MUST have a complete set of keys,
|
427
|
-
# in order for the parsing logic to work correctly!
|
428
|
-
IBIS_keywords = [
|
429
|
-
"model",
|
430
|
-
"end",
|
431
|
-
"ibis_ver",
|
432
|
-
"comment_char",
|
433
|
-
"file_name",
|
434
|
-
"file_rev",
|
435
|
-
"date",
|
436
|
-
"source",
|
437
|
-
"notes",
|
438
|
-
"disclaimer",
|
439
|
-
"copyright",
|
440
|
-
"component",
|
441
|
-
"model_selector",
|
442
|
-
"submodel",
|
443
|
-
"external_circuit",
|
444
|
-
"test_data",
|
445
|
-
"test_load",
|
446
|
-
"define_package_model",
|
447
|
-
"interconnect_model_set",
|
448
|
-
]
|
449
|
-
|
450
|
-
IBIS_kywrd_parsers = dict(zip(IBIS_keywords, [skip_keyword] * len(IBIS_keywords)))
|
451
|
-
IBIS_kywrd_parsers.update(
|
452
|
-
{
|
453
|
-
"model": model,
|
454
|
-
"end": end,
|
455
|
-
"ibis_ver": lexeme(number),
|
456
|
-
"file_name": lexeme(name),
|
457
|
-
"file_rev": lexeme(name),
|
458
|
-
"date": rest_line,
|
459
|
-
"component": comp,
|
460
|
-
"model_selector": modsel,
|
461
|
-
}
|
462
|
-
)
|
463
|
-
|
464
|
-
|
465
|
-
@generate("IBIS File")
|
466
|
-
def ibis_file():
|
467
|
-
"Parse IBIS file."
|
468
|
-
res = yield ignore >> many1True(node(IBIS_kywrd_parsers, {}, debug=DEBUG)) << end
|
469
|
-
return res
|
470
|
-
|
471
|
-
|
472
|
-
def parse_ibis_file(ibis_file_contents_str, debug=False):
|
473
|
-
"""Parse the contents of an IBIS file.
|
474
|
-
|
475
|
-
Args:
|
476
|
-
ibis_file_contents_str (str): The contents of the IBIS file, as a single string.
|
477
|
-
|
478
|
-
Keyword Args:
|
479
|
-
debug (bool): Output debugging info to console when true.
|
480
|
-
Default = False
|
481
|
-
|
482
|
-
Example:
|
483
|
-
::
|
484
|
-
|
485
|
-
with open(<ibis_file_name>) as ibis_file:
|
486
|
-
ibis_file_contents_str = ibis_file.read()
|
487
|
-
(err_str, model_dict) = parse_ibis_file(ibis_file_contents_str)
|
488
|
-
|
489
|
-
Returns:
|
490
|
-
(str, dict): A pair containing:
|
491
|
-
|
492
|
-
err_str:
|
493
|
-
A message describing the nature of any parse failure that occured.
|
494
|
-
model_dict:
|
495
|
-
Dictionary containing keyword definitions (empty upon failure).
|
496
|
-
"""
|
497
|
-
|
498
|
-
try:
|
499
|
-
nodes = ibis_file.parse_strict(ibis_file_contents_str) # Parse must consume the entire file.
|
500
|
-
if debug:
|
501
|
-
print("Parsed nodes:\n", nodes, flush=True)
|
502
|
-
except ParseError as pe:
|
503
|
-
return str(pe), {}
|
504
|
-
|
505
|
-
kw_dict = {}
|
506
|
-
components = {}
|
507
|
-
models = {}
|
508
|
-
model_selectors = {}
|
509
|
-
for kw, val in nodes:
|
510
|
-
if kw == "model":
|
511
|
-
models.update(val)
|
512
|
-
elif kw == "component":
|
513
|
-
components.update(val)
|
514
|
-
elif kw == "model_selector":
|
515
|
-
model_selectors.update(val)
|
516
|
-
else:
|
517
|
-
kw_dict.update({kw: val})
|
518
|
-
kw_dict.update(
|
519
|
-
{
|
520
|
-
"components": components,
|
521
|
-
"models": models,
|
522
|
-
"model_selectors": model_selectors,
|
523
|
-
}
|
524
|
-
)
|
525
|
-
return "Success!", kw_dict
|
1
|
+
"""Parse an IBIS model file.
|
2
|
+
|
3
|
+
Original Author: David Banas <capn.freako@gmail.com>
|
4
|
+
|
5
|
+
Original Date: November 1, 2019
|
6
|
+
|
7
|
+
For information regarding the IBIS modeling standard, visit:
|
8
|
+
https://ibis.org/
|
9
|
+
|
10
|
+
Copyright (c) 2019 by David Banas; All rights reserved World wide.
|
11
|
+
"""
|
12
|
+
|
13
|
+
import re
|
14
|
+
|
15
|
+
from parsec import (
|
16
|
+
ParseError,
|
17
|
+
Parser,
|
18
|
+
count,
|
19
|
+
eof,
|
20
|
+
fail_with,
|
21
|
+
generate,
|
22
|
+
letter,
|
23
|
+
many,
|
24
|
+
many1,
|
25
|
+
none_of,
|
26
|
+
one_of,
|
27
|
+
optional,
|
28
|
+
regex,
|
29
|
+
separated,
|
30
|
+
sepBy1,
|
31
|
+
string,
|
32
|
+
times,
|
33
|
+
)
|
34
|
+
|
35
|
+
from pyibisami.ibis.model import Component, Model
|
36
|
+
|
37
|
+
DEBUG = False
|
38
|
+
|
39
|
+
# Parser Definitions
|
40
|
+
|
41
|
+
whitespace = regex(r"\s+", re.MULTILINE)
|
42
|
+
comment = regex(r"\|.*") # To end of line only.
|
43
|
+
ignore = many(whitespace | comment) # None is okay; so, can be used completely safely.
|
44
|
+
|
45
|
+
|
46
|
+
def logf(p: Parser, preStr: str = "") -> Parser:
|
47
|
+
"""
|
48
|
+
Returns parser ``p`` wrapped in a thin shell, which logs any failure at the point of occurence.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
p: The original parser.
|
52
|
+
|
53
|
+
Keyword Args:
|
54
|
+
preStr: A prefix string to use in failure message.
|
55
|
+
(Default = <empty string>)
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
p': The original parser wrapped in a thin failure location shell.
|
59
|
+
"""
|
60
|
+
|
61
|
+
@Parser
|
62
|
+
def fn(txt, ix):
|
63
|
+
res = p(txt, ix)
|
64
|
+
if not res.status:
|
65
|
+
print(
|
66
|
+
f"{preStr}: Expected `{res.expected}` in `{txt[res.index: res.index + 5]}` at {ParseError.loc_info(txt, res.index)}.",
|
67
|
+
flush=True
|
68
|
+
)
|
69
|
+
return res
|
70
|
+
|
71
|
+
return fn
|
72
|
+
|
73
|
+
|
74
|
+
def lexeme(p):
|
75
|
+
"""Lexer for words.
|
76
|
+
|
77
|
+
Skips all ignored characters after word, including newlines.
|
78
|
+
"""
|
79
|
+
return p << ignore
|
80
|
+
|
81
|
+
|
82
|
+
def word(p):
|
83
|
+
"""Line limited word lexer.
|
84
|
+
|
85
|
+
Only skips space after words; dosen't skip comments or newlines.
|
86
|
+
Requires, at least, one white space character after word.
|
87
|
+
"""
|
88
|
+
return p << regex(r"\s+")
|
89
|
+
|
90
|
+
|
91
|
+
@generate("remainder of line")
|
92
|
+
def rest_line():
|
93
|
+
"Parse remainder of line."
|
94
|
+
chars = yield many(none_of("\n\r")) << ignore # So that we still function as a lexeme.
|
95
|
+
return "".join(chars)
|
96
|
+
|
97
|
+
|
98
|
+
skip_line = lexeme(rest_line).result("(Skipped.)")
|
99
|
+
name_only = regex(r"[_a-zA-Z0-9/\.()#-]+")
|
100
|
+
name = word(name_only)
|
101
|
+
symbol = lexeme(regex(r"[a-zA-Z_][^\s()\[\]]*"))
|
102
|
+
true = lexeme(string("True")).result(True)
|
103
|
+
false = lexeme(string("False")).result(False)
|
104
|
+
quoted_string = lexeme(regex(r'"[^"]*"'))
|
105
|
+
skip_keyword = (skip_line >> many(none_of("[") >> skip_line)).result(
|
106
|
+
"(Skipped.)"
|
107
|
+
) # Skip over everything until the next keyword begins.
|
108
|
+
|
109
|
+
IBIS_num_suf = {
|
110
|
+
"T": "e12",
|
111
|
+
"k": "e3",
|
112
|
+
"n": "e-9",
|
113
|
+
"G": "e9",
|
114
|
+
"m": "e-3",
|
115
|
+
"p": "e-12",
|
116
|
+
"M": "e6",
|
117
|
+
"u": "e-6",
|
118
|
+
"f": "e-15",
|
119
|
+
}
|
120
|
+
|
121
|
+
|
122
|
+
@generate("number")
|
123
|
+
def number():
|
124
|
+
"Parse an IBIS numerical value."
|
125
|
+
s = yield (regex(r"[-+]?[0-9]*\.?[0-9]+(([eE][-+]?[0-9]+)|([TknGmpMuf][a-zA-Z]*))?") << many(letter()) << ignore)
|
126
|
+
m = re.search(r"[^\d]+$", s)
|
127
|
+
if m:
|
128
|
+
ix = m.start()
|
129
|
+
c = s[ix]
|
130
|
+
if c in IBIS_num_suf:
|
131
|
+
res = float(s[:ix] + IBIS_num_suf[c])
|
132
|
+
else:
|
133
|
+
raise ParseError("IBIS numerical suffix", s[ix:], ix)
|
134
|
+
else:
|
135
|
+
res = float(s)
|
136
|
+
return res
|
137
|
+
|
138
|
+
|
139
|
+
na = word(string("NA") | string("na")).result(None)
|
140
|
+
|
141
|
+
|
142
|
+
@generate("typminmax")
|
143
|
+
def typminmax():
|
144
|
+
"Parse Typ/Min/Max values."
|
145
|
+
typ = yield number
|
146
|
+
minmax = yield optional(count(number, 2) | count(na, 2).result([]), [])
|
147
|
+
yield ignore # So that ``typminmax`` behaves as a lexeme.
|
148
|
+
res = [typ]
|
149
|
+
res.extend(minmax)
|
150
|
+
return res
|
151
|
+
|
152
|
+
|
153
|
+
vi_line = (number + typminmax) << ignore
|
154
|
+
|
155
|
+
|
156
|
+
@generate("ratio")
|
157
|
+
def ratio():
|
158
|
+
"Parse ratio."
|
159
|
+
[num, den] = yield (separated(number, string("/"), 2, maxt=2, end=False) | na.result([0, 0]))
|
160
|
+
if den:
|
161
|
+
return num / den
|
162
|
+
return None
|
163
|
+
|
164
|
+
|
165
|
+
ramp_line = string("dV/dt_") >> ((string("r").result("rising") | string("f").result("falling")) << ignore) + times(
|
166
|
+
ratio, 1, 3
|
167
|
+
)
|
168
|
+
ex_line = (
|
169
|
+
word(string("Executable"))
|
170
|
+
>> ( # noqa: W503
|
171
|
+
(
|
172
|
+
((string("L") | string("l")) >> string("inux")).result("linux")
|
173
|
+
| ((string("W") | string("w")) >> string("indows")).result("windows") # noqa: W503
|
174
|
+
)
|
175
|
+
<< string("_") # noqa: W503
|
176
|
+
<< many(none_of("_")) # noqa: W503
|
177
|
+
<< string("_") # noqa: W503
|
178
|
+
)
|
179
|
+
+ lexeme(string("32") | string("64")) # noqa: W503
|
180
|
+
+ count(name, 2) # noqa: W503
|
181
|
+
<< ignore # noqa: W503
|
182
|
+
)
|
183
|
+
|
184
|
+
|
185
|
+
def manyTrue(p):
|
186
|
+
"Run a parser multiple times, filtering ``False`` results."
|
187
|
+
|
188
|
+
@generate("manyTrue")
|
189
|
+
def fn():
|
190
|
+
"many(p) >> filter(True)"
|
191
|
+
nodes = yield many(p)
|
192
|
+
res = list(filter(None, nodes))
|
193
|
+
return res
|
194
|
+
|
195
|
+
return fn
|
196
|
+
|
197
|
+
|
198
|
+
def many1True(p):
|
199
|
+
"Run a parser at least once, filtering ``False`` results."
|
200
|
+
|
201
|
+
@generate("many1True")
|
202
|
+
def fn():
|
203
|
+
"many1(p) >> filter(True)"
|
204
|
+
nodes = yield many1(p)
|
205
|
+
res = list(filter(None, nodes))
|
206
|
+
return res
|
207
|
+
|
208
|
+
return fn
|
209
|
+
|
210
|
+
|
211
|
+
# IBIS file parser:
|
212
|
+
|
213
|
+
|
214
|
+
def keyword(kywrd=""):
|
215
|
+
"""Parse an IBIS keyword.
|
216
|
+
|
217
|
+
Keyword Args:
|
218
|
+
kywrd (str): The particular keyword to match; null for any keyword.
|
219
|
+
If provided, *must* be in canonicalized form (i.e. - underscores,
|
220
|
+
no spaces)!
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
Parser: A keyword parser.
|
224
|
+
"""
|
225
|
+
|
226
|
+
@generate("IBIS keyword")
|
227
|
+
def fn():
|
228
|
+
"Parse IBIS keyword."
|
229
|
+
yield regex(r"^\[", re.MULTILINE)
|
230
|
+
wordlets = yield sepBy1(name_only, one_of(" _")) # ``name`` gobbles up trailing space, which we don't want.
|
231
|
+
yield string("]")
|
232
|
+
yield ignore # So that ``keyword`` functions as a lexeme.
|
233
|
+
res = "_".join(wordlets) # Canonicalize to: "<wordlet1>_<wordlet2>_...".
|
234
|
+
if kywrd:
|
235
|
+
if res.lower() == kywrd.lower():
|
236
|
+
return res
|
237
|
+
return fail_with(f"Expecting: {kywrd}; got: {res}.")
|
238
|
+
return res
|
239
|
+
|
240
|
+
return fn
|
241
|
+
|
242
|
+
|
243
|
+
@generate("IBIS parameter")
|
244
|
+
def param():
|
245
|
+
"Parse IBIS parameter."
|
246
|
+
# Parameters must begin with a letter in column 1.
|
247
|
+
pname = yield word(regex(r"^[a-zA-Z]\w*", re.MULTILINE))
|
248
|
+
if DEBUG:
|
249
|
+
print(f"Parsing parameter {pname}...", end="", flush=True)
|
250
|
+
res = yield ((word(string("=")) >> (number | rest_line)) | typminmax | name | rest_line)
|
251
|
+
if DEBUG:
|
252
|
+
print(res, flush=True)
|
253
|
+
yield ignore # So that ``param`` functions as a lexeme.
|
254
|
+
return (pname.lower(), res)
|
255
|
+
|
256
|
+
|
257
|
+
def node(valid_keywords, stop_keywords, debug=False):
|
258
|
+
"""Build a node-specific parser.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
valid_keywords (dict): A dictionary with keys matching those
|
262
|
+
keywords we want parsed. The values are the parsers for
|
263
|
+
those keywords.
|
264
|
+
stop_keywords: Any iterable with primary values (i.e. - those
|
265
|
+
tested by the ``in`` function) matching those keywords we want
|
266
|
+
to stop the parsing of this node and pop us back up the
|
267
|
+
parsing stack.
|
268
|
+
|
269
|
+
Returns:
|
270
|
+
Parser: A parser for this node.
|
271
|
+
|
272
|
+
Notes:
|
273
|
+
1: Any keywords encountered that are _not_ found (via ``in``) in
|
274
|
+
either ``valid_keywords`` or ``stop_keywords`` are ignored.
|
275
|
+
"""
|
276
|
+
|
277
|
+
@generate("kywrd")
|
278
|
+
def kywrd():
|
279
|
+
"Parse keyword syntax."
|
280
|
+
nm = yield keyword()
|
281
|
+
nmL = nm.lower()
|
282
|
+
if debug:
|
283
|
+
print(f"Parsing keyword: [{nm}]...", flush=True)
|
284
|
+
if nmL in valid_keywords:
|
285
|
+
if nmL == "end": # Because ``ibis_file`` expects this to be the last thing it sees,
|
286
|
+
return fail_with("") # we can't consume it here.
|
287
|
+
res = yield logf(valid_keywords[nmL], f"[{nm}]") # Parse the sub-keyword.
|
288
|
+
elif nmL in stop_keywords:
|
289
|
+
return fail_with("") # Stop parsing.
|
290
|
+
else:
|
291
|
+
res = yield skip_keyword
|
292
|
+
yield ignore # So that ``kywrd`` behaves as a lexeme.
|
293
|
+
if debug:
|
294
|
+
print(f"Finished parsing keyword: [{nm}].", flush=True)
|
295
|
+
return (nmL, res)
|
296
|
+
|
297
|
+
return kywrd | param
|
298
|
+
|
299
|
+
|
300
|
+
# Individual IBIS keyword (i.e. - "node") parsers:
|
301
|
+
|
302
|
+
|
303
|
+
# [End]
|
304
|
+
@generate("[End]")
|
305
|
+
def end():
|
306
|
+
"Parse [End]."
|
307
|
+
yield keyword("End")
|
308
|
+
return eof
|
309
|
+
|
310
|
+
|
311
|
+
# [Model]
|
312
|
+
@generate("[Ramp]")
|
313
|
+
def ramp():
|
314
|
+
"Parse [Ramp]."
|
315
|
+
lines = yield count(ramp_line, 2).desc("Two ramp_lines")
|
316
|
+
return dict(lines) # .update(dict(params))
|
317
|
+
|
318
|
+
|
319
|
+
Model_keywords = {
|
320
|
+
"pulldown": many1(vi_line),
|
321
|
+
"pullup": many1(vi_line),
|
322
|
+
"ramp": ramp,
|
323
|
+
"algorithmic_model": many1(ex_line) << keyword("end_algorithmic_model"),
|
324
|
+
"voltage_range": typminmax,
|
325
|
+
"temperature_range": typminmax,
|
326
|
+
"gnd_clamp": many1(vi_line),
|
327
|
+
"power_clamp": many1(vi_line),
|
328
|
+
}
|
329
|
+
|
330
|
+
|
331
|
+
@generate("[Model]")
|
332
|
+
def model():
|
333
|
+
"Parse [Model]."
|
334
|
+
nm = yield name << ignore
|
335
|
+
if DEBUG:
|
336
|
+
print(f"Parsing model: {nm}...", flush=True)
|
337
|
+
res = yield many1(node(Model_keywords, IBIS_keywords, debug=DEBUG))
|
338
|
+
if DEBUG:
|
339
|
+
print(f"[Model] {nm} contains: {dict(res).keys()}", flush=True)
|
340
|
+
try:
|
341
|
+
theModel = Model(dict(res))
|
342
|
+
except LookupError as le:
|
343
|
+
return fail_with(f"[Model] {nm}: {str(le)}")
|
344
|
+
return {nm: theModel}
|
345
|
+
|
346
|
+
|
347
|
+
# [Component]
|
348
|
+
rlc = lexeme(string("R_pin") | string("L_pin") | string("C_pin"))
|
349
|
+
|
350
|
+
|
351
|
+
@generate("[Package]")
|
352
|
+
def package():
|
353
|
+
"Parse package RLC values."
|
354
|
+
rlcs = yield many1(param)
|
355
|
+
if DEBUG:
|
356
|
+
print(f"rlcs: {rlcs}", flush=True)
|
357
|
+
return dict(rlcs)
|
358
|
+
|
359
|
+
|
360
|
+
def pin(rlcs):
|
361
|
+
"Parse indiviual component pin."
|
362
|
+
|
363
|
+
@generate("Component Pin")
|
364
|
+
def fn():
|
365
|
+
"Parse an individual component pin."
|
366
|
+
[nm, sig] = yield count(name, 2)
|
367
|
+
mod = yield name_only
|
368
|
+
rem_line = yield rest_line
|
369
|
+
rlc_vals = optional(count(number, 3), []).parse(rem_line)
|
370
|
+
rlc_dict = {}
|
371
|
+
if rlcs:
|
372
|
+
rlc_dict.update(dict(zip(rlcs, rlc_vals)))
|
373
|
+
return ((nm + "(" + sig + ")"), (mod, rlc_dict))
|
374
|
+
|
375
|
+
return fn
|
376
|
+
|
377
|
+
|
378
|
+
@generate("[Component].[Pin]")
|
379
|
+
def pins():
|
380
|
+
"Parse [Component].[Pin]."
|
381
|
+
|
382
|
+
def filt(x):
|
383
|
+
(_, (mod, _)) = x
|
384
|
+
m = mod.upper()
|
385
|
+
return m not in ("POWER", "GND", "NC")
|
386
|
+
|
387
|
+
yield (lexeme(string("signal_name")) << lexeme(string("model_name")))
|
388
|
+
rlcs = yield optional(count(rlc, 3), [])
|
389
|
+
prs = yield many1(pin(rlcs))
|
390
|
+
prs_filt = list(filter(filt, prs))
|
391
|
+
return dict(prs_filt)
|
392
|
+
|
393
|
+
|
394
|
+
Component_keywords = {
|
395
|
+
"manufacturer": rest_line,
|
396
|
+
"package": package,
|
397
|
+
"pin": pins,
|
398
|
+
"diff_pin": skip_keyword,
|
399
|
+
}
|
400
|
+
|
401
|
+
|
402
|
+
@generate("[Component]")
|
403
|
+
def comp():
|
404
|
+
"Parse [Component]."
|
405
|
+
nm = yield lexeme(name)
|
406
|
+
if DEBUG:
|
407
|
+
print(f"Parsing component: {nm}", flush=True)
|
408
|
+
res = yield many1(node(Component_keywords, IBIS_keywords, debug=DEBUG))
|
409
|
+
try:
|
410
|
+
Component(dict(res))
|
411
|
+
except LookupError as le:
|
412
|
+
return fail_with(f"[Component] {nm}: {str(le)}")
|
413
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
414
|
+
return fail_with(f"[Component] {nm}: {str(err)}")
|
415
|
+
return {nm: Component(dict(res))}
|
416
|
+
|
417
|
+
|
418
|
+
@generate("[Model Selector]")
|
419
|
+
def modsel():
|
420
|
+
"Parse [Model Selector]."
|
421
|
+
nm = yield name
|
422
|
+
res = yield ignore >> many1(name + rest_line)
|
423
|
+
return {nm: res}
|
424
|
+
|
425
|
+
|
426
|
+
# Note: The following list MUST have a complete set of keys,
|
427
|
+
# in order for the parsing logic to work correctly!
|
428
|
+
IBIS_keywords = [
|
429
|
+
"model",
|
430
|
+
"end",
|
431
|
+
"ibis_ver",
|
432
|
+
"comment_char",
|
433
|
+
"file_name",
|
434
|
+
"file_rev",
|
435
|
+
"date",
|
436
|
+
"source",
|
437
|
+
"notes",
|
438
|
+
"disclaimer",
|
439
|
+
"copyright",
|
440
|
+
"component",
|
441
|
+
"model_selector",
|
442
|
+
"submodel",
|
443
|
+
"external_circuit",
|
444
|
+
"test_data",
|
445
|
+
"test_load",
|
446
|
+
"define_package_model",
|
447
|
+
"interconnect_model_set",
|
448
|
+
]
|
449
|
+
|
450
|
+
IBIS_kywrd_parsers = dict(zip(IBIS_keywords, [skip_keyword] * len(IBIS_keywords)))
|
451
|
+
IBIS_kywrd_parsers.update(
|
452
|
+
{
|
453
|
+
"model": model,
|
454
|
+
"end": end,
|
455
|
+
"ibis_ver": lexeme(number),
|
456
|
+
"file_name": lexeme(name),
|
457
|
+
"file_rev": lexeme(name),
|
458
|
+
"date": rest_line,
|
459
|
+
"component": comp,
|
460
|
+
"model_selector": modsel,
|
461
|
+
}
|
462
|
+
)
|
463
|
+
|
464
|
+
|
465
|
+
@generate("IBIS File")
|
466
|
+
def ibis_file():
|
467
|
+
"Parse IBIS file."
|
468
|
+
res = yield ignore >> many1True(node(IBIS_kywrd_parsers, {}, debug=DEBUG)) << end
|
469
|
+
return res
|
470
|
+
|
471
|
+
|
472
|
+
def parse_ibis_file(ibis_file_contents_str, debug=False):
|
473
|
+
"""Parse the contents of an IBIS file.
|
474
|
+
|
475
|
+
Args:
|
476
|
+
ibis_file_contents_str (str): The contents of the IBIS file, as a single string.
|
477
|
+
|
478
|
+
Keyword Args:
|
479
|
+
debug (bool): Output debugging info to console when true.
|
480
|
+
Default = False
|
481
|
+
|
482
|
+
Example:
|
483
|
+
::
|
484
|
+
|
485
|
+
with open(<ibis_file_name>) as ibis_file:
|
486
|
+
ibis_file_contents_str = ibis_file.read()
|
487
|
+
(err_str, model_dict) = parse_ibis_file(ibis_file_contents_str)
|
488
|
+
|
489
|
+
Returns:
|
490
|
+
(str, dict): A pair containing:
|
491
|
+
|
492
|
+
err_str:
|
493
|
+
A message describing the nature of any parse failure that occured.
|
494
|
+
model_dict:
|
495
|
+
Dictionary containing keyword definitions (empty upon failure).
|
496
|
+
"""
|
497
|
+
|
498
|
+
try:
|
499
|
+
nodes = ibis_file.parse_strict(ibis_file_contents_str) # Parse must consume the entire file.
|
500
|
+
if debug:
|
501
|
+
print("Parsed nodes:\n", nodes, flush=True)
|
502
|
+
except ParseError as pe:
|
503
|
+
return str(pe), {}
|
504
|
+
|
505
|
+
kw_dict = {}
|
506
|
+
components = {}
|
507
|
+
models = {}
|
508
|
+
model_selectors = {}
|
509
|
+
for kw, val in nodes:
|
510
|
+
if kw == "model":
|
511
|
+
models.update(val)
|
512
|
+
elif kw == "component":
|
513
|
+
components.update(val)
|
514
|
+
elif kw == "model_selector":
|
515
|
+
model_selectors.update(val)
|
516
|
+
else:
|
517
|
+
kw_dict.update({kw: val})
|
518
|
+
kw_dict.update(
|
519
|
+
{
|
520
|
+
"components": components,
|
521
|
+
"models": models,
|
522
|
+
"model_selectors": model_selectors,
|
523
|
+
}
|
524
|
+
)
|
525
|
+
return "Success!", kw_dict
|