pyibis-ami 7.2.1__py3-none-any.whl → 7.2.3__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.
pyibisami/ami/model.py CHANGED
@@ -1,596 +1,631 @@
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"].pvalue 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.")
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 __str__(self):
163
+ return "\n\t".join([
164
+ "AMIModelInitializer instance:",
165
+ f"`ami_params`: {self.ami_params}",
166
+ f"`info_params`: {self.ami_params}"])
167
+
168
+ def _getChannelResponse(self):
169
+ return list(map(float, self._init_data["channel_response"]))
170
+
171
+ def _setChannelResponse(self, h):
172
+ if isinstance(h, str) and Path(h).is_file():
173
+ h = interpFile(h, self.sample_interval)
174
+ Vector = c_double * len(h)
175
+ self._init_data["channel_response"] = Vector(*h)
176
+ self.row_size = len(h)
177
+
178
+ channel_response = property(
179
+ _getChannelResponse,
180
+ _setChannelResponse,
181
+ doc="Channel impulse response to be passed to AMI_Init(). May be a file name.",
182
+ )
183
+
184
+ def _getRowSize(self):
185
+ return self._init_data["row_size"]
186
+
187
+ def _setRowSize(self, n):
188
+ self._init_data["row_size"] = n
189
+
190
+ row_size = property(_getRowSize, _setRowSize, doc="Number of elements in channel response vector(s).")
191
+
192
+ def _getNumAggressors(self):
193
+ return self._init_data["num_aggressors"]
194
+
195
+ def _setNumAggressors(self, n):
196
+ self._init_data["num_aggressors"] = n
197
+
198
+ num_aggressors = property(
199
+ _getNumAggressors, _setNumAggressors, doc="Number of vectors in ``channel_response``, minus one."
200
+ )
201
+
202
+ def _getSampleInterval(self):
203
+ return float(self._init_data["sample_interval"].value)
204
+
205
+ def _setSampleInterval(self, T):
206
+ self._init_data["sample_interval"] = c_double(T)
207
+
208
+ sample_interval = property(
209
+ _getSampleInterval,
210
+ _setSampleInterval,
211
+ doc="Time interval between adjacent elements in channel response vector(s).",
212
+ )
213
+
214
+ def _getBitTime(self):
215
+ return float(self._init_data["bit_time"].value)
216
+
217
+ def _setBitTime(self, T):
218
+ self._init_data["bit_time"] = c_double(T)
219
+
220
+ bit_time = property(_getBitTime, _setBitTime, doc="Link unit interval.")
221
+
222
+
223
+ class AMIModel: # pylint: disable=too-many-instance-attributes
224
+ """
225
+ Class defining the structure and behavior of an IBIS-AMI Model.
226
+
227
+ Notes:
228
+ 1. Makes the calling of ``AMI_Close()`` automagic,
229
+ by calling it from the destructor.
230
+ """
231
+
232
+ def __init__(self, filename: str):
233
+ """
234
+ Load the dll and bind the 3 AMI functions.
235
+
236
+ Args:
237
+ filename: The DLL/SO file name.
238
+
239
+ Raises:
240
+ OSError: If given file cannot be opened.
241
+ """
242
+
243
+ self._filename = filename
244
+ self._ami_mem_handle = None
245
+ my_dll = CDLL(filename)
246
+ self._amiInit = my_dll.AMI_Init
247
+ self._amiClose = my_dll.AMI_Close
248
+ try:
249
+ self._amiGetWave = my_dll.AMI_GetWave
250
+ except Exception: # pylint: disable=broad-exception-caught
251
+ self._amiGetWave = None # type: ignore
252
+
253
+ def __del__(self):
254
+ """
255
+ Destructor - Calls ``AMI_Close()`` with handle to AMI model memory.
256
+
257
+ This obviates the need for the user to call the ``AMI_Close()``
258
+ function explicitly, and guards against memory leaks, during
259
+ PyLab command prompt operation, by ensuring that ``AMI_Close()``
260
+ gets called automagically when the model goes out of scope.
261
+ """
262
+ if self._ami_mem_handle:
263
+ self._amiClose(self._ami_mem_handle)
264
+
265
+ def __str__(self):
266
+ return "\n\t".join([
267
+ f"AMIModel instance: `{self._filename}`",
268
+ f"Length of initOut = {len(self._initOut)}",
269
+ f"row_size = {self._row_size}",
270
+ f"num_aggressors = {self._num_aggressors}",
271
+ f"sample_interval = {self._sample_interval}",
272
+ f"bit_time = {self._bit_time}",
273
+ f"samps_per_bit = {self._samps_per_bit}",
274
+ f"bits_per_call = {self._bits_per_call}",
275
+ f"ami_params_in = {self._ami_params_in}",
276
+ f"ami_params_out = {self._ami_params_out}",
277
+ f"&ami_mem_handle = {byref(self._ami_mem_handle)}",
278
+ f"Message = {self._msg}",
279
+ f"AMI_Init(): {self._amiInit}",
280
+ f"AMI_GetWave(): {self._amiGetWave}",
281
+ f"AMI_Close(): {self._amiClose}",])
282
+
283
+ def initialize(self, init_object: AMIModelInitializer):
284
+ """
285
+ Wraps the ``AMI_Init()`` function.
286
+
287
+ Args:
288
+ init_object: The model initialization data.
289
+
290
+ Notes:
291
+ 1. Takes an instance of ``AMIModelInitializer`` as its only argument.
292
+ This allows model initialization data to be constructed once,
293
+ and modified incrementally in between multiple calls of
294
+ ``initialize``. This is useful for *PyLab* command prompt testing.
295
+
296
+ ToDo:
297
+ 1. Allow for non-integral number of samples per unit interval.
298
+ """
299
+
300
+ # Free any memory allocated by the previous initialization.
301
+ if self._ami_mem_handle:
302
+ self._amiClose(self._ami_mem_handle)
303
+
304
+ # Set up the AMI_Init() arguments.
305
+ self._channel_response = ( # pylint: disable=attribute-defined-outside-init
306
+ init_object._init_data[ # pylint: disable=protected-access
307
+ "channel_response"
308
+ ]
309
+ )
310
+ self._initOut = cp.copy(self._channel_response) # type: ignore # pylint: disable=attribute-defined-outside-init
311
+ self._row_size = init_object._init_data[ # pylint: disable=protected-access,attribute-defined-outside-init
312
+ "row_size"
313
+ ]
314
+ self._num_aggressors = init_object._init_data[ # pylint: disable=protected-access,attribute-defined-outside-init
315
+ "num_aggressors"
316
+ ]
317
+ self._sample_interval = ( # pylint: disable=attribute-defined-outside-init
318
+ init_object._init_data[ # pylint: disable=protected-access
319
+ "sample_interval"
320
+ ]
321
+ )
322
+ self._bit_time = init_object._init_data[ # pylint: disable=protected-access,attribute-defined-outside-init
323
+ "bit_time"
324
+ ]
325
+ self._info_params = init_object.info_params # pylint: disable=attribute-defined-outside-init
326
+
327
+ # Check GetWave() consistency if possible.
328
+ if init_object.info_params and init_object.info_params["GetWave_Exists"]:
329
+ if not self._amiGetWave:
330
+ raise RuntimeError(
331
+ "Reserved parameter `GetWave_Exists` is True, but I can't bind to `AMI_GetWave()`!"
332
+ )
333
+
334
+ # Construct the AMI parameters string.
335
+ def sexpr(pname, pval):
336
+ """Create an S-expression from a parameter name/value pair, calling
337
+ recursively as needed to elaborate sub-parameter dictionaries."""
338
+ if isinstance(pval, str):
339
+ return f'({pname} "{pval}")'
340
+ if isinstance(pval, dict):
341
+ subs = []
342
+ for sname in pval:
343
+ subs.append(sexpr(sname, pval[sname]))
344
+ return sexpr(pname, " ".join(subs))
345
+ return f"({pname} {pval})"
346
+
347
+ ami_params_in = f"({init_object.ami_params['root_name']} "
348
+ for item in list(init_object.ami_params.items()):
349
+ if not item[0] == "root_name":
350
+ ami_params_in += sexpr(item[0], item[1])
351
+ ami_params_in += ")"
352
+ self._ami_params_in = ami_params_in.encode("utf-8") # pylint: disable=attribute-defined-outside-init
353
+
354
+ # Set handle types.
355
+ self._ami_params_out = c_char_p(b"") # pylint: disable=attribute-defined-outside-init
356
+ self._ami_mem_handle = c_char_p(None) # type: ignore # pylint: disable=attribute-defined-outside-init
357
+ self._msg = c_char_p(b"") # pylint: disable=attribute-defined-outside-init
358
+
359
+ # Call AMI_Init(), via our Python wrapper.
360
+ try:
361
+ self._amiInit(
362
+ byref(self._initOut), # type: ignore
363
+ self._row_size,
364
+ self._num_aggressors,
365
+ self._sample_interval,
366
+ self._bit_time,
367
+ self._ami_params_in, # Prevents model from mucking up our input parameter string.
368
+ byref(self._ami_params_out),
369
+ byref(self._ami_mem_handle), # type: ignore
370
+ byref(self._msg),
371
+ )
372
+ except OSError as err:
373
+ print("pyibisami.ami_model.AMIModel.initialize(): Call to AMI_Init() bombed:")
374
+ print(err)
375
+ print(f"AMI_Init() address = {self._amiInit}")
376
+ print("Values sent into AMI_Init():")
377
+ print(f"&initOut = {byref(self._initOut)}") # type: ignore
378
+ print(f"row_size = {self._row_size}")
379
+ print(f"num_aggressors = {self._num_aggressors}")
380
+ print(f"sample_interval = {self._sample_interval}")
381
+ print(f"bit_time = {self._bit_time}")
382
+ print(f"ami_params_in = {ami_params_in}")
383
+ print(f"&ami_params_out = {byref(self._ami_params_out)}")
384
+ print(f"&ami_mem_handle = {byref(self._ami_mem_handle)}") # type: ignore
385
+ print(f"&msg = {byref(self._msg)}")
386
+ raise err
387
+
388
+ # Initialize attributes used by getWave().
389
+ bit_time = init_object.bit_time
390
+ sample_interval = init_object.sample_interval
391
+ # ToDo: Fix this. There isn't actually a requirement that `bit_time` be an integral multiple of `sample_interval`.
392
+ # And there may be an advantage to having it not be!
393
+ # if (bit_time % sample_interval) > (sample_interval / 100):
394
+ # raise ValueError(
395
+ # f"Bit time ({bit_time * 1e9: 6.3G} ns) must be an integral multiple of sample interval ({sample_interval * 1e9: 6.3G} ns)."
396
+ # )
397
+ self._samps_per_bit = int(bit_time / sample_interval) # pylint: disable=attribute-defined-outside-init
398
+ self._bits_per_call = ( # pylint: disable=attribute-defined-outside-init
399
+ init_object.row_size / self._samps_per_bit
400
+ )
401
+
402
+ def getWave(self, wave: Rvec, bits_per_call: int = 0) -> tuple[Rvec, Rvec, list[str]]: # noqa: F405
403
+ """
404
+ Performs time domain processing of input waveform, using the ``AMI_GetWave()`` function.
405
+
406
+ Args:
407
+ wave: Waveform to be processed.
408
+
409
+ Keyword Args:
410
+ bits_per_call: Number of bits to use, per call to ``AMI_GetWave()``.
411
+ Default: 0 (Means "Use existing value.")
412
+
413
+ Returns:
414
+ A tuple containing
415
+
416
+ - the processed waveform,
417
+ - the recovered slicer sampling instants, and
418
+ - the list of output parameter strings received from each call to ``AMI_GetWave()``.
419
+
420
+ Notes:
421
+ 1. The returned clock times are given in "pre-edge-aligned" fashion,
422
+ which means their values are: sampling instant - ui/2.
423
+ """
424
+
425
+ if bits_per_call:
426
+ self._bits_per_call = int(bits_per_call) # pylint: disable=attribute-defined-outside-init
427
+ bits_per_call = int(self._bits_per_call)
428
+ samps_per_call = int(self._samps_per_bit * bits_per_call)
429
+
430
+ # Create the required C types.
431
+ Signal = c_double * samps_per_call
432
+ Clocks = c_double * (bits_per_call + 1) # The "+1" is critical, to prevent access violations by the model.
433
+
434
+ idx = 0 # Holds the starting index of the next processing chunk.
435
+ _clock_times = Clocks(0.0)
436
+ wave_out: list[float] = []
437
+ clock_times: list[float] = []
438
+ params_out: list[str] = []
439
+ input_len = len(wave)
440
+ while idx < input_len:
441
+ remaining_samps = input_len - idx
442
+ if remaining_samps < samps_per_call:
443
+ Signal = c_double * remaining_samps
444
+ tmp_wave = wave[idx:]
445
+ else:
446
+ tmp_wave = wave[idx: idx + samps_per_call]
447
+ _wave = Signal(*tmp_wave)
448
+ try:
449
+ self._amiGetWave(
450
+ byref(_wave), len(_wave), byref(_clock_times),
451
+ byref(self._ami_params_out), self._ami_mem_handle
452
+ ) # type: ignore
453
+ except OSError:
454
+ print(self)
455
+ print(f"byref(_wave): {byref(_wave)}")
456
+ print(f"len(_wave): {len(_wave)}")
457
+ print(f"byref(_clock_times): {byref(_clock_times)}")
458
+ print(f"byref(self._ami_params_out): {byref(self._ami_params_out)}")
459
+ print(f"self._ami_mem_handle: {self._ami_mem_handle}")
460
+ raise
461
+ wave_out.extend(_wave)
462
+ clock_times.extend(_clock_times)
463
+ params_out.append(self.ami_params_out)
464
+ idx += len(_wave)
465
+
466
+ return np.array(wave_out), np.array(clock_times[: len(wave_out) // self._samps_per_bit]), params_out
467
+
468
+ def get_responses( # pylint: disable=too-many-locals
469
+ self,
470
+ bits_per_call: int = 0,
471
+ pad_bits: int = 10,
472
+ nbits: int = 200,
473
+ calc_getw: bool = True
474
+ ) -> dict[str, Any]:
475
+ """
476
+ Get the impulse response of an initialized IBIS-AMI model, alone and convolved with the channel.
477
+
478
+ Keyword Args:
479
+ bits_per_call: Number of bits to include in the input to `GetWave()`.
480
+ Default: 0 (Means "use model's existing value".)
481
+ pad_bits: Number of bits to pad leading edge with when calling `GetWave()`,
482
+ to protect from initial garbage in `GetWave()` output.
483
+ Default: 10
484
+ nbits: Number of "real" bits to use for `GetWave()` testing.
485
+ Default: 200
486
+ calc_getw: Calculate ``GetWave()`` responses, also, when True.
487
+ Default: True
488
+
489
+ Returns:
490
+ Dictionary containing the responses under the following keys
491
+
492
+ - "imp_resp_init": The model's impulse response, from its `AMI_Init()` function (V/sample).
493
+ - "out_resp_init": `imp_resp_init` convolved with the channel.
494
+ - "imp_resp_getw": The model's impulse response, from its `AMI_GetWave()` function (V/sample).
495
+ - "out_resp_getw": `imp_resp_getw` convolved with the channel.
496
+
497
+ Notes:
498
+ 1. If either set of keys (i.e. - "..._init" or "..._getw")
499
+ is missing from the returned dictionary, it means that
500
+ that mode of operation (`AMI_Init()` or `AMI_GetWave()`)
501
+ was not available in the given model.
502
+
503
+ 2. An empty dictionary implies that neither the `Init_Returns_Impulse`
504
+ nor the `GetWave_Exists` AMI reserved parameter was True.
505
+
506
+ 3. Note that impulse responses are returned with units: (V/sample), not (V/s).
507
+
508
+ ToDo:
509
+ 1. Implement `bit_gen`.
510
+ 2. Implement `ignore_bits`.
511
+ """
512
+
513
+ rslt = {}
514
+
515
+ # Capture needed parameter definitions.
516
+ ui = self.bit_time
517
+ ts = self.sample_interval
518
+ info_params = self.info_params
519
+ ignore_bits = info_params["Ignore_Bits"].pvalue if "Ignore_Bits" in info_params else 0
520
+
521
+ # Capture/convert instance variables.
522
+ chnl_imp = np.array(self.channel_response) * ts # input (a.k.a. - "channel") impulse response (V/sample)
523
+ out_imp = np.array(self.initOut) * ts # output impulse response (V/sample)
524
+
525
+ # Calculate some needed intermediate values.
526
+ nspui = int(ui / ts) # samps per UI
527
+ pad_samps = pad_bits * nspui # leading edge padding samples for GetWave() calls
528
+ len_h = len(out_imp)
529
+ t = np.array([i * ts for i in range(-pad_samps, len_h - pad_samps)])
530
+ f = np.array([i * 1.0 / (ts * len_h) for i in range(len_h // 2 + 1)]) # Assumes `rfft()` is used.
531
+
532
+ # Extract and return the model responses.
533
+ if self.info_params["Init_Returns_Impulse"]:
534
+ h_model = deconv_same(out_imp, chnl_imp) # noqa: F405
535
+ rslt["imp_resp_init"] = np.roll(h_model, -len(h_model) // 2 + 3 * nspui)
536
+
537
+ h_init = np.roll(out_imp, pad_samps)
538
+ s_init = np.cumsum(h_init) # Step response.
539
+ p_init = s_init - np.pad(s_init[:-nspui], (nspui, 0), mode='constant', constant_values=0)
540
+ H_init = np.fft.rfft(self.initOut)
541
+ H_init *= s_init[-1] / np.abs(H_init[0]) # Normalize for proper d.c.
542
+ rslt["out_resp_init"] = (t, h_init, s_init, p_init, f, H_init)
543
+
544
+ if calc_getw and self.info_params["GetWave_Exists"]:
545
+ # Get model's step response.
546
+ rng = default_rng()
547
+ u = np.concatenate(
548
+ (rng.integers(low=0, high=2, size=ignore_bits),
549
+ np.array([0] * pad_bits + [1] * nbits))).repeat(nspui) - 0.5
550
+ wave_out, _, _ = self.getWave(u, bits_per_call=bits_per_call)
551
+
552
+ # Calculate impulse response from step response.
553
+ rslt["imp_resp_getw"] = np.diff(wave_out[(ignore_bits + pad_bits) * nspui:])
554
+
555
+ # Get step response of channel + model.
556
+ wave_in = np.convolve(u, chnl_imp)[:len(u)]
557
+ wave_out, _, _ = self.getWave(wave_in, bits_per_call=bits_per_call)
558
+ s_getw = wave_out[ignore_bits * nspui:][:len(t)] + 0.5
559
+ # Match the d.c. offset of Init() output, for easier comparison of Init() & GetWave() outputs.
560
+ s_getw -= s_getw[pad_samps - 1]
561
+ p_getw = s_getw - np.pad(s_getw[:-nspui], (nspui, 0), mode='constant', constant_values=0)
562
+ _s = s_getw[pad_samps:]
563
+ h_getw = np.insert(np.diff(_s), 0, _s[0])
564
+ len_hgw = len(h_getw)
565
+ if len_hgw > len_h:
566
+ h_getw = h_getw[:len_h]
567
+ else:
568
+ h_getw = np.pad(h_getw, (0, len_h - len_hgw))
569
+ H_getw = np.fft.rfft(h_getw)
570
+ rslt["out_resp_getw"] = (t, h_getw, s_getw, p_getw, f, H_getw)
571
+
572
+ return rslt
573
+
574
+ def _getInitOut(self):
575
+ return list(map(float, self._initOut))
576
+
577
+ initOut = property(_getInitOut, doc="Channel response convolved with model impulse response.")
578
+
579
+ def _getChannelResponse(self):
580
+ return list(map(float, self._channel_response))
581
+
582
+ channel_response = property(_getChannelResponse, doc="Channel response passed to initialize().")
583
+
584
+ def _getRowSize(self):
585
+ return self._row_size
586
+
587
+ row_size = property(_getRowSize, doc="Length of vector(s) passed to initialize().")
588
+
589
+ def _getNumAggressors(self):
590
+ return self._num_aggressors
591
+
592
+ num_aggressors = property(_getNumAggressors, doc="Number of rows in matrix passed to initialize(), minus one.")
593
+
594
+ def _getSampleInterval(self):
595
+ return float(self._sample_interval.value)
596
+
597
+ sample_interval = property(
598
+ _getSampleInterval, doc="Time interval between adjacent elements of the vector(s) passed to initialize()."
599
+ )
600
+
601
+ def _getBitTime(self):
602
+ return float(self._bit_time.value)
603
+
604
+ bit_time = property(_getBitTime, doc="Link unit interval, as passed to initialize().")
605
+
606
+ def _getAmiParamsIn(self):
607
+ return self._ami_params_in
608
+
609
+ ami_params_in = property(_getAmiParamsIn, doc="The AMI parameter string passed to AMI_Init() by initialize().")
610
+
611
+ def _getAmiParamsOut(self):
612
+ return self._ami_params_out.value
613
+
614
+ ami_params_out = property(
615
+ _getAmiParamsOut, doc="The AMI parameter string returned by either `AMI_Init()` or `AMI_GetWave()`."
616
+ )
617
+
618
+ def _getMsg(self):
619
+ return self._msg.value
620
+
621
+ msg = property(_getMsg, doc="Message returned by most recent call to AMI_Init() or AMI_GetWave().")
622
+
623
+ def _getClockTimes(self):
624
+ return self.clock_times
625
+
626
+ clock_times = property(_getClockTimes, doc="Clock times returned by most recent call to getWave().")
627
+
628
+ def _getInfoParams(self):
629
+ return self._info_params
630
+
631
+ info_params = property(_getInfoParams, doc="Reserved AMI parameter values for this model.")