The other axis

What is still proportional to n

An index whose size is a function of the run count still has an n in it, and at thirty-two copies of a text the n is half of it. Everything that stores the transform grew by 45 per cent; the two arrays that answer "where" grew by eighteen times, and neither of them has anything to do with repetition.

The previous essay’s headline is that a run-length index over thirty-two copies of a text is 11,900 bits against a compressed self-index’s 30,557, and that its run count has not moved since the second copy.

It was 4,440 bits at two copies. So it grew by a factor of 2.7 while the quantity it is supposed to be a function of grew by a factor of 1.00.

Where a run-length index's bits go at 32 copies16,384 characters whose transform has 224 runs — the same 224 it had at two copies. The parts proportional to r total 5,989 bits, 1.5x what they were at two copies. The two marked parts are the sampling, which is proportional to n: 5,911 bits, 18.3x what they were, and 50% of the structure. A structure whose size is a function of the run count still has an n in it, and the n is the answer to "where", not to "how many" — which is what the run-boundary sampling of an r-index removes and this structure does not.sampled positions3,855grows with nsample marks2,056grows with nrun starts2,016grows with rrun lengths per symbol1,885grows with rrun heads1,563grows with rC table525grows with r11,900 bits in total16,384 characters · r = 224 · sample one in 6450% is the sampling
Fig. 1 The structure opened up at thirty-two copies. Three parts are proportional to the run count and two are proportional to the length, and the second pair is half of the total.
at 2 copies at 32 copies grew by
proportional to rr 4,117 5,989 1.45×
proportional to nn 323 5,911 18.3×
the total 4,440 11,900 2.68×

At two copies the nn-sized terms are 7% of the structure. At thirty-two they are 50%.

What the two terms are

Everything that stores the transform is proportional to rr: the run starts, the run heads, the per-symbol run lengths. Those grew 1.45× across a sixteenfold growth in the collection, and the growth is entirely the widths — a position in 16,384 needs 14 bits where a position in 1,024 needs 10.

Everything that answers where is proportional to nn:

  • sampled positions — the text position of one row in every ss, so n/sn/s entries of log2n\lceil\log_2 n\rceil bits;
  • sample marks — a vector saying which rows are sampled, so that a row can ask in one operation whether it is one.

Neither has anything to do with the transform, with runs, or with repetition. They exist because locate has to turn a row into a position, and the only way to do that without walking to the end of the text is to have written some positions down.

A sample is a position, and positions do not compress because there are nn of them and they are all different. That is the whole of it, and it is why the term is there in every self-index this collection has built regardless of what the transform is stored as.

The shape of the claim, and where the term hides in it

“An index of size O(rlog(n/r))O(r \log(n/r))” is a true description of a structure this essay is about, and it is a description of the transform’s representation only. The full size is

O ⁣(rlognr)  +  O ⁣(nslogn)O\!\left(r \log \frac{n}{r}\right) \;+\; O\!\left(\frac{n}{s}\log n\right)

and the second term is not oo of the first. On a highly repetitive collection rr is small by design, so the first term is small — and the second is unchanged, which means the more repetitive the collection, the larger the share the sampling takes.

Where a run-length index's bits go at 4 copies2,048 characters whose transform has 224 runs — the same 224 it had at two copies. The parts proportional to r total 4,585 bits, 1.1x what they were at two copies. The two marked parts are the sampling, which is proportional to n: 660 bits, 2.0x what they were, and 13% of the structure. A structure whose size is a function of the run count still has an n in it, and the n is the answer to "where", not to "how many" — which is what the run-boundary sampling of an r-index removes and this structure does not.run heads1,563grows with rrun starts1,344grows with rrun lengths per symbol1,216grows with rC table462grows with rsampled positions396grows with nsample marks264grows with n5,245 bits in total2,048 characters · r = 224 · sample one in 6413% is the sampling
Fig. 2 The same parts at four copies, where the sampling is an eighth of the structure. The band that eventually dominates is the one that looks negligible at the size a first experiment is run at.
copies rr rr-sized bits nn-sized bits the sampling’s share
1 222 3,636 162 4%
4 224 4,585 660 13%
8 224 5,053 1,365 21%
16 224 5,521 2,838 34%
32 224 5,989 5,911 50%

This collection has met that shape before, from the other direction. The entropy-bounded index is “nHk+o(n)nH_k + o(n)”, and the o(n)o(n) turned out to be a quarter to three-fifths of it at the block lengths anybody actually uses. Here the leading term has been made small on purpose and the same thing happens harder: making the leading term small does not make the lower-order term smaller, it makes it the answer.

The obvious repair, and what it costs

The sampling rate is a parameter. Turn it down.

sample interval sampled positions sample marks whole structure worst locate walk
16 15,375 6,150 27,514 15
64 3,855 2,056 11,900 63
256 975 650 7,614 255
1,024 255 204 6,448 1,023

At one sample in 1,024 the nn-sized terms fall to 459 bits — 7% of the structure — and a locate walk costs up to 1,023 LF steps per occurrence. At one in sixteen they are 21,525 bits, which is three and a half times everything else in the index put together.

One query, against the length of the text it searchesA pattern of 8 characters looked up in texts from 1,024 to 16,384 characters. The backward search is flat: the same 16 rank queries whatever the text, because it never asks where in the text anything is. The binary search over a suffix array is not, and the gap between them grows. The unit is primitive operations; each structure's own primitive is counted in its own field, and no two of them are the same act. Both axes are logarithmic.10,000100characters of textprimitive operationssuffix array + textcounter array per symbolFM-index, plainFM-index, compressedevery query run with the text withheldEnglish-like
Fig. 3 The dial as this field first measured it: what the samples cost against what an occurrence costs to place. The curve does not care which representation of the transform sits underneath it.

That is not a repair, it is the trade the field has had since it opened, and on a repetitive collection it is a worse trade than usual. A pattern occurring in thirty-two documents has thirty-two occurrences, each needing its own walk — so the collection that makes the transform cheap is the collection that makes locate expensive, because repetition is exactly what multiplies the occurrence count.

The two costs move in the same direction and it is the wrong one.

The repair that works is a different sampling

Sample at the boundaries of the transform’s runs.

The observation behind it is that the suffix array is not arbitrary at a run boundary: within a run of the transform, consecutive rows have consecutive text positions offset by the same amount, so knowing the position at one end of a run gives every position in it for the cost of the walk within the run. Keep O(r)O(r) samples chosen that way and locate no longer needs n/sn/s of them.

That is the r-index, it is not built here, and the reason it is worth naming precisely rather than gesturing at is that it is the only part of this structure that would make the whole thing proportional to rr. Everything else already is.

What can be said from what is built: the term it removes is 50% of this structure at thirty-two copies and 4% at one, so the construction’s value is a function of the repetition and not of the size — the same statement that has been true of every part of this strand.

Why the r-sized terms grew at all

The 45% growth in the parts proportional to rr deserves an explanation, because rr did not move and a term proportional to a constant should be constant.

It is the widths. Every part of the transform’s representation stores positions, and a position in a longer collection needs more bits:

copies nn bits for a position rr-sized bits
2 1,024 10 4,117
8 4,096 12 5,053
32 16,384 14 5,989

Four extra bits per position across the sweep, and the structure holds each run’s position twice — once in the run-start vector and once in that character’s run-length vector — so the growth should be 224×4×2=1,792224 \times 4 \times 2 = 1{,}792 bits. Measured: 1,872.

Better, as a slope. Per run, the rr-sized bits go from 18.4 to 26.7 while log2(n/r)\log_2(n/r) goes from 2.2 to 6.2:

26.718.46.22.2=2.08\frac{26.7 - 18.4}{6.2 - 2.2} = 2.08

against a predicted 2, one for each of the two width-bearing structures. That is the log(n/r)\log(n/r) in the published bound, fitted rather than quoted, and the departure from the integer is the ceilings in the two widths rounding at different points.

Stated that way it also answers what a run-length index actually is. It is not O(r)O(r) and was never claimed to be: it is O(rlog(n/r))O(r\log(n/r)), and the logarithm is the width of a position rather than anything about how the runs are stored. The constant part of a run’s cost — its head, its share of the C table — does not move at all across this sweep, and the part that does is exactly the part with an nn inside it.

Which n-sized terms are avoidable and which are not

Three of them, and they are not the same kind of thing.

The sampled positions are avoidable, by the construction above, at the cost of a different and more intricate structure.

The sample marks are avoidable more cheaply and are not avoided here. A vector of nn bits with n/sn/s ones is stored plainly in this structure and could be stored the way the run starts are — log2s+2\lceil\log_2 s\rceil + 2 bits per one — which at s=64s = 64 would take 2,056 bits to about 1,570. That is a real 4% of the structure left on the floor for the sake of one representation being used in one place and a different one in another, and it is recorded rather than fixed because fixing it would change the plate this essay is about without changing its conclusion.

And the logn\log n in every width is not avoidable at all. A position in a text of nn characters needs log2n\lceil\log_2 n\rceil bits whatever else is true, so even a structure with O(r)O(r) samples has an nn inside it, under a logarithm. That is where the log(n/r)\log(n/r) in every published bound for this family comes from, and it is the one term nothing removes.

Bits per character, as the same text is repeatedEnglish-like of 512 characters, repeated up to 32 times at divergence 0, indexed three ways at a sampling rate of one in 64. The plain FM-index is proportional to the length. The compressed one follows the entropy, which does not fall, so it is proportional to the length too. The run-length index is 1.1x the compressed one at a single copy and 0.4x it at 32, and per character it falls from 7.42 to 0.73 bits while the other two are flat. Both axes are logarithmic.1,00010,0001characters in the collectionbits per characterFM-index, plainFM-index, compressedrun-length indexsample one in 64 · divergence 0 · r = 224per character
Fig. 4 The per-character cost of the three structures. The run-length index’s curve is flattening at the right-hand end rather than continuing to fall, and the flattening is the sampling arriving.

Where the sampling rate should actually be set

The table above is a trade and this collection’s habit is to say what decides it rather than to draw the curve and stop.

The samples cost ns(log2n+c)\frac{n}{s}(\lceil\log_2 n\rceil + c) bits and buy a locate walk of at most ss LF steps per occurrence. So the question is what a bit is worth against an LF step, and the answer depends on something outside the structure entirely: how many occurrences a typical query has.

On a repetitive collection that number is not small. A pattern present in one document of thirty-two copies is present in all thirty-two, so the occurrence count is multiplied by the same factor that made the transform cheap. A query returning 32 occurrences at s=1,024s = 1{,}024 costs up to 32,736 LF steps to place them — against a structure of 6,448 bits, which is to say the walk is five times the size of the entire index in operations.

That is the shape of the trade nobody states: on the collections this family of structures exists for, the sampling rate cannot be turned down, because the same repetition that makes the index small makes the walks numerous. The r-index’s run-boundary sampling is not an optimisation of the parameter — it is the only way out of a corner the parameter cannot leave.

And it explains the direction of the whole family’s development. The transform’s representation was solved first because it was the term in the headline; the sampling took longer because it is the term that only becomes visible once the first one is small.

And there is a second one, if the index has to extract

Everything above counts the sampling that locate needs. An index that can also extract from the middle of the text carries a second array of the same size, indexed the other way round — and it is proportional to nn for exactly the same reason.

at 32 copies bits share
proportional to rr 5,989 38%
row-to-position sampling 5,911 38%
position-to-row sampling 3,855 24%

Two thirds of a structure whose headline is that its size is a function of the run count.

The two arrays are not redundant and neither can be derived from the other in a query’s time. They answer inverse questions and the inverse of a sampled array is not a sampled array — knowing the positions of every sixty-fourth row says nothing about which row a given position sits in unless that position happens to be one of them.

So the honest description of a run-length self-index supporting counting, locating and extraction is a structure with one term proportional to rr and two terms proportional to nn, of which the second and third are the majority on any collection repetitive enough to justify the first. That sentence appears in no statement of the family’s size, and it is arithmetic rather than a measurement — every quantity in it is in the definitions.

The general shape, which this field keeps meeting

Four times now, in four fields, the same thing.

A windowed cardinality estimator’s registers are its payload and its timestamps are 2,392 bits of 4,592the clock is more than half the structure and no accuracy parameter touches it.

A compressed self-index is “nHk+o(n)nH_k + o(n)” and the o(n)o(n) is a quarter to three-fifths of it.

A run-length index is “O(rlog(n/r))O(r\log(n/r))” and the sampling is half of it.

And in every one of the three, the term that dominates is the one that is not what the structure is about — the clock rather than the count, the directory rather than the payload, the answer to “where” rather than the answer to “what”. A structure is described by the term its designers found interesting, and measured by the term nobody wrote down.

The remedy is the same in all three cases and it is a habit rather than a technique: every size reported here names its parts and the parts are required to sum to it. A total cannot show that one of its terms is the majority of it, and a description in asymptotic notation cannot either, because the whole function of the notation is to drop the term that turned out to matter.

FM-index, compressed bit vectors: 36,804 bits, and where they goThe same index the other plates weigh, opened up. The payload is 21,452 bits, 58% of the structure, and the rest is directories, sample marks and sampled positions. That is the o(n) which every statement of the form "n times the entropy, plus o(n)" carries and no statement of it quantifies. At n = 16,384 it is 42% of the index.wavelet tree21,45258%rank directories9,50626%sampled positions3,85510%sample marks1,4414%C table5501%one unit = one bit · 16,384 characters, sigma = 212.25 bits/char
Fig. 5 The entropy-bounded index opened up on ordinary text, where the same lesson was learned from the other side. The sampled positions are a band on this plate too, and there they are a fifteenth rather than a half.

The occurrences the query did not want

The corner the sampling rate cannot leave has an exit, and it is not a better structure. It is a better question.

A pattern present in one document of thirty-two near-identical copies is present in all thirty-two, and locate dutifully places every one — thirty-two walks, thirty-two positions, thirty-two lines of output that say the same thing about thirty-two copies of the same passage. A reader searching a version history for a phrase does not want thirty-two positions. They want to know which revisions contain it, or that it has been there since revision four, or simply that it is present.

The multiplication that makes locate expensive on a repetitive collection is a multiplication of answers nobody asked for. That is worth separating from the structure’s cost, because it changes what the fix should be. Turning the sampling rate down attacks the price per walk; asking a different question removes most of the walks.

The question that removes them is document listing — which documents contain this pattern — and it is a genuinely different problem with a genuinely different structure, not a filter applied to the occurrence list. Placing every occurrence and then taking the distinct documents is the expensive thing being avoided, so an implementation that answers the cheap question by doing the expensive one and deduplicating has kept every cost this essay is about.

Two things follow that are worth holding together.

The repetitive collections that motivate this whole family of structures are also the collections on which the occurrence list is most redundant. A hundred genomes give a hundred hits for every conserved region; a thousand revisions give a thousand for every line that survived. The property that makes the index small is the property that makes its output repetitive, and both come from the same place.

And it is a second instance of the field’s standing complaint, arriving at the query side rather than the size side. The structure is described by the operation its designers found interesting — counting, which is cheap and beautiful — and is used through the operation nobody wrote down, which on this data is neither.

Three parts, and two of them are subsampled permutations

Standing back from the accounting, the structure has a shape worth naming, because it says where the family’s remaining work is.

The transform’s representation, which is proportional to rr and is what every essay in this strand has been about. Solved, in the sense that it does what it claims.

The row-to-position array, which is a suffix array with all but one entry in ss thrown away.

The position-to-row array, which is the inverse suffix array with the same entries thrown away.

So a self-index supporting all three operations is a compressed transform plus two subsampled permutations, and the compression work has been done on the first of the three. The second and third are stored exactly as a suffix array stores them — full-width integers at regular intervals — which is to say that the part of the structure this whole field was invented to replace is still present, at one entry in ss instead of one in one.

Read that way, the r-index’s contribution is precise: it is the first construction to make one of the two permutations proportional to the same quantity as the transform. One of three parts remains, and this collection has said what it is, what it costs, and that nothing here removes it.

What is not measured here

The r-index itself. Named, priced by what it would remove, not built. Its sampling is a genuinely different construction and its locate cost is a different curve, and nothing above says what that curve looks like.

A collection whose documents differ. The copies here are identical, which makes the nn-sized share as large as it can be. On a collection with real divergence rr is larger, so the rr-sized terms are larger, so the sampling’s share is smaller — and the sweep that would say by how much is not run.

Whether the r-index’s samples are enough for extraction. Run-boundary sampling is stated above as the repair for locate. Whether the same samples serve the position-to-row direction is a separate question with a separate answer, and nothing here addresses it.

What a query costs on this structure. Everything in this essay is bits. The three counted operations a rank performs here are the previous essay’s subject, and the interaction between them and the sampling rate is unmeasured.

Where a run-length index's bits go at 32 copies16,384 characters whose transform has 935 runs — the same 935 it had at two copies. The parts proportional to r total 18,309 bits, 4.2x what they were at two copies. The two marked parts are the sampling, which is proportional to n: 5,911 bits, 18.3x what they were, and 24% of the structure. A structure whose size is a function of the run count still has an n in it, and the n is the answer to "where", not to "how many" — which is what the run-boundary sampling of an r-index removes and this structure does not.run starts6,545grows with rrun lengths per symbol6,210grows with rrun heads5,029grows with rsampled positions3,855grows with nsample marks2,056grows with nC table525grows with r24,220 bits in total16,384 characters · r = 935 · sample one in 6424% is the sampling
Fig. 6 The parts of a collection whose copies differ in one position in a hundred. The run-sized bands grow and the sampling does not move, so the share this essay is about falls — which is the same claim read from the other end.
Three indexes over the same collection, as it repeatsEnglish-like of 512 characters, repeated up to 32 times at divergence 0, indexed three ways at a sampling rate of one in 256. The plain FM-index is proportional to the length. The compressed one follows the entropy, which does not fall, so it is proportional to the length too. The run-length index is 1.1x the compressed one at a single copy and 0.3x it at 32. Every structure answers identically and hands the text back on request. Both axes are logarithmic.1,00010,00010⁴10⁵characters in the collectionbitsFM-index, plainFM-index, compressedrun-length indexsample one in 256 · divergence 0 · r = 2247,614 bits at 32 copies
Fig. 7 And the whole race at one sample in 256, where the sampling is a tenth of the structure rather than half. Every conclusion of the previous essay survives it; the curve simply gets further from flat, and every occurrence costs four times as much to place.

Turning the dial once shows the direction; turning it twice shows whether the term ever actually goes away. It does not: at one sample in a thousand the sampling has fallen to a few per cent of the structure and the remaining nn-sized parts — the ones that are not the sampling — are what is left, which is the whole point of separating them.

Bits per character, as the same text is repeatedEnglish-like of 512 characters, repeated up to 32 times at divergence 0, indexed three ways at a sampling rate of one in 1024. The plain FM-index is proportional to the length. The compressed one follows the entropy, which does not fall, so it is proportional to the length too. The run-length index is 1.1x the compressed one at a single copy and 0.2x it at 32, and per character it falls from 7.14 to 0.39 bits while the other two are flat. Both axes are logarithmic.1,00010,0001characters in the collectionbits per characterFM-index, plainFM-index, compressedrun-length indexsample one in 1024 · divergence 0 · r = 224per character
Fig. 8 The race at one sample in 1,024, where the n-sized terms are seven per cent of the structure and the per-character curve keeps falling instead of flattening. Every occurrence now costs up to a thousand LF steps to place.

What this makes readable

Essays that name this one as a prerequisite.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

What links here

The 8 essays that link to this one and share the most of its objects, of 15 that link here.

The objects this essay names

Each one links to every other essay that touches it.

Asymptotic notationHonest limitLocateLower order termMeasurementRepetitionRun-lengthSamplingSelf-indexSpace overheadSuffix arrayTrade off