pyibis-ami 7.1.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.
- pyibis_ami-7.1.0.dist-info/LICENSE +8 -0
- pyibis_ami-7.1.0.dist-info/METADATA +134 -0
- pyibis_ami-7.1.0.dist-info/RECORD +26 -0
- pyibis_ami-7.1.0.dist-info/WHEEL +5 -0
- pyibis_ami-7.1.0.dist-info/entry_points.txt +4 -0
- pyibis_ami-7.1.0.dist-info/top_level.txt +1 -0
- pyibisami/IBIS_AMI_Checker.ipynb +1693 -0
- pyibisami/IBIS_AMI_Tester.ipynb +1457 -0
- pyibisami/__init__.py +22 -0
- pyibisami/__main__.py +7 -0
- pyibisami/ami/__init__.py +0 -0
- pyibisami/ami/config.py +297 -0
- pyibisami/ami/generic.ami.em +20 -0
- pyibisami/ami/generic.ibs.em +139 -0
- pyibisami/ami/model.py +596 -0
- pyibisami/ami/parameter.py +375 -0
- pyibisami/ami/parser.py +635 -0
- pyibisami/common.py +40 -0
- pyibisami/ibis/__init__.py +0 -0
- pyibisami/ibis/file.py +325 -0
- pyibisami/ibis/model.py +399 -0
- pyibisami/ibis/parser.py +525 -0
- pyibisami/tools/__init__.py +0 -0
- pyibisami/tools/run_notebook.py +144 -0
- pyibisami/tools/run_tests.py +273 -0
- pyibisami/tools/test_results.xsl +42 -0
pyibisami/ami/model.py
ADDED
@@ -0,0 +1,596 @@
|
|
1
|
+
"""
|
2
|
+
Class definitions for working with IBIS-AMI models.
|
3
|
+
|
4
|
+
Original Author: David Banas
|
5
|
+
|
6
|
+
Original Date: July 3, 2012
|
7
|
+
|
8
|
+
Copyright (c) 2019 David Banas; All rights reserved World wide.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import copy as cp
|
12
|
+
from ctypes import CDLL, byref, c_char_p, c_double # pylint: disable=no-name-in-module
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Any, Optional
|
15
|
+
|
16
|
+
import numpy as np
|
17
|
+
from numpy.random import default_rng
|
18
|
+
|
19
|
+
from pyibisami.common import Rvec, deconv_same
|
20
|
+
|
21
|
+
|
22
|
+
def loadWave(filename: str) -> tuple[Rvec, Rvec]:
|
23
|
+
"""
|
24
|
+
Load a waveform file.
|
25
|
+
|
26
|
+
The file should consist of any number of lines, where each line
|
27
|
+
contains, first, a time value and, second, a voltage value.
|
28
|
+
Assume the first line is a header, and discard it.
|
29
|
+
|
30
|
+
Specifically, this function may be used to load in waveform files
|
31
|
+
saved from *CosmosScope*.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
filename: Name of waveform file to read in.
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
A pair of *NumPy* arrays containing the time and voltage values, respectively.
|
38
|
+
"""
|
39
|
+
|
40
|
+
with open(filename, "r", encoding="utf-8") as theFile:
|
41
|
+
theFile.readline() # Consume the header line.
|
42
|
+
time = []
|
43
|
+
voltage = []
|
44
|
+
for line in theFile:
|
45
|
+
tmp = list(map(float, line.split()))
|
46
|
+
time.append(tmp[0])
|
47
|
+
voltage.append(tmp[1])
|
48
|
+
return (np.array(time), np.array(voltage))
|
49
|
+
|
50
|
+
|
51
|
+
def interpFile(filename: str, sample_per: float) -> Rvec:
|
52
|
+
"""
|
53
|
+
Read in a waveform from a file, and convert it to the given sample rate,
|
54
|
+
using linear interpolation.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
filename: Name of waveform file to read in.
|
58
|
+
sample_per: New sample interval, in seconds.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
A *NumPy* array containing the resampled waveform.
|
62
|
+
"""
|
63
|
+
|
64
|
+
impulse = loadWave(filename)
|
65
|
+
ts = impulse[0]
|
66
|
+
ts = ts - ts[0]
|
67
|
+
vs = impulse[1]
|
68
|
+
tmax = ts[-1]
|
69
|
+
# Build new impulse response, at new sampling period, using linear interpolation.
|
70
|
+
res = []
|
71
|
+
t = 0.0
|
72
|
+
i = 0
|
73
|
+
while t < tmax:
|
74
|
+
while ts[i] <= t:
|
75
|
+
i = i + 1
|
76
|
+
res.append(vs[i - 1] + (vs[i] - vs[i - 1]) * (t - ts[i - 1]) / (ts[i] - ts[i - 1]))
|
77
|
+
t = t + sample_per
|
78
|
+
return np.array(res)
|
79
|
+
|
80
|
+
|
81
|
+
class AMIModelInitializer:
|
82
|
+
"""
|
83
|
+
Class containing the initialization data for an instance of ``AMIModel``.
|
84
|
+
|
85
|
+
Created primarily to facilitate use of the PyAMI package at the
|
86
|
+
pylab command prompt, this class can be used by the pylab user, in
|
87
|
+
order to store all the data required to initialize an instance of
|
88
|
+
class ``AMIModel``. In this way, the pylab user may assemble the
|
89
|
+
AMIModel initialization data just once, and modify it incrementally,
|
90
|
+
as she experiments with different initialization settings. In this
|
91
|
+
way, she can avoid having to type a lot of redundant constants every
|
92
|
+
time she invokes the AMIModel constructor.
|
93
|
+
"""
|
94
|
+
|
95
|
+
# pylint: disable=too-few-public-methods,too-many-instance-attributes
|
96
|
+
|
97
|
+
ami_params = {"root_name": ""}
|
98
|
+
|
99
|
+
_init_data = {
|
100
|
+
"channel_response": (c_double * 128)(0.0, 1.0, 0.0),
|
101
|
+
"row_size": 128,
|
102
|
+
"num_aggressors": 0,
|
103
|
+
"sample_interval": c_double(25.0e-12),
|
104
|
+
"bit_time": c_double(0.1e-9),
|
105
|
+
}
|
106
|
+
|
107
|
+
def __init__(self, ami_params: dict, info_params: Optional[dict] = None, **optional_args):
|
108
|
+
"""
|
109
|
+
Constructor accepts a mandatory dictionary containing the AMI
|
110
|
+
parameters, as well as optional information parameter dictionary
|
111
|
+
and initialization data overrides, and validates them, before
|
112
|
+
using them to update the local initialization data structures.
|
113
|
+
|
114
|
+
Valid names of optional initialization data overrides:
|
115
|
+
|
116
|
+
- channel_response
|
117
|
+
a matrix of ``c_double's`` where the first row represents the
|
118
|
+
impulse response of the analog channel, and the rest represent
|
119
|
+
the impulse responses of several aggressor-to-victim far end
|
120
|
+
crosstalk (FEXT) channels.
|
121
|
+
|
122
|
+
Default) a single 128 element vector containing an ideal impulse
|
123
|
+
|
124
|
+
- row_size
|
125
|
+
integer giving the size of the rows in ``channel_response``.
|
126
|
+
|
127
|
+
Default) 128
|
128
|
+
|
129
|
+
- num_aggressors
|
130
|
+
integer giving the number or rows in ``channel_response``, minus
|
131
|
+
one.
|
132
|
+
|
133
|
+
Default) 0
|
134
|
+
|
135
|
+
- sample_interval
|
136
|
+
c_double giving the time interval, in seconds, between
|
137
|
+
successive elements in any row of ``channel_response``.
|
138
|
+
|
139
|
+
Default) 25e-12 (40 GHz sampling rate)
|
140
|
+
|
141
|
+
- bit_time
|
142
|
+
c_double giving the bit period (i.e. - unit interval) of the
|
143
|
+
link, in seconds.
|
144
|
+
|
145
|
+
Default) 100e-12 (10 Gbits/s)
|
146
|
+
"""
|
147
|
+
|
148
|
+
self.ami_params = {"root_name": ""}
|
149
|
+
self.ami_params.update(ami_params)
|
150
|
+
self.info_params = info_params
|
151
|
+
|
152
|
+
# Need to reverse sort, in order to catch ``sample_interval`` and ``row_size``,
|
153
|
+
# before ``channel_response``, since ``channel_response`` depends upon ``sample_interval``,
|
154
|
+
# when ``h`` is a file name, and overwrites ``row_size``, in any case.
|
155
|
+
keys = list(optional_args.keys())
|
156
|
+
keys.sort(reverse=True)
|
157
|
+
if keys:
|
158
|
+
for key in keys:
|
159
|
+
if key in self._init_data:
|
160
|
+
self._init_data[key] = optional_args[key]
|
161
|
+
|
162
|
+
def _getChannelResponse(self):
|
163
|
+
return list(map(float, self._init_data["channel_response"]))
|
164
|
+
|
165
|
+
def _setChannelResponse(self, h):
|
166
|
+
if isinstance(h, str) and Path(h).is_file():
|
167
|
+
h = interpFile(h, self.sample_interval)
|
168
|
+
Vector = c_double * len(h)
|
169
|
+
self._init_data["channel_response"] = Vector(*h)
|
170
|
+
self.row_size = len(h)
|
171
|
+
|
172
|
+
channel_response = property(
|
173
|
+
_getChannelResponse,
|
174
|
+
_setChannelResponse,
|
175
|
+
doc="Channel impulse response to be passed to AMI_Init(). May be a file name.",
|
176
|
+
)
|
177
|
+
|
178
|
+
def _getRowSize(self):
|
179
|
+
return self._init_data["row_size"]
|
180
|
+
|
181
|
+
def _setRowSize(self, n):
|
182
|
+
self._init_data["row_size"] = n
|
183
|
+
|
184
|
+
row_size = property(_getRowSize, _setRowSize, doc="Number of elements in channel response vector(s).")
|
185
|
+
|
186
|
+
def _getNumAggressors(self):
|
187
|
+
return self._init_data["num_aggressors"]
|
188
|
+
|
189
|
+
def _setNumAggressors(self, n):
|
190
|
+
self._init_data["num_aggressors"] = n
|
191
|
+
|
192
|
+
num_aggressors = property(
|
193
|
+
_getNumAggressors, _setNumAggressors, doc="Number of vectors in ``channel_response``, minus one."
|
194
|
+
)
|
195
|
+
|
196
|
+
def _getSampleInterval(self):
|
197
|
+
return float(self._init_data["sample_interval"].value)
|
198
|
+
|
199
|
+
def _setSampleInterval(self, T):
|
200
|
+
self._init_data["sample_interval"] = c_double(T)
|
201
|
+
|
202
|
+
sample_interval = property(
|
203
|
+
_getSampleInterval,
|
204
|
+
_setSampleInterval,
|
205
|
+
doc="Time interval between adjacent elements in channel response vector(s).",
|
206
|
+
)
|
207
|
+
|
208
|
+
def _getBitTime(self):
|
209
|
+
return float(self._init_data["bit_time"].value)
|
210
|
+
|
211
|
+
def _setBitTime(self, T):
|
212
|
+
self._init_data["bit_time"] = c_double(T)
|
213
|
+
|
214
|
+
bit_time = property(_getBitTime, _setBitTime, doc="Link unit interval.")
|
215
|
+
|
216
|
+
|
217
|
+
class AMIModel: # pylint: disable=too-many-instance-attributes
|
218
|
+
"""
|
219
|
+
Class defining the structure and behavior of an IBIS-AMI Model.
|
220
|
+
|
221
|
+
Notes:
|
222
|
+
1. Makes the calling of ``AMI_Close()`` automagic,
|
223
|
+
by calling it from the destructor.
|
224
|
+
"""
|
225
|
+
|
226
|
+
def __init__(self, filename: str):
|
227
|
+
"""
|
228
|
+
Load the dll and bind the 3 AMI functions.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
filename: The DLL/SO file name.
|
232
|
+
|
233
|
+
Raises:
|
234
|
+
OSError: If given file cannot be opened.
|
235
|
+
"""
|
236
|
+
|
237
|
+
self._ami_mem_handle = None
|
238
|
+
my_dll = CDLL(filename)
|
239
|
+
self._amiInit = my_dll.AMI_Init
|
240
|
+
self._amiClose = my_dll.AMI_Close
|
241
|
+
try:
|
242
|
+
self._amiGetWave = my_dll.AMI_GetWave
|
243
|
+
except Exception: # pylint: disable=broad-exception-caught
|
244
|
+
self._amiGetWave = None # type: ignore
|
245
|
+
|
246
|
+
def __del__(self):
|
247
|
+
"""
|
248
|
+
Destructor - Calls ``AMI_Close()`` with handle to AMI model memory.
|
249
|
+
|
250
|
+
This obviates the need for the user to call the ``AMI_Close()``
|
251
|
+
function explicitly, and guards against memory leaks, during
|
252
|
+
PyLab command prompt operation, by ensuring that ``AMI_Close()``
|
253
|
+
gets called automagically when the model goes out of scope.
|
254
|
+
"""
|
255
|
+
if self._ami_mem_handle:
|
256
|
+
self._amiClose(self._ami_mem_handle)
|
257
|
+
|
258
|
+
def initialize(self, init_object: AMIModelInitializer):
|
259
|
+
"""
|
260
|
+
Wraps the ``AMI_Init()`` function.
|
261
|
+
|
262
|
+
Args:
|
263
|
+
init_object: The model initialization data.
|
264
|
+
|
265
|
+
Notes:
|
266
|
+
1. Takes an instance of ``AMIModelInitializer`` as its only argument.
|
267
|
+
This allows model initialization data to be constructed once,
|
268
|
+
and modified incrementally in between multiple calls of
|
269
|
+
``initialize``. This is useful for *PyLab* command prompt testing.
|
270
|
+
|
271
|
+
ToDo:
|
272
|
+
1. Allow for non-integral number of samples per unit interval.
|
273
|
+
"""
|
274
|
+
|
275
|
+
# Free any memory allocated by the previous initialization.
|
276
|
+
if self._ami_mem_handle:
|
277
|
+
self._amiClose(self._ami_mem_handle)
|
278
|
+
|
279
|
+
# Set up the AMI_Init() arguments.
|
280
|
+
self._channel_response = ( # pylint: disable=attribute-defined-outside-init
|
281
|
+
init_object._init_data[ # pylint: disable=protected-access
|
282
|
+
"channel_response"
|
283
|
+
]
|
284
|
+
)
|
285
|
+
self._initOut = cp.copy(self._channel_response) # type: ignore # pylint: disable=attribute-defined-outside-init
|
286
|
+
self._row_size = init_object._init_data[ # pylint: disable=protected-access,attribute-defined-outside-init
|
287
|
+
"row_size"
|
288
|
+
]
|
289
|
+
self._num_aggressors = init_object._init_data[ # pylint: disable=protected-access,attribute-defined-outside-init
|
290
|
+
"num_aggressors"
|
291
|
+
]
|
292
|
+
self._sample_interval = ( # pylint: disable=attribute-defined-outside-init
|
293
|
+
init_object._init_data[ # pylint: disable=protected-access
|
294
|
+
"sample_interval"
|
295
|
+
]
|
296
|
+
)
|
297
|
+
self._bit_time = init_object._init_data[ # pylint: disable=protected-access,attribute-defined-outside-init
|
298
|
+
"bit_time"
|
299
|
+
]
|
300
|
+
self._info_params = init_object.info_params # pylint: disable=attribute-defined-outside-init
|
301
|
+
assert self._info_params, RuntimeError(
|
302
|
+
f"`info_params` is None!\n`init_object: {init_object}"
|
303
|
+
) # pylint: disable=attribute-defined-outside-init
|
304
|
+
|
305
|
+
# Check GetWave() consistency if possible.
|
306
|
+
if init_object.info_params and init_object.info_params["GetWave_Exists"]:
|
307
|
+
assert self._amiGetWave, RuntimeError(
|
308
|
+
"Reserved parameter `GetWave_Exists` is True, but I can't bind to `AMI_GetWave()`!"
|
309
|
+
)
|
310
|
+
|
311
|
+
# Construct the AMI parameters string.
|
312
|
+
def sexpr(pname, pval):
|
313
|
+
"""Create an S-expression from a parameter name/value pair, calling
|
314
|
+
recursively as needed to elaborate sub-parameter dictionaries."""
|
315
|
+
if isinstance(pval, dict):
|
316
|
+
subs = []
|
317
|
+
for sname in pval:
|
318
|
+
subs.append(sexpr(sname, pval[sname]))
|
319
|
+
return sexpr(pname, " ".join(subs))
|
320
|
+
return f"({pname} {pval})"
|
321
|
+
|
322
|
+
ami_params_in = f"({init_object.ami_params['root_name']} "
|
323
|
+
for item in list(init_object.ami_params.items()):
|
324
|
+
if not item[0] == "root_name":
|
325
|
+
ami_params_in += sexpr(item[0], item[1])
|
326
|
+
ami_params_in += ")"
|
327
|
+
self._ami_params_in = ami_params_in.encode("utf-8") # pylint: disable=attribute-defined-outside-init
|
328
|
+
|
329
|
+
# Set handle types.
|
330
|
+
self._ami_params_out = c_char_p(b"") # pylint: disable=attribute-defined-outside-init
|
331
|
+
self._ami_mem_handle = c_char_p(None) # type: ignore # pylint: disable=attribute-defined-outside-init
|
332
|
+
self._msg = c_char_p(b"") # pylint: disable=attribute-defined-outside-init
|
333
|
+
|
334
|
+
# Call AMI_Init(), via our Python wrapper.
|
335
|
+
try:
|
336
|
+
self._amiInit(
|
337
|
+
byref(self._initOut), # type: ignore
|
338
|
+
self._row_size,
|
339
|
+
self._num_aggressors,
|
340
|
+
self._sample_interval,
|
341
|
+
self._bit_time,
|
342
|
+
self._ami_params_in, # Prevents model from mucking up our input parameter string.
|
343
|
+
byref(self._ami_params_out),
|
344
|
+
byref(self._ami_mem_handle), # type: ignore
|
345
|
+
byref(self._msg),
|
346
|
+
)
|
347
|
+
except OSError as err:
|
348
|
+
print("pyibisami.ami_model.AMIModel.initialize(): Call to AMI_Init() bombed:")
|
349
|
+
print(err)
|
350
|
+
print(f"AMI_Init() address = {self._amiInit}")
|
351
|
+
print("Values sent into AMI_Init():")
|
352
|
+
print(f"&initOut = {byref(self._initOut)}") # type: ignore
|
353
|
+
print(f"row_size = {self._row_size}")
|
354
|
+
print(f"num_aggressors = {self._num_aggressors}")
|
355
|
+
print(f"sample_interval = {self._sample_interval}")
|
356
|
+
print(f"bit_time = {self._bit_time}")
|
357
|
+
print(f"ami_params_in = {ami_params_in}")
|
358
|
+
print(f"&ami_params_out = {byref(self._ami_params_out)}")
|
359
|
+
print(f"&ami_mem_handle = {byref(self._ami_mem_handle)}") # type: ignore
|
360
|
+
print(f"&msg = {byref(self._msg)}")
|
361
|
+
raise err
|
362
|
+
|
363
|
+
# Initialize attributes used by getWave().
|
364
|
+
bit_time = init_object.bit_time
|
365
|
+
sample_interval = init_object.sample_interval
|
366
|
+
# ToDo: Fix this. There isn't actually a requirement that `bit_time` be an integral multiple of `sample_interval`.
|
367
|
+
# And there may be an advantage to having it not be!
|
368
|
+
# if (bit_time % sample_interval) > (sample_interval / 100):
|
369
|
+
# raise ValueError(
|
370
|
+
# f"Bit time ({bit_time * 1e9: 6.3G} ns) must be an integral multiple of sample interval ({sample_interval * 1e9: 6.3G} ns)."
|
371
|
+
# )
|
372
|
+
self._samps_per_bit = int(bit_time / sample_interval) # pylint: disable=attribute-defined-outside-init
|
373
|
+
self._bits_per_call = ( # pylint: disable=attribute-defined-outside-init
|
374
|
+
init_object.row_size / self._samps_per_bit
|
375
|
+
)
|
376
|
+
|
377
|
+
def getWave(self, wave: Rvec, bits_per_call: int = 0) -> tuple[Rvec, Rvec, list[str]]: # noqa: F405
|
378
|
+
"""
|
379
|
+
Performs time domain processing of input waveform, using the ``AMI_GetWave()`` function.
|
380
|
+
|
381
|
+
Args:
|
382
|
+
wave: Waveform to be processed.
|
383
|
+
|
384
|
+
Keyword Args:
|
385
|
+
bits_per_call: Number of bits to use, per call to ``AMI_GetWave()``.
|
386
|
+
Default: 0 (Means "Use existing value.")
|
387
|
+
|
388
|
+
Returns:
|
389
|
+
A tuple containing
|
390
|
+
|
391
|
+
- the processed waveform,
|
392
|
+
- the recovered slicer sampling instants, and
|
393
|
+
- the list of output parameter strings received from each call to ``AMI_GetWave()``.
|
394
|
+
|
395
|
+
Notes:
|
396
|
+
1. The returned clock times are given in "pre-edge-aligned" fashion,
|
397
|
+
which means their values are: sampling instant - ui/2.
|
398
|
+
"""
|
399
|
+
|
400
|
+
if bits_per_call:
|
401
|
+
self._bits_per_call = int(bits_per_call) # pylint: disable=attribute-defined-outside-init
|
402
|
+
bits_per_call = int(self._bits_per_call)
|
403
|
+
samps_per_call = int(self._samps_per_bit * bits_per_call)
|
404
|
+
|
405
|
+
# Create the required C types.
|
406
|
+
Signal = c_double * samps_per_call
|
407
|
+
Clocks = c_double * (bits_per_call + 1) # The "+1" is critical, to prevent access violations by the model.
|
408
|
+
|
409
|
+
idx = 0 # Holds the starting index of the next processing chunk.
|
410
|
+
_clock_times = Clocks(0.0)
|
411
|
+
wave_out: list[float] = []
|
412
|
+
clock_times: list[float] = []
|
413
|
+
params_out: list[str] = []
|
414
|
+
input_len = len(wave)
|
415
|
+
while idx < input_len:
|
416
|
+
remaining_samps = input_len - idx
|
417
|
+
if remaining_samps < samps_per_call:
|
418
|
+
Signal = c_double * remaining_samps
|
419
|
+
tmp_wave = wave[idx:]
|
420
|
+
else:
|
421
|
+
tmp_wave = wave[idx: idx + samps_per_call]
|
422
|
+
_wave = Signal(*tmp_wave)
|
423
|
+
self._amiGetWave(
|
424
|
+
byref(_wave), len(_wave), byref(_clock_times), byref(self._ami_params_out), self._ami_mem_handle
|
425
|
+
) # type: ignore
|
426
|
+
wave_out.extend(_wave)
|
427
|
+
clock_times.extend(_clock_times)
|
428
|
+
params_out.append(self.ami_params_out)
|
429
|
+
idx += len(_wave)
|
430
|
+
|
431
|
+
return np.array(wave_out), np.array(clock_times[: len(wave_out) // self._samps_per_bit]), params_out
|
432
|
+
|
433
|
+
def get_responses( # pylint: disable=too-many-locals
|
434
|
+
self,
|
435
|
+
bits_per_call: int = 0,
|
436
|
+
pad_bits: int = 10,
|
437
|
+
nbits: int = 200,
|
438
|
+
calc_getw: bool = True
|
439
|
+
) -> dict[str, Any]:
|
440
|
+
"""
|
441
|
+
Get the impulse response of an initialized IBIS-AMI model, alone and convolved with the channel.
|
442
|
+
|
443
|
+
Keyword Args:
|
444
|
+
bits_per_call: Number of bits to include in the input to `GetWave()`.
|
445
|
+
Default: 0 (Means "use model's existing value".)
|
446
|
+
pad_bits: Number of bits to pad leading edge with when calling `GetWave()`,
|
447
|
+
to protect from initial garbage in `GetWave()` output.
|
448
|
+
Default: 10
|
449
|
+
nbits: Number of "real" bits to use for `GetWave()` testing.
|
450
|
+
Default: 200
|
451
|
+
calc_getw: Calculate ``GetWave()`` responses, also, when True.
|
452
|
+
Default: True
|
453
|
+
|
454
|
+
Returns:
|
455
|
+
Dictionary containing the responses under the following keys
|
456
|
+
|
457
|
+
- "imp_resp_init": The model's impulse response, from its `AMI_Init()` function (V/sample).
|
458
|
+
- "out_resp_init": `imp_resp_init` convolved with the channel.
|
459
|
+
- "imp_resp_getw": The model's impulse response, from its `AMI_GetWave()` function (V/sample).
|
460
|
+
- "out_resp_getw": `imp_resp_getw` convolved with the channel.
|
461
|
+
|
462
|
+
Notes:
|
463
|
+
1. If either set of keys (i.e. - "..._init" or "..._getw")
|
464
|
+
is missing from the returned dictionary, it means that
|
465
|
+
that mode of operation (`AMI_Init()` or `AMI_GetWave()`)
|
466
|
+
was not available in the given model.
|
467
|
+
|
468
|
+
2. An empty dictionary implies that neither the `Init_Returns_Impulse`
|
469
|
+
nor the `GetWave_Exists` AMI reserved parameter was True.
|
470
|
+
|
471
|
+
3. Note that impulse responses are returned with units: (V/sample), not (V/s).
|
472
|
+
|
473
|
+
ToDo:
|
474
|
+
1. Implement `bit_gen`.
|
475
|
+
2. Implement `ignore_bits`.
|
476
|
+
"""
|
477
|
+
|
478
|
+
rslt = {}
|
479
|
+
|
480
|
+
# Capture needed parameter definitions.
|
481
|
+
ui = self.bit_time
|
482
|
+
ts = self.sample_interval
|
483
|
+
info_params = self.info_params
|
484
|
+
ignore_bits = info_params["Ignore_Bits"] if "Ignore_Bits" in info_params else 0
|
485
|
+
|
486
|
+
# Capture/convert instance variables.
|
487
|
+
chnl_imp = np.array(self.channel_response) * ts # input (a.k.a. - "channel") impulse response (V/sample)
|
488
|
+
out_imp = np.array(self.initOut) * ts # output impulse response (V/sample)
|
489
|
+
|
490
|
+
# Calculate some needed intermediate values.
|
491
|
+
nspui = int(ui / ts) # samps per UI
|
492
|
+
pad_samps = pad_bits * nspui # leading edge padding samples for GetWave() calls
|
493
|
+
len_h = len(out_imp)
|
494
|
+
t = np.array([i * ts for i in range(-pad_samps, len_h - pad_samps)])
|
495
|
+
f = np.array([i * 1.0 / (ts * len_h) for i in range(len_h // 2 + 1)]) # Assumes `rfft()` is used.
|
496
|
+
|
497
|
+
# Extract and return the model responses.
|
498
|
+
if self.info_params["Init_Returns_Impulse"]:
|
499
|
+
h_model = deconv_same(out_imp, chnl_imp) # noqa: F405
|
500
|
+
rslt["imp_resp_init"] = np.roll(h_model, -len(h_model) // 2 + 3 * nspui)
|
501
|
+
|
502
|
+
h_init = np.roll(out_imp, pad_samps)
|
503
|
+
s_init = np.cumsum(h_init) # Step response.
|
504
|
+
p_init = s_init - np.pad(s_init[:-nspui], (nspui, 0), mode='constant', constant_values=0)
|
505
|
+
H_init = np.fft.rfft(self.initOut)
|
506
|
+
H_init *= s_init[-1] / np.abs(H_init[0]) # Normalize for proper d.c.
|
507
|
+
rslt["out_resp_init"] = (t, h_init, s_init, p_init, f, H_init)
|
508
|
+
|
509
|
+
if calc_getw and self.info_params["GetWave_Exists"]:
|
510
|
+
# Get model's step response.
|
511
|
+
rng = default_rng()
|
512
|
+
u = np.concatenate(
|
513
|
+
(rng.integers(low=0, high=2, size=ignore_bits),
|
514
|
+
np.array([0] * pad_bits + [1] * nbits))).repeat(nspui) - 0.5
|
515
|
+
wave_out, _, _ = self.getWave(u, bits_per_call=bits_per_call)
|
516
|
+
|
517
|
+
# Calculate impulse response from step response.
|
518
|
+
rslt["imp_resp_getw"] = np.diff(wave_out[(ignore_bits + pad_bits) * nspui:])
|
519
|
+
|
520
|
+
# Get step response of channel + model.
|
521
|
+
wave_in = np.convolve(u, chnl_imp)[:len(u)]
|
522
|
+
wave_out, _, _ = self.getWave(wave_in, bits_per_call=bits_per_call)
|
523
|
+
s_getw = wave_out[ignore_bits * nspui:][:len(t)] + 0.5
|
524
|
+
# Match the d.c. offset of Init() output, for easier comparison of Init() & GetWave() outputs.
|
525
|
+
s_getw -= s_getw[pad_samps - 1]
|
526
|
+
p_getw = s_getw - np.pad(s_getw[:-nspui], (nspui, 0), mode='constant', constant_values=0)
|
527
|
+
_s = s_getw[pad_samps:]
|
528
|
+
h_getw = np.insert(np.diff(_s), 0, _s[0])
|
529
|
+
len_hgw = len(h_getw)
|
530
|
+
if len_hgw > len_h:
|
531
|
+
h_getw = h_getw[:len_h]
|
532
|
+
else:
|
533
|
+
h_getw = np.pad(h_getw, (0, len_h - len_hgw))
|
534
|
+
H_getw = np.fft.rfft(h_getw)
|
535
|
+
rslt["out_resp_getw"] = (t, h_getw, s_getw, p_getw, f, H_getw)
|
536
|
+
|
537
|
+
return rslt
|
538
|
+
|
539
|
+
def _getInitOut(self):
|
540
|
+
return list(map(float, self._initOut))
|
541
|
+
|
542
|
+
initOut = property(_getInitOut, doc="Channel response convolved with model impulse response.")
|
543
|
+
|
544
|
+
def _getChannelResponse(self):
|
545
|
+
return list(map(float, self._channel_response))
|
546
|
+
|
547
|
+
channel_response = property(_getChannelResponse, doc="Channel response passed to initialize().")
|
548
|
+
|
549
|
+
def _getRowSize(self):
|
550
|
+
return self._row_size
|
551
|
+
|
552
|
+
row_size = property(_getRowSize, doc="Length of vector(s) passed to initialize().")
|
553
|
+
|
554
|
+
def _getNumAggressors(self):
|
555
|
+
return self._num_aggressors
|
556
|
+
|
557
|
+
num_aggressors = property(_getNumAggressors, doc="Number of rows in matrix passed to initialize(), minus one.")
|
558
|
+
|
559
|
+
def _getSampleInterval(self):
|
560
|
+
return float(self._sample_interval.value)
|
561
|
+
|
562
|
+
sample_interval = property(
|
563
|
+
_getSampleInterval, doc="Time interval between adjacent elements of the vector(s) passed to initialize()."
|
564
|
+
)
|
565
|
+
|
566
|
+
def _getBitTime(self):
|
567
|
+
return float(self._bit_time.value)
|
568
|
+
|
569
|
+
bit_time = property(_getBitTime, doc="Link unit interval, as passed to initialize().")
|
570
|
+
|
571
|
+
def _getAmiParamsIn(self):
|
572
|
+
return self._ami_params_in
|
573
|
+
|
574
|
+
ami_params_in = property(_getAmiParamsIn, doc="The AMI parameter string passed to AMI_Init() by initialize().")
|
575
|
+
|
576
|
+
def _getAmiParamsOut(self):
|
577
|
+
return self._ami_params_out.value
|
578
|
+
|
579
|
+
ami_params_out = property(
|
580
|
+
_getAmiParamsOut, doc="The AMI parameter string returned by either `AMI_Init()` or `AMI_GetWave()`."
|
581
|
+
)
|
582
|
+
|
583
|
+
def _getMsg(self):
|
584
|
+
return self._msg.value
|
585
|
+
|
586
|
+
msg = property(_getMsg, doc="Message returned by most recent call to AMI_Init() or AMI_GetWave().")
|
587
|
+
|
588
|
+
def _getClockTimes(self):
|
589
|
+
return self.clock_times
|
590
|
+
|
591
|
+
clock_times = property(_getClockTimes, doc="Clock times returned by most recent call to getWave().")
|
592
|
+
|
593
|
+
def _getInfoParams(self):
|
594
|
+
return self._info_params
|
595
|
+
|
596
|
+
info_params = property(_getInfoParams, doc="Reserved AMI parameter values for this model.")
|