PyChOpMarg 3.3.0__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/PKG-INFO +1 -1
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/pyproject.toml +1 -1
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/PyChOpMarg.egg-info/PKG-INFO +1 -1
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/cli.py +7 -2
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/com.py +280 -79
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/common.py +4 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/noise.py +65 -27
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/optimize.py +67 -25
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/utility/filter.py +27 -3
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/utility/jupyter.py +12 -2
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/utility/probability.py +28 -24
- pychopmarg-3.4.0/tests/test_cli.py +64 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/tests/test_com.py +168 -1
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/tests/test_probability.py +21 -0
- pychopmarg-3.3.0/tests/test_cli.py +0 -24
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/LICENSE +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/README.md +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/setup.cfg +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/PyChOpMarg.egg-info/SOURCES.txt +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/PyChOpMarg.egg-info/dependency_links.txt +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/PyChOpMarg.egg-info/entry_points.txt +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/PyChOpMarg.egg-info/requires.txt +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/PyChOpMarg.egg-info/top_level.txt +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/__init__.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/__main__.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/config/__init__.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/config/ieee_8023by.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/config/ieee_8023dj.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/config/ieee_8023dj_d3p3.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/config/template.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/devmodel.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/excel.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/matlab.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/mfiles/pychopmarg_com.m +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/mlsd.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/plot.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/py.typed +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/utility/__init__.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/utility/general.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/src/pychopmarg/utility/sparams.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/tests/test_devmodel.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/tests/test_excel.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/tests/test_filter.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/tests/test_general.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/tests/test_imports.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/tests/test_matlab.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/tests/test_mlsd.py +0 -0
- {pychopmarg-3.3.0 → pychopmarg-3.4.0}/tests/test_template.py +0 -0
|
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "PyChOpMarg"
|
|
11
11
|
description = "Python implementation of COM, as per IEEE 802.3-22 Annex 93A/178A."
|
|
12
|
-
version = "3.
|
|
12
|
+
version = "3.4.0"
|
|
13
13
|
authors = [{name = "David Banas", email = "capn.freako@gmail.com"}]
|
|
14
14
|
urls = { documentation = "https://pychopmarg.readthedocs.io/en/latest/"}
|
|
15
15
|
readme = "README.md"
|
|
@@ -59,11 +59,16 @@ def cli(ctx):
|
|
|
59
59
|
# @cli.command(context_settings=dict(help_option_names=["-h", "--help"]))
|
|
60
60
|
@cli.command(context_settings={"help_option_names": ["-h", "--help"]})
|
|
61
61
|
@click.argument("notebook-file", type=click.Path(exists=True))
|
|
62
|
-
|
|
62
|
+
@click.option("--procs", "-p", type=click.IntRange(min=0), default=None,
|
|
63
|
+
help="Processes per EQ optimization (0: one per CPU). Default: $PYCHOPMARG_PROCS, else 1.")
|
|
64
|
+
def run_notebook(notebook_file: Path, procs: Optional[int] = None):
|
|
63
65
|
"""
|
|
64
66
|
Run a Jupyter notebook in place w/o opening a browser tab.
|
|
67
|
+
|
|
68
|
+
With --procs, each COM calculation's EQ optimization runs in that many processes,
|
|
69
|
+
when it's a full search (no "Local Search"), giving the same results, faster.
|
|
65
70
|
"""
|
|
66
|
-
execute_notebook(notebook_file)
|
|
71
|
+
execute_notebook(notebook_file, n_procs=procs)
|
|
67
72
|
|
|
68
73
|
|
|
69
74
|
# @cli.command(context_settings=dict(help_option_names=["-h", "--help"]))
|
|
@@ -21,6 +21,8 @@ ToDo:
|
|
|
21
21
|
|
|
22
22
|
from dataclasses import replace
|
|
23
23
|
from importlib.metadata import version
|
|
24
|
+
import multiprocessing
|
|
25
|
+
import os
|
|
24
26
|
from pathlib import Path
|
|
25
27
|
from typing import Any, Optional, cast
|
|
26
28
|
import warnings
|
|
@@ -31,17 +33,53 @@ from numpy import array, arange
|
|
|
31
33
|
from scipy.signal import fftconvolve # type: ignore
|
|
32
34
|
|
|
33
35
|
from pychopmarg.common import (
|
|
34
|
-
PI, TWOPI, Cvec, Rvec, Cmat, OptMode,
|
|
36
|
+
PI, PROCS_ENV_VAR, TWOPI, Cvec, Rvec, Cmat, OptMode,
|
|
35
37
|
COMChnl, COMNtwk, ChnlSet, ChnlGrpName, ChnlSetName)
|
|
36
38
|
from pychopmarg.config.template import COMParams
|
|
37
39
|
from pychopmarg.devmodel import (
|
|
38
40
|
DeviceModel, PartialHost, TlineStage, gamma_term, renormalize_to, s_partial_host, s_rx_model, s_tx_model)
|
|
39
41
|
from pychopmarg.mlsd import der_mlsd, inv_cdf, solve_g_an
|
|
40
42
|
from pychopmarg.noise import NoiseCalc
|
|
41
|
-
from pychopmarg.optimize import NormMode, mmse, przf
|
|
43
|
+
from pychopmarg.optimize import NormMode, mmse, mmse_noise_vars, przf
|
|
42
44
|
from pychopmarg.utility import (
|
|
43
45
|
import_s32p, sdd_21, sDieLadderSegment, sPkgTline, sCshunt, filt_pr_samps,
|
|
44
|
-
delta_pmf, mk_combs, calc_Hffe, calc_Hctle, calc_H21, calc_hJ, loc_curs, calc_vtf, interp_vtf)
|
|
46
|
+
delta_pmf, mk_combs, calc_Hffe, ffe_phase_matrix, calc_Hctle, calc_H21, calc_hJ, loc_curs, calc_vtf, interp_vtf)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def default_n_procs() -> int:
|
|
50
|
+
"""
|
|
51
|
+
The default number of EQ optimization processes (``COM.n_procs``): the value of the ``PYCHOPMARG_PROCS``
|
|
52
|
+
environment variable (0 for one per CPU), if set to a non-negative integer; otherwise, 1 (a serial search).
|
|
53
|
+
"""
|
|
54
|
+
val = os.environ.get(PROCS_ENV_VAR, "").strip()
|
|
55
|
+
if not val:
|
|
56
|
+
return 1
|
|
57
|
+
try:
|
|
58
|
+
n_procs = int(val)
|
|
59
|
+
if n_procs < 0:
|
|
60
|
+
raise ValueError
|
|
61
|
+
except ValueError:
|
|
62
|
+
warnings.warn(f"Ignoring {PROCS_ENV_VAR}={val!r}: not a non-negative integer.", stacklevel=2)
|
|
63
|
+
return 1
|
|
64
|
+
return n_procs
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_WORKER_COM: Optional["COM"] = None # This worker process's copy of the `COM` (see `COM.opt_eq()`'s `n_procs`).
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _init_worker(com: "COM") -> None:
|
|
71
|
+
"Keep this worker process's copy of the ``COM``, for ``_worker_foms()``."
|
|
72
|
+
global _WORKER_COM # pylint: disable=global-statement
|
|
73
|
+
_WORKER_COM = com
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _worker_foms(task: tuple[float, float, list[int], dict[str, Any]]) -> list[float]:
|
|
77
|
+
"The FOM of each of the given Tx FFE candidates (``calc_fom()``), for one CTLE setting, in a worker process."
|
|
78
|
+
gDC, gDC2, tx_ixs, kwargs = task
|
|
79
|
+
com = _WORKER_COM
|
|
80
|
+
assert com is not None, RuntimeError("Worker process not initialized!")
|
|
81
|
+
Hctf = com.calc_Hctf(gDC, gDC2)
|
|
82
|
+
return [com.calc_fom(tx_ix, Hctf, noise_vars=False, **kwargs) for tx_ix in tx_ixs]
|
|
45
83
|
|
|
46
84
|
|
|
47
85
|
def _conv_pmfs(p1: Rvec, p2: Rvec) -> Rvec:
|
|
@@ -131,8 +169,12 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
131
169
|
self.nRxPreTaps: int = 0 #: `dw` from `com_params`
|
|
132
170
|
self.gDC: float = 0.0 #: Choices are in `com_params.g_DC`.
|
|
133
171
|
self.gDC2: float = 0.0 #: Choices are in `com_params.g_DC2`.
|
|
134
|
-
self.
|
|
135
|
-
|
|
172
|
+
self._rx_ffe_phase_matrix: Optional[Cmat] = None # (See `rx_ffe_phase_matrix`.)
|
|
173
|
+
#: Number of processes for EQ optimization; 0 for one per CPU. (See `opt_eq()` and `default_n_procs()`.)
|
|
174
|
+
self.n_procs: int = default_n_procs()
|
|
175
|
+
self._Htx_last: Optional[tuple[int, Cvec]] = None # The last Tx FFE frequency response (see `Htx()`).
|
|
176
|
+
self._tx_phase_matrix: Optional[Cmat] = None # Tx FFE phase matrix, once calculated (see `Htx()`).
|
|
177
|
+
self._next_pr_cache: tuple[Optional[Cvec], dict[int, tuple[Cvec, Rvec]]] = (None, {}) # (`gen_pulse_resps()`)
|
|
136
178
|
|
|
137
179
|
# Channel data
|
|
138
180
|
self.chnls: list[COMChnl] = []
|
|
@@ -210,10 +252,7 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
210
252
|
sPkgTx, sPkgNext, sPkgFext, sPkgRx = self._pkg_models(self.freqs)
|
|
211
253
|
self._sPkgTx, self._sPkgNext, self._sPkgFext, self._sPkgRx = [sPkgTx], [sPkgNext], [sPkgFext], [sPkgRx]
|
|
212
254
|
self.chnls = list(map(self.add_pkg, ntwks))
|
|
213
|
-
|
|
214
|
-
self.chnls_noPkg = [(ntwks[0], calc_H21(f, ntwks[0][0], self.gamma1_Tx, self.gamma2_Rx))]
|
|
215
|
-
self.pulse_resps_nopkg = self.gen_pulse_resps(chnls=[self.chnls_noPkg[0]], apply_eq=False)
|
|
216
|
-
self.pulse_resps_noeq = self.gen_pulse_resps(chnls=[self.chnls[0]], apply_eq=False)
|
|
255
|
+
self._calc_uneq_pulse_resps()
|
|
217
256
|
|
|
218
257
|
# Generate all possible combinations of Tx FFE tap weights, in grid order (first tap varying slowest),
|
|
219
258
|
# along with each one's per-tap grid indices (used by the local search in ``opt_eq()``).
|
|
@@ -233,8 +272,8 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
233
272
|
# Set Rx FFE quantities.
|
|
234
273
|
self.nRxTaps = len(com_params.rx_taps_max)
|
|
235
274
|
self.nRxPreTaps = com_params.dw
|
|
236
|
-
self.rx_ffe_phase_matrix = np.exp(np.outer(np.arange(self.nRxTaps), -1j * TWOPI * ui * f))
|
|
237
275
|
self.null_rx_ffe: Rvec = array([0] * self.nRxPreTaps + [1.0] + [0] * (self.nRxTaps - self.nRxPreTaps - 1))
|
|
276
|
+
self._Hrx_null: Optional[Cvec] = None # `Hffe_Rx(self.null_rx_ffe)`, once calculated.
|
|
238
277
|
self.empty_array: Rvec = array([])
|
|
239
278
|
|
|
240
279
|
# Misc.
|
|
@@ -308,6 +347,7 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
308
347
|
|
|
309
348
|
#: Instance state that depends on the package test case (restored to the worst case's, by ``__call__()``).
|
|
310
349
|
_case_state = ("com_params", "chnls", "_sPkgTx", "_sPkgNext", "_sPkgFext", "_sPkgRx", "_pkg_cache",
|
|
350
|
+
"chnls_noPkg", "pulse_resps_nopkg", "pulse_resps_noeq",
|
|
311
351
|
"tx_ix", "gDC", "gDC2", "rx_taps", "dfe_taps", "cursor_ix", "com_rslts", "fom_rslts",
|
|
312
352
|
"theNoiseCalc", "mmse_rslt", "n_fom_evals")
|
|
313
353
|
|
|
@@ -381,6 +421,18 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
381
421
|
sPkgTx, sPkgNext, sPkgFext, sPkgRx = self._pkg_models(self.freqs)
|
|
382
422
|
self._sPkgTx, self._sPkgNext, self._sPkgFext, self._sPkgRx = [sPkgTx], [sPkgNext], [sPkgFext], [sPkgRx]
|
|
383
423
|
self.chnls = list(map(self.add_pkg, self._ntwks))
|
|
424
|
+
self._calc_uneq_pulse_resps()
|
|
425
|
+
|
|
426
|
+
def _calc_uneq_pulse_resps(self) -> None:
|
|
427
|
+
"""
|
|
428
|
+
The victim's unequalized pulse responses, w/ the Tx and Rx filters, for the current package test case:
|
|
429
|
+
``pulse_resps_noeq`` (w/ its packages; MATLAB COM's ``uneq_pulse_response``, whose peak it reports as
|
|
430
|
+
``peak_uneq_pulse_mV``), and ``pulse_resps_nopkg`` (w/o them, from ``chnls_noPkg``).
|
|
431
|
+
"""
|
|
432
|
+
ntwk = self._ntwks[0]
|
|
433
|
+
self.chnls_noPkg = [(ntwk, calc_H21(self.freqs, ntwk[0], self.gamma1_Tx, self.gamma2_Rx))]
|
|
434
|
+
self.pulse_resps_nopkg = self.gen_pulse_resps(chnls=self.chnls_noPkg, apply_eq=False)
|
|
435
|
+
self.pulse_resps_noeq = self.gen_pulse_resps(chnls=[self.chnls[0]], apply_eq=False)
|
|
384
436
|
|
|
385
437
|
# Instance Properties
|
|
386
438
|
def _pkg_models(self, freqs: Rvec) -> tuple[rf.Network, rf.Network, rf.Network, rf.Network]:
|
|
@@ -486,6 +538,18 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
486
538
|
"System frequency vector (Hz); decoupled from system time vector!"
|
|
487
539
|
return self.f
|
|
488
540
|
|
|
541
|
+
@property
|
|
542
|
+
def rx_ffe_phase_matrix(self) -> Cmat:
|
|
543
|
+
"Rx FFE phase matrix (see ``utility.filter.ffe_phase_matrix()``), calculated when first needed."
|
|
544
|
+
if self._rx_ffe_phase_matrix is None: # (~230 MB, for P802.3dj's 87 taps; so, not kept in pickles.)
|
|
545
|
+
self._rx_ffe_phase_matrix = ffe_phase_matrix(self.freqs, self.ui, self.nRxTaps)
|
|
546
|
+
return self._rx_ffe_phase_matrix
|
|
547
|
+
|
|
548
|
+
def __getstate__(self) -> dict[str, Any]:
|
|
549
|
+
state = self.__dict__.copy()
|
|
550
|
+
state["_rx_ffe_phase_matrix"] = None # (Recalculated when needed; see `rx_ffe_phase_matrix`.)
|
|
551
|
+
return state
|
|
552
|
+
|
|
489
553
|
@property
|
|
490
554
|
def Xsinc(self) -> Rvec:
|
|
491
555
|
"""Frequency domain sinc(f) corresponding to Rect(ui) in time domain."""
|
|
@@ -739,10 +803,15 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
739
803
|
"""
|
|
740
804
|
assert 0 <= tx_taps_ix < self.num_tx_combs, ValueError(
|
|
741
805
|
f"tx_taps_ix ({tx_taps_ix}) must be an integer in: [0, {self.num_tx_combs})!")
|
|
742
|
-
#
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
806
|
+
# Only the last is kept (each EQ candidate's channels share it): all of them would take ~3.5 GB
|
|
807
|
+
# for P802.3dj's 1,279 Tx FFE combinations; and each is cheap, from the shared phase matrix.
|
|
808
|
+
if self._Htx_last is None or self._Htx_last[0] != tx_taps_ix:
|
|
809
|
+
taps = array(self._tx_combs[tx_taps_ix])
|
|
810
|
+
if self._tx_phase_matrix is None: # (Same for every combination; cursor tap included.)
|
|
811
|
+
self._tx_phase_matrix = ffe_phase_matrix(self.freqs, 1 / self.fb, len(taps) + 1)
|
|
812
|
+
self._Htx_last = (tx_taps_ix, calc_Hffe(
|
|
813
|
+
self.freqs, 1 / self.fb, taps, 3, phase_matrix=self._tx_phase_matrix))
|
|
814
|
+
return self._Htx_last[1]
|
|
746
815
|
|
|
747
816
|
def Hffe_Rx(self, taps: Optional[Rvec] = None) -> Cvec:
|
|
748
817
|
"""
|
|
@@ -764,6 +833,15 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
764
833
|
assert len(taps) == self.nRxTaps, ValueError(
|
|
765
834
|
f"If `taps` is given then its length ({len(taps)}) must equal `nRxTaps` ({self.nRxTaps})!")
|
|
766
835
|
|
|
836
|
+
# The null Rx FFE (a `dw` UI delay) is applied to every pulse response, for every EQ candidate;
|
|
837
|
+
# so, it's calculated once (read-only, for safety).
|
|
838
|
+
if taps is self.null_rx_ffe:
|
|
839
|
+
if self._Hrx_null is None:
|
|
840
|
+
# Its one tap's row, which is identical to `taps @ self.rx_ffe_phase_matrix`, w/o forming
|
|
841
|
+
# that whole matrix (~230 MB, for P802.3dj's 87 taps), which EQ optimization doesn't otherwise need.
|
|
842
|
+
self._Hrx_null = ffe_phase_matrix(self.freqs, self.ui, self.nRxTaps, tap_ixs=[self.nRxPreTaps])[0]
|
|
843
|
+
self._Hrx_null.flags.writeable = False
|
|
844
|
+
return self._Hrx_null
|
|
767
845
|
return taps @ self.rx_ffe_phase_matrix
|
|
768
846
|
|
|
769
847
|
def H( # pylint: disable=too-many-arguments,too-many-locals,too-many-positional-arguments
|
|
@@ -826,7 +904,7 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
826
904
|
if nRxTaps:
|
|
827
905
|
Hrx = self.Hffe_Rx(rx_taps)
|
|
828
906
|
if passive_RxFFE:
|
|
829
|
-
Hrx
|
|
907
|
+
Hrx = Hrx / max(np.abs(Hrx)) # (Not in place: `Hffe_Rx()` may return a cached, read-only array.)
|
|
830
908
|
rslt *= Hrx
|
|
831
909
|
|
|
832
910
|
return rslt
|
|
@@ -906,12 +984,22 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
906
984
|
if Hctf is None:
|
|
907
985
|
Hctf = self.calc_Hctf(self.gDC, self.gDC2)
|
|
908
986
|
|
|
987
|
+
# A NEXT aggressor's pre-Rx-FFE pulse response is the same for every Tx EQ; so, it's cached for the
|
|
988
|
+
# last `Hctf` (by identity: `opt_eq()` passes the same array for each Tx EQ candidate), read-only.
|
|
989
|
+
cache_next = apply_eq and rx_taps is self.null_rx_ffe
|
|
990
|
+
if cache_next and self._next_pr_cache[0] is not Hctf:
|
|
991
|
+
self._next_pr_cache = (Hctf, {})
|
|
992
|
+
next_prs = self._next_pr_cache[1]
|
|
993
|
+
|
|
909
994
|
pulse_resps = []
|
|
910
995
|
for (_, ntype), H21 in chnls:
|
|
996
|
+
if cache_next and ntype == 'NEXT' and id(H21) in next_prs and next_prs[id(H21)][0] is H21:
|
|
997
|
+
pulse_resps.append(next_prs[id(H21)][1])
|
|
998
|
+
continue
|
|
911
999
|
if apply_eq:
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
pr = self.pulse_resp(self.H(H21,
|
|
1000
|
+
# NEXT aggressors have no Tx EQ. (Only they: `import_s32p()` interleaves NEXT and FEXT aggressors.)
|
|
1001
|
+
ix = 0 if ntype == 'NEXT' else tx_ix
|
|
1002
|
+
pr = self.pulse_resp(self.H(H21, ix, Hctf=Hctf, rx_taps=rx_taps, dfe_taps=dfe_taps))
|
|
915
1003
|
else: # Unequalized, but w/ the Tx and Rx filters (as MATLAB COM's `uneq_pulse_response`).
|
|
916
1004
|
pr = self.pulse_resp(self.Ht * H21 * self.Hr)
|
|
917
1005
|
|
|
@@ -919,6 +1007,9 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
919
1007
|
pr *= self.com_params.A_v
|
|
920
1008
|
elif ntype == 'NEXT':
|
|
921
1009
|
pr *= self.com_params.A_ne
|
|
1010
|
+
if cache_next:
|
|
1011
|
+
pr.flags.writeable = False
|
|
1012
|
+
next_prs[id(H21)] = (H21, pr)
|
|
922
1013
|
else:
|
|
923
1014
|
pr *= self.com_params.A_fe
|
|
924
1015
|
|
|
@@ -937,7 +1028,8 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
937
1028
|
unit_amp: Optional[bool] = None,
|
|
938
1029
|
n_pre: Optional[int] = None,
|
|
939
1030
|
ts_window: int = 0,
|
|
940
|
-
ts_center: int = 0
|
|
1031
|
+
ts_center: int = 0,
|
|
1032
|
+
noise_vars: bool = True
|
|
941
1033
|
) -> float:
|
|
942
1034
|
"""
|
|
943
1035
|
Calculate the **figure of merit** (FOM), given the existing linear EQ settings.
|
|
@@ -963,6 +1055,9 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
963
1055
|
Default: 0
|
|
964
1056
|
ts_center: MMSE cursor timing search starting point, relative to the pulse peak (vector samples).
|
|
965
1057
|
Default: 0
|
|
1058
|
+
noise_vars: Calculate the MMSE solution's noise variance breakdown ("varTx", "varJ", "varXT", "varN")
|
|
1059
|
+
when True; else, leave them ``None``. (See ``optimize.mmse_noise_vars()``.)
|
|
1060
|
+
Default: True
|
|
966
1061
|
|
|
967
1062
|
Returns:
|
|
968
1063
|
The resultant figure of merit.
|
|
@@ -1000,6 +1095,10 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
1000
1095
|
pulse_resps_preFFE = self.gen_pulse_resps( # Assumes no Rx FFE/DFE.
|
|
1001
1096
|
tx_ix=tx_ix, Hctf=Hctf,
|
|
1002
1097
|
rx_taps=self.null_rx_ffe, dfe_taps=self.empty_array)
|
|
1098
|
+
varTx: Optional[float] # (`None` from `mmse()`, w/ `noise_vars` False; likewise below.)
|
|
1099
|
+
varJ: Optional[float]
|
|
1100
|
+
varXT: Optional[float]
|
|
1101
|
+
varN: Optional[float]
|
|
1003
1102
|
match opt_mode:
|
|
1004
1103
|
case OptMode.PRZF:
|
|
1005
1104
|
# Step a - Pulse response construction.
|
|
@@ -1078,7 +1177,7 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
1078
1177
|
norm_mode=norm_mode, ts_window=ts_window, ts_center=ts_center,
|
|
1079
1178
|
ts_range=self.com_params.ts_sample_adj,
|
|
1080
1179
|
n_fix=self.com_params.Nfix, n_float_groups=self.com_params.Nwg,
|
|
1081
|
-
n_float_taps=self.com_params.Nwf)
|
|
1180
|
+
n_float_taps=self.com_params.Nwf, noise_vars=noise_vars)
|
|
1082
1181
|
fom = rslt["fom"]
|
|
1083
1182
|
rx_taps = rslt["rx_taps"]
|
|
1084
1183
|
dfe_tap_weights = rslt["dfe_tap_weights"]
|
|
@@ -1087,11 +1186,11 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
1087
1186
|
vic_peak_loc = np.argmax(vic_pulse_resp)
|
|
1088
1187
|
cursor_ix = rslt["cursor_ix"]
|
|
1089
1188
|
As = vic_pulse_resp[cursor_ix]
|
|
1090
|
-
varTx = rslt
|
|
1189
|
+
varTx = rslt.get("varTx")
|
|
1091
1190
|
varISI = rslt["varISI"]
|
|
1092
|
-
varJ = rslt
|
|
1093
|
-
varXT = rslt
|
|
1094
|
-
varN = rslt
|
|
1191
|
+
varJ = rslt.get("varJ")
|
|
1192
|
+
varXT = rslt.get("varXT")
|
|
1193
|
+
varN = rslt.get("varN")
|
|
1095
1194
|
self.fom_rslts['mse'] = rslt.get('mse')
|
|
1096
1195
|
self.theNoiseCalc = theNoiseCalc # pylint: disable=attribute-defined-outside-init
|
|
1097
1196
|
self.mmse_rslt = rslt # pylint: disable=attribute-defined-outside-init
|
|
@@ -1115,11 +1214,33 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
1115
1214
|
|
|
1116
1215
|
return fom
|
|
1117
1216
|
|
|
1118
|
-
def
|
|
1217
|
+
def _parallel_foms(
|
|
1218
|
+
self, cands: list[tuple[int, float, float, int]], n_procs: int, fom_kwargs: dict[str, Any]
|
|
1219
|
+
) -> list[float]:
|
|
1220
|
+
"""
|
|
1221
|
+
The FOM of each EQ candidate, ``(i_gDC2, gDC, gDC2, tx_ix)``, calculated in ``n_procs`` worker processes.
|
|
1222
|
+
|
|
1223
|
+
Notes:
|
|
1224
|
+
1. The candidates are dealt out in runs of those sharing a CTLE setting, about four per worker;
|
|
1225
|
+
so, each run's CTLE response is calculated once, and its NEXT pulse responses are reused.
|
|
1226
|
+
"""
|
|
1227
|
+
chunk = max(1, -(-len(cands) // (4 * n_procs)))
|
|
1228
|
+
tasks: list[tuple[float, float, list[int], dict[str, Any]]] = []
|
|
1229
|
+
for _, gDC, gDC2, tx_ix in cands:
|
|
1230
|
+
if tasks and tasks[-1][:2] == (gDC, gDC2) and len(tasks[-1][2]) < chunk:
|
|
1231
|
+
tasks[-1][2].append(tx_ix)
|
|
1232
|
+
else:
|
|
1233
|
+
tasks.append((gDC, gDC2, [tx_ix], fom_kwargs))
|
|
1234
|
+
n_procs = min(n_procs, len(tasks))
|
|
1235
|
+
with multiprocessing.get_context("spawn").Pool(n_procs, initializer=_init_worker, initargs=(self,)) as pool:
|
|
1236
|
+
return [fom for foms in pool.map(_worker_foms, tasks, chunksize=1) for fom in foms]
|
|
1237
|
+
|
|
1238
|
+
def opt_eq( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
1119
1239
|
self,
|
|
1120
1240
|
opt_mode: Optional[OptMode] = None,
|
|
1121
1241
|
norm_mode: Optional[NormMode] = None,
|
|
1122
|
-
unit_amp: Optional[bool] = None
|
|
1242
|
+
unit_amp: Optional[bool] = None,
|
|
1243
|
+
n_procs: Optional[int] = None
|
|
1123
1244
|
) -> bool:
|
|
1124
1245
|
"""
|
|
1125
1246
|
Find and set the optimum values for the linear equalization parameters:
|
|
@@ -1134,6 +1255,8 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
1134
1255
|
unit_amp: Enforce unit pulse response amplitude when True.
|
|
1135
1256
|
(For comparing ``przf()`` results to ``mmse()`` results.)
|
|
1136
1257
|
Default: None (i.e. - Use ``self.unit_amp``.)
|
|
1258
|
+
n_procs: Number of processes to evaluate EQ candidates in; 0 for one per CPU. (See Note 5.)
|
|
1259
|
+
Default: None (i.e. - Use ``self.n_procs``.)
|
|
1137
1260
|
|
|
1138
1261
|
Returns:
|
|
1139
1262
|
True if no errors encountered; False otherwise.
|
|
@@ -1154,6 +1277,13 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
1154
1277
|
4. With ``com_params.ts_srch_mode`` == "middle", each candidate's cursor timing search
|
|
1155
1278
|
works outward from the pulse peak, as MATLAB COM's (see ``optimize.mmse()``);
|
|
1156
1279
|
with ``com_params.ts_track`` also, from the best cursor offset so far, after a full search for the first.
|
|
1280
|
+
|
|
1281
|
+
5. With ``n_procs`` > 1, the candidates' FOMs are calculated in that many worker processes,
|
|
1282
|
+
each w/ its own copy of this instance; then, the winner's results are recalculated here.
|
|
1283
|
+
The outcome is identical to a serial search's. Since that needs the candidates to be independent,
|
|
1284
|
+
it's done only w/o a local search (``com_params.local_search`` = 0); otherwise, the search is serial.
|
|
1285
|
+
The workers are spawned; so, a script using this must guard its top level code,
|
|
1286
|
+
w/ ``if __name__ == "__main__":``.
|
|
1157
1287
|
"""
|
|
1158
1288
|
|
|
1159
1289
|
# Honor any mode overrides.
|
|
@@ -1161,6 +1291,9 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
1161
1291
|
norm_mode = norm_mode or self.norm_mode
|
|
1162
1292
|
if unit_amp is None: # Don't try the more Pythonic syntax above; it doesn't work for Boolean values.
|
|
1163
1293
|
unit_amp = self.unit_amp
|
|
1294
|
+
if n_procs is None:
|
|
1295
|
+
n_procs = self.n_procs
|
|
1296
|
+
n_procs = n_procs or os.cpu_count() or 1
|
|
1164
1297
|
|
|
1165
1298
|
cp = self.com_params
|
|
1166
1299
|
local_search = cp.local_search
|
|
@@ -1186,49 +1319,81 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
1186
1319
|
return True
|
|
1187
1320
|
return i_gDC > 0 and abs(i_gDC2 - best['i_gDC2']) > local_search
|
|
1188
1321
|
|
|
1189
|
-
|
|
1322
|
+
def record(i_gDC2: int, gDC: float, gDC2: float, tx_ix: int) -> dict[str, Any]:
|
|
1323
|
+
"The results the last ``calc_fom()`` left behind, for the given candidate."
|
|
1324
|
+
rslts = self.fom_rslts
|
|
1325
|
+
return {
|
|
1326
|
+
'gDC2': gDC2, 'gDC': gDC, 'tx_ix': tx_ix,
|
|
1327
|
+
'i_gDC2': i_gDC2, 'tx_grid_ixs': self._tx_grid_ixs[tx_ix],
|
|
1328
|
+
**{k: rslts[k] for k in ('rx_taps', 'pr_samps', 'dfe_tap_weights', 'cursor_ix', 'As',
|
|
1329
|
+
'varTx', 'varISI', 'varJ', 'varXT', 'varN', 'vic_pulse_resp')},
|
|
1330
|
+
'mse': rslts.get('mse', 0),
|
|
1331
|
+
# Everything `calc_fom()` leaves behind for this candidate, overwritten by later ones;
|
|
1332
|
+
# notably its MMSE noise model, which `calc_noise()` takes the Tx noise from.
|
|
1333
|
+
'fom_rslts': dict(rslts),
|
|
1334
|
+
'theNoiseCalc': getattr(self, 'theNoiseCalc', None),
|
|
1335
|
+
'mmse_rslt': getattr(self, 'mmse_rslt', None),
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
fom_kwargs: dict[str, Any] = {"opt_mode": opt_mode, "norm_mode": norm_mode, "unit_amp": unit_amp}
|
|
1190
1339
|
fom_max = -1000.0
|
|
1191
1340
|
best: Optional[dict[str, Any]] = None
|
|
1192
1341
|
self.n_fom_evals = 0 # pylint: disable=attribute-defined-outside-init
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1342
|
+
if n_procs > 1 and local_search:
|
|
1343
|
+
warnings.warn(f"EQ optimization is serial: parallel search ({n_procs} processes) "
|
|
1344
|
+
f"needs a full search (`local_search` = 0, not {local_search}).", stacklevel=2)
|
|
1345
|
+
if n_procs > 1 and not local_search:
|
|
1346
|
+
# Every candidate, in the serial search's order, w/ its FOM calculated in the worker processes.
|
|
1347
|
+
# (W/o a local search, the timing search is full, so independent of the best so far.)
|
|
1348
|
+
cands = [(i_gDC2, _gDC, _gDC2, _tx_ix)
|
|
1349
|
+
for _gDC in cp.g_DC for i_gDC2, _gDC2 in enumerate(cp.g_DC2) for _tx_ix in tx_ixs]
|
|
1350
|
+
foms = self._parallel_foms(cands, n_procs, {**fom_kwargs, "ts_window": 0, "ts_center": 0})
|
|
1351
|
+
self.n_fom_evals = len(cands)
|
|
1352
|
+
winner = None
|
|
1353
|
+
for cand, fom in zip(cands, foms):
|
|
1354
|
+
if fom > fom_max: # (The first of any equal ones, as the serial search.)
|
|
1355
|
+
fom_max, winner = fom, cand
|
|
1356
|
+
if winner is not None: # Recalculate the winner's results, here.
|
|
1357
|
+
i_gDC2, _gDC, _gDC2, _tx_ix = winner
|
|
1358
|
+
fom = self.calc_fom(_tx_ix, self.calc_Hctf(_gDC, _gDC2), **fom_kwargs,
|
|
1359
|
+
ts_window=0, ts_center=0, noise_vars=False)
|
|
1360
|
+
assert fom == fom_max, RuntimeError(
|
|
1361
|
+
f"The winning EQ's FOM ({fom} dB) differs from its worker's ({fom_max} dB)!")
|
|
1362
|
+
best = record(i_gDC2, _gDC, _gDC2, _tx_ix)
|
|
1363
|
+
else:
|
|
1364
|
+
# Run the nested optimization loops, keeping the best candidate's results.
|
|
1365
|
+
for i_gDC, _gDC in enumerate(cp.g_DC):
|
|
1366
|
+
for i_gDC2, _gDC2 in enumerate(cp.g_DC2):
|
|
1367
|
+
_Hctle: Optional[Cvec] = None
|
|
1368
|
+
for _tx_ix in tx_ixs:
|
|
1369
|
+
if skip(i_gDC, i_gDC2, _tx_ix, best):
|
|
1370
|
+
continue
|
|
1371
|
+
if _Hctle is None:
|
|
1372
|
+
_Hctle = self.calc_Hctf(_gDC, _gDC2)
|
|
1373
|
+
ts_window = local_search if middle else 0
|
|
1374
|
+
ts_center = 0
|
|
1375
|
+
if middle and cp.ts_track:
|
|
1376
|
+
if best is None:
|
|
1377
|
+
ts_window = 0
|
|
1378
|
+
else:
|
|
1379
|
+
ts_center = int(best['fom_rslts']['cursor_ix'] - best['fom_rslts']['vic_peak_loc'])
|
|
1380
|
+
fom = self.calc_fom(
|
|
1381
|
+
_tx_ix, _Hctle, **fom_kwargs,
|
|
1382
|
+
ts_window=ts_window, ts_center=ts_center, noise_vars=False)
|
|
1383
|
+
self.n_fom_evals += 1
|
|
1384
|
+
if fom > fom_max:
|
|
1385
|
+
fom_max = fom
|
|
1386
|
+
best = record(i_gDC2, _gDC, _gDC2, _tx_ix)
|
|
1228
1387
|
|
|
1229
1388
|
# Check for error and save the best results.
|
|
1230
1389
|
if best is None:
|
|
1231
1390
|
return False # Flags the caller that the following settings have NOT been made.
|
|
1391
|
+
# The winner's MMSE noise variance breakdown, skipped above, since it's reported, not optimized.
|
|
1392
|
+
# (Its noise calculator was left at its chosen sampling time.)
|
|
1393
|
+
if best['mmse_rslt'] is not None and best['varTx'] is None:
|
|
1394
|
+
noise_vars = mmse_noise_vars(best['theNoiseCalc'], best['mmse_rslt']['rx_taps_raw'], self.nRxPreTaps)
|
|
1395
|
+
for d in (best, best['fom_rslts'], best['mmse_rslt']):
|
|
1396
|
+
d.update(noise_vars)
|
|
1232
1397
|
# Note the normalization of the Rx FFE tap weights, to produce a unit amplitude cursor tap.
|
|
1233
1398
|
# This is not dictated by the spec., but is what the MATLAB code does.
|
|
1234
1399
|
self.gDC2 = best['gDC2']
|
|
@@ -1445,13 +1610,32 @@ class COM: # pylint: disable=too-many-instance-attributes,too-many-public-metho
|
|
|
1445
1610
|
"""
|
|
1446
1611
|
|
|
1447
1612
|
|
|
1448
|
-
def
|
|
1613
|
+
def _run_one_com( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1614
|
+
com_params: COMParams, ch_set: ChnlSet, opt_mode: OptMode, norm_mode: NormMode, unit_amp: bool,
|
|
1615
|
+
debug: bool, n_procs: int
|
|
1616
|
+
) -> COM:
|
|
1617
|
+
"COM for one channel set (see ``run_com()``)."
|
|
1618
|
+
theCOM = COM(com_params, ch_set, debug=debug)
|
|
1619
|
+
theCOM.n_procs = n_procs
|
|
1620
|
+
# Calling the object calculates the COM value, as well as many other intermediate results.
|
|
1621
|
+
theCOM(opt_mode=opt_mode, norm_mode=norm_mode, unit_amp=unit_amp)
|
|
1622
|
+
return theCOM
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
def _run_one_com_task(task: tuple[COMParams, ChnlSet, OptMode, NormMode, bool, bool]) -> COM:
|
|
1626
|
+
"``_run_one_com()``, in a worker process (see ``run_com()``), w/ a serial EQ search."
|
|
1627
|
+
com_params, ch_set, opt_mode, norm_mode, unit_amp, debug = task
|
|
1628
|
+
return _run_one_com(com_params, ch_set, opt_mode, norm_mode, unit_amp, debug, n_procs=1)
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
def run_com( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
|
|
1449
1632
|
chnl_sets: list[tuple[ChnlGrpName, ChnlSet]],
|
|
1450
1633
|
com_params: COMParams,
|
|
1451
1634
|
opt_mode: OptMode = OptMode.MMSE,
|
|
1452
1635
|
norm_mode: NormMode = NormMode.P8023dj,
|
|
1453
1636
|
unit_amp: bool = True,
|
|
1454
|
-
dbg_dict: Optional[dict[str, Any]] = None
|
|
1637
|
+
dbg_dict: Optional[dict[str, Any]] = None,
|
|
1638
|
+
n_procs: Optional[int] = None
|
|
1455
1639
|
) -> dict[ChnlGrpName, dict[ChnlSetName, COM]]:
|
|
1456
1640
|
"""
|
|
1457
1641
|
Run COM on a list of grouped channel sets.
|
|
@@ -1470,28 +1654,45 @@ def run_com( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
1470
1654
|
dbg_dict: Optional dictionary into which debugging values may be stashed,
|
|
1471
1655
|
for later analysis.
|
|
1472
1656
|
Default: None
|
|
1657
|
+
n_procs: Number of processes; 0 for one per CPU. (See Note 1.)
|
|
1658
|
+
Default: None (``default_n_procs()``)
|
|
1473
1659
|
|
|
1474
1660
|
Returns:
|
|
1475
1661
|
2D dictionary indexed by channel group name, then by channel set name,
|
|
1476
1662
|
containing the completed COM objects.
|
|
1477
|
-
"""
|
|
1478
1663
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1664
|
+
Notes:
|
|
1665
|
+
1. With ``n_procs`` > 1 and several channel sets, they're run in that many worker processes, one channel set
|
|
1666
|
+
each at a time, each w/ a serial EQ search (``COM.opt_eq()``): that works w/ a local search, too, and helps
|
|
1667
|
+
however small each search is. With one channel set, its EQ search is parallel instead, when it can be
|
|
1668
|
+
(see ``COM.opt_eq()``). Either way, the results are identical to a serial run's.
|
|
1669
|
+
The workers are spawned; so, a script using this must guard its top level code,
|
|
1670
|
+
w/ ``if __name__ == "__main__":``.
|
|
1671
|
+
"""
|
|
1487
1672
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1673
|
+
n_procs = default_n_procs() if n_procs is None else n_procs
|
|
1674
|
+
n_procs = n_procs or os.cpu_count() or 1
|
|
1675
|
+
debug = dbg_dict is not None
|
|
1676
|
+
labels = [(grp, ch_set['THRU'][0].stem) for grp, ch_set in chnl_sets]
|
|
1677
|
+
if n_procs > 1 and len(chnl_sets) > 1:
|
|
1678
|
+
tasks = [(com_params, ch_set, opt_mode, norm_mode, unit_amp, debug) for _, ch_set in chnl_sets]
|
|
1679
|
+
with multiprocessing.get_context("spawn").Pool(min(n_procs, len(tasks))) as pool:
|
|
1680
|
+
coms = pool.imap(_run_one_com_task, tasks) # (In order; so, reported in order, as each finishes.)
|
|
1681
|
+
results = []
|
|
1682
|
+
for (grp, lbl), theCOM in zip(labels, coms):
|
|
1683
|
+
print(f"{grp} : {lbl}")
|
|
1684
|
+
results.append(theCOM)
|
|
1685
|
+
else:
|
|
1686
|
+
results = []
|
|
1687
|
+
for (grp, lbl), (_, ch_set) in zip(labels, chnl_sets):
|
|
1688
|
+
print(f"{grp} : {lbl}")
|
|
1689
|
+
results.append(_run_one_com(com_params, ch_set, opt_mode, norm_mode, unit_amp, debug, n_procs))
|
|
1490
1690
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1691
|
+
theCOMs: dict[ChnlGrpName, dict[ChnlSetName, COM]] = {}
|
|
1692
|
+
for (grp, lbl), theCOM in zip(labels, results):
|
|
1693
|
+
if debug:
|
|
1694
|
+
theCOM.dbg_dict = dbg_dict # (The caller's own, rather than a worker's copy.)
|
|
1695
|
+
theCOMs.setdefault(grp, {})[lbl] = theCOM
|
|
1495
1696
|
|
|
1496
1697
|
return theCOMs
|
|
1497
1698
|
|
|
@@ -31,6 +31,10 @@ COMChnl: TypeAlias = tuple[COMNtwk, Cvec]
|
|
|
31
31
|
PI = math.pi #: Value of Pi (`math.pi`, a plain float; `np.pi` made autodoc barf)
|
|
32
32
|
TWOPI = 2.0 * PI #: 2 * Pi
|
|
33
33
|
|
|
34
|
+
#: Environment variable giving the default number of EQ optimization processes (``COM.n_procs``);
|
|
35
|
+
#: set by ``pychopmarg run-notebook --procs``.
|
|
36
|
+
PROCS_ENV_VAR = "PYCHOPMARG_PROCS"
|
|
37
|
+
|
|
34
38
|
ChnlGrpName: TypeAlias = str # channel group name
|
|
35
39
|
ChnlSetName: TypeAlias = str # channel set name (the stem of the thru channel s4p file name)
|
|
36
40
|
ChnlTypName: TypeAlias = str # channel type name ("thru", "next", or "fext")
|