# OptiPine

> OptiPine is an open-source performance library for TradingView, published as a Pine Script v6 library by Alien_Algorithms. It provides caching, sparse updates, stable storage, rolling buffers, adaptive keyed lookup and weighted selection, so demanding scripts spend Pine's execution budget on the feature instead of on recalculating unchanged results, shifting arrays, scanning untouched collections and moving stored objects.

- Product page: https://alienalgorithms.com/optipine
- Library and source code on TradingView: https://www.tradingview.com/script/UiiesMWO-OptiPine-High-Performance-Caching-and-Data-Pipelines/
- Import: `import Alien_Algorithms/OptiPine/1 as op` (use the latest version number shown on the TradingView publication)
- Language: Pine Script v6
- Author: Alien_Algorithms, https://www.tradingview.com/u/Alien_Algorithms/?aff_id=169033#published-scripts
- Licence: CC BY-NC-SA 4.0. Free for non-commercial use when Alien_Algorithms is credited. Commercial use needs a licence from Alien_Algorithms.

This file is the plain-text reference for the product page. It is written so that a person or an AI assistant can write correct OptiPine code from it. Signatures, parameter names and defaults are taken from the library source.

## When to use OptiPine

In a small indicator, optimization is often optional. In a rendering engine, a machine learning library, a simulation, a dashboard or an object system it can decide whether a feature runs at all. The cost is rarely one slow formula; it is the work around it. OptiPine's techniques come from projects such as Pine3D and NeuraLib. The idea: do less work, move less data, and let the representation follow the workload.

| The workload | Use |
| --- | --- |
| An expensive pure calculation whose inputs repeat between executions | `FloatMemo` (and the other Memos) with `staleOn()` |
| Work that should rerun only when a scalar, an array or a managed ring changed | `Watch` with `changed()`, or `begin()`, `watch*()`, `finish()` |
| Work that should run at most every N bars | `CadenceGate` |
| A fixed-size rolling history of rows | `FloatRowRing`, `IntRowRing` |
| Objects with IDs that appear and disappear, stored in parallel arrays | `StablePool` (explicit lifetime) or `SlotCache` (frame-based) |
| A large collection where few entries change per bar | `DirtySet` |
| Integer keys to one value each | `IntIntStore`, `IntFloatStore`, `IntBoolStore`, `IntStringStore`, `IntColorStore` |
| Integer keys to slots for your own payload | `IntIndex` |
| One key to many integers | `IntBuckets` |
| A pass that reads one array while writing the next | `IntDoubleBuffer`, `FloatDoubleBuffer` |
| Selecting entries in proportion to their weights | `WeightedSampler`, `WeightedIndex` |

In our tests against conventional Pine implementations of the same task, optimized paths commonly ran 15% to 40% faster, and sparse updates and indexed lookups exceeded 90% when the alternative scanned or searched the full collection. Component results: DirtySet 93% faster than clearing and scanning the full universe with 1% of entries changed; Watch's compare-first array pattern 35% to 60% faster than rewriting a snapshot every time; a ring's chronological output 90% faster than rebuilding a 512-cell, width-four output row by row; direct integer addressing 21% faster than a map; a map 91% faster than repeated linear lookup with 32 entries; IntBuckets 19% faster than a wrapper UDT holding an array in a 64-key traversal.

When not to use it: ordinary series history such as `close[50]` stays native Pine. For a few fixed objects, manual indices are simpler. If a producer already provides one reliable change flag, use the flag. Watch's array comparison is O(N), so use it when the calculation it guards costs more than the comparison.

## Three levels of control

- Tier 1, Quick: ready-to-use APIs with automatic defaults. Watch, Memo, CadenceGate, typed stores, StablePool, DirtySet, row rings, double buffers and WeightedSampler.
- Tier 2, Composable: explicit lifecycles, configuration and representation policies. IntIndex, IntBuckets, SlotCache, RingCursor, Revision, WeightedIndex, WeightedPrefix and FloatFenwick.
- Tier 3, Expert: physical addressing, unchecked writes and scoped raw mutation for measured hot paths, such as `reserveRowUnchecked()`, `writeUnchecked()`, `editBegin()` / `rawUnsafe()` / `editFinish()` and `weightsEditBegin()` / `weightsUnsafe()` / `weightsEditFinish()`. Read-only views are not Tier 3.

Start with Tier 1 and move deeper only when a measured workload needs it.

## Minimal example

A complete indicator. It runs a 401-scenario model only when the rounded trend or volatility regime changes, and serves the cached result in between.

```pine
//@version=6
indicator("OptiPine - Cached Regime Stress", overlay = false)

import Alien_Algorithms/OptiPine/1 as op

// Test 401 possible moves, giving more weight to common moves.
// This function is pure: its result depends only on its inputs.
estimateDownside(float trendInAtr, float atrPercent) =>
    float result = na

    if not na(trendInAtr) and not na(atrPercent) and atrPercent > 0
        float weightedDownside = 0.0
        float totalWeight = 0.0

        for scenario = -200 to 200
            float standardShock = scenario / 40.0
            float weight = math.exp(-0.5 * standardShock * standardShock)
            float projectedMove = (trendInAtr + standardShock) * atrPercent
            float downside = math.max(-projectedMove, 0.0)

            weightedDownside += downside * weight
            totalWeight += weight

        result := totalWeight > 0 ? weightedDownside / totalWeight : na

    result

// Stateful Pine calculations stay outside the Memo guard.
float ema20 = ta.ema(close, 20)
float ema50 = ta.ema(close, 50)
float atr14 = ta.atr(14)

float trendInAtr = atr14 > 0 ? (ema20 - ema50) / atr14 : na
float atrPercent = close > 0 ? atr14 / close * 100.0 : na

// Quantization makes the dependencies describe a regime, not every tick.
float trendRegime = math.round(
  math.max(-3.0, math.min(3.0, trendInAtr)) * 10.0) / 10.0
float volatilityRegime = math.round(atrPercent * 4.0) / 4.0

var op.FloatMemo downsideStress = op.floatMemo()

if downsideStress.staleOn(trendRegime, volatilityRegime)
    downsideStress.store(
      estimateDownside(trendRegime, volatilityRegime))

float stress = downsideStress.get()

plot(stress, "Expected downside (%)", color.orange, linewidth = 2)
plot(trendRegime, "Trend regime (ATR units)", display = display.data_window)
plot(volatilityRegime, "Volatility regime (%)", display = display.data_window)
```

## Rules that keep a script correct

- Create every OptiPine object once, with `var`. A Memo, Watch, pool, store or ring created on every bar starts empty every bar.
- Keep stateful series calls such as `ta.ema()`, `ta.atr()` or `ta.pivothigh()` outside Memo and Watch guards. Compute them on every bar and pass their results into the guarded calculation.
- `staleOn()` watches a fixed set of eight slots: four floats, two ints, one bool and one string. Omitting trailing arguments does not register as a change. Round raw inputs into regimes before passing them, or the cache misses on every tiny change.
- A Watch remembers dependencies; it does not store the result. For one dependency use `changed(source)`. For several, call `begin()`, then the typed `watch*()` calls, then `finish()`, on every comparison pass.
- `CadenceGate.due()` and `dueWhenChanged()` consume the gate when they return true. Use `peek()` to ask without consuming. Use it for intentionally delayed work, not for results that must update immediately.
- `StablePool.acquire(id)` returns `[slot, created]`. Calling it again for a live ID returns the same slot. `release(id)` frees the slot but does not erase payload data, so overwrite every field when a slot is reused. Grow payload arrays with `ensureSizeFloat()` and the other `ensureSize*()` calls when `acquire()` reports a new slot.
- IDs carry identity and slots are only storage addresses. A new object needs a new ID even when it reuses an old slot.
- `SlotCache` is frame-based: `begin()`, `acquire()` every active key, then `finish()`. Keys not touched in the frame are retired.
- `DirtySet.begin()` starts a new cycle without clearing the universe. A second `mark()` of the same index in a cycle is ignored. `values()` lists the distinct marks in discovery order.
- Views such as `values()`, `slots()`, `keysView()`, `rowView()` and `rawView()` are borrowed, read-only and valid only until the structure changes. Use the `*Copy()` calls to keep or modify the data.
- `IntIndex` and `IntBuckets` follow `buildBegin()`, `add()` or `addMany()`, then `buildFinish()`, before any lookup. `IntBuckets.buildFromPairs()` and the stores' `build()` do the whole lifecycle in one call. Duplicate keys raise an error unless `IndexConfig.duplicates` says otherwise.
- A row ring's `push()` takes exactly `width` values; `pushValue()` needs a width of 1. `capacity * width` may not exceed 100,000.
- `WeightedSampler` does not generate randomness. Pass `math.random()` or a repeatable sequence of fractions to `sample()`.
- Pine's limits apply underneath: arrays hold at most 100,000 elements and maps 50,000 pairs. StablePool and SlotCache hold at most 50,000 live keys.
- Methods such as `get()`, `set()`, `push()` and `clear()` match Pine's collection vocabulary on purpose. A shadowing warning in the editor is cosmetic; the receiver's type decides which method runs.

## API reference

All names below are exported by the library. With `import Alien_Algorithms/OptiPine/1 as op`, functions are called as `op.name(...)` and methods on the object they belong to. Receiver names in the tables are examples.

### Storage utilities

They work on caller-owned primitive arrays and reuse the storage. Constants: `op.MAX_ARRAY_ELEMENTS` (100,000), `op.MAX_MAP_PAIRS` (50,000) and `op.MAX_GENERATION` (2,000,000,000).

| Call | Description |
| --- | --- |
| `op.ensureSizeInt(array<int> values, int minimumSize, int fillValue = 0)` | Grows `values` to at least `minimumSize` by appending `fillValue`. Mutates in place, never shrinks, and preserves every existing element. Returns the resulting size. |
| `op.ensureSizeFloat(array<float> values, int minimumSize, float fillValue = 0.0)` | Grows `values` to at least `minimumSize` by appending `fillValue`. Mutates in place, never shrinks, and preserves every existing element. Returns the resulting size. |
| `op.ensureSizeBool(array<bool> values, int minimumSize, bool fillValue = false)` | Grows `values` to at least `minimumSize` by appending `fillValue`. Mutates in place, never shrinks, and preserves every existing element. Returns the resulting size. |
| `op.ensureSizeColor(array<color> values, int minimumSize, color fillValue = na)` | Grows `values` to at least `minimumSize` by appending `fillValue`. Mutates in place, never shrinks, and preserves every existing element. Returns the resulting size. |
| `op.ensureSizeString(array<string> values, int minimumSize, string fillValue = na)` | Grows `values` to at least `minimumSize` by appending `fillValue`. Mutates in place, never shrinks, and preserves every existing element. Returns the resulting size. |
| `op.resizeInt(array<int> values, int size, int fillValue = 0)` | Resizes `values` to exactly `size`, preserving retained elements. Appends `fillValue` when growing and pops from the end when shrinking. Mutates in place and retains the allocation. Returns the resulting size. |
| `op.resizeFloat(array<float> values, int size, float fillValue = 0.0)` | Resizes `values` to exactly `size`, preserving retained elements. Appends `fillValue` when growing and pops from the end when shrinking. Mutates in place and retains the allocation. Returns the resulting size. |
| `op.resizeBool(array<bool> values, int size, bool fillValue = false)` | Resizes `values` to exactly `size`, preserving retained elements. Appends `fillValue` when growing and pops from the end when shrinking. Mutates in place and retains the allocation. Returns the resulting size. |
| `op.resizeColor(array<color> values, int size, color fillValue = na)` | Resizes `values` to exactly `size`, preserving retained elements. Appends `fillValue` when growing and pops from the end when shrinking. Mutates in place and retains the allocation. Returns the resulting size. |
| `op.resizeString(array<string> values, int size, string fillValue = na)` | Resizes `values` to exactly `size`, preserving retained elements. Appends `fillValue` when growing and pops from the end when shrinking. Mutates in place and retains the allocation. Returns the resulting size. |
| `op.refillInt(array<int> values, int size, int fillValue = 0)` | Resizes `values` to exactly `size` and overwrites every element with `fillValue`. Retains the allocation. Returns the resulting size. |
| `op.refillFloat(array<float> values, int size, float fillValue = 0.0)` | Resizes `values` to exactly `size` and overwrites every element with `fillValue`. Retains the allocation. Returns the resulting size. |
| `op.refillBool(array<bool> values, int size, bool fillValue = false)` | Resizes `values` to exactly `size` and overwrites every element with `fillValue`. Retains the allocation. Returns the resulting size. |
| `op.refillColor(array<color> values, int size, color fillValue = na)` | Resizes `values` to exactly `size` and overwrites every element with `fillValue`. Retains the allocation. Returns the resulting size. |
| `op.refillString(array<string> values, int size, string fillValue = na)` | Resizes `values` to exactly `size` and overwrites every element with `fillValue`. Retains the allocation. Returns the resulting size. |
| `op.sameExactInt(array<int> left, array<int> right)` | Exact element-by-element equality of two int arrays. Returns true when both arrays hold exactly the same elements in order. |
| `op.sameExactFloat(array<float> left, array<float> right)` | Exact element-by-element equality of two float arrays. Returns true when both arrays hold exactly the same elements in order. |
| `op.sameExactBool(array<bool> left, array<bool> right)` | Exact element-by-element equality of two bool arrays. Returns true when both arrays hold exactly the same elements in order. |
| `op.sameExactColor(array<color> left, array<color> right)` | Exact element-by-element equality of two color arrays. Returns true when both arrays hold exactly the same elements in order. |
| `op.sameExactString(array<string> left, array<string> right)` | Exact element-by-element equality of two string arrays. Returns true when both arrays hold exactly the same elements in order. |

### Revision

`op.Revision`: Monotonic invalidation token for state OptiPine cannot observe. Producers call `bump()` after a meaningful state change; consumers store the last observed value and compare with `changedSince()` instead of rescanning the producer.
Complexity: all operations O(1). No allocation.
Limits: wraps to 0 after 2,000,000,000 bumps. A consumer holding a snapshot taken exactly one full wrap earlier would miss the change. That requires 2e9 bumps and is documented rather than guarded.

| Call | Description |
| --- | --- |
| `op.revision(int initialValue = 0)` | Creates a revision token. Returns a new Revision. |
| `revision.bump()` | Advances the revision. Returns the new revision value. |
| `revision.current()` | Reads the current revision without changing it. Returns the current revision value. |
| `revision.changedSince(int snapshot)` | Tests a consumer snapshot against the current revision. Returns true when the snapshot is stale. |
| `revision.set(int value)` | Assigns an explicit revision value. Returns the receiver, so calls chain. |
| `revision.reset()` | Returns the revision to 0. Returns the receiver, so calls chain. |

### Watch

`op.Watch`: Remembers whether something changed since it last looked.

`changed()` has one overload per source type: int, float, bool, string, color, the five primitive arrays, `FloatRowRing` and `IntRowRing`. The row-ring overloads compare the ring's revision in O(1); the array overloads compare with a retained snapshot in O(N) and rewrite it only after a change.

| Call | Description |
| --- | --- |
| `op.watch()` | Creates an exact dependency watch. Returns a new Watch with empty retained storage. |
| `watch.watchInts(array<int> values, NaCollectionPolicy naPolicy = NaCollectionPolicy.exact)` | Adds one int collection dependency to the current pass. Returns the receiver, so calls chain. |
| `watch.watchFloats(array<float> values, NaCollectionPolicy naPolicy = NaCollectionPolicy.exact)` | Adds one float collection dependency to the current pass. Returns the receiver, so calls chain. |
| `watch.watchColors(array<color> values, NaCollectionPolicy naPolicy = NaCollectionPolicy.exact)` | Adds one color collection dependency to the current pass. Returns the receiver, so calls chain. |
| `watch.watchStrings(array<string> values, NaCollectionPolicy naPolicy = NaCollectionPolicy.exact)` | Adds one string collection dependency to the current pass. Returns the receiver, so calls chain. |
| `watch.watchBools(array<bool> values)` | Adds one bool collection dependency to the current pass. Returns the receiver, so calls chain. |
| `watch.begin()` | Starts one dependency comparison pass. Returns the receiver, so calls chain. |
| `watch.watchInt(int value)` | Adds one integer dependency to the current pass. Returns the receiver, so calls chain. |
| `watch.watchBool(bool value)` | Adds one Boolean dependency, stored as 0 or 1. Pine v6 booleans are never na, so no na branch is needed here. Returns the receiver, so calls chain. |
| `watch.watchFloat(float value)` | Adds one float dependency using exact na-aware equality. Returns the receiver, so calls chain. |
| `watch.watchString(string value)` | Adds one string dependency using exact na-aware equality. Returns the receiver, so calls chain. |
| `watch.watchColor(color value)` | Adds one color dependency using exact na-aware equality. Returns the receiver, so calls chain. |
| `watch.finish()` | Completes the pass. Returns true when any dependency changed, when the dependency arity changed, when this is the first pass, or when `invalidate()` was called. |
| `watch.invalidate()` | Forces the next observation to report a change, whichever API the caller uses. Returns the receiver, so calls chain. |
| `watch.isChanged()` | Reports whether the pass in progress has already seen a change. Returns the current pass change flag. |
| `watch.reset()` | Discards every stored dependency value and arity, retaining the array allocations. The next observation reports a change, through either `finish()` or any `changed()` overload. Returns the receiver, so calls chain. |
| `watch.arity()` | Total number of dependencies recorded by the last completed pass. Returns the summed arity across every scalar type plus every watched collection. One collection counts as one dependency regardless of how many elements it holds. |
| `watch.changed(int value)` | Reports whether this int differs from the one last observed, and adopts it as the new baseline. Complexity: O(1). No allocation. Returns true on the first observation and whenever `value` differs exactly from the previously observed one. |
| `watch.changed(float value)` | Reports whether this float differs from the one last observed, and adopts it as the new baseline. Complexity: O(1). No allocation. Returns true on the first observation and whenever `value` differs exactly from the previously observed one. |
| `watch.changed(bool value)` | Reports whether this bool differs from the one last observed, and adopts it as the new baseline. Complexity: O(1). No allocation. Returns true on the first observation and whenever `value` differs exactly from the previously observed one. |
| `watch.changed(string value)` | Reports whether this string differs from the one last observed, and adopts it as the new baseline. Complexity: O(1). No allocation. Returns true on the first observation and whenever `value` differs exactly from the previously observed one. |
| `watch.changed(color value)` | Reports whether this color differs from the one last observed, and adopts it as the new baseline. Complexity: O(1). No allocation. Returns true on the first observation and whenever `value` differs exactly from the previously observed one. |
| `watch.changed(array<int> values)` | Reports whether this int collection differs from the one last observed, and adopts it as the new baseline. Complexity: O(N) to compare. The O(N) copy is paid only on the passes that report a change, so a steady collection costs one comparison per bar and no writes. Returns true on the first observation and whenever the contents differ. |
| `watch.changed(array<float> values)` | Reports whether this float collection differs from the one last observed, and adopts it as the new baseline. Complexity: O(N) to compare. The O(N) copy is paid only on the passes that report a change, so a steady collection costs one comparison per bar and no writes. Returns true on the first observation and whenever the contents differ. |
| `watch.changed(array<bool> values)` | Reports whether this bool collection differs from the one last observed, and adopts it as the new baseline. Complexity: O(N) to compare. The O(N) copy is paid only on the passes that report a change, so a steady collection costs one comparison per bar and no writes. Returns true on the first observation and whenever the contents differ. |
| `watch.changed(array<color> values)` | Reports whether this color collection differs from the one last observed, and adopts it as the new baseline. Complexity: O(N) to compare. The O(N) copy is paid only on the passes that report a change, so a steady collection costs one comparison per bar and no writes. Returns true on the first observation and whenever the contents differ. |
| `watch.changed(array<string> values)` | Reports whether this string collection differs from the one last observed, and adopts it as the new baseline. Complexity: O(N) to compare. The O(N) copy is paid only on the passes that report a change, so a steady collection costs one comparison per bar and no writes. Returns true on the first observation and whenever the contents differ. |
| `watch.changed(FloatRowRing source)` | Reports whether the ring's contents changed since last observed, and adopts the current state as the new baseline. Complexity: O(1), whatever the ring's capacity and width. The backing array is never compared or copied. Returns true on the first observation and whenever the ring's content revision differs from the one last seen. |
| `watch.changed(IntRowRing source)` | Reports whether the ring's contents changed since last observed, and adopts the current state as the new baseline. Complexity: O(1), whatever the ring's capacity and width. The backing array is never compared or copied. Returns true on the first observation and whenever the ring's content revision differs from the one last seen. |

`op.NaCollectionPolicy`:

| Value | Meaning |
| --- | --- |
| `op.NaCollectionPolicy.exact` | na equals na, using OptiPine's exact comparators. Two passes holding na in the same position report no change. |
| `op.NaCollectionPolicy.alwaysChanged` | A collection containing any na element reports changed on every pass. Use this when na means "not computable yet" and a cached result derived from it must never be reused. |
| `op.NaCollectionPolicy.reject` | Raise on the first na element. |

### Memo

`op.FloatMemo`: One cached float plus an exact dependency Watch.
Complexity: O(number of dependencies) per pass.

`op.intMemo()`, `op.boolMemo()`, `op.stringMemo()` and `op.colorMemo()` create `IntMemo`, `BoolMemo`, `StringMemo` and `ColorMemo`, which have the same methods with an int, bool, string or color value. `BoolMemo.get()` falls back to `false`; the others fall back to `na`.

| Call | Description |
| --- | --- |
| `op.floatMemo()` | Creates a memoized float holder. Returns a new FloatMemo. |
| `memo.begin()` | Starts an explicit dependency pass. Returns the receiver, so calls chain. |
| `memo.miss()` | Completes an explicit pass. Returns true when the value must be recomputed. |
| `memo.staleOn(float firstValue = na, float secondValue = na, float thirdValue = na, float fourthValue = na, int firstKey = na, int secondKey = na, bool flag = false, string tag = na)` | One-call dependency check. The watched arity is fixed at eight slots regardless of how many arguments are supplied, so omitting trailing arguments does not register as a dependency change. Returns true when the value must be recomputed. |
| `memo.store(float value)` | Stores a recomputed value and marks the memo valid. Returns the receiver, so calls chain. |
| `memo.get(float fallback = na)` | Reads the cached value. Returns the cached value, or `fallback`. |
| `memo.isValid()` | Reports whether a value has been stored. Returns true once `store()` has been called and not invalidated since. |
| `memo.invalidate()` | Discards the cached value and forces the next check to miss. Returns the receiver, so calls chain. |

### CadenceGate

`op.CadenceGate`: Stateful gate for work that should run at a minimum tick interval, and optionally only when its producer changed as well.
Complexity: O(1). No allocation.

| Call | Description |
| --- | --- |
| `op.cadenceGate()` | Creates a cadence gate that is due on its first check. Returns a new CadenceGate. |
| `gate.peek(int tick, int interval = 1, bool force = false)` | Tests whether the cadence is open, without consuming the gate. Returns true when a `due()` call with the same arguments would run. |
| `gate.due(int tick, int interval = 1, bool force = false)` | Consumes the gate and reports whether work is due. Returns true when work should run now. The tick is recorded only when this returns true, so a gate that stays closed does not drift. |
| `gate.dueWhenChanged(int tick, int revision, int interval = 1, bool force = false)` | Consumes the gate and reports whether work is due *and* the producer changed since the last run. Returns true when work should run now. The tick and revision are recorded only when this returns true. |
| `gate.runs()` | Number of consumed runs. Returns the run counter. |
| `gate.reset()` | Returns the gate to its initial state, so the next check is due. Returns the receiver, so calls chain. |

### Double buffers

`op.FloatDoubleBuffer`: Two retained float arrays whose references swap without copying.
Complexity: `swap()` is O(1) plus the cost of clearing the new current.

`IntDoubleBuffer` has the same methods with int arrays. Its fields `current` and `previous` are the two arrays.

| Call | Description |
| --- | --- |
| `op.floatDoubleBuffer()` | Creates a double buffer for float values. Returns a new FloatDoubleBuffer with two empty retained arrays. |
| `op.intDoubleBuffer()` | Creates a double buffer for int values. Returns a new IntDoubleBuffer with two empty retained arrays. |
| `buffer.swap()` | Exchanges the two buffers and clears the new current buffer. Returns [current, previous] as structure-owned mutable views. |
| `buffer.swapSized(int size, float fillValue = 0.0)` | Exchanges the two buffers and resizes the new current buffer to a fixed length, overwriting every element. Returns [current, previous] as structure-owned mutable views. |
| `buffer.clear()` | Empties both buffers, retaining their allocations. Returns the receiver, so calls chain. |

### DirtySet

`op.DirtySet`: Deduplicated sparse integer set using generation stamps and a compact discovery-order list.
Complexity: `begin()` O(1) amortized (O(growth) when the universe grows, O(universe) once per two billion cycles at rollover); `mark()` O(1); `markMany()` O(batch); `values()` O(1) to obtain and O(dirty count) to traverse, never O(universe).
Limits: the universe may not exceed 100,000 entries.

| Call | Description |
| --- | --- |
| `op.dirtySet(int universeSize = 0)` | Creates a sparse dirty set. Returns a new DirtySet with stamps preallocated. |
| `dirty.begin(int universeSize = na)` | Starts a sparse-work cycle and optionally resizes the universe. Growing the universe keeps existing stamps; shrinking it only narrows the accepted index range and retains the allocation. Returns the receiver, so calls chain. |
| `dirty.mark(int index)` | Marks one index for the current cycle. Returns true only for the first mark of this index in this cycle. |
| `dirty.markMany(array<int> indices)` | Marks every index in an array. Duplicate and out-of-range indices are ignored. Complexity: O(batch), independent of the universe size. Returns the number of indices this call added to the dirty list, which is the count of first marks and so excludes duplicates and rejects. |
| `dirty.markUnchecked(int index)` | Marks one index without bounds validation. Returns true only for the first mark of this index in this cycle. |
| `dirty.markRange(int first, int last)` | Marks every index in an inclusive range, clipped to the universe. Returns the receiver, so calls chain. |
| `dirty.contains(int index)` | Tests whether an index was marked in the current cycle. Returns true when the index carries the current generation stamp. |
| `dirty.values()` | The indices marked in the current cycle, in discovery order. Ownership: borrowed structure-owned view. Mutation: read-only by contract; mutation is unsupported. The list and the generation stamps describe each other, so appending or clearing it corrupts both. Use `valuesCopy()` to own or modify it. Lifetime: valid until the next `begin()` or `clear()`. Persistence: do not retain it across that boundary; reacquire it. Returns the dirty indices in discovery order. |
| `dirty.valuesCopy(array<int> output = na)` | Copies the dirty indices into caller-owned storage. Returns the dirty indices in discovery order, owned by the caller. |
| `dirty.size()` | Number of distinct indices marked in the current cycle. Returns the dirty count. |
| `dirty.universe()` | Current universe size. Returns the number of tracked indices. |
| `dirty.cycle()` | Current cycle stamp, mainly for diagnostics. Named distinctly from the `generation` field so a method call is never mistaken for field access. Returns the generation counter. |
| `dirty.clear()` | Discards all marks and stamps while retaining the allocations. The universe size is preserved. Returns the receiver, so calls chain. |

### Index configuration

`op.IndexConfig`: Configuration for `IntIndex` and every keyed store built on it.

Automatic mode, the default, decides at `buildFinish()`: an empty index uses a linear scan; consecutive ascending keys use arithmetic addressing with no table; a span of at most 16,384 keys and at most twice the entry count uses a dense table; other unordered keys up to 50,000 use a map; ascending keys up to 50,000 use a map when `expectedQueries` reaches the promotion horizon (the larger of 32 and 10% of the entries); everything else uses binary search. `indexConfigDynamic()` also lets a sorted index promote itself to a map once enough queries arrive.

| Call | Description |
| --- | --- |
| `op.indexConfig(LookupPolicy policy = LookupPolicy.automatic, DuplicatePolicy duplicates = DuplicatePolicy.raiseError, float promoteQueryRatio = 0.10, int promoteMinQueries = 32, bool autoPromote = false, bool validateAscending = true, int maxDenseSpan = 100000, float autoDenseSpanRatio = 2.0, int autoDenseMaxSpan = 16384, int linearMaxEntries = 0, int duplicateScanLimit = 64, bool collectStats = false, bool strictLifecycle = true)` | Creates a keyed-index configuration. Returns a new IndexConfig with every argument clamped to a valid range. |
| `op.indexConfigStaticSorted()` | Preset: a directory built once and queried across many bars. Forces sorted binary search, which needs no extra storage for an ascending build and never allocates a map. Returns an IndexConfig. |
| `op.indexConfigDense()` | Preset: direct addressing for validated dense integer keys. Raises when the key span exceeds `maxDenseSpan`. Returns an IndexConfig. |
| `op.indexConfigDictionary()` | Preset: a persistent map-backed directory for lookup-heavy workloads. Returns an IndexConfig. |
| `op.indexConfigTiny()` | Preset: a very small directory rebuilt far more often than it is queried. No build cost at all; queries use the native scan. Returns an IndexConfig. |
| `op.indexConfigDynamic()` | Preset: automatic selection with online sorted-to-map promotion and counters enabled, for directories whose query volume is unknown until it happens. Returns an IndexConfig. |

`op.LookupPolicy`:

| Value | Meaning |
| --- | --- |
| `op.LookupPolicy.automatic` | Select a representation at `buildFinish()` from the observed key layout and, when supplied, the expected query horizon. |
| `op.LookupPolicy.linear` | No build at all. Queries scan the insertion-order key array with the native `indexof`/`lastindexof` built-ins. Correct for any key layout and the cheapest choice when a directory is rebuilt far more often than it is queried. |
| `op.LookupPolicy.sorted` | Native binary search. Strictly ascending builds search the key array directly with no extra storage; any other order builds a sorted key array plus a slot indirection array once. |
| `op.LookupPolicy.dictionary` | Map-backed directory. Limited to 50,000 keys by Pine. |
| `op.LookupPolicy.dense` | Direct addressing. A consecutive ascending run needs no table at all; any other layout allocates a span-sized slot table. |

`op.DuplicatePolicy`:

| Value | Meaning |
| --- | --- |
| `op.DuplicatePolicy.raiseError` | Always verify uniqueness at `buildFinish()` and stop with a descriptive error when a duplicate is present. Every representation checks; the method varies from a free adjacent scan to one native sort, but the guarantee does not. |
| `op.DuplicatePolicy.firstWins` | Every lookup resolves to the slot of the first occurrence. |
| `op.DuplicatePolicy.lastWins` | Every lookup resolves to the slot of the last occurrence. |

### IntIndex

`op.IntIndex`: Universal integer-key directory.
Complexity: `add()` O(1). `buildFinish()` is O(entries) for dense, map and ordered sorted builds; O(entries log entries) for unordered sorted builds; and O(1) for permissive linear mode. Linear duplicate checking may add O(entries squared) below `duplicateScanLimit` or O(entries log entries) above it. `find()` is O(1) for dense and map, O(log entries) for sorted, and O(entries) for linear.
Limits: 100,000 keys; 50,000 when the map representation is selected.

| Call | Description |
| --- | --- |
| `op.intIndex(IndexConfig settings = na)` | Creates an integer-key directory. Returns a new IntIndex in the empty state. |
| `index.buildBegin(bool expectedAscending = true)` | Starts a build and clears prior key data, retaining every allocation. Calling it on a sealed index restarts the build. Returns the receiver, so calls chain. |
| `index.add(int key)` | Adds one key and returns its payload slot. Slots are assigned in insertion order starting at 0. Returns the slot for this key. |
| `index.addMany(array<int> keyBatch)` | Adds every key of an array in order. Returns the slot of the first added key, or -1 for an empty batch. |
| `index.buildFinish(int expectedQueries = na)` | Completes construction and selects the lookup representation. Calling it again on a sealed index is a no-op. Returns the active representation. |
| `index.find(int key)` | Finds one key using the active lookup representation. Returns the slot, or -1 on a miss. |
| `index.contains(int key)` | Tests whether a key is present. Returns true when the key resolves to a slot. |
| `index.findMany(array<int> queryKeys, array<int> output = na)` | Resolves a batch of keys into slots, dispatching once. Returns the slot for each query key, in query order. -1 marks a miss. |
| `index.keyAt(int slot)` | Reads the key stored in a slot. Returns the key, or na when the slot does not exist. |
| `index.entryCount()` | Number of keys added. Returns the entry count. |
| `index.mode()` | Representation currently in use. Returns the active LookupPolicy. Never `automatic`. |
| `index.sealed()` | Whether the index is sealed and ready for queries. Returns true after `buildFinish()`. |
| `index.keysView()` | The insertion-order key array, indexed by slot. Ownership: borrowed structure-owned view. Mutation: read-only by contract. Rebuild to change the keys. Lifetime: valid until the next `buildBegin()`, `clear()` or `reset()`. Persistence: reacquire after that boundary. Returns the keys, in insertion order, so element `slot` is `keyAt(slot)`. |
| `index.stats()` | Snapshot of index telemetry. Returns a newly allocated IndexStats owned by the caller. |
| `index.clear()` | Empties the index, retaining every allocation. The configuration and the cumulative build and promotion counters are preserved. Returns the receiver, so calls chain. |
| `index.reset(IndexConfig settings = na)` | Replaces the configuration and empties the index. Returns the receiver, so calls chain. |

### IntBuckets

`op.IntBuckets`: One key to many integer values, in contiguous flat storage.
Complexity: build is O(pairs) plus the IntIndex build. `find()` is the configured IntIndex lookup. `rangeByKey()` is that plus O(1).
Limits: 100,000 associations and 50,000 distinct keys.

| Call | Description |
| --- | --- |
| `op.intBuckets(IndexConfig settings = na, bool strictLifecycle = true)` | Creates an empty bucket multimap. Returns a new IntBuckets. |
| `buckets.buildBegin()` | Opens an explicit build. Returns the receiver, so calls chain. |
| `buckets.add(int key, int value)` | Records one key/value pair. Returns the receiver, so calls chain. |
| `buckets.addMany(array<int> keyArray, array<int> valueArray)` | Records a batch of key/value pairs. Returns the receiver, so calls chain. |
| `buckets.buildFinish(int expectedQueries = na)` | Closes the build and packs every bucket into contiguous storage. Returns the receiver, so calls chain. |
| `buckets.buildFromPairs(array<int> keyArray, array<int> valueArray, int expectedQueries = na)` | Builds every bucket from two parallel arrays in one call. Returns the receiver, so calls chain. |
| `buckets.find(int key)` | Resolves a key to its bucket slot. Returns the bucket slot, or -1 when the key has no bucket. |
| `buckets.contains(int key)` | Reports whether a key has a bucket. Returns true when the key was seen during the build. |
| `buckets.rangeBySlot(int slot)` | Contiguous value range of one bucket slot. Returns [start, count] into the flat value array. [0, 0] for an invalid slot. A zero count still needs a guard before a counted loop. |
| `buckets.rangeByKey(int key)` | Contiguous value range of one key. Returns [start, count] into the flat value array, or [-1, 0] on a miss. |
| `buckets.valueAt(int flatIndex)` | Reads one value from the flat value array. Returns the value, or na when the index is outside the built range. |
| `buckets.bucketValueAt(int slot, int offset)` | Reads one value from a bucket by its position within the bucket. Returns the value, or na when either coordinate is out of range. |
| `buckets.bucketSize(int slot)` | Number of values in one bucket. Returns the occupancy, or 0 for an invalid slot. |
| `buckets.bucketCount()` | Number of distinct keys. Returns the bucket count. |
| `buckets.pairCount()` | Total number of stored pairs. Returns the flat value count. |
| `buckets.keyAt(int slot)` | Key owning one bucket slot. Returns the key, or na for an invalid slot. |
| `buckets.isSealed()` | Reports whether a build has completed. Returns true once `buildFinish()` has run. |
| `buckets.clear()` | Empties every bucket while retaining all allocations. Returns the receiver, so calls chain. |
| `buckets.offsetsView()` | The CSR bucket boundary array. Ownership: borrowed structure-owned view. Mutation: read-only by contract. Lifetime: valid until the next `buildBegin()`, `buildFinish()`, `buildFromPairs()` or `clear()`. Persistence: reacquire after that boundary. Returns the bucket boundaries, `bucketCount() + 1` elements long. |
| `buckets.valuesView()` | The flat value array every bucket is packed into. Ownership: borrowed structure-owned view. Mutation: read-only by contract. Lifetime: valid until the next `buildBegin()`, `buildFinish()`, `buildFromPairs()` or `clear()`. Persistence: reacquire after that boundary. Returns every bucket's values, grouped and contiguous, in slot order. |
| `buckets.keysView()` | The key array of the underlying directory. Ownership: borrowed structure-owned view from the nested `IntIndex`. Mutation: read-only by contract. Lifetime: valid until the next `buildBegin()`, `buildFinish()`, `buildFromPairs()` or `clear()`. Persistence: reacquire after that boundary. Returns the distinct keys, in slot order. |

### Typed stores

`op.IntFloatStore`: Integer-keyed float store backed by an adaptive `IntIndex`.
Complexity: inherited from `IntIndex`, plus O(1) per value access. `build()` is one index build plus one native payload refill.

`IntIntStore`, `IntBoolStore`, `IntStringStore` and `IntColorStore` have the same methods with int, bool, string or color values. Each store is built on an `IntIndex` and takes the same `IndexConfig`.

| Call | Description |
| --- | --- |
| `op.intIntStore(IndexConfig settings = na)` | Creates an integer-keyed int store. Returns a new IntIntStore. |
| `op.intFloatStore(IndexConfig settings = na)` | Creates an integer-keyed float store. Returns a new IntFloatStore. |
| `op.intBoolStore(IndexConfig settings = na)` | Creates an integer-keyed bool store. Returns a new IntBoolStore. |
| `op.intStringStore(IndexConfig settings = na)` | Creates an integer-keyed string store. Returns a new IntStringStore. |
| `op.intColorStore(IndexConfig settings = na)` | Creates an integer-keyed color store. Returns a new IntColorStore. |
| `store.build(array<int> keys, array<float> values, int expectedQueries = na)` | Fills the store from parallel key and value arrays, replacing its previous contents. revision: bump. Returns the active representation the index selected. |
| `store.revision()` | Current content revision. Complexity: O(1). Returns the current revision. Always >= 0. |
| `store.get(int key, float fallback = na)` | Reads one value. Returns the stored value, or `fallback`. |
| `store.read(int key)` | Reads one value and reports whether it was present. Returns [found, value]. `value` is na on a miss. |
| `store.set(int key, float value)` | Overwrites the value of an existing key. Keys cannot be added after a build; only values may change. Call `build()` to change the key set. revision: bump-if-changed. Returns true when the key existed. An identical value leaves the revision unchanged. |
| `store.contains(int key)` | Tests whether a key is present. Returns true when the key resolves to a slot. |
| `store.getMany(array<int> queryKeys, array<float> output = na)` | Resolves a batch of keys into values with one representation dispatch. Returns the value for each query key, in query order. na marks a miss. |
| `store.size()` | Number of stored entries. Returns the entry count. |
| `store.valuesView()` | The stored values in slot order, without copying them. Ownership: borrowed structure-owned view. Mutation: read-only by contract. Use `set()` for writes. Lifetime: valid until the next `build()` or `clear()`. Persistence: do not retain it across that boundary; reacquire it. Returns the values, indexed by slot. |
| `store.clear()` | Empties the store, retaining every allocation. revision: bump-if-changed. Returns the receiver, so calls chain. |

### Slot pools

`op.SlotPoolConfig`: Configuration for frame-scoped and explicitly managed slot pools.

`op.StablePool`: Maps integer keys to stable, reusable slots with explicit lifetime.
Complexity: `acquire()`, `release()`, `find()` are O(1); `slots()` is O(1) to obtain and O(live count) to traverse.
Limits: 50,000 live keys, or `SlotPoolConfig.maxSlots` when lower.

`op.SlotCache`: Maps integer keys to stable, reusable slots with frame-scoped lifetime.
Complexity: `acquire()` O(1); `finish()` O(slots active in the previous frame).
Limits: 50,000 live keys, or `SlotPoolConfig.maxSlots` when lower.

| Call | Description |
| --- | --- |
| `op.slotPoolConfig(bool strictLifecycle = true, int maxSlots = 50000)` | Creates a slot-pool configuration. Returns a new SlotPoolConfig. |
| `op.poolConfigFrameRetained()` | Preset: slots live for exactly one frame unless re-acquired. Returns a SlotPoolConfig. |
| `op.poolConfigLenient()` | Preset: lifecycle violations degrade instead of raising, for scripts that must never halt on a misuse. Returns a SlotPoolConfig. |
| `op.stablePool(SlotPoolConfig settings = na)` | Creates an explicitly managed slot pool. Returns a new StablePool. |
| `op.slotCache(SlotPoolConfig settings = na)` | Creates a frame-scoped slot cache. Returns a new SlotCache. |
| `pool.acquire(int key)` | Gets or creates the stable slot for a key. Returns [slot, created]. A created slot may be recycled storage whose payload still holds the previous occupant's values. |
| `pool.release(int key)` | Releases the slot owned by a key and returns it to the free list. Returns the released slot, or -1 when the key was not live. |
| `pool.releaseSlot(int slot)` | Releases a slot by index rather than by key. Returns true when the slot was live and has been released. |
| `pool.find(int key)` | Looks up a key. Returns the slot, or -1 on a miss. |
| `pool.contains(int key)` | Tests whether a key is live. Returns true when the key currently owns a slot. |
| `pool.keyAt(int slot)` | Reads the key currently assigned to a slot. Returns the key, or na when the slot does not exist. A released slot still reports its last key until it is recycled. |
| `pool.slots()` | The slots currently live in the pool. Ownership: borrowed structure-owned view. Mutation: read-only by contract; mutation is unsupported. The pool releases in O(1) by swapping with the last element and mirrors each position in `slotPosition`, so reordering it desynchronizes both. Lifetime: valid until the next `acquire()`, `release()`, `releaseSlot()` or `clear()`. Persistence: do not retain it across that boundary; reacquire it. Order is unspecified, and a `release()` reorders the survivors. Returns the live slots, in unspecified order. |
| `pool.size()` | Number of live keys. Returns the live count. |
| `pool.slotCount()` | Total slots ever allocated, including released ones. Returns the slot count. |
| `pool.freeCount()` | Number of slots available for reuse. Returns the free count. |
| `pool.clear()` | Empties the pool, retaining every allocation. Returns the receiver, so calls chain. |
| `cache.begin()` | Opens a frame. The slots active in the previous frame become the retirement candidates, and the retired list is emptied. Returns the receiver, so calls chain. |
| `cache.acquire(int key)` | Gets or creates the stable slot for a key and marks it active. Returns [slot, created]. A created slot may be recycled storage whose payload still holds the previous occupant's values. |
| `cache.finish()` | Closes the frame, retires every slot not acquired during it, and returns the retired list so the caller can reset those payloads. Ownership: borrowed structure-owned view. It is the same array `retired()` returns. Mutation: read-only by contract; mutation is unsupported. Lifetime: valid until the next `begin()` or `clear()`. Persistence: do not retain it across that boundary; use `retiredCopy()` when the result must outlive the frame. Returns the slots retired by this call, in the order they were acquired. Their payloads may be reset now or left intact for overwrite when the slots are recycled. |
| `cache.active()` | The slots acquired during the frame in progress. Ownership: borrowed structure-owned view. Mutation: read-only by contract; mutation is unsupported. This is the array `finish()` reads to decide what survives the frame. Lifetime: valid until the next `begin()` or `clear()`. Persistence: do not retain it across that boundary; use `activeCopy()` for an owned snapshot. Returns the active slots, in acquisition order. |
| `cache.retired()` | The slots retired by the most recent `finish()`. Ownership: borrowed structure-owned view. Mutation: read-only by contract; mutation is unsupported. Lifetime: valid until the next `begin()` or `clear()`. Persistence: do not retain it across that boundary; use `retiredCopy()` for an owned snapshot. Returns the retired slots, in retirement order. |
| `cache.activeCopy(array<int> output = na)` | Copies the active slot list into caller-owned storage. Returns the active slots in acquisition order, owned by the caller. |
| `cache.retiredCopy(array<int> output = na)` | Copies the retired slot list into caller-owned storage. Returns the retired slots in retirement order, owned by the caller. |
| `cache.find(int key)` | Looks up a key without changing its active status. Returns the slot, or -1 on a miss. |
| `cache.contains(int key)` | Tests whether a key is live. Returns true when the key currently owns a slot. |
| `cache.keyAt(int slot)` | Reads the key currently assigned to a slot. Returns the key, or na when the slot does not exist. A retired slot still reports its last key until it is recycled. |
| `cache.slotCount()` | Total slots ever allocated, including free ones. Returns the slot count. |
| `cache.size()` | Number of live keys. Returns the live count. |
| `cache.freeCount()` | Number of slots available for reuse. Returns the free count. |
| `cache.frame()` | Current frame counter, mainly for diagnostics. Returns the generation. |
| `cache.clear()` | Empties the cache, retaining every allocation. Returns the receiver, so calls chain. |

### Rings

`op.RingConfig`: Configuration for row rings.

`op.RingCursor`: Ring-buffer layout independent of payload type.
Complexity: every operation O(1). No allocation.

`op.FloatRowRing`: Fixed-capacity ring of equal-width float rows in one flat array.
Complexity: `push()` is O(width); `pushValue()` and every scalar accessor are O(1); `revision()` is O(1).
Limits: capacity * width may not exceed 100,000.

`IntRowRing` has the same methods with int values. `watch.changed(ring)` detects a change to either ring type in O(1) through its revision.

| Call | Description |
| --- | --- |
| `op.ringConfig(int capacity = 1, int width = 1, bool preallocate = true)` | Creates a row-ring configuration. Returns a new RingConfig with capacity and width clamped to at least 1. |
| `op.ringCursor(int capacity)` | Creates a ring cursor. Returns a new empty RingCursor. |
| `op.floatRowRing(int capacity, int width, bool preallocate = true, float fillValue = 0.0)` | Creates a flat float row ring. Returns a new FloatRowRing. |
| `op.floatRowRingFrom(RingConfig settings, float fillValue = 0.0)` | Creates a flat float row ring from a configuration object. Returns a new FloatRowRing. |
| `op.intRowRing(int capacity, int width, bool preallocate = true, int fillValue = 0)` | Creates a flat int row ring. Returns a new IntRowRing. |
| `op.intRowRingFrom(RingConfig settings, int fillValue = 0)` | Creates a flat int row ring from a configuration object. Returns a new IntRowRing. |
| `cursor.reserve()` | Reserves the physical slot for one new newest item and advances the ring. Once the ring is full this overwrites the oldest slot. Returns the physical slot to write. |
| `cursor.physical(int logicalIndex)` | Maps a chronological logical index to a physical slot. Returns the physical slot, or -1 when the logical index is not occupied. |
| `cursor.logical(int physicalSlot)` | Maps a physical slot back to its chronological logical index. Returns the logical index, or -1 when the slot is not occupied. |
| `cursor.newest(int offset = 0)` | Physical slot at an offset back from the newest item. Returns the physical slot, or -1 when the offset is out of range. |
| `cursor.oldest(int offset = 0)` | Physical slot at an offset forward from the oldest item. Returns the physical slot, or -1 when the offset is out of range. |
| `cursor.full()` | Whether every physical slot is occupied. Returns true when size has reached capacity. |
| `cursor.count()` | Number of occupied slots. Returns the occupied count. |
| `cursor.limit()` | Physical slot count. Returns the capacity. |
| `cursor.clear()` | Empties the ring without touching any payload storage. Returns the receiver, so calls chain. |
| `ring.push(array<float> row)` | Appends one complete row as the newest row. revision: bump. Returns the physical slot written. |
| `ring.pushValue(float value)` | Appends one scalar as the newest row of a width-1 ring without allocating a row array. revision: bump. Returns the physical slot written. |
| `ring.reserveRowUnchecked()` | Advances the ring and returns the physical slot for the new newest row without writing anything. Precondition: write every column with `writeUnchecked()` before the reserved row is observed. revision: bump. Returns the physical slot to write. |
| `ring.writePhysical(int slot, array<float> row)` | Writes a complete row into an existing physical slot without advancing the ring. revision: bump on a valid write. Returns true when the slot and row width were valid. |
| `ring.writeUnchecked(int slot, int column, float value)` | Writes one value by physical address with no validation. revision: bump. Returns the receiver, so calls chain. |
| `ring.at(int logicalRow, int column)` | Reads one value by chronological row and column. Returns the value, or na when the address is not occupied. |
| `ring.setAt(int logicalRow, int column, float value)` | Writes one value by chronological row and column. revision: bump-if-changed. Returns true when the address was occupied. An identical value leaves the revision unchanged. |
| `ring.newestAt(int offset, int column)` | Reads one value at an offset back from the newest row. Returns the value, or na when the address is not occupied. |
| `ring.gather(array<int> logicalRows, array<float> output = na)` | Gathers selected chronological rows into one flat array. Returns the gathered rows concatenated in request order. Rows that are not occupied contribute `width` na values. |
| `ring.chronological(array<float> output = na)` | Copies every occupied row in chronological order. Returns all occupied rows concatenated oldest to newest. |
| `ring.window(int startLogicalRow, int rowCount, array<float> output = na)` | Copies a contiguous run of chronological rows. Returns the requested rows concatenated oldest to newest, `rowCount * width` elements long. Raises when the window is outside the retained range. |
| `ring.rowView(int logicalRow)` | One chronological row, without copying it. Ownership: borrowed read-only slice of the backing array. Mutation: read-only by contract. Use `setAt()` or an edit scope. Lifetime: valid until the backing array is resized. Persistence: reacquire after that boundary. Returns the row's `width` values, or na when the row is not occupied. |
| `ring.rawView()` | The flat backing array, in row-major physical order. Ownership: borrowed structure-owned view. Mutation: read-only by contract. Use an edit scope for writes. Lifetime: valid until the backing array is resized. Persistence: reacquire after that boundary. Returns the backing storage. |
| `ring.editBegin()` | Opens a scope in which `rawUnsafe()` may be called. revision: caller-repairs. `editFinish()` performs the bump. Returns the receiver, so calls chain. |
| `ring.rawUnsafe()` | The flat backing array, for direct mutation. Ownership: borrowed structure-owned mutable view. Mutation: overwrite elements only inside an open edit scope. Do not resize the array. Lifetime: do not retain it past `editFinish()`. Repair: `editFinish()` advances the content revision. revision: caller-repairs. Returns the backing storage, in row-major physical order. |
| `ring.editFinish()` | Closes the edit scope and performs the invalidation the raw writes could not. revision: bump. Returns the receiver, so calls chain. |
| `ring.physicalRow(int logicalRow)` | Physical slot for a chronological row. Returns the physical slot, or -1 when the row is not occupied. |
| `ring.rowCount()` | Number of occupied rows. Returns the row count. |
| `ring.rowWidth()` | Values per row. Returns the row width. |
| `ring.rowCapacity()` | Maximum number of rows. Returns the ring capacity. |
| `ring.clear()` | Empties the ring. Preallocated storage keeps its values until they are overwritten; on-demand storage is released. revision: bump-if-changed. Returns the receiver, so calls chain. |
| `ring.revision()` | Current content revision. Complexity: O(1). Returns the current revision. Always >= 0. |

### Ring and matrix helpers

For caller-owned arrays laid out by a `RingCursor`, and for converting between flat arrays and matrices.

| Call | Description |
| --- | --- |
| `op.copyLogicalFloatRow(array<float> store, int width, RingCursor cursor, int logicalRow, array<float> output, int outputRow = 0)` | Copies one logical row into `output` at row `outputRow`. Returns the output array. |
| `op.gatherLogicalFloatRows(array<float> store, int width, RingCursor cursor, array<int> logicalRows, array<float> output = na)` | Copies the rows named in `logicalRows`, in request order, into one flat row-major array. Returns the caller-owned output array, `logicalRows.size() * width` long. |
| `op.copyLogicalFloatWindow(array<float> store, int width, RingCursor cursor, int startLogicalRow, int rowCount, array<float> output = na)` | Copies `rowCount` consecutive logical rows starting at `startLogicalRow` into one flat row-major array. Returns the caller-owned output array, `rowCount * width` long. |
| `op.copyChronologicalFloatRows(array<float> store, int width, RingCursor cursor, array<float> output = na)` | Copies every retained row, oldest first, into one flat row-major array. Returns the caller-owned output array, `cursor.count() * width` long. |
| `op.transposeFlatFloat(array<float> source, int rows, int columns, array<float> output = na)` | Transposes a flat row-major matrix into a flat column-major one. Returns the caller-owned output array, `rows * columns` long, holding the transposed matrix in row-major order. |
| `op.copyLogicalIntRow(array<int> store, int width, RingCursor cursor, int logicalRow, array<int> output, int outputRow = 0)` | Copies one logical row into `output` at row `outputRow`. Returns the output array. |
| `op.gatherLogicalIntRows(array<int> store, int width, RingCursor cursor, array<int> logicalRows, array<int> output = na)` | Copies the rows named in `logicalRows`, in request order, into one flat row-major array. Returns the caller-owned output array, `logicalRows.size() * width` long. |
| `op.copyLogicalIntWindow(array<int> store, int width, RingCursor cursor, int startLogicalRow, int rowCount, array<int> output = na)` | Copies `rowCount` consecutive logical rows starting at `startLogicalRow` into one flat row-major array. Returns the caller-owned output array, `rowCount * width` long. |
| `op.copyChronologicalIntRows(array<int> store, int width, RingCursor cursor, array<int> output = na)` | Copies every retained row, oldest first, into one flat row-major array. Returns the caller-owned output array, `cursor.count() * width` long. |
| `op.transposeFlatInt(array<int> source, int rows, int columns, array<int> output = na)` | Transposes a flat row-major matrix into a flat column-major one. Returns the caller-owned output array, `rows * columns` long, holding the transposed matrix in row-major order. |
| `op.matrixFromFlat(array<float> source, int rows, int columns, int startIndex = 0)` | Converts a contiguous flat float range into a matrix by adding one native row and reshaping. Returns a newly allocated caller-owned matrix. When the requested range is not fully present in `source`, a zero-filled matrix of the requested shape is returned instead. |
| `op.flatFromFloatMatrix(matrix<float> source, array<float> output = na)` | Copies a matrix into caller-owned flat row-major storage. Returns the matrix contents in row-major order. |
| `op.intMatrixFromFlat(array<int> source, int rows, int columns, int startIndex = 0)` | Converts a contiguous flat int range into a matrix by adding one native row and reshaping. Returns a newly allocated caller-owned matrix. When the requested range is not fully present in `source`, a zero-filled matrix of the requested shape is returned instead. |
| `op.flatFromIntMatrix(matrix<int> source, array<int> output = na)` | Copies a matrix into caller-owned flat row-major storage. Returns the matrix contents in row-major order. |

### Weight configuration

`op.WeightConfig`: Configuration for weighted selection.

| Call | Description |
| --- | --- |
| `op.weightConfig(WeightPolicy policy = WeightPolicy.prefix, NaWeightPolicy naWeights = NaWeightPolicy.treatAsZero, NegativeWeightPolicy negativeWeights = NegativeWeightPolicy.clampToZero, ExponentZeroPolicy exponentZero = ExponentZeroPolicy.uniform, int promoteAfterCycles = 2, float sparseUpdateRatio = 0.05)` | Creates a weighted-selection configuration. Returns a new WeightConfig with every argument clamped to a valid range. |
| `op.weightConfigDeterministic()` | Preset: bit-reproducible selection. Forces the linear prefix, whose left-to-right addition order never changes. Returns a WeightConfig. |
| `op.weightConfigStatic()` | Preset: weights that rarely change under a stable exponent, where an O(log N) tree pays for itself across many draws. Returns a WeightConfig. |
| `op.weightConfigSparseUpdates()` | Preset: automatic promotion for sparse point updates, such as a prioritized replay buffer whose priorities change a few at a time. Returns a WeightConfig. |
| `op.weightConfigStrict()` | Preset: reject na and negative weights instead of clamping them, for callers that want a bad weight to surface immediately. Returns a WeightConfig. |

`op.WeightPolicy`:

| Value | Meaning |
| --- | --- |
| `op.WeightPolicy.automatic` | Start with the linear prefix and promote to a Fenwick tree once the exponent has been stable and updates have stayed sparse. |
| `op.WeightPolicy.prefix` | Rebuilt left-to-right cumulative sums. This is the deterministic baseline: it preserves the exact addition order a hand written prefix implementation would use. |
| `op.WeightPolicy.fenwick` | Binary indexed tree. Point updates and selection are O(log N), but its summation order differs from a linear prefix and can therefore change bit-exact selections at tie boundaries. |

`op.NaWeightPolicy`:

| Value | Meaning |
| --- | --- |
| `op.NaWeightPolicy.treatAsZero` | Store 0.0, making the entry unselectable. |
| `op.NaWeightPolicy.raiseError` | Stop with a descriptive error. |

`op.NegativeWeightPolicy`:

| Value | Meaning |
| --- | --- |
| `op.NegativeWeightPolicy.clampToZero` | Store 0.0, making the entry unselectable. |
| `op.NegativeWeightPolicy.raiseError` | Stop with a descriptive error. |

`op.ExponentZeroPolicy`:

| Value | Meaning |
| --- | --- |
| `op.ExponentZeroPolicy.uniform` | Every entry gets weight 1.0, including zero-weight entries. This is the mathematical result of `pow(w, 0)` and gives uniform sampling across the whole prepared range. |
| `op.ExponentZeroPolicy.preserveZeros` | Strictly positive weights get 1.0 and zero weights stay 0.0, so a zero-weight entry remains unselectable at every exponent. |

### WeightedSampler

`op.WeightedSampler`: Tier 1 facade over `WeightedIndex` for the common non-circular case.
Complexity: inherited from `WeightedIndex`. `setWeight()` is O(1) under the prefix policy and O(log N) while a Fenwick tree is active; `setWeights()` and `setMany()` are one batch each.

| Call | Description |
| --- | --- |
| `op.weightedSampler(int capacity, float exponent = 1.0, WeightConfig settings = na)` | Creates a weighted sampler. Returns a new WeightedSampler. |
| `sampler.setWeight(int slot, float weight)` | Sets one slot weight. Returns true when the slot was in range. |
| `sampler.setWeights(array<float> weights)` | Replaces every slot weight from one array. Complexity: O(capacity), plus one rebuild of the selection structure on the next draw. Returns the receiver, so calls chain. |
| `sampler.setMany(array<int> slots, array<float> weights)` | Writes weights into selected slots. Complexity: O(batch), plus one rebuild of the selection structure on the next draw. Returns the number of slots actually written, so out-of-range entries are visible to the caller rather than silent. |
| `sampler.weight(int slot)` | Reads one stored slot weight. Returns the normalized weight, or na when the slot is out of range. |
| `sampler.fill(float weight)` | Sets every slot weight to one value. Returns the receiver, so calls chain. |
| `sampler.setExponent(float exponent)` | Changes the selection exponent. Returns the receiver, so calls chain. |
| `sampler.sample(float fraction)` | Draws one slot from a fraction in [0, 1). Returns the selected slot, or -1 when every weight is 0. |
| `sampler.sampleMany(array<float> fractions, array<int> output = na)` | Draws a batch of slots, refreshing derived state once. Returns one slot per fraction, in request order. |
| `sampler.probability(int slot)` | Selection probability of one slot. Returns the probability in [0, 1], or 0.0 when every weight is 0. |
| `sampler.total()` | Transformed total weight across every slot. Returns the total. |
| `sampler.size()` | Number of slots. Returns the capacity. |
| `sampler.mode()` | Representation currently in use. Returns the active WeightPolicy. |
| `sampler.indexView()` | The owned weighted index, for circular ranges, forced policies and scoped weight editing. Ownership: borrowed structure-owned object, not a raw collection. Mutation: supported through the `WeightedIndex` API. Lifetime: valid for the lifetime of the sampler. Returns the owned WeightedIndex. |
| `sampler.clear()` | Sets every weight to 0 and forgets prepared state. Returns the receiver, so calls chain. |

### WeightedIndex

`op.WeightedIndex`: Reusable weighted-selection index over a circular physical range.
Complexity: `setWeight()` O(1) in prefix mode and O(log N) while a Fenwick tree is valid. `draw()` O(log N) plus an O(N) refresh on the first read after a weight change in prefix mode.
Limits: at most 99,999 weights.

| Call | Description |
| --- | --- |
| `op.weightedIndex(int capacity, WeightConfig settings = na)` | Creates a weighted index with every weight at 0. Returns a new WeightedIndex. |
| `weighted.setWeight(int physicalIndex, float weight)` | Sets one physical weight. Writing the value it already holds is a no-op and does not invalidate any derived state. Returns true when the slot was in range. |
| `weighted.setWeightUnchecked(int physicalIndex, float weight)` | Sets one physical weight without bounds checking or weight normalization. Returns the receiver, so calls chain. |
| `weighted.getWeight(int physicalIndex)` | Reads one stored physical weight. Returns the normalized weight, or na when the slot is out of range. |
| `weighted.fill(float weight)` | Sets every weight to one value. Returns the receiver, so calls chain. |
| `weighted.clear()` | Sets every weight to 0 and forgets the prepared exponent, range and representation. Capacity and allocations are retained. Returns the receiver, so calls chain. |
| `weighted.weightsEditBegin()` | Opens a scope in which `weightsUnsafe()` may be called. Returns the receiver, so calls chain. |
| `weighted.weightsUnsafe()` | Returns the backing weight array for direct mutation. Ownership: borrowed structure-owned mutable view. Mutation: supported only inside an open `weightsEditBegin()` scope. Retaining the reference past `weightsEditFinish()` is a contract violation: the structure has no way to observe later writes, and every derived total would silently go stale. Lifetime: do not retain it past `weightsEditFinish()`. Repair: `weightsEditFinish()` normalizes the array and rebuilds every derived structure. `touchWeights()` does the same without closing a scope. |
| `weighted.touchWeights(bool normalize = true)` | Announces that the backing weight array changed and repairs every derived structure. Safe to call without an open edit scope. Returns the receiver, so calls chain. |
| `weighted.weightsEditFinish(bool normalize = true)` | Closes the unsafe edit scope and repairs every derived structure. Returns the receiver, so calls chain. |
| `weighted.weightsCopy(array<float> output = na)` | Copies the weights into caller-owned storage. Returns the weights by physical slot. |
| `weighted.prepare(float exponent, int startPhysical, int count)` | Fixes the selection exponent, the circular range and the representation policy. Returns the receiver, so calls chain. |
| `weighted.prepareAll(float exponent = 1.0)` | Prepares selection over every physical slot. Returns the receiver, so calls chain. |
| `weighted.draw(float fraction)` | Draws one physical slot from a fraction in [0, 1). Returns the selected physical slot, or -1 when the prepared range has no positive total weight or `prepare()` has not run. |
| `weighted.drawMany(array<float> fractions, array<int> output = na)` | Draws a batch of physical slots, refreshing derived state once. Returns one physical slot per fraction, in request order. -1 marks a range with no positive total weight. |
| `weighted.total()` | Transformed total weight of the prepared range. Returns the total, or 0.0 when `prepare()` has not run. |
| `weighted.probability(int physicalIndex)` | Transformed selection probability of one physical slot. Returns the probability in [0, 1], or 0.0 when the slot is outside the prepared range or the range has no positive total weight. |
| `weighted.mode()` | Representation currently in use. Returns the active WeightPolicy. Never `automatic`. |
| `weighted.limit()` | Number of physical slots. Returns the capacity. |
| `weighted.isPrepared()` | Whether `prepare()` has run. Returns true once a range has been prepared. |
| `weighted.currentExponent()` | Exponent fixed by the last `prepare()`. Returns the exponent, or na before the first prepare. |
| `weighted.preparedRange()` | Circular range fixed by the last `prepare()`. Returns [startPhysical, count]. |
| `weighted.pendingUpdates()` | Weight mutations counted since the last `prepare()`. Named distinctly from the `updatesSincePrepare` field. Returns the update count. |
| `weighted.weightRevision()` | Monotonic weight revision, for callers that cache derived results. Named distinctly from the free `revision()` constructor. Returns the revision. |

### WeightedPrefix

`op.WeightedPrefix`: Reusable left-to-right cumulative weights over a circular range.
Complexity: build O(count); `lowerBound()` O(log count).

| Call | Description |
| --- | --- |
| `op.weightedPrefix()` | Creates an empty prefix index. Returns a new WeightedPrefix. |
| `prefix.buildCircular(array<float> weights, int startPhysical, int count, float exponent = 1.0, bool preserveZeroExponent = false)` | Rebuilds cumulative weights over a circular logical range. Returns the receiver, so calls chain. |
| `prefix.build(array<float> weights, int count = na, float exponent = 1.0, bool preserveZeroExponent = false)` | Rebuilds cumulative weights over a non-circular range starting at physical slot 0. Returns the receiver, so calls chain. |
| `prefix.lowerBound(float threshold)` | Selects the smallest logical index whose cumulative sum reaches a threshold. Returns the logical index, or -1 when the range is empty or its total is 0. |
| `prefix.probability(int logicalIndex)` | Transformed probability of one logical index in the built range. Returns the probability, or 0.0 when the index is outside the range or the total is 0. |
| `prefix.total()` | Sum of every transformed weight in the built range. Returns the total. |
| `prefix.count()` | Number of logical entries in the built range. Returns the entry count. |
| `prefix.cumulativeView()` | The running cumulative totals, in logical order. Ownership: borrowed structure-owned view. Mutation: read-only by contract. Rebuild to change the weights. Lifetime: valid until the next `build()`, `buildCircular()` or `clear()`. Persistence: reacquire after that boundary. Returns the cumulative totals, `count()` elements long. |
| `prefix.clear()` | Empties the index, retaining the allocation. Returns the receiver, so calls chain. |

### FloatFenwick

`op.FloatFenwick`: Dynamic prefix-sum index over non-negative float weights.
Complexity: `reset()` and `build()` O(N); `set()`, `add()`, `prefix()` and `lowerBound()` O(log N).
Limits: at most 99,999 weights, because the tree uses a sentinel slot.

| Call | Description |
| --- | --- |
| `op.floatFenwick(int size = 0)` | Creates a Fenwick tree. Returns a new FloatFenwick with every weight at 0. |
| `tree.reset(int size)` | Resizes the tree and clears every weight to 0. Returns the receiver, so calls chain. |
| `tree.build(array<float> weights, int count = na)` | Rebuilds the whole tree from a weight array in O(N). Returns the receiver, so calls chain. |
| `tree.set(int index, float value)` | Sets one zero-based weight. Returns true when the index was in range. |
| `tree.add(int index, float delta)` | Adds a delta to one zero-based weight, clamping the result at 0. Returns true when the index was in range. |
| `tree.get(int index)` | Reads one zero-based weight. Returns the stored weight, or na when the index is out of range. |
| `tree.prefix(int endExclusive)` | Sum of weights in [0, endExclusive). Returns the prefix sum. |
| `tree.rangeSum(int startInclusive, int endExclusive)` | Sum of weights in [startInclusive, endExclusive). Returns the range sum. |
| `tree.total()` | Sum of every weight. Returns the total. |
| `tree.count()` | Number of weights. Returns the weight count. |
| `tree.lowerBound(float threshold)` | Selects the smallest zero-based index whose prefix sum reaches a threshold, using binary lifting over the tree. Returns the index, or -1 when the tree is empty or its total is 0. |
| `tree.circularTotal(int startPhysical, int count)` | Exact total over a circular physical range. Returns the exact total. Complexity O(count). |
| `tree.lowerBoundCircular(float threshold, int startPhysical, int count)` | Selects over a circular physical range. Returns the selected physical slot, or -1 when the range is empty or its total is 0. |
| `tree.clear()` | Clears every weight to 0, retaining the allocation and size. Returns the receiver, so calls chain. |

## Patterns from the publication

These fragments isolate one idea each. Lines described as pseudocode use names the script supplies.

### Watch several dependencies

```pine
// Pseudocode dependencies: int length, float multiplier,
// and array<float> features.
var op.Watch settingsWatch = op.watch()
var float result = na

settingsWatch.begin()
settingsWatch.watchInt(length)
settingsWatch.watchFloat(multiplier)
settingsWatch.watchFloats(features)

bool dependenciesChanged = settingsWatch.finish()
if dependenciesChanged
    result := rebuild(length, multiplier, features)

// Output: result is rebuilt when any observed dependency changes.
```

### A rolling history

```pine
var op.FloatRowRing history = op.floatRowRing(200, 3)

float atr14 = ta.atr(14)

if barstate.isconfirmed
    history.push(array.from(close, volume, atr14))

float oldestPrice = history.at(0, 0)
float latestPrice = history.newestAt(0, 0)

// Output: after a confirmed push, these are the oldest and newest retained closes.
```

### Stable slots for objects that come and go

```pine
const int ZONE_A_ID = 1001
const int ZONE_B_ID = 1002
const int ZONE_C_ID = 1003

var op.StablePool zonePool = op.stablePool()
var array<float> zonePrices = array.new<float>()
var array<int> zoneTimes = array.new<int>()
var array<float> zoneStrengths = array.new<float>()
var array<color> zoneColors = array.new<color>()

if barstate.isfirst
    [slotA, _] = zonePool.acquire(ZONE_A_ID)
    [slotB, _] = zonePool.acquire(ZONE_B_ID)

    // Grow every payload array to cover the allocated slots.
    int required = zonePool.slotCount()
    op.ensureSizeFloat(zonePrices, required, na)
    op.ensureSizeInt(zoneTimes, required, na)
    op.ensureSizeFloat(zoneStrengths, required, na)
    op.ensureSizeColor(zoneColors, required, na)

    zonePrices.set(slotA, 100.0)
    zoneTimes.set(slotA, 10)
    zoneStrengths.set(slotA, 0.40)
    zoneColors.set(slotA, color.blue)

    zonePrices.set(slotB, 200.0)
    zoneTimes.set(slotB, 20)
    zoneStrengths.set(slotB, 0.80)
    zoneColors.set(slotB, color.red)

    zonePool.release(ZONE_A_ID)
    [slotC, _] = zonePool.acquire(ZONE_C_ID)

    // C reuses A's slot, so every field at that slot must be overwritten.
    zonePrices.set(slotC, 300.0)
    zoneTimes.set(slotC, 30)
    zoneStrengths.set(slotC, 0.60)
    zoneColors.set(slotC, color.lime)

// Output: B keeps slot 1 in every array. C owns slot 0 in every array.
//         Nothing is removed or shifted.
```

### Only the entries that changed

```pine
int MAX_ZONES = 50000

var op.StablePool zones = op.stablePool()
var op.DirtySet dirtySlots = op.dirtySet(MAX_ZONES)

var array<float> tops = array.new<float>()
var array<float> bottoms = array.new<float>()
var array<float> midpoints = array.new<float>()

// Start this bar's sparse-work cycle.
dirtySlots.begin()

if zoneGeometryChanged
    // StablePool converts the logical ID into a reusable physical slot.
    [zoneSlot, created] = zones.acquire(zoneId)
    if created
        op.ensureSizeFloat(tops, zoneSlot + 1, na)
        op.ensureSizeFloat(bottoms, zoneSlot + 1, na)
        op.ensureSizeFloat(midpoints, zoneSlot + 1, na)

    tops.set(zoneSlot, newTop)
    bottoms.set(zoneSlot, newBottom)
    dirtySlots.mark(zoneSlot)

if zoneStyleChanged
    int styleSlot = zones.find(zoneId)
    if styleSlot >= 0
        dirtySlots.mark(styleSlot)  // A second mark of the same slot is ignored.

// Process only the distinct physical slots marked during this bar.
for dirtySlot in dirtySlots.values()
    float midpoint = (tops.get(dirtySlot) + bottoms.get(dirtySlot)) * 0.5
    midpoints.set(dirtySlot, midpoint)
    redrawZone(zones.keyAt(dirtySlot), midpoint)

// Output: one zone is rebuilt once even if geometry and style both mark it.
```

### A typed store

```pine
var op.IntFloatStore scores = op.intFloatStore()

if barstate.isfirst
    // Four entries are shown for readability; both arrays may be much larger.
    scores.build(
      array.from(410, 411, 412, 413),
      array.from(0.80, 0.30, 0.95, 0.50))

float selected = scores.get(412)

// Output: integer key 412 resolves to float value 0.95.
```

### One key to many integers

```pine
var op.IntBuckets cellMembers = op.intBuckets()
var array<int> matches = array.new<int>()

if barstate.isfirst
    // Six (cell, object slot) pairs. Cell 7 appears three times.
    array<int> cellKeys = array.from(7, 2, 7, 5, 2, 7)
    array<int> objectSlots = array.from(101, 205, 412, 990, 777, 888)

    cellMembers.buildFromPairs(cellKeys, objectSlots)

    // Read cell 7's group from flat storage. matches is only demo output.
    [start, count] = cellMembers.rangeByKey(7)
    if count > 0
        for position = start to start + count - 1
            matches.push(cellMembers.valueAt(position))

// Output: matches contains [101, 412, 888], the object slots assigned to cell 7.
```

### A double buffer

```pine
var op.IntDoubleBuffer searchFrontier = op.intDoubleBuffer()

if barstate.isfirst
    searchFrontier.current.push(1)

    for depth = 1 to 3
        [nextFrontier, activeFrontier] = searchFrontier.swap()

        for nodeId in activeFrontier
            nextFrontier.push(nodeId * 2)
            nextFrontier.push(nodeId * 2 + 1)

// Output: current contains [8, 9, 10, 11, 12, 13, 14, 15].
//         previous contains [4, 5, 6, 7].
```

### Weighted selection

```pine
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.
```

## Complete example: zone cluster engine

StablePool preserves drawing slots, an `IntRowRing` tracks retirement order, DirtySet queues redraws, IntBuckets forms price clusters and IntFloatStore looks up their strength.

```pine
//@version=6
indicator("OptiPine - Zone Cluster Engine", overlay = true, max_lines_count = 100)

import Alien_Algorithms/OptiPine/1 as op

int pivotLength = input.int(5, "Pivot length", minval = 1)
int maxZones = input.int(32, "Maximum zones", minval = 4, maxval = 100)
int bucketTicks = input.int(25, "Cluster size in ticks", minval = 1)
float bucketSize = syminfo.mintick * bucketTicks

// Stateful Pine calculations remain outside every conditional rebuild.
float pivotHigh = ta.pivothigh(high, pivotLength, pivotLength)
float pivotLow = ta.pivotlow(low, pivotLength, pivotLength)
float pivotStrength = math.max(nz(volume[pivotLength], 1.0), 1.0)
float highlightDistance = ta.atr(14)

var op.StablePool zones = op.stablePool()
var op.IntRowRing zoneOrder = op.intRowRing(maxZones, 1)
var op.DirtySet dirtyZones = op.dirtySet(maxZones)

var array<float> zonePrices = array.new<float>(maxZones, na)
var array<float> zoneStrengths = array.new<float>(maxZones, 0.0)
var array<int> zoneTimes = array.new<int>(maxZones, na)
var array<bool> resistance = array.new<bool>(maxZones, false)
var array<bool> highlighted = array.new<bool>(maxZones, false)
var array<line> zoneLines = array.new<line>(maxZones)

var op.IntBuckets zonesByBucket = op.intBuckets()
var op.IntFloatStore strengthByBucket = op.intFloatStore()

// Retained build storage is resized and overwritten, never cleared and repopulated.
var array<int> bucketKeyByPosition = array.new<int>()
var array<int> aggregateKeys = array.new<int>()
var array<float> aggregateStrengths = array.new<float>()
var int strongestBucketKey = na
var float strongestBucketStrength = na
var int strongestZoneCount = 0

dirtyZones.begin()
bool topologyChanged = barstate.isfirst

// Logical pivot IDs receive stable, reusable physical drawing slots.
for event = 0 to 1
    float level = event == 0 ? pivotHigh : pivotLow
    if barstate.isconfirmed and not na(level)
        int pivotBar = bar_index - pivotLength
        int pivotTime = time[pivotLength]
        int zoneId = pivotBar * 2 + event
        int slot = zones.find(zoneId)

        // Only a new logical pivot enters the retirement queue.
        if slot < 0
            if zoneOrder.rowCount() == maxZones
                int oldestId = zoneOrder.at(0, 0)
                zones.release(oldestId)

            [newSlot, _] = zones.acquire(zoneId)
            slot := newSlot
            zoneOrder.pushValue(zoneId)
            zonePrices.set(slot, level)
            zoneStrengths.set(slot, pivotStrength)
            zoneTimes.set(slot, pivotTime)
            resistance.set(slot, event == 0)
            highlighted.set(slot, false)
            dirtyZones.mark(slot)
            topologyChanged := true

// Proximity can mark a newly created slot again; DirtySet still stores it once.
for slot in zones.slots()
    bool isHighlighted = math.abs(close - zonePrices.get(slot)) <= highlightDistance
    if isHighlighted != highlighted.get(slot)
        highlighted.set(slot, isHighlighted)
        dirtyZones.mark(slot)

// Only changed drawings cross the line API boundary.
for slot in dirtyZones.values()
    float level = zonePrices.get(slot)
    color baseColor = resistance.get(slot) ? color.red : color.lime
    line zoneLine = zoneLines.get(slot)

    if na(zoneLine)
        zoneLine := line.new(zoneTimes.get(slot), level, time, level,
          xloc = xloc.bar_time)
        zoneLines.set(slot, zoneLine)

    line.set_xy1(zoneLine, zoneTimes.get(slot), level)
    line.set_xy2(zoneLine, time, level)
    line.set_extend(zoneLine, extend.right)
    line.set_width(zoneLine, highlighted.get(slot) ? 3 : 1)
    line.set_color(zoneLine,
      color.new(baseColor, highlighted.get(slot) ? 0 : 55))

// Rebuild grouped lookup only after the explicit creation event.
if topologyChanged
    array<int> liveSlots = zones.slots()
    int liveCount = liveSlots.size()

    op.resizeInt(bucketKeyByPosition, liveCount, 0)
    if liveCount > 0
        for position = 0 to liveCount - 1
            int slot = liveSlots.get(position)
            int bucketKey = int(math.round(zonePrices.get(slot) / bucketSize))
            bucketKeyByPosition.set(position, bucketKey)

    // Repeated bucket keys accumulate several physical zone slots.
    zonesByBucket.buildFromPairs(bucketKeyByPosition, liveSlots)

    int bucketCount = zonesByBucket.bucketCount()
    op.resizeInt(aggregateKeys, bucketCount, 0)
    op.resizeFloat(aggregateStrengths, bucketCount, 0.0)
    strongestBucketKey := na
    strongestBucketStrength := na
    strongestZoneCount := 0

    if bucketCount > 0
        for bucketSlot = 0 to bucketCount - 1
            int bucketKey = zonesByBucket.keyAt(bucketSlot)
            [start, count] = zonesByBucket.rangeBySlot(bucketSlot)
            float totalStrength = 0.0

            if count > 0
                for position = start to start + count - 1
                    int zoneSlot = zonesByBucket.valueAt(position)
                    totalStrength += zoneStrengths.get(zoneSlot)

            aggregateKeys.set(bucketSlot, bucketKey)
            aggregateStrengths.set(bucketSlot, totalStrength)
            if na(strongestBucketStrength) or totalStrength > strongestBucketStrength
                strongestBucketKey := bucketKey
                strongestBucketStrength := totalStrength
                strongestZoneCount := count

    strengthByBucket.build(aggregateKeys, aggregateStrengths)

// Query the current price cluster directly and display the strongest cluster.
int currentBucketKey = int(math.round(close / bucketSize))
float nearbyStrength = strengthByBucket.get(currentBucketKey, 0.0)

float strongestClusterPrice = na(strongestBucketKey) ?
  na : strongestBucketKey * bucketSize

plot(strongestClusterPrice, "Strongest zone cluster", color.orange,
  linewidth = 2, style = plot.style_stepline)
plot(nearbyStrength, "Strength near current price", display = display.data_window)
plot(strongestBucketStrength, "Strongest cluster strength",
  display = display.data_window)
plot(strongestZoneCount, "Zones in strongest cluster",
  display = display.data_window)
plot(dirtyZones.size(), "Drawings updated", display = display.data_window)
```

## Limits

- Arrays hold at most 100,000 elements and maps at most 50,000 pairs. `IntIndex` holds up to 100,000 keys, 50,000 when the map representation is selected. `IntBuckets` holds up to 100,000 associations and 50,000 distinct keys.
- StablePool and SlotCache hold at most 50,000 live keys, or `SlotPoolConfig.maxSlots` when lower.
- `DirtySet` accepts a universe of at most 100,000 indices.
- A row ring's `capacity * width` may not exceed 100,000. `WeightedIndex` and `FloatFenwick` support at most 99,999 weights.
- Watch's array comparison and a double buffer's clear are O(N). Memo and Watch avoid the guarded work, not the cost of computing their dependencies.
- `Revision` wraps to 0 after 2,000,000,000 bumps.

TradingView's own Pine Script limits still apply: https://www.tradingview.com/pine-script-docs/writing/limitations/

## Runtime errors and what they mean

Every error starts with `OptiPine.` and the type or function that raised it.

| Message | Cause |
| --- | --- |
| `IntIndex: buildFinish found N duplicate key(s)` | The same key was added twice. Remove the duplicate or set `IndexConfig.duplicates` to `firstWins` or `lastWins`. |
| `IntIndex: find() before buildFinish()` | A lookup ran before the build was completed. |
| `IntIndex: add() after buildFinish(). Call buildBegin() first.` | A sealed index was given more keys. Start a new build. |
| `IntIndex: map representation needs N pairs, above Pine's 50,000-pair limit` | A map was requested for more than 50,000 keys. Use `LookupPolicy.sorted`. |
| `IntIndex: dense policy needs a key span of at most N` | A forced dense index has keys spread too far apart. Raise `maxDenseSpan` or choose another policy. |
| `IntStore: build() expects exactly one value per key.` | The key and value arrays given to a store's `build()` differ in length. |
| `IntBuckets: keyArray and valueArray must be the same length.` | `buildFromPairs()` or `addMany()` was given arrays of different lengths. |
| `StablePool: na is not a valid key.`, `SlotCache: na is not a valid key.` | An ID was `na`. |
| `StablePool: slot limit of N` | More live keys than the pool allows. |
| `SlotCache: begin called while a frame was already open.`, `finish called without begin.` | The frame lifecycle was called out of order. |
| `FloatRowRing: push expects a row of exactly N values` | The row given to `push()` does not match the ring's width. |
| `FloatRowRing: pushValue requires width 1` | `pushValue()` was called on a ring wider than one column. |
| `FloatRowRing: capacity * width exceeds the 100,000-element array limit.` | The ring is too large for one array. |
| `DirtySet: universe size exceeds the 100,000-element array limit.` | The universe passed to `dirtySet()` or `begin()` is too large. |
| `Watch.watchFloats: na element at index N` | The array contains `na` and the watch uses `NaCollectionPolicy.reject`. |
| `WeightedIndex: negative weight rejected by the configured policy.` | A negative weight under `NegativeWeightPolicy.reject`. |
| `ensureSizeFloat: values cannot be na.` | A storage utility was given an `na` array. The same applies to the other `ensureSize*()`, `resize*()` and `refill*()` calls. |

## Related

- PineCanvas, a high-resolution graphics engine for Pine Script v6: https://alienalgorithms.com/pinecanvas
- Pine3D, a 3D rendering engine for Pine Script v6: https://alienalgorithms.com/pine3d
- NeuraLib, a machine learning runtime for Pine Script v6: https://alienalgorithms.com/neurallib
