qlat_utils.jackknife_utils — Jackknife Resampling¶
Source: qlat-utils/qlat_utils/jackknife_utils.py
Note: Update this document when updating the source file.
Outline¶
Overview¶
The qlat_utils.jackknife_utils module provides jackknife resampling and error
estimation for lattice QCD measurements. It includes:
Jackknife resampling — the plain jackknife of a data list, and the
jk_avg/jk_errextraction from the resulting array.Super-jackknife — resampling over the jackknife blocks of the data, with the
sjk_*extraction functions.Randomized jackknife-bootstrap hybrid (RJK) — random samples with a blocking dependent shift, the default of the unified API, with the
rjk_*extraction functions.The unified
g_*API —g_mk_jkand friends, configured bydefault_g_jk_kwargs(or theJkKwargscontext manager), which dispatch onjk_typeand cache their intermediate state throughget_jk_state/set_jk_state.Collective MPI variants —
is_sync_node=True(every node holds the whole input and obtains the whole result) andg_mk_jk_distributed(the input is split between the nodes), built on thempi4pycollectives.
The generic helpers used here (q, the type tuples, use_kwargs, average,
block_data, filter_np_results, fsqr, fsqrt, qnorm and
NewDictValues) live in qlat_utils.data.
import qlat_utils as q
data = [1.0, 1.1, 0.9, 1.05]
jk_arr = q.g_mk_jk(data, list(range(len(data))))
avg, err = q.g_jk_avg_err(jk_arr)
Jackknife Resampling¶
jackknife(data_list, *, eps=1)¶
Perform standard jackknife. Returns jk_arr of length N + 1 where:
jk_arr[0]= averagejk_arr[i]=avg - (eps / N) * (data[i] - avg)fori >= 1
jk_avg(jk_arr)¶
Return the average (first element) of a jackknife array.
jk_err(jk_arr, *, eps=1, block_size=1)¶
Return the jackknife error estimate:
The eps and block_size must match those used in the corresponding
jackknife call. Note: len(jk_arr) = N + 1.
jk_avg_err(jk_arr, *, eps=1, block_size=1)¶
Return (jk_avg, jk_err).
Super-Jackknife¶
sjackknife(data_list, jk_idx_list, *, avg=None, ...)¶
Perform super-jackknife resampling. Data from different ensembles (identified
by jk_idx_list) are combined into a single jackknife array.
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
list/ndarray |
— |
Original data |
|
list |
— |
Index for each data point (e.g., |
|
any |
|
Pre-computed average (auto-computed if |
|
bool |
|
Use hash when jk_idx not in |
|
int |
|
Hash table size |
|
|
|
RNG state (default: |
|
list |
|
All possible indices; |
|
callable |
|
Function returning |
|
callable |
|
|
|
float |
|
Scaling factor |
sjackknife_distributed(data_list, jk_idx_list, *, avg=None, ...)¶
sjackknife for a data set that is split between the MPI nodes: data_list
and jk_idx_list are the local parts of the data set and the returned
jk_arr is the local part of the result, so that
jk_arr = np.concatenate(q.get_comm().allgather(jk_local))
is the complete data set (node r owns the samples in
range(*get_distributed_range(len(all_jk_idx), r, num_node))). It is a
collective operation and every node must call it with the same parameters.
The result agrees with sjackknife up to the floating-point roundoff but not
bit-for-bit. g_mk_jk_distributed dispatches to it.
sjackknife_sync_node(data_list, jk_idx_list, *, avg=None, ...)¶
sjackknife as a collective MPI operation where every node has the whole
input: the input is split between the nodes, sjackknife_distributed is
called on the local parts and the parts are gathered, so that every node
obtains the complete jk_arr. The result agrees with sjackknife up to the
floating-point roundoff but not bit-for-bit. g_mk_jk_sync_node dispatches to
it.
sjk_avg(jk_arr) / sjk_err(jk_arr, *, eps=1) / sjk_avg_err(jk_arr, *, eps=1)¶
Average, error, and (avg, err) for super-jackknife arrays. The error formula
differs from standard jackknife: no N/(N-1) factor.
sjk_mk_jk_val(rs_tag, val, err, *, ...)¶
Create a synthetic jackknife array from a central value and error using Gaussian random numbers.
Randomized Jackknife-Bootstrap Hybrid¶
rjackknife(data_list, jk_idx_list, *, avg=None, ...)¶
Jackknife-bootstrap hybrid resampling. Returns jk_arr of length
1 + n_rand_sample. The distribution of jk_arr approximates the
distribution of the average.
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
list/ndarray |
— |
Original data |
|
list |
— |
Index for each data point |
|
any |
|
Pre-computed average |
|
|
|
RNG state |
|
int |
|
Number of random samples |
|
callable |
|
|
|
bool |
|
Normalize random vectors |
|
bool |
|
Shift blocking per sample |
|
float |
|
Scaling factor |
|
bool |
|
Collective MPI operation (see below) |
The formula is:
where \(r_{i,j} \sim \mathcal{N}(0, 1)\) and \(b(i,j)\) is the block size.
If is_sync_node is True, the operation is assumed to be a collective
operation in a MPI program where every node has the same input. It is then
performed by rjackknife_sync_node: the input is split between the nodes and
rjackknife_distributed is called on the local parts; the parts are gathered
with mpi4py’s Allgatherv on the communicator returned by q.get_comm(), so
that every node obtains the complete jk_arr. The result agrees with the
is_sync_node=False result up to the floating-point roundoff, but not
bit-for-bit, because the average and the sums over the data set are reduced
across the nodes. The input is split using the qlat node numbering
(q.get_id_node()), which for some Grid processor layouts differs from the
MPI_COMM_WORLD rank — q.get_comm() is the communicator that matches it (it
must satisfy comm.rank == q.get_id_node()). qlat and mpi4py are imported
only when is_sync_node is True. This requires qlat to be initialized on the
whole MPI communicator (q.begin_with_mpi(), q.begin_with_gpt() or
q.begin_with_grid()), and the data must have a numeric dtype supported by
MPI.
rjackknife_distributed(data_list, jk_idx_list, *, avg=None, ...)¶
rjackknife for a data set that is split between the MPI nodes: data_list
and jk_idx_list are the local parts of the data set and the returned
jk_arr is the local part of the result, so that
jk_arr = np.concatenate(q.get_comm().allgather(jk_local))
is the complete data set (node r owns the samples in
range(*get_distributed_range(1 + n_rand_sample, r, num_node))). It is a
collective operation and every node must call it with the same parameters.
The result agrees with rjackknife up to the floating-point roundoff but not
bit-for-bit. g_mk_jk_distributed dispatches to it.
rjackknife_sync_node(data_list, jk_idx_list, *, avg=None, ...)¶
rjackknife as a collective MPI operation where every node has the whole
input: the input is split between the nodes, rjackknife_distributed is
called on the local parts and the parts are gathered, so that every node
obtains the complete jk_arr. The result agrees with rjackknife up to the
floating-point roundoff but not bit-for-bit. g_mk_jk_sync_node dispatches to
it.
rjk_avg(jk_arr) / rjk_err(jk_arr, eps=1) / rjk_avg_err(rjk_list, eps=1)¶
Average, error, and (avg, err) for randomized jackknife arrays.
rjk_mk_jk_val(rs_tag, val, err, *, ...)¶
Create a synthetic RJK array from a central value and error.
Unified Jackknife API¶
The g_* functions provide a unified interface that dispatches to either
super-jackknife or RJK based on global settings in default_g_jk_kwargs.
default_g_jk_kwargs¶
Global dictionary controlling jackknife behavior. Key settings:
Key |
Default |
Description |
|---|---|---|
|
|
|
|
|
Scaling factor |
|
|
Number of random samples ( |
|
|
Normalize the random vectors ( |
|
|
Shift the blocking for every random sample ( |
|
|
Reproduce the old random numbers ( |
|
|
Hash the |
|
|
Number of hash based samples ( |
|
|
The samples of |
|
|
Callable returning |
|
|
Default blocking size |
|
|
Per- |
|
|
|
|
|
RNG state (random numbers of |
|
|
Run as a collective MPI operation |
is_sync_node is not part of get_jk_state / set_jk_state: it changes only
how the result is computed (the result agrees up to the floating-point
roundoff), so it is deliberately excluded from the q.cache_call cache key.
g_mk_jk(data_list, jk_idx_list, *, avg=None, ...)¶
Create a (randomized) super-jackknife data set from un-jackknifed data:
jk_arr[0] is the average of the data and jk_arr[1:] are the resampled
values used by g_jk_avg_err. The data set has g_jk_size() samples
(1 + n_rand_sample for "rjk" and 1 + len(all_jk_idx) for "super") and
the dtype of the data. It dispatches to sjackknife or rjackknife based on
jk_type, and to g_mk_jk_sync_node or g_mk_jk_distributed for the
collective MPI modes.
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
list/ndarray |
— |
The un-jackknifed data ( |
|
list |
— |
Names of the entries of |
|
any |
|
Average of the whole data set; computed from |
|
bool |
|
Collective MPI operation with the whole input on every node (see |
All other keyword parameters are the shared settings listed under
default_g_jk_kwargs above, e.g. jk_type, eps, n_rand_sample,
block_size/block_size_dict, all_jk_idx and rng_state. When the
data_list is already jackknifed, increase eps by the factor
len(data_list).
g_mk_jk_sync_node(data_list, jk_idx_list, *, avg=None, ...)¶
Create a (randomized) super-jackknife data set as a collective MPI operation
in which every node holds the whole input and obtains the whole result; this is
what g_mk_jk(..., is_sync_node=True) calls. Every node must call it with the
same parameters and with the complete data_list/jk_idx_list, and every node
obtains the complete jk_arr in the same order as the one from g_mk_jk on a
single node. The input is split between the nodes in the order of the nodes
(get_distributed_range), the local parts are computed by
rjackknife_sync_node or sjackknife_sync_node and the parts are gathered, so
the work is parallelized over the nodes.
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
list/ndarray |
— |
The whole data set, the same on every node |
|
list |
— |
Indices of the whole data set, the same on every node |
|
any |
|
Average of the whole data set, the same on every node; computed with |
Returns the complete jk_arr on every node. Both jk_type == "rjk" and
jk_type == "super" are supported, and the remaining keyword parameters are
the same as for g_mk_jk. Requires the qlat communicator initialized on the
whole MPI communicator (q.begin_with_mpi(), q.begin_with_gpt(),
q.begin_with_grid() or q.set_comm(...)) and an MPI-supported data dtype.
The result agrees with g_mk_jk up to the floating-point roundoff, but not
bit-for-bit.
g_mk_jk_distributed(data_list, jk_idx_list, *, avg=None, ...)¶
Create a (randomized) super-jackknife data set when the data set itself is
split between the MPI nodes. This is a collective MPI operation: every node
must call it with the same parameters and with its own disjoint part of the
data set (data_list and jk_idx_list are the local parts, and together they
must cover the whole data set exactly once), and every node returns its own
part of the result:
import numpy as np
import qlat as q
comm = q.get_comm()
i_start, i_end = q.get_distributed_range(len(data_list), comm.rank, comm.size)
jk_local = q.g_mk_jk_distributed(
data_list[i_start:i_end], jk_idx_list[i_start:i_end]
)
jk_arr = np.concatenate(comm.allgather(jk_local)) # the complete data set
The split of the input between the nodes is free as long as the local parts are
disjoint and cover the whole data set; the split of the output is fixed: the
samples are split between the nodes in the order of the nodes, i.e. node r
out of num_node owns the samples in
range(*get_distributed_range(g_jk_size(), r, num_node)) (a node owns 0 samples
when there are more nodes than samples), so concatenating the local parts in
the order of the nodes reproduces the complete data set in the same order as
the one obtained with g_mk_jk. It dispatches to rjackknife_distributed for
jk_type == "rjk" and to sjackknife_distributed for jk_type == "super".
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
list/ndarray |
— |
The local part of the un-jackknifed data |
|
list |
— |
Indices of the local part, same length as the local |
|
any |
|
Average of the whole data set, the same on every node; computed with |
Returns the local part of jk_arr (an array with 0 samples on a node which
owns no sample). The remaining keyword parameters are the same as for
g_mk_jk. Requires the qlat communicator initialized on the whole MPI
communicator and an MPI-supported data dtype. Only the small jk_idx metadata
is gathered on every node, so no node needs to hold the whole data set, the
whole random matrix or the whole result.
The result agrees with g_mk_jk (and with g_mk_jk(..., is_sync_node=True))
up to the floating-point roundoff, but not bit-for-bit: the average and the
sums over the data set are reduced across the nodes, which changes the order of
the floating-point additions. The is_sync_node setting of
default_g_jk_kwargs is ignored, since the output is always distributed.
get_distributed_range(total_size, id_node, num_node)¶
Return (i_start, i_end), the contiguous range of the total_size indices
owned by node id_node out of num_node nodes:
i_start = (total_size * id_node) // num_node
i_end = (total_size * (id_node + 1)) // num_node
The parts are ordered by id_node, cover range(total_size) exactly once and
differ in size by at most one.
get_collective_comm(tag) / is_mpi_dtype(dtype)¶
Helpers for the collective MPI operations. get_collective_comm(tag) returns
(comm, id_node, num_node) from q.get_comm() and raises an Exception
(with tag naming the caller) when the qlat communicator is missing or does
not cover the whole MPI communicator; it asserts comm.rank == q.get_id_node(). is_mpi_dtype(dtype) reports whether a numpy dtype can be
summed by MPI.
get_distributed_jk_input(fname, comm, data_list, jk_idx_list)¶
Return the common state of the distributed jackknife functions:
(data_arr, jk_idx_list_local, jk_idx_list_glb, n, elem_shape, dtype), i.e.
the local data (shape (n_local, *elem_shape)), the local and the gathered
jk_idx, the total number of data points and the description of a single data
point; n is reduced and jk_idx_list_glb is gathered over the nodes.
get_distributed_avg(comm, data_arr, n, avg=None)¶
Return the average of the whole distributed data set: avg itself when it is
not None, otherwise the local sums reduced with Allreduce and divided by
n.
get_reduce_scattered_jk_arr(partial_arr, comm, id_node, num_node, avg)¶
Return the local part of the result from partial_arr (shape
(total_size, *elem_shape), the contribution of the local data to every
sample, with the sample 0 equal to avg on its owner): the contributions are
summed with Reduce_scatter and avg is added to the samples. partial_arr
may be any array, in particular a non-contiguous view such as a column of a 2-D
array (a contiguous copy is made when needed, since the buffer of the
collective must be contiguous).
get_gathered_jk_arr(jk_local, comm, num_node)¶
Return the complete result from the local parts jk_local, gathered with
Allgatherv in the order of the nodes. jk_local may be any array, in
particular a non-contiguous view such as a column of a 2-D array (a contiguous
copy is made when needed, since the send buffer of the collective must be
contiguous).
g_mk_jk_val(rs_tag, val, err, *, ...)¶
Create a synthetic jackknife array from a value and error. Dispatches to
sjk_mk_jk_val or rjk_mk_jk_val.
g_jk_avg(jk_arr) / g_jk_err(jk_arr) / g_jk_avg_err(jk_arr)¶
Unified average, error, and (avg, err) extraction.
g_jk_avg_err_arr(jk_arr)¶
Return an array with shape jk_arr[0].shape + (2,) where the last axis is
(avg, err).
g_jk_size(*, jk_type, ...)¶
Return the number of samples in the jackknife array (1 + n_samples).
g_jk_blocking_func(i, jk_idx)¶
Apply the configured blocking function.
g_jk_sample_size(job_tag, traj_list)¶
Return the number of distinct blocks for a given job_tag and trajectory list.
get_jk_state() / set_jk_state(state)¶
Save and restore the current default_g_jk_kwargs state (for use with
@cache_call).
Context Managers¶
class JkKwargs(**kwargs)¶
Context manager that temporarily overrides default_g_jk_kwargs.
with q.JkKwargs(n_rand_sample=2048, block_size=10):
jk_arr = q.g_mk_jk(data_list, jk_idx_list)
Examples¶
Jackknife Resampling¶
import qlat_utils as q
import numpy as np
data = [1.0, 1.1, 0.9, 1.05, 0.95, 1.02, 0.98, 1.03]
# Standard jackknife
jk_arr = q.jackknife(data)
avg = q.jk_avg(jk_arr)
err = q.jk_err(jk_arr)
print(f"Jackknife: avg = {avg:.4f}, err = {err:.4f}")
# Unified API (uses RJK by default)
jk_arr = q.g_mk_jk(data, list(range(len(data))))
avg, err = q.g_jk_avg_err(jk_arr)
print(f"RJK: avg = {avg:.4f}, err = {err:.4f}")
Distributed MPI¶
import numpy as np
import qlat_utils as q
# Every node holds the whole data set and obtains the whole result.
jk_arr = q.g_mk_jk(data_list, jk_idx_list, is_sync_node=True)
# Or let every node contribute its own part of the data set and of the result.
comm = q.get_comm()
i_start, i_end = q.get_distributed_range(len(data_list), comm.rank, comm.size)
jk_local = q.g_mk_jk_distributed(
data_list[i_start:i_end], jk_idx_list[i_start:i_end]
)
jk_arr = np.concatenate(comm.allgather(jk_local))