Weighted Sampling
Choose entries in proportion to their weights, for simulations, randomized search and priority sampling. Weights can change between draws without rebuilding everything.
WeightedSampler
WeightedSampler is the high-level interface. Each slot holds a weight; sample() turns a fraction into the slot whose share of the total weight covers it.
var op.WeightedSampler sampler = op.weightedSampler(512)
if barstate.isfirst
sampler.setWeight(10, 0.25)
sampler.setWeight(11, 0.80)
sampler.setWeight(12, 0.10)
float fraction = 0.50
int selected = sampler.sample(fraction)
// Output: selected is 11 for the supplied fraction of 0.50.The total is 1.15, so slot 10 covers the first 22% of the range, slot 11 the next 70%, and slot 12 the rest. A fraction of 0.50 lands in slot 11. Each slot can stand for a candidate model, a simulation outcome or a work item: update its weight when its score changes, then keep sampling through the same interface.
- Every slot starts at weight 0, and a slot with weight 0 is never selected.
sample()returns-1when every weight is 0.setWeights(weights)replaces every weight from an array of exactlysize()values.setMany(slots, weights)writes chosen slots and returns how many were in range.probability(slot)is a slot's chance of selection, andtotal()the total weight.
Where fractions come from
The sampler does not generate randomness. Pass math.random() for a random draw, or a fixed sequence of fractions for draws you can reproduce. Any real value is wrapped into the range from 0 up to 1, so 1.0 selects the same slot as 0.0.
// Pseudocode: modelScores holds 64 non-negative scores, one per model.
var op.WeightedSampler models = op.weightedSampler(64)
var array<float> fractions = array.new<float>(8, 0.0)
var array<int> picks = array.new<int>()
models.setWeights(modelScores)
for i = 0 to 7
fractions.set(i, math.random())
models.sampleMany(fractions, picks)
// Output: picks holds eight model slots, each chosen in proportion to its score.sampleMany() refreshes the selection structure once for the whole batch, then draws each fraction in order.
The exponent
Every weight is raised to the sampler's exponent before selection, which is 1 by default. Above 1, high weights dominate more strongly; below 1, the choice flattens towards uniform. Change it with setExponent(), or pass it to op.weightedSampler(capacity, exponent). Negative values become 0.
At exponent 0, every weight becomes 1 and the choice is uniform, including slots whose weight is 0. With ExponentZeroPolicy.preserveZeros in the configuration, zero weights stay unselectable at every exponent.
Prefix sum or Fenwick tree
Selection needs running totals of the weights. OptiPine keeps them in one of two ways:
| Representation | Weight change | Draw | Suits |
|---|---|---|---|
| Linear prefix sum (default) | O(1), then O(n) once on the next draw | O(log n) | Weights that are rebuilt together or change rarely. Bit-reproducible. |
| Fenwick tree | O(log n) | O(log n) | A few weights changing between many draws. |
A WeightConfig chooses between them. Four presets cover the usual cases:
| Preset | What it does |
|---|---|
op.weightConfigDeterministic() | Forces the linear prefix sum, whose left-to-right addition order never changes. This is also the default. |
op.weightConfigStatic() | Forces a Fenwick tree, for weights that rarely change under a stable exponent and many draws. |
op.weightConfigSparseUpdates() | Automatic: starts with the prefix sum and moves to a Fenwick tree once the exponent is stable and updates stay sparse, such as a prioritized replay buffer whose priorities change a few at a time. |
op.weightConfigStrict() | Rejects na and negative weights instead of clamping them. |
var op.WeightedSampler replay = op.weightedSampler(4096, 1.0, op.weightConfigSparseUpdates())In automatic mode the tree is used once two preparations in a row have kept the same exponent, as long as the weight updates since the last preparation number no more than 5% of the range. Denser updates fall back to the prefix sum. Both thresholds are fields of op.weightConfig(): promoteAfterCycles and sparseUpdateRatio.
na and negative weights
By default an na weight is stored as 0, and a negative weight is clamped to 0; both make the slot unselectable. NaWeightPolicy.raiseError and NegativeWeightPolicy.raiseError stop the script instead, which is what op.weightConfigStrict() sets.
WeightedIndex over a circular range
The sampler draws from all of its slots. WeightedIndex, the Tier 2 structure underneath it, draws from a circular range of physical slots, which is exactly the layout of a RingCursor. prepare(exponent, startPhysical, count) fixes the exponent, the slot holding logical index 0, and the length of the range.
// Pseudocode: newEvent and eventStrength come from the script.
var op.RingCursor events = op.ringCursor(200)
var op.WeightedIndex eventWeights = op.weightedIndex(200)
if newEvent
int slot = events.reserve()
// Overwrites the weight of the event the ring just dropped.
eventWeights.setWeight(slot, eventStrength)
int picked = -1
if events.count() > 0
// Logical index 0 is the oldest retained event.
eventWeights.prepare(1.0, events.physical(0), events.count())
picked := eventWeights.draw(math.random())
// Output: picked is the physical slot of one retained event, chosen by strength.draw()anddrawMany()return physical slots, so they index your arrays directly. They return-1before the firstprepare()or when the range has no positive weight.prepareAll(exponent)prepares every slot, which is what the sampler does.setWeight()with the value already stored does nothing and invalidates nothing.weightRevision()advances on every effective change, for callers that cache results derived from the weights.mode(),currentExponent(),preparedRange()andpendingUpdates()describe the prepared state.- A sampler's
indexView()returns its WeightedIndex, for forced policies and scoped editing.
Editing the weight array directly
For a measured hot path, Tier 3 calls write the weights without per-call checks. weightsEditBegin() opens a scope, weightsUnsafe() returns the backing array for direct writes, and weightsEditFinish() clamps invalid values and rebuilds every derived structure. Never keep the array past the end of the scope. setWeightUnchecked() writes one weight without bounds checks or clamping, and touchWeights() repairs the derived state without a scope.
WeightedPrefix and FloatFenwick
The two representations are also exported on their own:
- WeightedPrefix builds cumulative weights over a plain or circular range in O(n), with an optional exponent, and answers
lowerBound(threshold)in O(log n): the first index whose running total reaches the threshold. - FloatFenwick is a Fenwick tree over up to 99,999 non-negative weights.
set(),add(),prefix(),rangeSum()andlowerBound()are O(log n), andlowerBoundCircular()selects within a circular range.naand negative values become 0.
Use them when you need prefix sums for something other than sampling, such as cumulative volume between two indices.
Calls
| Call | What it does |
|---|---|
op.weightedSampler(capacity, exponent = 1.0, settings = na) | Creates a sampler with every weight at 0. |
sampler.setWeight(slot, weight), sampler.setWeights(weights), sampler.setMany(slots, weights), sampler.fill(weight) | Write weights. |
sampler.sample(fraction), sampler.sampleMany(fractions, output = na) | Draw one slot or a batch. |
sampler.weight(slot), sampler.probability(slot), sampler.total() | Read weights and chances. |
sampler.setExponent(exponent), sampler.size(), sampler.mode(), sampler.indexView(), sampler.clear() | Exponent, size, representation, the underlying index, and reset. |
op.weightedIndex(capacity, settings = na) | Creates a circular-range index. |
index.prepare(exponent, startPhysical, count), index.prepareAll(exponent = 1.0) | Fix the exponent and range. |
index.draw(fraction), index.drawMany(fractions, output = na) | Draw physical slots. |
op.weightConfig(...) | A configuration. Also the four presets above. |
op.weightedPrefix(), op.floatFenwick(size = 0) | The two cumulative-weight representations, on their own. |
Every signature is in the API Reference. Next: Optimizing Pine Script.