PyIBIS-AMI 10.0.0__tar.gz → 10.0.1__tar.gz

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.
Files changed (78) hide show
  1. {pyibis_ami-10.0.0/src/pyibis_ami.egg-info → pyibis_ami-10.0.1}/PKG-INFO +1 -2
  2. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/docs/source/conf.py +1 -1
  3. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/docs/source/index.rst +2 -1
  4. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/pyproject.toml +2 -3
  5. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/requirements.txt +1 -1
  6. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1/src/pyibis_ami.egg-info}/PKG-INFO +1 -2
  7. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibis_ami.egg-info/requires.txt +0 -1
  8. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ami/parameter.py +199 -195
  9. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ibis/model.py +43 -2
  10. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ibis/parser.py +27 -0
  11. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/CHANGELOG.md +0 -0
  12. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/LICENSE +0 -0
  13. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/MANIFEST.in +0 -0
  14. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/README.md +0 -0
  15. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/docs/source/ami.rst +0 -0
  16. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/docs/source/gui.rst +0 -0
  17. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/docs/source/ibis.rst +0 -0
  18. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/docs/source/install.rst +0 -0
  19. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/docs/source/modules.rst +0 -0
  20. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/docs/source/tools.rst +0 -0
  21. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/all_taps.run +0 -0
  22. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/bandwidth.run +0 -0
  23. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/dummy.em +0 -0
  24. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/examples.md +0 -0
  25. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/freq_resp.em +0 -0
  26. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/impulse.em +0 -0
  27. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/impulse_response_8ma.txt +0 -0
  28. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/mode.run +0 -0
  29. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/stratix4_ami_tx_runs/all_taps.run +0 -0
  30. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/stratix4_ami_tx_runs/test_results.xml +0 -0
  31. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/examples/test_results/test_results.xsl +0 -0
  32. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/setup.cfg +0 -0
  33. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibis_ami.egg-info/SOURCES.txt +0 -0
  34. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibis_ami.egg-info/dependency_links.txt +0 -0
  35. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibis_ami.egg-info/entry_points.txt +0 -0
  36. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibis_ami.egg-info/top_level.txt +0 -0
  37. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/IBIS_AMI_Checker.ipynb +0 -0
  38. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/IBIS_AMI_Tester.ipynb +0 -0
  39. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/__init__.py +0 -0
  40. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/__main__.py +0 -0
  41. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ami/__init__.py +0 -0
  42. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ami/config.py +0 -0
  43. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ami/configurator.py +0 -0
  44. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ami/generic.ami.em +0 -0
  45. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ami/generic.ibs.em +0 -0
  46. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ami/model.py +0 -0
  47. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ami/parser.py +0 -0
  48. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ami/reserved_parameter_names.py +0 -0
  49. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/cli_logging.py +0 -0
  50. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/common.py +0 -0
  51. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/gui/__init__.py +0 -0
  52. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/gui/ami_config.py +0 -0
  53. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/gui/ibis.py +0 -0
  54. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ibis/__init__.py +0 -0
  55. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ibis/file.py +0 -0
  56. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/ibis/iv.py +0 -0
  57. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/report/__init__.py +0 -0
  58. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/report/ami_tests.py +0 -0
  59. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/report/ami_tests_helpers.py +0 -0
  60. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/report/ibis_file_tests.py +0 -0
  61. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/report/pdf.py +0 -0
  62. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/report/plot.py +0 -0
  63. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/report/test_models.py +0 -0
  64. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/testing/__init__.py +0 -0
  65. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/testing/ami_test_config.py +0 -0
  66. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/testing/test_defs.py +0 -0
  67. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/testing/util.py +0 -0
  68. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/tools/__init__.py +0 -0
  69. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/tools/run_notebook.py +0 -0
  70. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/tools/run_tests.py +0 -0
  71. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/tools/test_results.xsl +0 -0
  72. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/util/ami.py +0 -0
  73. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/src/pyibisami/util/misc.py +0 -0
  74. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/tests/test_ami_test_config.py +0 -0
  75. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/tests/test_core_only.py +0 -0
  76. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/tests/test_gui_render.py +0 -0
  77. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/tests/test_report.py +0 -0
  78. {pyibis_ami-10.0.0 → pyibis_ami-10.0.1}/tests/test_run_tests.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyibis-ami
3
- Version: 10.0.0
3
+ Version: 10.0.1
4
4
  Summary: Facilitates working directly with IBIS-AMI DLLs from the Python command prompt.
5
5
  Author-email: David Banas <capn.freako@gmail.com>
6
6
  Project-URL: Documentation, https://pyibis-ami.readthedocs.io/en/latest/
@@ -40,7 +40,6 @@ Requires-Dist: scipy>=1.15; extra == "report"
40
40
  Provides-Extra: notebook
41
41
  Requires-Dist: pyibis-ami[report]; extra == "notebook"
42
42
  Requires-Dist: jupyter==1.1.1; extra == "notebook"
43
- Requires-Dist: lxml-html-clean==0.4.5; extra == "notebook"
44
43
  Requires-Dist: papermill==2.7.0; extra == "notebook"
45
44
  Provides-Extra: all
46
45
  Requires-Dist: pyibis-ami[gui,notebook,report]; extra == "all"
@@ -35,7 +35,7 @@ extensions = [
35
35
  "sphinx.ext.coverage",
36
36
  "sphinx.ext.todo",
37
37
  # "sphinx.ext.autosummary",
38
- "m2r2",
38
+ "myst_parser",
39
39
  ]
40
40
  autodoc_default_options = {
41
41
  "undoc-members": True,
@@ -24,4 +24,5 @@ Indices and tables
24
24
  * :ref:`modindex`
25
25
  * :ref:`search`
26
26
 
27
- .. mdinclude:: ../../README.md
27
+ .. include:: ../../README.md
28
+ :parser: myst_parser.sphinx_
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "pyibis-ami"
7
7
  description = "Facilitates working directly with IBIS-AMI DLLs from the Python command prompt."
8
- version = "10.0.0"
8
+ version = "10.0.1"
9
9
  authors = [ {name = "David Banas", email = "capn.freako@gmail.com"}
10
10
  ]
11
11
  readme = "README.md"
@@ -48,7 +48,6 @@ report = [
48
48
  notebook = [
49
49
  "pyibis-ami[report]", # The Tester notebook plots with `pyibisami.report.plot`.
50
50
  "jupyter==1.1.1",
51
- "lxml-html-clean==0.4.5",
52
51
  "papermill==2.7.0",
53
52
  ]
54
53
  all = [
@@ -144,7 +143,7 @@ publish-url = "https://test.pypi.org/legacy/"
144
143
  [dependency-groups]
145
144
  dev = [
146
145
  "pyibis-ami[all]",
147
- "m2r2>=0.3.4",
146
+ "myst-parser>=4.0.0",
148
147
  "mypy>=1.19.1",
149
148
  "pyside6==6.10.2",
150
149
  "pytest>=9.0.2",
@@ -14,7 +14,7 @@ numpy==2.5.1 ; python_full_version >= '3.12'
14
14
  # via pyibis-ami
15
15
  parsec==3.17
16
16
  # via pyibis-ami
17
- setuptools==83.0.0
17
+ setuptools==84.1.0
18
18
  # via parsec
19
19
  traits==7.1.0
20
20
  # via pyibis-ami
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyibis-ami
3
- Version: 10.0.0
3
+ Version: 10.0.1
4
4
  Summary: Facilitates working directly with IBIS-AMI DLLs from the Python command prompt.
5
5
  Author-email: David Banas <capn.freako@gmail.com>
6
6
  Project-URL: Documentation, https://pyibis-ami.readthedocs.io/en/latest/
@@ -40,7 +40,6 @@ Requires-Dist: scipy>=1.15; extra == "report"
40
40
  Provides-Extra: notebook
41
41
  Requires-Dist: pyibis-ami[report]; extra == "notebook"
42
42
  Requires-Dist: jupyter==1.1.1; extra == "notebook"
43
- Requires-Dist: lxml-html-clean==0.4.5; extra == "notebook"
44
43
  Requires-Dist: papermill==2.7.0; extra == "notebook"
45
44
  Provides-Extra: all
46
45
  Requires-Dist: pyibis-ami[gui,notebook,report]; extra == "all"
@@ -15,7 +15,6 @@ traitsui==8.0.0
15
15
  [notebook]
16
16
  pyibis-ami[report]
17
17
  jupyter==1.1.1
18
- lxml-html-clean==0.4.5
19
18
  papermill==2.7.0
20
19
 
21
20
  [report]
@@ -47,6 +47,205 @@ class AMIParameter: # pylint: disable=too-many-instance-attributes,too-few-publ
47
47
  parameter.
48
48
  """
49
49
 
50
+ def __init__(
51
+ self,
52
+ name: str,
53
+ tags: list[tuple[str, list[Any]]]
54
+ ): # pylint: disable=too-many-branches,too-many-statements
55
+ """
56
+ Args:
57
+ name: The name of the AMI parameter being created.
58
+ tags: A list of pairs, each containing
59
+
60
+ - a parameter definition tag name
61
+ (Must be one of the keys from the '_param_def_tag_procs' dictionary.)
62
+
63
+ - a list of values to be associated with that tag.
64
+ """
65
+ # Initialization
66
+ self._usage = None
67
+ self._type = None
68
+ self._format = None
69
+ self._value: Optional[list[list[Any]]] = None
70
+ self._min = None
71
+ self._max = None
72
+ self._default = None
73
+ self._description = ""
74
+ self._list_tip = None
75
+ self._types: list[str] = []
76
+ self._values: Optional[list] = None
77
+ self._corners: Optional[dict[str, Any]] = None
78
+ self._labels: Optional[list[str]] = None
79
+
80
+ # Holds any warnings encountered, during initialization.
81
+ # (Any errors encountered will prevent initialization from completing.)
82
+ self._msg = ""
83
+
84
+ # Process all parameter definition tags.
85
+ for tag in tags:
86
+ tag_name = tag[0]
87
+ if tag_name in self._param_def_tag_procs:
88
+ try:
89
+ self._param_def_tag_procs[tag_name](self, tag[1])
90
+ except AMIParamError as err:
91
+ raise AMIParamError(f"Problem initializing parameter, '{name}': {err}\n") from err
92
+ else:
93
+ self._msg += f"Ignoring unrecognized tag: {tag_name}.\n"
94
+
95
+ # Validate and complete the instance.
96
+ # Check for required tags.
97
+ param_usage = self._usage
98
+ param_type = self._type
99
+ param_format = self._format
100
+ param_default = self._default
101
+ if param_usage is None:
102
+ raise AMIParamError("Missing 'Usage' tag!\n")
103
+ if param_type is None:
104
+ raise AMIParamError("Missing 'Type' tag!\n")
105
+ if param_format is None:
106
+ if param_default is None:
107
+ raise AMIParamError("Missing both 'Format' and 'Default' tags!\n")
108
+ self._value = param_default
109
+ param_format = "Value"
110
+ self._format_rem = [param_default]
111
+ # Check for mutual exclusivity of 'Format Value' and 'Default'.
112
+ elif (param_format == "Value") and (param_default is not None):
113
+ self._msg += "'Format Value' and 'Default' both found! (They are mutually exclusive.)\n"
114
+ # Canonicalize Boolean if necessary.
115
+ elif (param_format == "List") and (param_type == "Boolean"):
116
+ if param_default:
117
+ self._value = param_default
118
+ elif self._format_rem:
119
+ self._value = self._format_rem[0] # No Default: the first List value (IBIS 8.0 p.241).
120
+ else:
121
+ raise AMIParamError("Missing List values.\n")
122
+ param_format = "Value"
123
+ self._format_rem = [self._value]
124
+ self._format = param_format
125
+
126
+ # Check for 'Default' used with parameter type 'Out'.
127
+ if (param_usage == "Out") and (param_default is not None):
128
+ raise AMIParamError("'Default' may not be used with parameter type 'Out'!\n")
129
+
130
+ # Complete the instance.
131
+ # Allowed Type/Format combinations: IBIS 8.0 Section 10.3.5, p.245, and Table 18, p.247.
132
+ if len(self._types) > 1 and param_format != "Table":
133
+ raise AMIParamError(f"Only Format Table may have more than one Type; got {self._types}.\n")
134
+ allowed_types = _FORMAT_TYPES.get(param_format)
135
+ if allowed_types and any(t not in allowed_types for t in self._types):
136
+ raise AMIParamError(f"Illegal type, '{param_type}', for use with {param_format}.\n")
137
+ if param_default is not None and param_format in _NO_DEFAULT_FORMATS:
138
+ self._msg += f"Default is not allowed with {param_format}; ignoring it.\n"
139
+ self._default = None
140
+ vals = self._format_rem
141
+ if param_format == "Value":
142
+ self._value = _read_value(param_type, vals[0])
143
+ if self._default is not None:
144
+ self._default = self._typed_default(param_type)
145
+ elif param_format in ("Range", "Increment", "Steps"):
146
+ self._init_range(param_format, param_type, vals)
147
+ elif param_format == "List":
148
+ temp_vals = [_read_value(param_type, v) for v in vals]
149
+ self._value = temp_vals
150
+ # The current List selection lives on the trait ``make_param_traits()`` builds from ``pdefault``.
151
+ default = self._typed_default(param_type)
152
+ if default is not None and default not in temp_vals:
153
+ self._msg += f"Default {default} is not a member of the List {temp_vals}; using {temp_vals[0]}.\n"
154
+ default = None
155
+ self._default = default
156
+ if self._list_tip is not None and len(self._list_tip) != len(temp_vals):
157
+ self._msg += (f"List_Tip has {len(self._list_tip)} entries, but List has {len(temp_vals)}; "
158
+ "they must match.\n")
159
+ elif param_format == "Corner":
160
+ if param_usage == "Out":
161
+ raise AMIParamError("Corner is not allowed with Usage Out.\n")
162
+ if len(vals) != 3:
163
+ raise AMIParamError(f"Corner needs 3 values (typ, slow, fast); got {len(vals)}.\n")
164
+ typ, slow, fast = (_read_value(param_type, v) for v in vals)
165
+ self._value = typ
166
+ self._corners = {"typ": typ, "min": slow, "max": fast}
167
+ # The value follows the simulation corner (``AMIParamConfigurator.sim_corner``), so a Default
168
+ # (allowed, IBIS 8.0 p.245) is checked but not used.
169
+ default = self._typed_default(param_type)
170
+ if default is not None and default not in (typ, slow, fast):
171
+ self._msg += f"Default {default} is not one of the Corner values {[typ, slow, fast]}.\n"
172
+ self._default = default
173
+ elif param_format == "Table":
174
+ self._init_table(vals)
175
+ else: # Gaussian, Dual-Dirac, DjRj
176
+ nvals = 2 if param_format == "Gaussian" else 3
177
+ if len(vals) != nvals:
178
+ raise AMIParamError(f"{param_format} needs {nvals} values; got {len(vals)}.\n")
179
+ self._value = [_read_value(param_type, v) for v in vals]
180
+
181
+ self._name = name
182
+
183
+ def _init_range(self, param_format: str, param_type: str, vals: list) -> None:
184
+ """Complete a Range, Increment or Steps parameter.
185
+
186
+ Increment ``<typ> <min> <max> <delta>`` allows ``typ + N*delta`` within ``[min, max]``; Steps
187
+ ``<typ> <min> <max> <# steps>`` is Increment with ``delta = (max - min) / steps`` (IBIS 8.0 p.241).
188
+ """
189
+ nvals = 3 if param_format == "Range" else 4
190
+ if len(vals) < nvals:
191
+ raise AMIParamError(f"Insufficient number of values, {len(vals)}, provided for {param_format}.\n")
192
+ typ, pmin, pmax = (_read_value(param_type, v) for v in vals[:3])
193
+ if not pmin <= typ <= pmax:
194
+ self._msg += f"{param_format} values must be min <= typ <= max; got {vals[:3]}.\n"
195
+ self._value, self._min, self._max = typ, pmin, pmax
196
+ if param_format != "Range":
197
+ step = _read_value("Float", vals[3])
198
+ delta = step if param_format == "Increment" else (pmax - pmin) / step if step else 0.0
199
+ if delta <= 0:
200
+ raise AMIParamError(f"{param_format} needs a positive step; got {vals[3]}.\n")
201
+ if (pmax - pmin) / delta > MAX_DISCRETE_VALUES:
202
+ raise AMIParamError(f"{param_format} has more than {MAX_DISCRETE_VALUES} values.\n")
203
+ n_lo, n_hi = math.ceil((pmin - typ) / delta - 1e-9), math.floor((pmax - typ) / delta + 1e-9)
204
+ values = [typ + n * delta for n in range(n_lo, n_hi + 1)]
205
+ self._values = ([round(v) for v in values] if param_type == "Integer"
206
+ else [float(f"{v:.12g}") for v in values]) # Drop float noise (0.30000000000000004).
207
+ default = self._typed_default(param_type)
208
+ self._default = None # Typed, and kept only when it is used (as for List).
209
+ if default is None:
210
+ return
211
+ if self._values is not None and default not in self._values:
212
+ self._msg += f"Default {default} is not one of the {param_format} values {self._values}; using {typ}.\n"
213
+ elif not pmin <= default <= pmax:
214
+ self._msg += f"Default {default} is outside the {param_format} [{pmin}, {pmax}]; using {typ}.\n"
215
+ else:
216
+ self._value = self._default = default
217
+
218
+ def _init_table(self, items: list) -> None:
219
+ """Complete a Table parameter: optional ``Labels``, then one or more rows of equal length (IBIS 8.0 p.242)."""
220
+ if items and isinstance(items[0], tuple) and items[0][0] == "Labels":
221
+ self._labels = [label.strip('"') for label in items[0][1]]
222
+ items = items[1:]
223
+ if not items or not all(isinstance(row, list) for row in items):
224
+ raise AMIParamError(f"A Table needs one or more rows of values; got {items}.\n")
225
+ ncols = len(items[0])
226
+ if any(len(row) != ncols for row in items):
227
+ raise AMIParamError("All Table rows must have the same number of entries.\n")
228
+ types = self._types if len(self._types) > 1 else self._types * ncols
229
+ if len(types) != ncols:
230
+ raise AMIParamError(f"Table has {ncols} columns but {len(types)} Types.\n")
231
+ self._value = [[_read_value(t, v) for t, v in zip(types, row)] for row in items]
232
+
233
+ def _typed_default(self, param_type: str):
234
+ """``Default``, read as ``param_type``; ``None`` if absent or unreadable (with a warning in ``msg``).
235
+
236
+ Range and List use it, if present, as the initial value (IBIS 8.0 Section 10.3.4, p.245).
237
+ """
238
+ if self._default is None:
239
+ return None
240
+ try:
241
+ return _read_value(param_type, self._default)
242
+ except AMIParamError as err:
243
+ self._msg += f"Ignoring Default: {err}"
244
+ return None
245
+
246
+ def __str__(self):
247
+ return f"{self.pvalue}"
248
+
50
249
  # Properties.
51
250
 
52
251
  # Note: They are read-only, despite the presence of apparent setters.
@@ -242,201 +441,6 @@ class AMIParameter: # pylint: disable=too-many-instance-attributes,too-few-publ
242
441
  "Labels": _set_list_tip,
243
442
  }
244
443
 
245
- def __init__(self, name, tags): # pylint: disable=too-many-branches,too-many-statements
246
- """
247
- Args:
248
- name (str): The name of the AMI parameter being created.
249
- tags ([(str, [a])]): A list of pairs, each containing
250
-
251
- - a parameter definition tag name
252
- (Must be one of the keys from the '_param_def_tag_procs' dictionary.)
253
-
254
- - a list of values to be associated with that tag.
255
- """
256
- # Initialization
257
- self._usage = None
258
- self._type = None
259
- self._format = None
260
- self._value = None
261
- self._min = None
262
- self._max = None
263
- self._default = None
264
- self._description = ""
265
- self._list_tip = None
266
- self._types: list[str] = []
267
- self._values: Optional[list] = None
268
- self._corners: Optional[dict[str, Any]] = None
269
- self._labels: Optional[list[str]] = None
270
-
271
- # Holds any warnings encountered, during initialization.
272
- # (Any errors encountered will prevent initialization from completing.)
273
- self._msg = ""
274
-
275
- # Process all parameter definition tags.
276
- for tag in tags:
277
- tag_name = tag[0]
278
- if tag_name in self._param_def_tag_procs:
279
- try:
280
- self._param_def_tag_procs[tag_name](self, tag[1])
281
- except AMIParamError as err:
282
- raise AMIParamError(f"Problem initializing parameter, '{name}': {err}\n") from err
283
- else:
284
- self._msg += f"Ignoring unrecognized tag: {tag_name}.\n"
285
-
286
- # Validate and complete the instance.
287
- # Check for required tags.
288
- param_usage = self._usage
289
- param_type = self._type
290
- param_format = self._format
291
- param_default = self._default
292
- if param_usage is None:
293
- raise AMIParamError("Missing 'Usage' tag!\n")
294
- if param_type is None:
295
- raise AMIParamError("Missing 'Type' tag!\n")
296
- if param_format is None:
297
- if param_default is None:
298
- raise AMIParamError("Missing both 'Format' and 'Default' tags!\n")
299
- self._value = param_default
300
- param_format = "Value"
301
- self._format_rem = [param_default]
302
- # Check for mutual exclusivity of 'Format Value' and 'Default'.
303
- elif (param_format == "Value") and (param_default is not None):
304
- self._msg += "'Format Value' and 'Default' both found! (They are mutually exclusive.)\n"
305
- # Canonicalize Boolean if necessary.
306
- elif (param_format == "List") and (param_type == "Boolean"):
307
- if param_default:
308
- self._value = param_default
309
- elif self._format_rem:
310
- self._value = self._format_rem[0] # No Default: the first List value (IBIS 8.0 p.241).
311
- else:
312
- raise AMIParamError("Missing List values.\n")
313
- param_format = "Value"
314
- self._format_rem = [self._value]
315
- self._format = param_format
316
-
317
- # Check for 'Default' used with parameter type 'Out'.
318
- if (param_usage == "Out") and (param_default is not None):
319
- raise AMIParamError("'Default' may not be used with parameter type 'Out'!\n")
320
-
321
- # Complete the instance.
322
- # Allowed Type/Format combinations: IBIS 8.0 Section 10.3.5, p.245, and Table 18, p.247.
323
- if len(self._types) > 1 and param_format != "Table":
324
- raise AMIParamError(f"Only Format Table may have more than one Type; got {self._types}.\n")
325
- allowed_types = _FORMAT_TYPES.get(param_format)
326
- if allowed_types and any(t not in allowed_types for t in self._types):
327
- raise AMIParamError(f"Illegal type, '{param_type}', for use with {param_format}.\n")
328
- if param_default is not None and param_format in _NO_DEFAULT_FORMATS:
329
- self._msg += f"Default is not allowed with {param_format}; ignoring it.\n"
330
- self._default = None
331
- vals = self._format_rem
332
- if param_format == "Value":
333
- self._value = _read_value(param_type, vals[0])
334
- if self._default is not None:
335
- self._default = self._typed_default(param_type)
336
- elif param_format in ("Range", "Increment", "Steps"):
337
- self._init_range(param_format, param_type, vals)
338
- elif param_format == "List":
339
- temp_vals = [_read_value(param_type, v) for v in vals]
340
- self._value = temp_vals
341
- # The current List selection lives on the trait ``make_param_traits()`` builds from ``pdefault``.
342
- default = self._typed_default(param_type)
343
- if default is not None and default not in temp_vals:
344
- self._msg += f"Default {default} is not a member of the List {temp_vals}; using {temp_vals[0]}.\n"
345
- default = None
346
- self._default = default
347
- if self._list_tip is not None and len(self._list_tip) != len(temp_vals):
348
- self._msg += (f"List_Tip has {len(self._list_tip)} entries, but List has {len(temp_vals)}; "
349
- "they must match.\n")
350
- elif param_format == "Corner":
351
- if param_usage == "Out":
352
- raise AMIParamError("Corner is not allowed with Usage Out.\n")
353
- if len(vals) != 3:
354
- raise AMIParamError(f"Corner needs 3 values (typ, slow, fast); got {len(vals)}.\n")
355
- typ, slow, fast = (_read_value(param_type, v) for v in vals)
356
- self._value = typ
357
- self._corners = {"typ": typ, "min": slow, "max": fast}
358
- # The value follows the simulation corner (``AMIParamConfigurator.sim_corner``), so a Default
359
- # (allowed, IBIS 8.0 p.245) is checked but not used.
360
- default = self._typed_default(param_type)
361
- if default is not None and default not in (typ, slow, fast):
362
- self._msg += f"Default {default} is not one of the Corner values {[typ, slow, fast]}.\n"
363
- self._default = default
364
- elif param_format == "Table":
365
- self._init_table(vals)
366
- else: # Gaussian, Dual-Dirac, DjRj
367
- nvals = 2 if param_format == "Gaussian" else 3
368
- if len(vals) != nvals:
369
- raise AMIParamError(f"{param_format} needs {nvals} values; got {len(vals)}.\n")
370
- self._value = [_read_value(param_type, v) for v in vals]
371
-
372
- self._name = name
373
-
374
- def _init_range(self, param_format: str, param_type: str, vals: list) -> None:
375
- """Complete a Range, Increment or Steps parameter.
376
-
377
- Increment ``<typ> <min> <max> <delta>`` allows ``typ + N*delta`` within ``[min, max]``; Steps
378
- ``<typ> <min> <max> <# steps>`` is Increment with ``delta = (max - min) / steps`` (IBIS 8.0 p.241).
379
- """
380
- nvals = 3 if param_format == "Range" else 4
381
- if len(vals) < nvals:
382
- raise AMIParamError(f"Insufficient number of values, {len(vals)}, provided for {param_format}.\n")
383
- typ, pmin, pmax = (_read_value(param_type, v) for v in vals[:3])
384
- if not pmin <= typ <= pmax:
385
- self._msg += f"{param_format} values must be min <= typ <= max; got {vals[:3]}.\n"
386
- self._value, self._min, self._max = typ, pmin, pmax
387
- if param_format != "Range":
388
- step = _read_value("Float", vals[3])
389
- delta = step if param_format == "Increment" else (pmax - pmin) / step if step else 0.0
390
- if delta <= 0:
391
- raise AMIParamError(f"{param_format} needs a positive step; got {vals[3]}.\n")
392
- if (pmax - pmin) / delta > MAX_DISCRETE_VALUES:
393
- raise AMIParamError(f"{param_format} has more than {MAX_DISCRETE_VALUES} values.\n")
394
- n_lo, n_hi = math.ceil((pmin - typ) / delta - 1e-9), math.floor((pmax - typ) / delta + 1e-9)
395
- values = [typ + n * delta for n in range(n_lo, n_hi + 1)]
396
- self._values = ([round(v) for v in values] if param_type == "Integer"
397
- else [float(f"{v:.12g}") for v in values]) # Drop float noise (0.30000000000000004).
398
- default = self._typed_default(param_type)
399
- self._default = None # Typed, and kept only when it is used (as for List).
400
- if default is None:
401
- return
402
- if self._values is not None and default not in self._values:
403
- self._msg += f"Default {default} is not one of the {param_format} values {self._values}; using {typ}.\n"
404
- elif not pmin <= default <= pmax:
405
- self._msg += f"Default {default} is outside the {param_format} [{pmin}, {pmax}]; using {typ}.\n"
406
- else:
407
- self._value = self._default = default
408
-
409
- def _init_table(self, items: list) -> None:
410
- """Complete a Table parameter: optional ``Labels``, then one or more rows of equal length (IBIS 8.0 p.242)."""
411
- if items and isinstance(items[0], tuple) and items[0][0] == "Labels":
412
- self._labels = [label.strip('"') for label in items[0][1]]
413
- items = items[1:]
414
- if not items or not all(isinstance(row, list) for row in items):
415
- raise AMIParamError(f"A Table needs one or more rows of values; got {items}.\n")
416
- ncols = len(items[0])
417
- if any(len(row) != ncols for row in items):
418
- raise AMIParamError("All Table rows must have the same number of entries.\n")
419
- types = self._types if len(self._types) > 1 else self._types * ncols
420
- if len(types) != ncols:
421
- raise AMIParamError(f"Table has {ncols} columns but {len(types)} Types.\n")
422
- self._value = [[_read_value(t, v) for t, v in zip(types, row)] for row in items]
423
-
424
- def _typed_default(self, param_type: str):
425
- """``Default``, read as ``param_type``; ``None`` if absent or unreadable (with a warning in ``msg``).
426
-
427
- Range and List use it, if present, as the initial value (IBIS 8.0 Section 10.3.4, p.245).
428
- """
429
- if self._default is None:
430
- return None
431
- try:
432
- return _read_value(param_type, self._default)
433
- except AMIParamError as err:
434
- self._msg += f"Ignoring Default: {err}"
435
- return None
436
-
437
- def __str__(self):
438
- return f"{self.pvalue}"
439
-
440
444
 
441
445
  def _read_value(param_type: str, value_str: str):
442
446
  """One AMI value, as the given ``Type``.
@@ -184,6 +184,11 @@ class Model(HasTraits): # pylint: disable=too-many-instance-attributes
184
184
 
185
185
  self._mtype = maybe("model_type")
186
186
  self._ccomp = maybe("c_comp")
187
+ if self._ccomp is None: # IBIS 5.0+ split C_comp: the parts are in parallel, so they add up.
188
+ parts = [maybe(f"c_comp_{kw}") for kw in ("pullup", "pulldown", "power_clamp", "gnd_clamp")]
189
+ parts = [part for part in parts if part is not None]
190
+ if parts:
191
+ self._ccomp = [sum(part[n] if n < len(part) else part[0] for part in parts) for n in range(3)]
187
192
  self._cref = maybe("cref")
188
193
  self._vref = maybe("vref")
189
194
  self._vmeas = maybe("vmeas")
@@ -191,6 +196,13 @@ class Model(HasTraits): # pylint: disable=too-many-instance-attributes
191
196
  self._trange = maybe("temperature_range")
192
197
  self._vrange = maybe("voltage_range")
193
198
  self._ramp = maybe("ramp")
199
+ self._add_submodel = maybe("add_submodel") or []
200
+ # Receiver operating point for the input impedance: ``Vmeas``,
201
+ # else the threshold midpoint (``Vinl`` + ``Vinh``) / 2.
202
+ vinl, vinh = maybe("vinl"), maybe("vinh")
203
+ self._vin_op = self._vmeas
204
+ if self._vin_op is None and isinstance(vinl, float) and isinstance(vinh, float):
205
+ self._vin_op = (vinl + vinh) / 2
194
206
 
195
207
  # Check for the required keywords.
196
208
  if not self._mtype:
@@ -271,7 +283,35 @@ class Model(HasTraits): # pylint: disable=too-many-instance-attributes
271
283
  if self._ramp:
272
284
  self._slew = (self._ramp["rising"][0] + self._ramp["falling"][0]) / 2e9 # (V/ns)
273
285
  if mtype in _RECEIVER_TYPES:
274
- self._zin = _clamp_impedance(curves, self._vmeas)
286
+ self._zin = _clamp_impedance(curves, self._vin_op)
287
+
288
+ def apply_submodels(self, submodels: Mapping[str, dict]) -> list[str]:
289
+ """Put the clamps of the ``Dynamic_clamp`` submodels that ``[Add Submodel]`` adds in ``Non-Driving``
290
+ or ``All`` mode in parallel with the receiver input impedance (e.g. - DRAM ODT, IBIS 8.0 Section 6.3).
291
+
292
+ Returns:
293
+ Warnings, e.g. - for a submodel that isn't in the file.
294
+ """
295
+ warnings: list[str] = []
296
+ if self._zin is None:
297
+ return warnings
298
+ conductance = 0.0
299
+ for sub_name, mode in self._add_submodel:
300
+ sub = submodels.get(sub_name)
301
+ if sub is None:
302
+ warnings.append(f"[Model] {self._name}: [Add Submodel] {sub_name} has no [Submodel]; ignored.")
303
+ continue
304
+ if mode.lower() == "driving" or str(sub.get("submodel_type", "")).lower() != "dynamic_clamp":
305
+ continue
306
+ for curve in IVData.from_model_dict(sub, self._vrange[0], required=()).curves.values():
307
+ try:
308
+ conductance += 1 / curve.impedance(self._vin_op)
309
+ except ValueError: # Vmeas off the table: the clamp is off there.
310
+ pass
311
+ if conductance:
312
+ self._zin = 1 / (1 / self._zin + conductance)
313
+ self.add_trait("zin", Float(self._zin))
314
+ return warnings
275
315
 
276
316
  def __str__(self):
277
317
  res = "Model Type:\t" + self._mtype + "\n"
@@ -337,7 +377,8 @@ class Model(HasTraits): # pylint: disable=too-many-instance-attributes
337
377
 
338
378
  @property
339
379
  def zin(self) -> Optional[float]:
340
- """The receiver input impedance (Ohms), from the ``[GND Clamp]``/``[POWER Clamp]`` curves at ``Vmeas``.
380
+ """The receiver input impedance (Ohms), from the ``[GND Clamp]``/``[POWER Clamp]`` curves, and those of any
381
+ ``Dynamic_clamp`` ``[Add Submodel]`` (ODT), at ``Vmeas`` (else at (``Vinl`` + ``Vinh``) / 2).
341
382
 
342
383
  Both clamps act in parallel; a missing one counts as ``NO_CLAMP_Z``. Set for ``Input`` and the
343
384
  ``I/O`` types; ``None`` for other ``Model_type`` s, or ones not supported yet (``*_ECL``, ``*_diff``).
@@ -445,8 +445,24 @@ Model_keywords: dict[str, Parser] = {
445
445
  "rising_waveform": skip_keyword,
446
446
  "falling_waveform": skip_keyword,
447
447
  "composite_current": skip_keyword,
448
+ # ``[Add Submodel]`` rows: submodel name and mode (IBIS 8.0 Section 6.3).
449
+ "add_submodel": many1(token + lexeme(regex(r"(?i)(Non-Driving|Driving|All)\b"))),
448
450
  }
449
451
 
452
+ # ``[Submodel]`` I-V tables used for the receiver input impedance (``Dynamic_clamp`` ODT, IBIS 8.0 Section 6.3).
453
+ Submodel_keywords: dict[str, Parser] = {
454
+ "gnd_clamp": many1(vi_line),
455
+ "power_clamp": many1(vi_line),
456
+ }
457
+
458
+
459
+ @generate("[Submodel]")
460
+ def submodel():
461
+ "Parse [Submodel]."
462
+ nm = yield token << ignore
463
+ res = yield many(node(Submodel_keywords, IBIS_keywords))
464
+ return {nm: dict(res)}
465
+
450
466
 
451
467
  @generate("[Model]")
452
468
  def model():
@@ -591,6 +607,7 @@ IBIS_kywrd_parsers.update(
591
607
  "date": rest_line,
592
608
  "component": comp,
593
609
  "model_selector": modsel,
610
+ "submodel": submodel,
594
611
  }
595
612
  )
596
613
 
@@ -703,6 +720,7 @@ def parse_ibis_file(
703
720
  components = {}
704
721
  models = {}
705
722
  model_selectors = {}
723
+ submodels = {}
706
724
  for kw, val in nodes:
707
725
  if kw == "model":
708
726
  models.update(val)
@@ -710,12 +728,20 @@ def parse_ibis_file(
710
728
  components.update(val)
711
729
  elif kw == "model_selector":
712
730
  model_selectors.update(val)
731
+ elif kw == "submodel":
732
+ submodels.update(val)
733
+ if any("algorithmic_model" in sub for sub in val.values()):
734
+ warnings.append(
735
+ "[Algorithmic Model] found outside a [Model] (e.g. - under [Submodel]), where it is not "
736
+ "permitted; ignored (IBIS 8.0 Section 10.1, p.213)."
737
+ )
713
738
  elif kw == "algorithmic_model": # Not inside a [Model]; e.g. - under [Submodel], where it's skipped.
714
739
  warnings.append("[Algorithmic Model] found outside a [Model] (e.g. - under [Submodel]), where it is not "
715
740
  "permitted; ignored (IBIS 8.0 Section 10.1, p.213).")
716
741
  else:
717
742
  kw_dict.update({kw: val})
718
743
  for mdl in models.values():
744
+ warnings.extend(mdl.apply_submodels(submodels))
719
745
  warnings.extend(mdl.spec_warnings)
720
746
  for warning in warnings:
721
747
  logger.warning("%s", warning)
@@ -724,6 +750,7 @@ def parse_ibis_file(
724
750
  "components": components,
725
751
  "models": models,
726
752
  "model_selectors": model_selectors,
753
+ "submodels": submodels,
727
754
  "warnings": warnings,
728
755
  }
729
756
  )
File without changes
File without changes
File without changes
File without changes
File without changes