pyjuice.queries.sample

pyjuice.queries.sample(pc: TensorCircuit, num_samples: int | None = None, conditional: bool = False, use_cudagraph: bool = False, _use_scope_plan: bool = True, _sample_input_ns: bool = True, _do_calibration: bool = False, **kwargs)

Draw samples from a PC by performing a top-down ancestral sampling pass.

For unconditional sampling, num_samples must be specified. For conditional sampling (conditional = True), run a forward pass on the evidence first (e.g., via marginal()); the sampler then reuses the cached pc.node_mars and draws one sample per example in that batch, so num_samples is ignored.

Parameters:
  • pc (TensorCircuit) – the input PC

  • num_samples (Optional[int]) – number of samples to draw; required for unconditional sampling, ignored when conditional = True

  • conditional (bool) – whether to sample conditioned on the evidence cached by a preceding forward pass

Per-sample external sum parameters are supplied through sum_external_params, in kwargs, exactly as they are to TensorCircuit.forward()

samples = juice.queries.sample(pc, num_samples = 1024, sum_external_params = {ns: phi})

with one gate per DRAWN SAMPLE, so their batch axis is num_samples. A sum layer that is given none is sampled from its shared parameters, which is what an ungated forward pass computes for it too. Under conditional = True they are instead taken from the forward pass that produced pc.node_marselement_mars was built under those gates, so the draw has to use them – and passing them here only has to name the same nodes.

Parameters:
  • use_cudagraph (bool) – capture the top-down pass into a CUDA graph and replay it. OPT-IN, because a graph owns a private memory pool and the frontier buffers it was captured with, held for the circuit’s lifetime – worth it for a sampling loop, not for a one-off draw. Needs pc.is_structured_decomposable (the index plan must repeat, or a replay is simply wrong) and is refused otherwise. One graph is kept per (num_samples, conditional), bounded with the plans.

  • _sample_input_ns (bool) – whether to finish the draw by emitting a value from each selected input node. Default True, which is what makes the return a tensor of VALUES. Setting it False stops one step earlier and returns the FRONTIER instead – see the note below, which is the whole contract.

Returns:

with _sample_input_ns = True (the default), samples of size [num_samples, num_vars] in the input distributions’ own dtype.

With _sample_input_ns = False, the frontier: an int64 tensor of shape [rows, num_samples] holding the NODE ID each sample selected, padded with -1. rows is the frontier’s height, which is at least num_vars and usually more; each column is a dense prefix of live ids followed by nothing but -1, so >= 0 selects exactly the live entries, and every column of one draw holds exactly num_vars of them – one per variable, never two.

Return type:

torch.Tensor

Note:

the frontier’s ids are GLOBAL node ids, and a circuit may have more than one input layer – it has one per distribution TYPE, so a model mixing, say, Categorical and Bernoulli variables has two. Decoding therefore has to attribute each id to its owning layer BEFORE subtracting a start index:

for node_id in frontier[frontier[:, j] >= 0, j].tolist():
    for layer in pc.input_layer_group:
        start, end = layer._output_ind_range
        if start <= node_id < end:
            var = int(layer.vids[node_id - start, 0])
            params_start = int(layer.s_pids[node_id - start])
            break

The shortcut of subtracting pc.input_layer_group[0]._output_ind_range[0] and indexing that layer’s vids is correct only while the circuit has ONE input layer. It does not fail gracefully when it stops being true: ids from the second layer index past the end of the first layer’s vids, which is a device-side assert that poisons the CUDA context.