Parameter sweeps#
Parameter sweeps (decision D35).
A Monte-Carlo sweep is a stateless for loop: reconfigure
(set_params(), decision D34),
reseed (seed(), decision D6),
run, collect. The loop below stays visible on purpose – a student must
be able to read where the SNR is swept – and the caller writes one
line. The Trainer/hook pattern is explicitly rejected by the
architecture document.
Extracted from the identical skeleton of three validation/ scripts
(core_awgn_ser.py, fec_coding_gain.py, ofdm_awgn_ser.py),
per the decision’s trigger.
- monte_carlo(chain: Sequential, param: str | Sequence[str], values: Sequence[Any], metrics: Mapping[str, Callable[[...], Any]], stimulus: Any, *, reference: str | None = None, seed: int | None = None, n_jobs: int = 1) dict[str, ndarray][source]#
Run a chain over a range of parameter values and collect metrics.
- Parameters:
chain (Sequential) – The chain to sweep. It is reconfigured in place.
param (str or sequence of str) – Dotted parameter path(s), e.g.
"noise.snr_dB"(seeSequential.block_ids()). With several paths, each entry ofvaluesprovides one value per path (zip semantics; the cartesian product is out of scope, per the decision).values (sequence) – Parameter values, one sweep point each.
metrics (mapping of str to callable) – Metrics collected at every point. Called as
metric(target, y)whenreferencenames an observed block, elsemetric(y). Smaller-is-better quantities (BER, SER, EVM); there is noscore()(decision D24).stimulus – Input passed to the chain at every point (e.g. a symbol count).
reference (str, optional, keyword-only) – Block id whose observed output is the metric target. The observation is declared on the chain if it is not already (
Sequential.observations).seed (int, optional, keyword-only) – Master seed; every sweep point gets an independent child seed through the chain (decisions D6/D35), so the whole curve is reproducible.
n_jobs (int, optional, keyword-only) – Sweep points to run at once. Default 1, the plain loop below.
-1uses every core. See Running points in parallel.
- Returns:
dict of str to np.ndarray – One array per metric, aligned with
values.- Raises:
ValueError – If
n_jobsis not 1 and noseedwas given, or if the chain or a metric cannot be sent to a worker process.
Notes
Running points in parallel. The points of a sweep are independent by construction – each one reconfigures and reseeds the chain from scratch – so they can be run at the same time, and
n_jobssays how many at once. Three consequences are worth knowing before turning it on.The result does not change. Each point already draws from its own child seed, so a curve computed with
n_jobs=4is identical to the serial one, value for value, not merely statistically equivalent. This is asserted in the test suite. It is also whyseed=becomes mandatory: without it, what a point draws would depend on which worker picked it up and in which order.A worker is a process, so everything it needs is pickled – the chain, the metrics and the stimulus. A metric written as a
lambdaor as a closure cannot cross that boundary; define it at module level instead. The error says so rather than lettingpickleexplain it.A stateful chain should stay serial. Each worker holds its own copy of the chain, so a block that carries something between points (an adaptive equalizer, an estimator fitted at the previous point) sees a different history than it would in the loop. Sweeps of such chains are order-dependent to begin with; this only makes it visible.
It is not free. Starting a worker and pickling the chain into it costs about 130 ms, once per worker, so the sweep has to be long enough to pay for it. Measured on four cores, a 24-point sweep of a sphere-decoded 4x4 link:
cost of one point
n_jobs=1
n_jobs=2
n_jobs=4
19 ms
0.46 s
0.51 s
0.39 s
222 ms
5.34 s
2.99 s
1.99 s
Below ~20 ms a point the pool costs more than it saves; at ~200 ms it returns 2.7x on four cores. The values are identical in every column.
Examples
>>> from comnumpy import AWGN, Sequential, SymbolGenerator >>> from comnumpy.core.metrics import compute_ser >>> chain = Sequential([SymbolGenerator(4), AWGN(snr_dB=0, name="noise")]) >>> out = monte_carlo(chain, "noise.snr_dB", [0, 10], ... {"power": lambda y: float(np.mean(np.abs(y)**2))}, ... stimulus=1000, seed=1) >>> print(out["power"].shape) (2,)