PyChOpMarg 0.1__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.
- PyChOpMarg-0.1.dist-info/LICENSE +28 -0
- PyChOpMarg-0.1.dist-info/METADATA +35 -0
- PyChOpMarg-0.1.dist-info/RECORD +10 -0
- PyChOpMarg-0.1.dist-info/WHEEL +5 -0
- PyChOpMarg-0.1.dist-info/entry_points.txt +2 -0
- PyChOpMarg-0.1.dist-info/top_level.txt +1 -0
- pychopmarg/__init__.py +0 -0
- pychopmarg/com.py +955 -0
- pychopmarg/common.py +23 -0
- pychopmarg/utility.py +177 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, David Banas
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: PyChOpMarg
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Python implementation of COM, as per IEEE 802.3-22 Annex 93A.
|
|
5
|
+
Author-email: David Banas <capn.freako@gmail.com>
|
|
6
|
+
License: BSD3
|
|
7
|
+
Keywords: com,channel,margin
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Education
|
|
12
|
+
Classifier: Intended Audience :: Telecommunications Industry
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
+
Classifier: Topic :: Adaptive Technologies
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
25
|
+
Classifier: Topic :: System :: Emulators
|
|
26
|
+
Classifier: Topic :: System :: Networking
|
|
27
|
+
Classifier: Topic :: Utilities
|
|
28
|
+
Requires-Python: <3.12,>=3.9
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: scikit-rf >=0.29
|
|
32
|
+
Requires-Dist: traitsui >=8
|
|
33
|
+
|
|
34
|
+
# PyChOpMarg
|
|
35
|
+
Python implementation of COM, as per IEEE 802.3-22 Annex 93A.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pychopmarg/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pychopmarg/com.py,sha256=DrwhRpp78MKjTFH-5-5GUZJjnDuVMbKxNI0zlQLTv3k,35096
|
|
3
|
+
pychopmarg/common.py,sha256=5eD4JSl1aZRkWzY4aFVaV9KRide1MfOzkSs_RzuhZf4,695
|
|
4
|
+
pychopmarg/utility.py,sha256=I3DfhEvCYs0Il0ukphEX7QXLS_L8q9BcJiMxI6mz3pc,6895
|
|
5
|
+
PyChOpMarg-0.1.dist-info/LICENSE,sha256=VRMCdafVsUW1f27QIl9ux4hpudwS4xelQF0tE7iK0L8,1498
|
|
6
|
+
PyChOpMarg-0.1.dist-info/METADATA,sha256=YzGdERj1UyBQ7Ap026NXMc2leiss82hx3SJP3Qh5Xro,1433
|
|
7
|
+
PyChOpMarg-0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
8
|
+
PyChOpMarg-0.1.dist-info/entry_points.txt,sha256=2fyoTM8oK6ZBo91T6aHJ6biwJD8C_UcerXr3b2BlA9Q,50
|
|
9
|
+
PyChOpMarg-0.1.dist-info/top_level.txt,sha256=QwSERRGWYBocozXwQoX7n-dnlaT7IKgx8dnOXCABp6g,11
|
|
10
|
+
PyChOpMarg-0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pychopmarg
|
pychopmarg/__init__.py
ADDED
|
File without changes
|
pychopmarg/com.py
ADDED
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Channel Operating Margin (COM) model, as per IEEE 802.3-22 Annex 93A.
|
|
3
|
+
|
|
4
|
+
Original author: David Banas <capn.freako@gmail.com>
|
|
5
|
+
|
|
6
|
+
Original date: February 29, 2024
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 David Banas; all rights reserved World wide.
|
|
9
|
+
|
|
10
|
+
Notes:
|
|
11
|
+
1. Throughout this file, equation numbers refer to Annex 93A of the IEEE 802.3-22 standard.
|
|
12
|
+
|
|
13
|
+
ToDo:
|
|
14
|
+
1. Provide type hints for imports.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numpy as np # type: ignore
|
|
18
|
+
import skrf as rf # type: ignore
|
|
19
|
+
|
|
20
|
+
from scipy.interpolate import interp1d
|
|
21
|
+
from traits.api import HasTraits, Property, Array, Float, cached_property # type: ignore
|
|
22
|
+
from typing import TypeVar, Any, Dict
|
|
23
|
+
|
|
24
|
+
from pychopmarg.common import Rvec, Cvec, COMParams, PI, TWOPI
|
|
25
|
+
from pychopmarg.utility import import_s32p, sdd_21
|
|
26
|
+
|
|
27
|
+
# Globals used by `calc_Hffe()`, to minimize the size of its cache.
|
|
28
|
+
# They are initialized by `COM.__init__()`.
|
|
29
|
+
gFreqs: Rvec = None # type: ignore
|
|
30
|
+
gFb: float = None # type: ignore
|
|
31
|
+
gC0min: float = None # type: ignore
|
|
32
|
+
gNtaps: int = None # type: ignore
|
|
33
|
+
|
|
34
|
+
T = TypeVar('T', Any, Any)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def all_combs(xss: list[list[T]]) -> list[list[T]]:
|
|
38
|
+
"""
|
|
39
|
+
Generate all combinations of input.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
xss([[T]]): The lists of candidates for each position in the final output.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
[[T]]: All possible combinations of input lists.
|
|
46
|
+
"""
|
|
47
|
+
if not xss:
|
|
48
|
+
return [[]]
|
|
49
|
+
head, *tail = xss
|
|
50
|
+
yss = all_combs(tail)
|
|
51
|
+
return [[x] + ys for x in head for ys in yss]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# @cache
|
|
55
|
+
# def calc_Hffe(tap_weights: Rvec) -> Cvec:
|
|
56
|
+
def calc_Hffe(tap_weights: list[float]) -> Cvec:
|
|
57
|
+
"""
|
|
58
|
+
Calculate the voltage transfer function, H(f), for the Tx FFE,
|
|
59
|
+
according to (93A-21).
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
tap_weights: The vector of filter tap weights, excluding the cursor tap.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The complex voltage transfer function, H(f), for the Tx FFE.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
RuntimeError: If the global variables above haven't been initialized.
|
|
69
|
+
ValueError: If the length of the given tap weight vector is incorrect.
|
|
70
|
+
|
|
71
|
+
Notes:
|
|
72
|
+
1. This function has been (awkwardly) pulled outside of the
|
|
73
|
+
`COM` class and made to use global variables, strictly for
|
|
74
|
+
performance reasons.
|
|
75
|
+
(Note that `@cached_property` decorated instance functions
|
|
76
|
+
of `HasTraits` subclasses are not actually memoized, like
|
|
77
|
+
`@cache` decorated ordinary functions are.)
|
|
78
|
+
(It is used in the innermost layer of the nested loop structure
|
|
79
|
+
used to find the optimal EQ solution. And its input argument
|
|
80
|
+
is repeated often.)
|
|
81
|
+
(See the `opt_eq()` method of the `COM` class.)
|
|
82
|
+
2. Currently, a single post-cursor tap is assumed.
|
|
83
|
+
|
|
84
|
+
ToDo:
|
|
85
|
+
1. Remove the single post-cursor tap assumption.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
assert len(gFreqs) and gFb and gC0min and gNtaps, RuntimeError(
|
|
89
|
+
"Called before global variables were initialized!")
|
|
90
|
+
assert len(tap_weights) == gNtaps, ValueError(
|
|
91
|
+
"Length of given tap weight vector is incorrect!")
|
|
92
|
+
|
|
93
|
+
c0 = 1 - sum(list(map(abs, tap_weights)))
|
|
94
|
+
if c0 < gC0min:
|
|
95
|
+
return np.ones(len(gFreqs))
|
|
96
|
+
else:
|
|
97
|
+
cs = tap_weights
|
|
98
|
+
cs.insert(-1, c0) # Note the assumption of only one post-cursor tap!
|
|
99
|
+
return sum(list(map(lambda n_c: n_c[1] * np.exp(-1j * TWOPI * n_c[0] * gFreqs / gFb),
|
|
100
|
+
enumerate(cs))))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class COM(HasTraits):
|
|
104
|
+
"""
|
|
105
|
+
Encoding of the IEEE 802.3-22 Annex 93A "Channel Operating Margin"
|
|
106
|
+
(COM) specification, as a Python class making use of the Enthought
|
|
107
|
+
Traits/UI machinery, for both calculation efficiency and easy GUI display.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# Independent variable definitions
|
|
111
|
+
ui = Float(100e-12) # Unit interval (s).
|
|
112
|
+
freqs = Array(value=np.arange(0, 40_010e6, 10e6)) # System frequencies (Hz).
|
|
113
|
+
gDC = Float(0) # D.C. gain of Rx CTLE first stage (dB).
|
|
114
|
+
gDC2 = Float(0) # D.C. gain of Rx CTLE first stage (dB).
|
|
115
|
+
|
|
116
|
+
# Dependent variable definitions
|
|
117
|
+
Xsinc = Property(Array, depends_on=["ui", "freqs"])
|
|
118
|
+
|
|
119
|
+
@cached_property
|
|
120
|
+
def _get_Xsinc(self):
|
|
121
|
+
"""Frequency domain sinc(f) corresponding to Rect(ui)."""
|
|
122
|
+
ui = self.ui
|
|
123
|
+
w = int(ui / self.Ts)
|
|
124
|
+
return w * np.sinc(ui * self.freqs)
|
|
125
|
+
|
|
126
|
+
Hr = Property(Array, depends_on=['freqs'])
|
|
127
|
+
|
|
128
|
+
@cached_property
|
|
129
|
+
def _get_Hr(self):
|
|
130
|
+
"""
|
|
131
|
+
Return the voltage transfer function, H(f), of the Rx AFE,
|
|
132
|
+
according to (93A-20).
|
|
133
|
+
"""
|
|
134
|
+
f = self.freqs / (self.params['fr'] * self.fb)
|
|
135
|
+
return 1 / (1 - 3.414214 * f**2 + f**4 + 2.613126j * (f - f**3))
|
|
136
|
+
|
|
137
|
+
Hctf = Property(Array, depends_on=['freqs', 'gDC', 'gDC2'])
|
|
138
|
+
|
|
139
|
+
@cached_property
|
|
140
|
+
def _get_Hctf(self):
|
|
141
|
+
"""
|
|
142
|
+
Return the voltage transfer function, H(f), of the Rx CTLE,
|
|
143
|
+
according to (93A-22).
|
|
144
|
+
"""
|
|
145
|
+
f = self.freqs
|
|
146
|
+
g1 = pow(10, self.gDC / 20)
|
|
147
|
+
g2 = pow(10, self.gDC2 / 20)
|
|
148
|
+
fz = self.params['fz'] * 1e9
|
|
149
|
+
fp1 = self.params['fp1'] * 1e9
|
|
150
|
+
fp2 = self.params['fp2'] * 1e9
|
|
151
|
+
fLF = self.params['fLF'] * 1e9
|
|
152
|
+
num = (g1 + 1j * f / fz) * (g2 + 1j * f / fLF)
|
|
153
|
+
den = (1 + 1j * f / fp1) * (1 + 1j * f / fp2) * (1 + 1j * f / fLF)
|
|
154
|
+
return num / den
|
|
155
|
+
|
|
156
|
+
# Reserved functions
|
|
157
|
+
|
|
158
|
+
def __call__(self, do_opt_eq=True, tx_taps: Rvec = None):
|
|
159
|
+
"""
|
|
160
|
+
Calculate the COM value.
|
|
161
|
+
|
|
162
|
+
KeywordArgs:
|
|
163
|
+
opt_eq: Perform optimization of linear equalization when True.
|
|
164
|
+
Default: True
|
|
165
|
+
tx_taps: Used when `do_opt_eq` = False.
|
|
166
|
+
Default: None
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
assert self.opt_eq(do_opt_eq=do_opt_eq, tx_taps=tx_taps), RuntimeError(
|
|
170
|
+
"EQ optimization failed!")
|
|
171
|
+
self.gDC = self.rslts['gDC']
|
|
172
|
+
self.gDC2 = self.rslts['gDC2']
|
|
173
|
+
self.tx_taps = self.rslts['tx_taps']
|
|
174
|
+
|
|
175
|
+
As, Ani, cursor_ix = self.calc_noise()
|
|
176
|
+
com = 20 * np.log10(As / Ani)
|
|
177
|
+
self.com = com
|
|
178
|
+
self.rslts['As'] = As * 1_000 # (mV)
|
|
179
|
+
self.rslts['Ani'] = Ani * 1_000 # (mV)
|
|
180
|
+
self.rslts['com'] = com
|
|
181
|
+
self.cursor_ix = cursor_ix
|
|
182
|
+
return com
|
|
183
|
+
|
|
184
|
+
def __init__(self, params: COMParams, chnl_files: list[str], vic_chnl_ix: int,
|
|
185
|
+
zp_sel: int = 1, num_ui: int = 100, gui: bool = True):
|
|
186
|
+
"""
|
|
187
|
+
COM class initializer.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
params: COM configuration parameters for desired standard.
|
|
191
|
+
Note: Assumed immutable. ToDo: Can we encode this assumption, using type annotations?
|
|
192
|
+
chnl_files: Touchstone file(s) representing channel, either:
|
|
193
|
+
1. 8 s4p files: [victim, ], or
|
|
194
|
+
2. 1 s32p file, according to VITA 68.2 convention.
|
|
195
|
+
vic_chnl_ix: Victim channel index (from 1).
|
|
196
|
+
|
|
197
|
+
KeywordArgs:
|
|
198
|
+
zp_sel: User selection of package T-line length option (from 1).
|
|
199
|
+
Default: 1
|
|
200
|
+
num_ui: Number of unit intervals to include in system time vector.
|
|
201
|
+
Default: 100
|
|
202
|
+
gui: Set to `False` for script/CLI based usage.
|
|
203
|
+
Default: True
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
# Super-class initialization is ABSOLUTELY NECESSARY, in order
|
|
207
|
+
# to get all the Traits/UI machinery setup correctly.
|
|
208
|
+
super().__init__()
|
|
209
|
+
|
|
210
|
+
# Set default parameter values, as necessary.
|
|
211
|
+
if 'zc' not in params:
|
|
212
|
+
params['zc'] = 78.2
|
|
213
|
+
if 'fLF' not in params:
|
|
214
|
+
params['fLF'] = 1
|
|
215
|
+
|
|
216
|
+
# Stash function parameters.
|
|
217
|
+
self.params = params
|
|
218
|
+
self.chnl_files = chnl_files
|
|
219
|
+
self.gui = gui
|
|
220
|
+
|
|
221
|
+
# Calculate intermediate variable values.
|
|
222
|
+
self.rslts = {}
|
|
223
|
+
self.fom_rslts: Dict[str, Any] = {}
|
|
224
|
+
self.dbg: Dict[str, Any] = {}
|
|
225
|
+
fb = params['fb'] * 1e9
|
|
226
|
+
ui = 1 / fb
|
|
227
|
+
M = self.params['M']
|
|
228
|
+
sample_per = ui / M
|
|
229
|
+
fstep = self.params['fstep']
|
|
230
|
+
trips = list(zip(self.params['tx_min'], self.params['tx_max'], self.params['tx_step']))
|
|
231
|
+
self.tx_combs = all_combs(list(map(
|
|
232
|
+
lambda trip: list(np.arange(trip[0], trip[1] + trip[2], trip[2])),
|
|
233
|
+
trips)))
|
|
234
|
+
self.gamma1 = (self.params['Rd'] - self.params['R0']) / (self.params['Rd'] + self.params['R0'])
|
|
235
|
+
self.gamma2 = self.gamma1
|
|
236
|
+
|
|
237
|
+
# Import channel Touchstone file(s).
|
|
238
|
+
if len(chnl_files) == 1:
|
|
239
|
+
_ntwks = import_s32p(chnl_files[0], vic_chnl_ix)
|
|
240
|
+
fmax = _ntwks[0][0].f[-1]
|
|
241
|
+
else: # ToDo: Do I have the ordering correct here?
|
|
242
|
+
_ntwks = []
|
|
243
|
+
fmax = 1000e9
|
|
244
|
+
n_files = len(chnl_files)
|
|
245
|
+
for n, chnl_file in enumerate(chnl_files):
|
|
246
|
+
ntwk = sdd_21(rf.Network(chnl_file))
|
|
247
|
+
fmax = min(fmax, ntwk.f[-1])
|
|
248
|
+
if n >= n_files // 2: # This one is a NEXT aggressor.
|
|
249
|
+
_ntwks.append((ntwk, 'NEXT'))
|
|
250
|
+
elif n > 0: # This one is a FEXT aggressor.
|
|
251
|
+
_ntwks.append((ntwk, 'FEXT'))
|
|
252
|
+
else: # This is the victim.
|
|
253
|
+
_ntwks.append((ntwk, 'THRU'))
|
|
254
|
+
|
|
255
|
+
# Calculate system time/frequency vectors.
|
|
256
|
+
# - We use "decoupled" time/frequency vectors.
|
|
257
|
+
freqs = np.arange(0, fmax + fstep, fstep) # [0,fN], i.e., one extra element
|
|
258
|
+
times = np.array([n * sample_per for n in range(num_ui * M)]) # independent of `freqs`
|
|
259
|
+
# - That requires a third vector for correctly timing `irfft()` results.
|
|
260
|
+
Ts = 0.5 / fmax # Sample period satisfying Nyquist criteria.
|
|
261
|
+
t_irfft = np.array([n * Ts for n in range(2 * (len(freqs) - 1))])
|
|
262
|
+
self.freqs = freqs
|
|
263
|
+
self.times = times
|
|
264
|
+
self.Ts = Ts # Not the same as `sample_per`!
|
|
265
|
+
self.t_irfft = t_irfft
|
|
266
|
+
|
|
267
|
+
# Augment w/ packages.
|
|
268
|
+
sTx_NEXT = self.sPkg(zp_opt=1, isTx=True)
|
|
269
|
+
sTx = self.sPkg(zp_opt=zp_sel, isTx=True)
|
|
270
|
+
sRx = self.sPkg(zp_opt=zp_sel, isTx=False)
|
|
271
|
+
ntwks = []
|
|
272
|
+
for ntwk, ntype in _ntwks:
|
|
273
|
+
if ntype == 'NEXT':
|
|
274
|
+
ntwks.append((sTx_NEXT ** ntwk ** sRx, ntype))
|
|
275
|
+
else:
|
|
276
|
+
ntwks.append((sTx ** ntwk ** sRx, ntype))
|
|
277
|
+
|
|
278
|
+
# Generate the pulse responses before/after adding the packages, for reference.
|
|
279
|
+
self.ntwks = _ntwks
|
|
280
|
+
self.pulse_resps_nopkg = self.gen_pulse_resps([], apply_eq=False)
|
|
281
|
+
self.ntwks = ntwks
|
|
282
|
+
self.pulse_resps_noeq = self.gen_pulse_resps([], apply_eq=False)
|
|
283
|
+
self.rslts['vic_pulse_pk'] = params['Av'] * max(self.pulse_resps_noeq[0]) * 1_000 # (mV)
|
|
284
|
+
|
|
285
|
+
# Store calculated results.
|
|
286
|
+
self.fb = fb
|
|
287
|
+
self.ui = ui
|
|
288
|
+
self.sample_per = sample_per
|
|
289
|
+
self.fmax = fmax
|
|
290
|
+
self.nDFE = len(self.params['dfe_max'])
|
|
291
|
+
|
|
292
|
+
# Initialize global variables.
|
|
293
|
+
global gFreqs
|
|
294
|
+
global gFb
|
|
295
|
+
global gC0min
|
|
296
|
+
global gNtaps
|
|
297
|
+
gFreqs = freqs
|
|
298
|
+
gFb = fb
|
|
299
|
+
gC0min = params['c0min']
|
|
300
|
+
gNtaps = len(params['tx_min'])
|
|
301
|
+
|
|
302
|
+
# General functions
|
|
303
|
+
def sC(self, c: float) -> rf.Network:
|
|
304
|
+
"""
|
|
305
|
+
Return the 2-port network corresponding to a shunt capacitance,
|
|
306
|
+
according to (93A-8).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
c: Value of shunt capacitance (F).
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
ntwk: 2-port network equivalent to shunt capacitance, calculated at given frequencies.
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
None
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
r0 = self.params['R0']
|
|
319
|
+
freqs = self.freqs
|
|
320
|
+
w = TWOPI * freqs
|
|
321
|
+
s = 1j * w
|
|
322
|
+
s2p = np.array(
|
|
323
|
+
[1 / (2 + _s * c * r0) * np.array(
|
|
324
|
+
[[-_s * c * r0, 2],
|
|
325
|
+
[2, -_s * c * r0]])
|
|
326
|
+
for _s in s])
|
|
327
|
+
return rf.Network(s=s2p, f=freqs, z0=[2 * r0, 2 * r0])
|
|
328
|
+
|
|
329
|
+
def sZp(self, zp_opt: int = 1) -> rf.Network:
|
|
330
|
+
"""
|
|
331
|
+
Return the 2-port network corresponding to a package transmission line,
|
|
332
|
+
according to (93A-9:14).
|
|
333
|
+
|
|
334
|
+
KeywordArgs:
|
|
335
|
+
zp_opt: Package TL length option (from 1).
|
|
336
|
+
Default: 1
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
ntwk: 2-port network equivalent to package transmission line.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
None
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
n_zp_opts = len(self.params['zp'])
|
|
346
|
+
assert zp_opt <= n_zp_opts, ValueError(
|
|
347
|
+
f"Asked for zp option {zp_opt}, but there are only {n_zp_opts}!")
|
|
348
|
+
zc = self.params['zc']
|
|
349
|
+
r0 = self.params['R0']
|
|
350
|
+
zp = self.params['zp'][zp_opt - 1]
|
|
351
|
+
|
|
352
|
+
f_GHz = self.freqs / 1e9
|
|
353
|
+
a1 = 1.734e-3 # sqrt(ns)/mm
|
|
354
|
+
a2 = 1.455e-4 # ns/mm
|
|
355
|
+
tau = 6.141e-3 # ns/mm
|
|
356
|
+
rho = (zc - 2 * r0) / (zc + 2 * r0)
|
|
357
|
+
gamma0 = 0 # 1/mm
|
|
358
|
+
gamma1 = a1 * (1 + 1j)
|
|
359
|
+
|
|
360
|
+
def gamma2(f):
|
|
361
|
+
"f in GHz!"
|
|
362
|
+
return a2 * (1 - 1j * (2 / PI) * np.log(f)) + 1j * TWOPI * tau
|
|
363
|
+
|
|
364
|
+
def gamma(f: float) -> complex:
|
|
365
|
+
"Return complex propagation coefficient at frequency f (GHz)."
|
|
366
|
+
if f == 0:
|
|
367
|
+
return gamma0
|
|
368
|
+
else:
|
|
369
|
+
return gamma0 + gamma1 * np.sqrt(f) + gamma2(f) * f
|
|
370
|
+
|
|
371
|
+
g = np.array(list(map(gamma, f_GHz)))
|
|
372
|
+
s11 = s22 = rho * (1 - np.exp(-g * 2 * zp)) / (1 - rho**2 * np.exp(-g * 2 * zp))
|
|
373
|
+
s21 = s12 = (1 - rho**2) * np.exp(-g * zp) / (1 - rho**2 * np.exp(-g * 2 * zp))
|
|
374
|
+
s2p = np.array(
|
|
375
|
+
[[[_s11, _s12],
|
|
376
|
+
[_s21, _s22]]
|
|
377
|
+
for _s11, _s12, _s21, _s22 in zip(s11, s12, s21, s22)])
|
|
378
|
+
return rf.Network(s=s2p, f=self.freqs, z0=[2 * r0, 2 * r0])
|
|
379
|
+
|
|
380
|
+
def sPkg(self, zp_opt: int = 1, isTx: bool = True) -> rf.Network:
|
|
381
|
+
"""
|
|
382
|
+
Return the 2-port network corresponding to a complete package model,
|
|
383
|
+
according to (93A-15:16).
|
|
384
|
+
|
|
385
|
+
KeywordArgs:
|
|
386
|
+
zp_opt: Package TL length option (from 1).
|
|
387
|
+
Default: 1
|
|
388
|
+
isTx: Requesting Tx package when True.
|
|
389
|
+
Default: True
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
ntwk: 2-port network equivalent to complete package model.
|
|
393
|
+
|
|
394
|
+
Raises:
|
|
395
|
+
None
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
sd = self.sC(self.params['Cd'] / 1e12)
|
|
399
|
+
sp = self.sC(self.params['Cp'] / 1e12)
|
|
400
|
+
sl = self.sZp(zp_opt)
|
|
401
|
+
if isTx:
|
|
402
|
+
return sd ** sl ** sp
|
|
403
|
+
else:
|
|
404
|
+
return sp ** sl ** sd
|
|
405
|
+
|
|
406
|
+
def H21(self, s2p: rf.Network) -> Cvec:
|
|
407
|
+
"""
|
|
408
|
+
Return the voltage transfer function, H21(f), of a terminated two
|
|
409
|
+
port network, according to (93A-18).
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
s2p: Two port network of interest.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Complex voltage transfer function at given frequencies.
|
|
416
|
+
|
|
417
|
+
Raises:
|
|
418
|
+
ValueError: If given network is not two port.
|
|
419
|
+
|
|
420
|
+
Notes:
|
|
421
|
+
1. It is at this point in the analysis that the "raw" Touchstone
|
|
422
|
+
data gets interpolated to our system frequency vector.
|
|
423
|
+
2. After this step, the package and R0/Rd mismatch have been
|
|
424
|
+
accounted for, but not the EQ.
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
assert s2p.s[0].shape == (2, 2), ValueError("I can only convert 2-port networks.")
|
|
428
|
+
s2p.interpolate_self(self.freqs)
|
|
429
|
+
g1 = self.gamma1
|
|
430
|
+
g2 = self.gamma2
|
|
431
|
+
s11 = s2p.s11.s.flatten()
|
|
432
|
+
s12 = s2p.s12.s.flatten()
|
|
433
|
+
s21 = s2p.s21.s.flatten()
|
|
434
|
+
s22 = s2p.s22.s.flatten()
|
|
435
|
+
dS = s11 * s22 - s12 * s21
|
|
436
|
+
return (s21 * (1 - g1) * (1 + g2)) / (1 - s11 * g1 - s22 * g2 + g1 * g2 * dS)
|
|
437
|
+
|
|
438
|
+
def H(self, s2p: rf.Network, tap_weights: Rvec) -> Cvec:
|
|
439
|
+
"""
|
|
440
|
+
Return the voltage transfer function, H(f), of a complete COM signal path,
|
|
441
|
+
according to (93A-19).
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
s2p: Two port network of interest.
|
|
445
|
+
tap_weights: Tx FFE tap weights.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Complex voltage transfer function of complete path.
|
|
449
|
+
|
|
450
|
+
Raises:
|
|
451
|
+
ValueError: If given network is not two port,
|
|
452
|
+
or length of `tap_weights` is incorrect.
|
|
453
|
+
|
|
454
|
+
Notes:
|
|
455
|
+
1. Assumes `self.gDC` and `self.gDC2` have been set correctly.
|
|
456
|
+
2. It is in this processing step that linear EQ is first applied.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
assert s2p.s[0, :, :].shape == (2, 2), ValueError(
|
|
460
|
+
f"I can only convert 2-port networks. {s2p}")
|
|
461
|
+
self.H21_temp = self.H21(s2p)
|
|
462
|
+
return calc_Hffe(list(tap_weights)) * self.H21_temp * self.Hr * self.Hctf
|
|
463
|
+
|
|
464
|
+
def pulse_resp(self, H: Cvec) -> Rvec:
|
|
465
|
+
"""
|
|
466
|
+
Return the unit pulse response, p(t), corresponding to the given
|
|
467
|
+
voltage transfer function, H(f), according to (93A-24).
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
H: The voltage transfer function, H(f).
|
|
471
|
+
Note: Possitive frequency components only, including fN.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
p: The pulse response corresponding to the given voltage transfer function.
|
|
475
|
+
|
|
476
|
+
Raises:
|
|
477
|
+
ValueError: If the length of the given voltage transfer
|
|
478
|
+
function differs from that of the system frequency vector.
|
|
479
|
+
|
|
480
|
+
Notes:
|
|
481
|
+
1. It is at this point in the signal processing chain that we change
|
|
482
|
+
time domains.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
assert len(H) == len(self.freqs), ValueError(
|
|
486
|
+
"Length of given H(f) does not match length of f!")
|
|
487
|
+
|
|
488
|
+
Xsinc = self.Xsinc
|
|
489
|
+
p = np.fft.irfft(Xsinc * H)
|
|
490
|
+
p_mag = np.abs(p)
|
|
491
|
+
p_beg = np.where(p_mag > 0.01 * max(p_mag))[0][0] - int(5 * self.ui / self.Ts) # Give it some "front porch".
|
|
492
|
+
spln = interp1d(self.t_irfft, np.roll(p, -p_beg)) # `p` is not yet in our system time domain!
|
|
493
|
+
return spln(self.times) # Now, it is.
|
|
494
|
+
|
|
495
|
+
def gen_pulse_resps(self, tx_taps: Rvec, apply_eq=True) -> list[Rvec]:
|
|
496
|
+
"""
|
|
497
|
+
Generate pulse responses for all networks.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
tx_taps: Desired Tx tap weights.
|
|
501
|
+
|
|
502
|
+
KeywordArgs:
|
|
503
|
+
apply_eq: Include linear EQ when True; otherwise, exclude it.
|
|
504
|
+
Default: True
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
List of pulse responses.
|
|
508
|
+
|
|
509
|
+
Raises:
|
|
510
|
+
None
|
|
511
|
+
|
|
512
|
+
Notes:
|
|
513
|
+
1. Assumes `self.gDC` and `self.gDC2` have been set correctly.
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
pulse_resps = []
|
|
517
|
+
for ntwk, ntype in self.ntwks:
|
|
518
|
+
if apply_eq:
|
|
519
|
+
pr = self.pulse_resp(self.H(ntwk, tx_taps))
|
|
520
|
+
else:
|
|
521
|
+
pr = self.pulse_resp(self.H21(ntwk))
|
|
522
|
+
|
|
523
|
+
if ntype == 'THRU':
|
|
524
|
+
pr *= self.params['Av']
|
|
525
|
+
elif ntype == 'NEXT':
|
|
526
|
+
pr *= self.params['Ane']
|
|
527
|
+
else:
|
|
528
|
+
pr *= self.params['Afe']
|
|
529
|
+
|
|
530
|
+
pulse_resps.append(pr)
|
|
531
|
+
|
|
532
|
+
return pulse_resps
|
|
533
|
+
|
|
534
|
+
def filt_pr_samps(self, pr_samps: Rvec, As: float, rel_thresh: float = 0.001) -> Rvec:
|
|
535
|
+
"""
|
|
536
|
+
Filter a list of pulse response samples for minimum magnitude.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
pr_samps: The pulse response samples to filter.
|
|
540
|
+
As: Signal amplitude, as per 93A.1.6.c.
|
|
541
|
+
|
|
542
|
+
KeywordArgs:
|
|
543
|
+
rel_thresh: Filtration threshold (As).
|
|
544
|
+
Default: 0.001 (i.e. - 0.1%, as per Note 2 of 93A.1.7.1)
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
filtered_samps: The subset of `pr_samps` passing filtration.
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
thresh = As * rel_thresh
|
|
551
|
+
return np.array(list(filter(lambda x: abs(x) >= thresh, pr_samps)))
|
|
552
|
+
|
|
553
|
+
def calc_hJ(self, pulse_resp: Rvec, As: float, cursor_ix: int, rel_thresh: float = 0.001) -> Rvec:
|
|
554
|
+
"""
|
|
555
|
+
Calculate the set of slopes for valid pulse response samples.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
pulse_resp: The pulse response of interest.
|
|
559
|
+
As: Signal amplitude, as per 93A.1.6.c.
|
|
560
|
+
cursor_ix: Cursor index.
|
|
561
|
+
|
|
562
|
+
KeywordArgs:
|
|
563
|
+
rel_thresh: Filtration threshold (As).
|
|
564
|
+
Default: 0.001 (i.e. - 0.1%, as per Note 2 of 93A.1.7.1)
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
slopes: The calculated slopes around the valid samples.
|
|
568
|
+
"""
|
|
569
|
+
|
|
570
|
+
M = self.params['M']
|
|
571
|
+
thresh = As * rel_thresh
|
|
572
|
+
valid_pr_samp_ixs = np.array(list(filter(lambda ix: abs(pulse_resp[ix]) >= thresh,
|
|
573
|
+
range(cursor_ix, len(pulse_resp) - 1, M))))
|
|
574
|
+
m1s = pulse_resp[valid_pr_samp_ixs - 1]
|
|
575
|
+
p1s = pulse_resp[valid_pr_samp_ixs + 1]
|
|
576
|
+
return (p1s - m1s) / (2 / M) # (93A-28)
|
|
577
|
+
|
|
578
|
+
def loc_curs(self, pulse_resp: Rvec, max_range: int = 4) -> int:
|
|
579
|
+
"""
|
|
580
|
+
Locate the cursor position for the given pulse response,
|
|
581
|
+
according to (93A-25) and (93A-26).
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
pulse_resp: The pulse response of interest.
|
|
585
|
+
|
|
586
|
+
KeywordArgs:
|
|
587
|
+
max_range: The search radius, from the peak.
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
The index in the given pulse response vector of the cursor.
|
|
591
|
+
|
|
592
|
+
Notes:
|
|
593
|
+
1. As per v3.70 of the COM MATLAB code, we only minimize the
|
|
594
|
+
residual of (93A-25); we don't try to solve it exactly.
|
|
595
|
+
"""
|
|
596
|
+
|
|
597
|
+
M = self.params['M']
|
|
598
|
+
dfe_max = self.params['dfe_max']
|
|
599
|
+
dfe_min = self.params['dfe_min']
|
|
600
|
+
|
|
601
|
+
# Find zero crossings.
|
|
602
|
+
peak_loc = np.argmax(pulse_resp)
|
|
603
|
+
peak_val = pulse_resp[peak_loc]
|
|
604
|
+
search_start = max(0, peak_loc - 4 * M)
|
|
605
|
+
zxi = np.where(np.diff(np.sign(pulse_resp[search_start:peak_loc] - .01 * peak_val)) >= 1)[0] + search_start
|
|
606
|
+
assert zxi, RuntimeError("No zero crossings found!")
|
|
607
|
+
zxi = zxi[-1]
|
|
608
|
+
|
|
609
|
+
# Minimize Muller-Mueller criterion within a 2UI range after zero crossing.
|
|
610
|
+
ix_best = zxi
|
|
611
|
+
res_min = 1e6
|
|
612
|
+
for ix in range(zxi, zxi + 2 * M):
|
|
613
|
+
b_1 = min(dfe_max[0],
|
|
614
|
+
max(dfe_min[0],
|
|
615
|
+
pulse_resp[ix + M] / pulse_resp[ix])) # (93A-26)
|
|
616
|
+
res = abs(pulse_resp[ix - M] - (pulse_resp[ix + M] - b_1 * pulse_resp[ix])) # (93A-25)
|
|
617
|
+
if res < res_min:
|
|
618
|
+
ix_best = ix
|
|
619
|
+
res_min = res
|
|
620
|
+
return ix_best
|
|
621
|
+
|
|
622
|
+
def calc_fom(self, tx_taps: Rvec) -> float:
|
|
623
|
+
"""
|
|
624
|
+
Calculate the _figure of merit_ (FOM), given the existing linear EQ settings.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
tx_taps: The Tx FFE tap weights, excepting the cursor.
|
|
628
|
+
(The cursor takes whatever is left.)
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
FOM: The resultant figure of merit.
|
|
632
|
+
|
|
633
|
+
Raises:
|
|
634
|
+
None.
|
|
635
|
+
|
|
636
|
+
Notes:
|
|
637
|
+
1. Assumes that `self.gDC` and `self.gDC2` have been set correctly.
|
|
638
|
+
2. See: IEEE 802.3-2022 93A.1.6.
|
|
639
|
+
"""
|
|
640
|
+
|
|
641
|
+
L = self.params['L']
|
|
642
|
+
M = self.params['M']
|
|
643
|
+
freqs = self.freqs
|
|
644
|
+
|
|
645
|
+
# Step a - Pulse response construction.
|
|
646
|
+
pulse_resps = self.gen_pulse_resps(np.array(tx_taps))
|
|
647
|
+
|
|
648
|
+
# Step b - Cursor identification.
|
|
649
|
+
vic_pulse_resp = np.array(pulse_resps[0])
|
|
650
|
+
vic_peak_loc = np.argmax(vic_pulse_resp)
|
|
651
|
+
cursor_ix = self.loc_curs(vic_pulse_resp)
|
|
652
|
+
|
|
653
|
+
# Step c - As.
|
|
654
|
+
vic_curs_val = vic_pulse_resp[cursor_ix]
|
|
655
|
+
As = self.params['RLM'] * vic_curs_val / (L - 1)
|
|
656
|
+
|
|
657
|
+
# Step d - Tx noise.
|
|
658
|
+
varX = (L**2 - 1) / (3 * (L - 1)**2) # (93A-29)
|
|
659
|
+
varTx = vic_curs_val**2 * pow(10, -self.params['TxSNR'] / 10) # (93A-30)
|
|
660
|
+
|
|
661
|
+
# Step e - ISI.
|
|
662
|
+
nDFE = self.nDFE
|
|
663
|
+
# This is not compliant to the standaard, but is consistent w/ v2.60 of MATLAB code.
|
|
664
|
+
n_pre = cursor_ix // M
|
|
665
|
+
first_pre_ix = cursor_ix - n_pre * M
|
|
666
|
+
vic_pulse_resp_isi_samps = np.concatenate((vic_pulse_resp[first_pre_ix:cursor_ix:M],
|
|
667
|
+
vic_pulse_resp[cursor_ix + M::M]))
|
|
668
|
+
vic_pulse_resp_post_samps = vic_pulse_resp_isi_samps[n_pre:]
|
|
669
|
+
dfe_tap_weights = np.maximum( # (93A-26)
|
|
670
|
+
self.params['dfe_min'],
|
|
671
|
+
np.minimum(
|
|
672
|
+
self.params['dfe_max'],
|
|
673
|
+
(vic_pulse_resp_post_samps[:nDFE] / vic_curs_val)))
|
|
674
|
+
hISI = vic_pulse_resp_isi_samps \
|
|
675
|
+
- vic_curs_val * np.pad(dfe_tap_weights,
|
|
676
|
+
(n_pre, len(vic_pulse_resp_post_samps) - nDFE),
|
|
677
|
+
mode='constant',
|
|
678
|
+
constant_values=0) # (93A-27)
|
|
679
|
+
varISI = varX * sum(hISI**2) # (93A-31)
|
|
680
|
+
self.dbg['vic_pulse_resp_isi_samps'] = vic_pulse_resp_isi_samps
|
|
681
|
+
self.dbg['vic_pulse_resp_post_samps'] = vic_pulse_resp_post_samps
|
|
682
|
+
self.dbg['hISI'] = hISI
|
|
683
|
+
|
|
684
|
+
# Step f - Jitter noise.
|
|
685
|
+
hJ = self.calc_hJ(vic_pulse_resp, As, cursor_ix)
|
|
686
|
+
varJ = (self.params['Add']**2 + self.params['sigma_Rj']**2) * varX * sum(hJ**2) # (93A-32)
|
|
687
|
+
|
|
688
|
+
# Step g - Crosstalk.
|
|
689
|
+
varXT = 0
|
|
690
|
+
for pulse_resp in pulse_resps[1:]: # (93A-34)
|
|
691
|
+
varXT += max([sum(np.array(self.filt_pr_samps(pulse_resp[m::M], As))**2) for m in range(M)]) # (93A-33)
|
|
692
|
+
varXT *= varX
|
|
693
|
+
|
|
694
|
+
# Step h - Spectral noise.
|
|
695
|
+
df = freqs[1]
|
|
696
|
+
varN = self.params['eta0'] * sum(abs(self.Hr * self.Hctf)**2) * (df / 1e9) # (93A-35)
|
|
697
|
+
|
|
698
|
+
# Step i - FOM calculation.
|
|
699
|
+
fom = 10 * np.log10(As**2 / (varTx + varISI + varJ + varXT + varN)) # (93A-36)
|
|
700
|
+
|
|
701
|
+
# Stash our calculation results.
|
|
702
|
+
self.fom_rslts['pulse_resps'] = pulse_resps
|
|
703
|
+
self.fom_rslts['vic_pulse_resp'] = vic_pulse_resp
|
|
704
|
+
self.fom_rslts['vic_peak_loc'] = vic_peak_loc
|
|
705
|
+
self.fom_rslts['cursor_ix'] = cursor_ix
|
|
706
|
+
self.fom_rslts['As'] = As
|
|
707
|
+
self.fom_rslts['varTx'] = varTx
|
|
708
|
+
self.fom_rslts['dfe_tap_weights'] = dfe_tap_weights
|
|
709
|
+
self.fom_rslts['varISI'] = varISI
|
|
710
|
+
self.fom_rslts['varJ'] = varJ
|
|
711
|
+
self.fom_rslts['varXT'] = varXT
|
|
712
|
+
self.fom_rslts['varN'] = varN
|
|
713
|
+
|
|
714
|
+
return fom
|
|
715
|
+
|
|
716
|
+
def opt_eq(self, do_opt_eq: bool = True, tx_taps: Rvec = None) -> bool:
|
|
717
|
+
"""
|
|
718
|
+
Find the optimum values for the linear equalization parameters:
|
|
719
|
+
c(-2), c(-1), c(1), gDC, and gDC2, as per IEEE 802.3-22 93A.1.6.
|
|
720
|
+
|
|
721
|
+
KeywordArgs:
|
|
722
|
+
do_opt_eq: Perform optimization of linear EQ when True.
|
|
723
|
+
Default: True
|
|
724
|
+
tx_taps: Used when `do_opt_eq` = False.
|
|
725
|
+
Default: None
|
|
726
|
+
|
|
727
|
+
Returns:
|
|
728
|
+
success: True if no errors encountered; False otherwise.
|
|
729
|
+
"""
|
|
730
|
+
|
|
731
|
+
if do_opt_eq:
|
|
732
|
+
# Run the nested optimization loops.
|
|
733
|
+
def check_taps(tx_taps: Rvec) -> bool:
|
|
734
|
+
if (1 - sum(abs(np.array(tx_taps)))) < self.params['c0min']:
|
|
735
|
+
return False
|
|
736
|
+
else:
|
|
737
|
+
return True
|
|
738
|
+
|
|
739
|
+
fom_max = -100.0
|
|
740
|
+
fom_max_changed = False
|
|
741
|
+
foms = []
|
|
742
|
+
for gDC2 in self.params['gDC2']:
|
|
743
|
+
self.gDC2 = gDC2
|
|
744
|
+
for gDC in self.params['gDC']:
|
|
745
|
+
self.gDC = gDC
|
|
746
|
+
for n, tx_taps in enumerate(self.tx_combs):
|
|
747
|
+
if not check_taps(np.array(tx_taps)):
|
|
748
|
+
continue
|
|
749
|
+
fom = self.calc_fom(tx_taps)
|
|
750
|
+
foms.append(fom)
|
|
751
|
+
if fom > fom_max:
|
|
752
|
+
fom_max_changed = True
|
|
753
|
+
fom_max = fom
|
|
754
|
+
gDC2_best = gDC2
|
|
755
|
+
gDC_best = gDC
|
|
756
|
+
tx_taps_best = tx_taps
|
|
757
|
+
dfe_tap_weights_best = self.fom_rslts['dfe_tap_weights']
|
|
758
|
+
cursor_ix_best = self.fom_rslts['cursor_ix']
|
|
759
|
+
As_best = self.fom_rslts['As']
|
|
760
|
+
varTx_best = self.fom_rslts['varTx']
|
|
761
|
+
varISI_best = self.fom_rslts['varISI']
|
|
762
|
+
varJ_best = self.fom_rslts['varJ']
|
|
763
|
+
varXT_best = self.fom_rslts['varXT']
|
|
764
|
+
varN_best = self.fom_rslts['varN']
|
|
765
|
+
else:
|
|
766
|
+
assert tx_taps, RuntimeError("You must define `tx_taps` when setting `do_opt_eq` False!")
|
|
767
|
+
fom = self.calc_fom(tx_taps)
|
|
768
|
+
foms = [fom]
|
|
769
|
+
fom_max = fom
|
|
770
|
+
fom_max_changed = True
|
|
771
|
+
gDC2_best = self.gDC2
|
|
772
|
+
gDC_best = self.gDC
|
|
773
|
+
tx_taps_best = tx_taps
|
|
774
|
+
dfe_tap_weights_best = self.fom_rslts['dfe_tap_weights']
|
|
775
|
+
cursor_ix_best = self.fom_rslts['cursor_ix']
|
|
776
|
+
As_best = self.fom_rslts['As']
|
|
777
|
+
varTx_best = self.fom_rslts['varTx']
|
|
778
|
+
varISI_best = self.fom_rslts['varISI']
|
|
779
|
+
varJ_best = self.fom_rslts['varJ']
|
|
780
|
+
varXT_best = self.fom_rslts['varXT']
|
|
781
|
+
varN_best = self.fom_rslts['varN']
|
|
782
|
+
|
|
783
|
+
# Check for error and save the best results.
|
|
784
|
+
if not fom_max_changed:
|
|
785
|
+
return False
|
|
786
|
+
self.rslts['fom'] = fom_max
|
|
787
|
+
self.rslts['gDC2'] = gDC2_best
|
|
788
|
+
self.rslts['gDC'] = gDC_best
|
|
789
|
+
self.rslts['tx_taps'] = tx_taps_best
|
|
790
|
+
self.rslts['dfe_tap_weights'] = dfe_tap_weights_best
|
|
791
|
+
self.rslts['cursor_ix'] = cursor_ix_best
|
|
792
|
+
self.rslts['As'] = As_best * 1_000 # (mV)
|
|
793
|
+
self.rslts['sigma_ISI'] = np.sqrt(varISI_best) * 1_000 # (mV)
|
|
794
|
+
self.rslts['sigma_J'] = np.sqrt(varJ_best) * 1_000 # (mV)
|
|
795
|
+
self.rslts['sigma_XT'] = np.sqrt(varXT_best) * 1_000 # (mV)
|
|
796
|
+
# These two are also calculated by `calc_noise()`; so, add "_best".
|
|
797
|
+
self.rslts['sigma_Tx_best'] = np.sqrt(varTx_best) * 1_000 # (mV)
|
|
798
|
+
self.rslts['sigma_N_best'] = np.sqrt(varN_best) * 1_000 # (mV)
|
|
799
|
+
self.rslts['foms'] = foms
|
|
800
|
+
return True
|
|
801
|
+
|
|
802
|
+
def calc_noise(self) -> tuple[float, float, int]:
|
|
803
|
+
"""
|
|
804
|
+
Calculate the interference and noise for COM.
|
|
805
|
+
|
|
806
|
+
KeywordArgs:
|
|
807
|
+
npts: Number of vector points.
|
|
808
|
+
Default: 2001
|
|
809
|
+
|
|
810
|
+
Returns:
|
|
811
|
+
(As, Ani, cursor_ix): Triple containing:
|
|
812
|
+
- signal amplitude,
|
|
813
|
+
- noise + interference amplitude (V), and
|
|
814
|
+
- cursor location within victim pulse response vector.
|
|
815
|
+
|
|
816
|
+
Raises:
|
|
817
|
+
None
|
|
818
|
+
|
|
819
|
+
Warns:
|
|
820
|
+
1. If `2*As/npts` rises above 10 uV, against standard's recommendation.
|
|
821
|
+
|
|
822
|
+
Notes:
|
|
823
|
+
1. Assumes the following instance variables have been set:
|
|
824
|
+
- gDC
|
|
825
|
+
- gDC2
|
|
826
|
+
- tx_taps
|
|
827
|
+
"""
|
|
828
|
+
|
|
829
|
+
L = self.params['L']
|
|
830
|
+
M = self.params['M']
|
|
831
|
+
freqs = self.freqs
|
|
832
|
+
nDFE = self.nDFE
|
|
833
|
+
|
|
834
|
+
pulse_resps = self.gen_pulse_resps(np.array(self.tx_taps))
|
|
835
|
+
vic_pulse_resp = pulse_resps[0]
|
|
836
|
+
cursor_ix = self.loc_curs(vic_pulse_resp)
|
|
837
|
+
vic_curs_val = vic_pulse_resp[cursor_ix]
|
|
838
|
+
As = self.params['RLM'] * vic_curs_val / (L - 1)
|
|
839
|
+
npts = 2 * max(int(As / 0.001), 1_000) + 1 # Note 1 of 93A.1.7.1; MUST BE ODD!
|
|
840
|
+
y = np.linspace(-As, As, npts)
|
|
841
|
+
ystep = 2 * As / (npts - 1)
|
|
842
|
+
|
|
843
|
+
delta = np.zeros(npts)
|
|
844
|
+
delta[npts // 2] = 1
|
|
845
|
+
varX = (L**2 - 1) / (3 * (L - 1)**2) # (93A-29)
|
|
846
|
+
df = freqs[1]
|
|
847
|
+
|
|
848
|
+
def pn(hn: float) -> Rvec:
|
|
849
|
+
"""
|
|
850
|
+
(93A-39)
|
|
851
|
+
"""
|
|
852
|
+
return 1 / L * sum([np.roll(delta, int((2 * l / (L - 1) - 1) * hn / ystep))
|
|
853
|
+
for l in range(L)])
|
|
854
|
+
|
|
855
|
+
def p(h_samps: Rvec) -> Rvec:
|
|
856
|
+
"""
|
|
857
|
+
Calculate the "delta-pmf" for a set of pulse response samples,
|
|
858
|
+
as per (93A-40).
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
h_samps: Vector of pulse response samples.
|
|
862
|
+
As: Signal amplitude, as per 93A.1.6.c.
|
|
863
|
+
|
|
864
|
+
Returns:
|
|
865
|
+
Vector of "deltas" giving amplitude probability distribution.
|
|
866
|
+
|
|
867
|
+
Raises:
|
|
868
|
+
None
|
|
869
|
+
|
|
870
|
+
Notes:
|
|
871
|
+
1. The input set of pulse response samples is filtered,
|
|
872
|
+
as per Note 2 of 93A.1.7.1.
|
|
873
|
+
"""
|
|
874
|
+
|
|
875
|
+
pns = []
|
|
876
|
+
rslts = []
|
|
877
|
+
rslt = delta
|
|
878
|
+
rslts.append(rslt)
|
|
879
|
+
for hn in h_samps:
|
|
880
|
+
_pn = pn(hn)
|
|
881
|
+
pns.append(_pn)
|
|
882
|
+
rslt = np.convolve(rslt, _pn, mode='same')
|
|
883
|
+
rslts.append(rslt)
|
|
884
|
+
rslt /= sum(rslt) # Enforce a PMF. Commenting out didn't make a difference.
|
|
885
|
+
self.dbg['pns'] = pns
|
|
886
|
+
self.dbg['rslts'] = rslts
|
|
887
|
+
return rslt
|
|
888
|
+
|
|
889
|
+
# Sec. 93A.1.7.2
|
|
890
|
+
varN = self.params['eta0'] * sum(abs(self.Hr * self.Hctf)**2) * (df / 1e9) # (93A-35)
|
|
891
|
+
varTx = vic_curs_val**2 * pow(10, -self.params['TxSNR'] / 10) # (93A-30)
|
|
892
|
+
hJ = self.calc_hJ(vic_pulse_resp, As, cursor_ix)
|
|
893
|
+
pJ = p(self.params['Add'] * hJ)
|
|
894
|
+
self.dbg['pJ'] = pJ
|
|
895
|
+
self.dbg['hJ'] = hJ
|
|
896
|
+
varG = varTx + self.params['sigma_Rj']**2 * varX * sum(hJ**2) + varN # (93A-41)
|
|
897
|
+
pG = np.exp(-y**2 / (2 * varG)) / np.sqrt(TWOPI * varG) # (93A-42)
|
|
898
|
+
pN = np.convolve(pG, pJ, mode='same') # (93A-43)
|
|
899
|
+
|
|
900
|
+
# Sec. 93A.1.7.3
|
|
901
|
+
# - ISI (Inconsistent w/ IEEE 802.3-22, but consistent w/ v2.60 of MATLAB code.)
|
|
902
|
+
n_pre = cursor_ix // M
|
|
903
|
+
first_pre_ix = cursor_ix - n_pre * M
|
|
904
|
+
vic_pulse_resp_isi_samps = np.concatenate((vic_pulse_resp[first_pre_ix:cursor_ix:M],
|
|
905
|
+
vic_pulse_resp[cursor_ix + M::M]))
|
|
906
|
+
vic_pulse_resp_post_samps = vic_pulse_resp_isi_samps[n_pre:]
|
|
907
|
+
dfe_tap_weights = np.maximum( # (93A-26)
|
|
908
|
+
self.params['dfe_min'],
|
|
909
|
+
np.minimum(
|
|
910
|
+
self.params['dfe_max'],
|
|
911
|
+
(vic_pulse_resp_post_samps[:nDFE] / vic_curs_val)))
|
|
912
|
+
hISI = vic_pulse_resp_isi_samps \
|
|
913
|
+
- vic_curs_val * np.pad(dfe_tap_weights,
|
|
914
|
+
(n_pre, len(vic_pulse_resp_post_samps) - nDFE),
|
|
915
|
+
mode='constant',
|
|
916
|
+
constant_values=0) # (93A-27)
|
|
917
|
+
hISI = self.filt_pr_samps(hISI, As)
|
|
918
|
+
py = p(hISI) # `hISI` from (93A-27); `p(y)` as per (93A-40)
|
|
919
|
+
|
|
920
|
+
# - Crosstalk
|
|
921
|
+
self.rslts['py0'] = py.copy() # For debugging.
|
|
922
|
+
xt_samps = []
|
|
923
|
+
pks = [] # For debugging.
|
|
924
|
+
for pulse_resp in pulse_resps[1:]: # (93A-44)
|
|
925
|
+
i = np.argmax([sum(np.array(self.filt_pr_samps(pulse_resp[m::M], As))**2) for m in range(M)]) # (93A-33)
|
|
926
|
+
samps = self.filt_pr_samps(pulse_resp[i::M], As)
|
|
927
|
+
xt_samps.append(samps)
|
|
928
|
+
pk = p(samps) # For debugging.
|
|
929
|
+
pks.append(pk)
|
|
930
|
+
py = np.convolve(py, pk, mode='same')
|
|
931
|
+
self.rslts['py1'] = py.copy() # For debugging.
|
|
932
|
+
self.xt_samps = xt_samps
|
|
933
|
+
self.pks = pks
|
|
934
|
+
py = np.convolve(py, pN, mode='same') # (93A-45)
|
|
935
|
+
|
|
936
|
+
# Final calculation
|
|
937
|
+
Py = np.cumsum(py)
|
|
938
|
+
Py /= Py[-1] # Enforce cumulative probability distribution.
|
|
939
|
+
|
|
940
|
+
# Store some results.
|
|
941
|
+
self.pulse_resps = pulse_resps
|
|
942
|
+
self.cursor_ix = cursor_ix
|
|
943
|
+
self.rslts['sigma_Tx'] = np.sqrt(varTx) * 1_000 # (mV)
|
|
944
|
+
self.rslts['sigma_G'] = np.sqrt(varG) * 1_000 # (mV)
|
|
945
|
+
self.rslts['sigma_N'] = np.sqrt(varN) * 1_000 # (mV)
|
|
946
|
+
self.rslts['pG'] = pG
|
|
947
|
+
self.rslts['pN'] = pN
|
|
948
|
+
self.rslts['py'] = py
|
|
949
|
+
self.rslts['Py'] = Py
|
|
950
|
+
self.rslts['y'] = y
|
|
951
|
+
self.rslts['dfe_taps'] = dfe_tap_weights
|
|
952
|
+
|
|
953
|
+
return (As,
|
|
954
|
+
abs(np.where(Py >= self.params['DER'])[0][0] - npts // 2) * ystep,
|
|
955
|
+
cursor_ix)
|
pychopmarg/common.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Definitions common to all PyChOpMarg modules.
|
|
3
|
+
|
|
4
|
+
Original author: David Banas <capn.freako@gmail.com>
|
|
5
|
+
|
|
6
|
+
Original date: March 3, 2024 (Copied from `pybert.utility`.)
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 David Banas; all rights reserved World wide.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np # type: ignore
|
|
12
|
+
import numpy.typing as npt # type: ignore
|
|
13
|
+
from typing import TypeVar, TypeAlias, Any
|
|
14
|
+
|
|
15
|
+
Real = TypeVar('Real', float, float)
|
|
16
|
+
Comp = TypeVar('Comp', complex, complex)
|
|
17
|
+
Rvec: TypeAlias = npt.NDArray[Real]
|
|
18
|
+
Cvec: TypeAlias = npt.NDArray[Comp]
|
|
19
|
+
COMParams: TypeAlias = dict[str, Any] # ToDo: Specify this concretely, perhaps in `standards` module.
|
|
20
|
+
COMFiles: TypeAlias = str | list[str]
|
|
21
|
+
|
|
22
|
+
PI: float = np.pi
|
|
23
|
+
TWOPI: float = 2 * np.pi
|
pychopmarg/utility.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
General purpose utilities for PyChOpMarg.
|
|
3
|
+
|
|
4
|
+
Original author: David Banas <capn.freako@gmail.com>
|
|
5
|
+
|
|
6
|
+
Original date: March 3, 2024 (Copied from `pybert.utility`.)
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 David Banas; all rights reserved World wide.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np # type: ignore
|
|
12
|
+
import skrf as rf
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def sdd_21(ntwk: rf.Network, norm: float = 0.5, renumber: bool = False) -> rf.Network:
|
|
16
|
+
"""
|
|
17
|
+
Given a 4-port single-ended network, return its differential throughput
|
|
18
|
+
as a 2-port network.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
ntwk: 4-port single ended network.
|
|
22
|
+
|
|
23
|
+
KeywordArgs:
|
|
24
|
+
norm: Normalization factor. (Default = 0.5)
|
|
25
|
+
renumber: Automatically detect correct through path when True.
|
|
26
|
+
Default: False
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Sdd (2-port).
|
|
30
|
+
|
|
31
|
+
Notes:
|
|
32
|
+
1. A "1->2/3->4" port ordering convention is assumed when `renumber` is False.
|
|
33
|
+
2. Automatic renumbering should not be used unless a solid d.c. thru path exists.
|
|
34
|
+
"""
|
|
35
|
+
mm = se2mm(ntwk, norm=norm, renumber=renumber)
|
|
36
|
+
return rf.Network(frequency=ntwk.f, s=mm.s[:, 0:2, 0:2], z0=mm.z0[:, 0:2])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def se2mm(ntwk: rf.Network, norm: float = 0.5, renumber: bool = False) -> rf.Network:
|
|
40
|
+
"""
|
|
41
|
+
Given a 4-port single-ended network, return its mixed mode equivalent.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
ntwk: 4-port single ended network.
|
|
45
|
+
|
|
46
|
+
KeywordArgs:
|
|
47
|
+
norm: Normalization factor. (Default = 0.5)
|
|
48
|
+
renumber: Automatically detect correct through path when True.
|
|
49
|
+
Default: False
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
skrf.Network: Mixed mode equivalent network, in the following format:
|
|
53
|
+
Sdd11 Sdd12 Sdc11 Sdc12
|
|
54
|
+
Sdd21 Sdd22 Sdc21 Sdc22
|
|
55
|
+
Scd11 Scd12 Scc11 Scc12
|
|
56
|
+
Scd21 Scd22 Scc21 Scc22
|
|
57
|
+
|
|
58
|
+
Notes:
|
|
59
|
+
1. A "1->2/3->4" port ordering convention is assumed when `renumber` is False.
|
|
60
|
+
2. Automatic renumbering should not be used unless a solid d.c. thru path exists.
|
|
61
|
+
"""
|
|
62
|
+
# Confirm correct network dimmensions.
|
|
63
|
+
(fs, rs, cs) = ntwk.s.shape
|
|
64
|
+
assert rs == cs, "Non-square Touchstone file S-matrix!"
|
|
65
|
+
assert rs == 4, "Touchstone file must have 4 ports!"
|
|
66
|
+
|
|
67
|
+
# Detect/correct "1 => 3" port numbering if requested.
|
|
68
|
+
if renumber:
|
|
69
|
+
ix = 1
|
|
70
|
+
if abs(ntwk.s21.s[ix, 0, 0]) < abs(ntwk.s31.s[ix, 0, 0]): # 1 ==> 3 port numbering?
|
|
71
|
+
ntwk.renumber((1, 2), (2, 1))
|
|
72
|
+
|
|
73
|
+
# Convert S-parameter data.
|
|
74
|
+
s = np.zeros(ntwk.s.shape, dtype=complex)
|
|
75
|
+
s[:, 0, 0] = norm * (ntwk.s11 - ntwk.s13 - ntwk.s31 + ntwk.s33).s.flatten()
|
|
76
|
+
s[:, 0, 1] = norm * (ntwk.s12 - ntwk.s14 - ntwk.s32 + ntwk.s34).s.flatten()
|
|
77
|
+
s[:, 0, 2] = norm * (ntwk.s11 + ntwk.s13 - ntwk.s31 - ntwk.s33).s.flatten()
|
|
78
|
+
s[:, 0, 3] = norm * (ntwk.s12 + ntwk.s14 - ntwk.s32 - ntwk.s34).s.flatten()
|
|
79
|
+
s[:, 1, 0] = norm * (ntwk.s21 - ntwk.s23 - ntwk.s41 + ntwk.s43).s.flatten()
|
|
80
|
+
s[:, 1, 1] = norm * (ntwk.s22 - ntwk.s24 - ntwk.s42 + ntwk.s44).s.flatten()
|
|
81
|
+
s[:, 1, 2] = norm * (ntwk.s21 + ntwk.s23 - ntwk.s41 - ntwk.s43).s.flatten()
|
|
82
|
+
s[:, 1, 3] = norm * (ntwk.s22 + ntwk.s24 - ntwk.s42 - ntwk.s44).s.flatten()
|
|
83
|
+
s[:, 2, 0] = norm * (ntwk.s11 - ntwk.s13 + ntwk.s31 - ntwk.s33).s.flatten()
|
|
84
|
+
s[:, 2, 1] = norm * (ntwk.s12 - ntwk.s14 + ntwk.s32 - ntwk.s34).s.flatten()
|
|
85
|
+
s[:, 2, 2] = norm * (ntwk.s11 + ntwk.s13 + ntwk.s31 + ntwk.s33).s.flatten()
|
|
86
|
+
s[:, 2, 3] = norm * (ntwk.s12 + ntwk.s14 + ntwk.s32 + ntwk.s34).s.flatten()
|
|
87
|
+
s[:, 3, 0] = norm * (ntwk.s21 - ntwk.s23 + ntwk.s41 - ntwk.s43).s.flatten()
|
|
88
|
+
s[:, 3, 1] = norm * (ntwk.s22 - ntwk.s24 + ntwk.s42 - ntwk.s44).s.flatten()
|
|
89
|
+
s[:, 3, 2] = norm * (ntwk.s21 + ntwk.s23 + ntwk.s41 + ntwk.s43).s.flatten()
|
|
90
|
+
s[:, 3, 3] = norm * (ntwk.s22 + ntwk.s24 + ntwk.s42 + ntwk.s44).s.flatten()
|
|
91
|
+
|
|
92
|
+
# Convert port impedances.
|
|
93
|
+
f = ntwk.f
|
|
94
|
+
z = np.zeros((len(f), 4), dtype=complex)
|
|
95
|
+
z[:, 0] = ntwk.z0[:, 0] + ntwk.z0[:, 2]
|
|
96
|
+
z[:, 1] = ntwk.z0[:, 1] + ntwk.z0[:, 3]
|
|
97
|
+
z[:, 2] = (ntwk.z0[:, 0] + ntwk.z0[:, 2]) / 2
|
|
98
|
+
z[:, 3] = (ntwk.z0[:, 1] + ntwk.z0[:, 3]) / 2
|
|
99
|
+
|
|
100
|
+
return rf.Network(frequency=f, s=s, z0=z)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def import_s32p(filename: str, vic_chnl: int = 1) -> list[tuple[rf.Network, str]]:
|
|
104
|
+
"""Read in a 32-port Touchstone file, and return an equivalent list
|
|
105
|
+
of 8 2-port differential networks: a single victim through channel and
|
|
106
|
+
7 crosstalk aggressors, according to the VITA 68.2 convention.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
filename: Name of Touchstone file to read in.
|
|
110
|
+
|
|
111
|
+
KeywordArgs:
|
|
112
|
+
vic_chnl: Victim channel number (from 1).
|
|
113
|
+
Default = 1
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
[(skrf.Network, str)]: List of 8 pairs, each consisting of:
|
|
117
|
+
- a 2-port network representing a *differential* channel, and
|
|
118
|
+
- the type of that channel, one of: 'THRU', 'NEXT', or 'FEXT.
|
|
119
|
+
(First element is the victim and the only one of type 'THRU'.)
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: If Touchstone file is not 32-port.
|
|
123
|
+
|
|
124
|
+
Notes:
|
|
125
|
+
1. Input Touchstone file is assumed single-ended.
|
|
126
|
+
2. The differential through and xtalk channels are returned.
|
|
127
|
+
3. Port 2 of all returned channels correspond to the same
|
|
128
|
+
physical circuit node, typically, the Rx input node.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
# Import and sanity check the Touchstone file.
|
|
132
|
+
ntwk = rf.Network(filename)
|
|
133
|
+
(fs, rs, cs) = ntwk.s.shape
|
|
134
|
+
assert rs == cs, "Non-square Touchstone file S-matrix!"
|
|
135
|
+
assert rs == 32, f"Touchstone file must have 32 ports!\n\t{ntwk}"
|
|
136
|
+
|
|
137
|
+
# Extract the victim and aggressors.
|
|
138
|
+
def ports_from_chnls(left, right):
|
|
139
|
+
"""
|
|
140
|
+
Return list of 4 ports (from 0) corresponding to a particular
|
|
141
|
+
left and right channel ID (from 1), assuming "1=>2/3=>4" convention.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
left(int): Left side channel number (from 1).
|
|
145
|
+
right(int): Right side channel number (from 1).
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
[int]: List of ports (from 0) for desired channel.
|
|
149
|
+
"""
|
|
150
|
+
left0 = left - 1 # 0-based
|
|
151
|
+
right0 = right - 1
|
|
152
|
+
return [left0 * 4, right0 * 4 + 1, left0 * 4 + 2, right0 * 4 + 3]
|
|
153
|
+
|
|
154
|
+
vic_ports = ports_from_chnls(vic_chnl, vic_chnl)
|
|
155
|
+
vic = sdd_21(rf.subnetwork(ntwk, vic_ports))
|
|
156
|
+
vic = (vic, 'THRU')
|
|
157
|
+
if vic_chnl % 2: # odd?
|
|
158
|
+
vic_rx_ports = [vic_ports[n] for n in [0, 2]]
|
|
159
|
+
else:
|
|
160
|
+
vic_rx_ports = [vic_ports[n] for n in [1, 3]]
|
|
161
|
+
agg_chnls = list(np.array(range(8)) + 1)
|
|
162
|
+
agg_chnls.remove(vic_chnl)
|
|
163
|
+
aggs = []
|
|
164
|
+
for agg_chnl in agg_chnls:
|
|
165
|
+
agg_ports = ports_from_chnls(agg_chnl, agg_chnl)
|
|
166
|
+
if agg_chnl % 2: # odd?
|
|
167
|
+
agg_tx_ports = [agg_ports[n] for n in [1, 3]]
|
|
168
|
+
else:
|
|
169
|
+
agg_tx_ports = [agg_ports[n] for n in [0, 2]]
|
|
170
|
+
sub_ports = np.concatenate(list(zip(agg_tx_ports, vic_rx_ports)))
|
|
171
|
+
subntwk = sdd_21(ntwk.subnetwork(sub_ports))
|
|
172
|
+
if (vic_chnl + agg_chnl) % 2:
|
|
173
|
+
subntwk = (subntwk, 'NEXT')
|
|
174
|
+
else:
|
|
175
|
+
subntwk = (subntwk, 'FEXT')
|
|
176
|
+
aggs.append(subntwk)
|
|
177
|
+
return [vic] + aggs
|