Skip to content

Commit 3386948

Browse files
marius-bughiuclaude
andcommitted
Merge branch 'main' into chore/code-review/argsort-alias-doc
Resolves the CHANGELOG conflict: main's PartialSort.TopK overlap-guard entry and this branch's RadixSort.ArgSort doc entry were both added at the head of the Unreleased "Fixed" section. Both are kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 parents 25f7cd4 + 89026ce commit 3386948

29 files changed

Lines changed: 2123 additions & 10 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ All notable changes to Celerity are documented here. This project follows [Keep
66

77
### Added
88

9+
- **`SegmentTree<T, TMonoid>`** in `Celerity.Collections` — range aggregates over any **associative** fold, with point update and range query both `O(log n)`. Answers the range min / max / gcd / bitwise questions `FenwickTree<T>` structurally cannot, since a Fenwick range is the *difference* of two prefix folds and so needs an inverse. Against the `O(n)` scan the BCL leaves you with: **14.8x** at 100k elements on interleaved update + range-min, **81x** on a query batch, **1.4x** at 1k. Range *updates* are not supported. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
10+
- **`IMonoid<T>`** with `SumMonoid<T>`, `MinMonoid<T>`, `MaxMonoid<T>`, `BitwiseAndMonoid<T>` and `BitwiseOrMonoid<T>` — taken as a `struct` type parameter, like the hashers, so `Combine` inlines. `MinMonoid` / `MaxMonoid` document a finite-values domain for floating-point `T`. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
11+
- `SegmentTreeTests`, `SegmentTreeDifferentialTests` and `MonoidTests`, including an exhaustive length × range sweep under a **non-commutative** fold — the only kind that can catch the `2n` layout mis-ordering a combine. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
12+
- Cross-collection rows in `ClearNoOpVersionTests` (a third fixed-length exception, alongside `BitSet` and `FenwickTree`), `EnumeratorInvalidationAndClearCoverageTests` and `OversizedSourceAndResidualGuardTests`. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
13+
- A `SegmentTree` `Celerity.Fuzz` target and Native AOT smoke coverage over three monoid instantiations. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
14+
- `SegmentTreeBenchmark` in the CI-tracked suite, with its dashboard cards and landing-page ship card. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
15+
- API-reference and README sections for `SegmentTree` and `IMonoid`, correcting `FenwickTree`'s docs, which said a segment tree was "not shipped". Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
916
- **`Celerity.Sorting`** — a new package with the non-comparison sorts the BCL has no path for: `RadixSort` (LSD radix over `uint` / `int` / `ulong` / `long` / `float` / `double`, in keys-only, key+payload and `ArgSort` forms, **stable**), `CountingSort` (bounded `byte` / `ushort` / declared-`[min, max]` `int` ranges, **stable**) and `PartialSort` (`O(n)` introselect plus an `O(n log k)` `TopK`). `Array.Sort` is contractually in-place while radix needs `O(n)` scratch, so this is a gap the BCL cannot close. `SortWithScratch` overloads make `RadixSort` / `CountingSort` allocation-free; `PartialSort` allocates nothing in any form. Depends only on `Celerity.Primitives`. Closes [#309](https://github.com/marius-bughiu/Celerity/issues/309).
1017
- Crossovers are documented rather than claimed away: `RadixSort` loses below a few hundred elements, `CountingSort` loses once the key range approaches the element count, and `PartialSort` beats LINQ on allocation rather than asymptotics. `RadixSort` orders `NaN` by sign bit and `-0.0` before `+0.0`, where `Array.Sort` moves all NaNs to the front. Closes [#309](https://github.com/marius-bughiu/Celerity/issues/309).
1118
- Dedicated and differential tests, three `Celerity.Fuzz` targets reconciled against `Array.Sort`, Native AOT smoke coverage, `RadixSortBenchmark` / `CountingSortBenchmark` / `PartialSortBenchmark` in the CI-tracked suite with their dashboard cards, `docs/api/sorting.md`, and README entries. Closes [#309](https://github.com/marius-bughiu/Celerity/issues/309).
@@ -20,6 +27,7 @@ All notable changes to Celerity are documented here. This project follows [Keep
2027

2128
### Fixed
2229

30+
- `PartialSort.TopK` now throws `ArgumentException` when its `destination` overlaps its `source`, instead of silently returning a wrong answer and writing to the source it documents as untouched. Disjoint slices of one array are still accepted, matching `RadixSort` and `CountingSort`.
2331
- `RadixSort.ArgSort`'s XML docs promised an `ArgumentException` when `indices` shares storage with `keys`, but only the `int`-keyed overload can throw it — the aliasing check is a same-element-type test by design. The doc now says which overload it covers, and why the rest treat a reinterpreted alias as out of contract. Documentation only.
2432
- Eight documentation links pointed at anchors that do not exist: seven `CeleritySet` / `SwissSet` references in `docs/api/collections.md` and one in `CHANGELOG.md`. GitHub deletes `<`, `>` and `,` from a heading without substituting a separator, so `CeleritySet&lt;T, THasher&gt;` anchors as `#celeritysett-thasher`, not the `#celerityset-t-thasher` everyone writes. Closes [#339](https://github.com/marius-bughiu/Celerity/issues/339).
2533

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ The mutable sets (`CeleritySet`, `SwissSet`, `RobinHoodSet`, `HashCachingSet`, `
100100

101101
Both take their ordering as a **struct** `IComparer<T>` type parameter (`DefaultComparer<T>` by default), exactly as the hashers are struct type parameters, so the comparison inlines instead of costing a virtual call per key inspected inside a node.
102102

103-
**Prefix sums**
103+
**Range aggregates**
104104

105105
- `FenwickTree<T>` — a **Binary Indexed Tree** over a fixed-length numeric sequence (`where T : struct, INumber<T>`): **point update** and **prefix / range sum** both in `O(log n)`, in one flat array with no per-node overhead. The prefix-sum structure the BCL lacks — running aggregates, rank / order-statistics counters, cumulative-frequency tables — where a plain array is `O(n)` per query (recompute the slice) *or* `O(n)` per update (fix the suffix). Wins precisely when updates and partial-sum queries interleave.
106+
- `SegmentTree<T, TMonoid>` — range aggregates over an arbitrary **associative** fold: **point update** and **range query** both in `O(log n)`, in one flat array of `2n` cells. The half of the range-query space a Fenwick tree cannot reach — its query is the *difference* of two prefix folds, so it needs an inverse, while a segment tree stores each node's fold outright. That puts range **min**, **max**, **gcd**, bitwise **and**/**or** and any monoid you write in reach. The BCL has no range-aggregate structure at all, so the baseline is a plain array scanned per query. `SumMonoid` / `MinMonoid` / `MaxMonoid` / `BitwiseAndMonoid` / `BitwiseOrMonoid` ship built in, as struct type parameters so the fold inlines; non-commutative folds are safe, since the query preserves index order.
106107

107108
**Probabilistic & bit-level**
108109

@@ -555,6 +556,35 @@ Console.WriteLine(tree.Total); // 33
555556

556557
</details>
557558

559+
<details>
560+
<summary><b>Range min / max / any associative fold with live updates</b> — SegmentTree</summary>
561+
562+
`SegmentTree<T, TMonoid>` answers the aggregate of any half-open range under an arbitrary **associative** fold, with **point updates** and **range queries** both in `O(log n)`. It is the half of the range-query space `FenwickTree<T>` cannot reach: a Fenwick range query is the *difference* of two prefix sums, so the operation must have an inverse — minimum has none. The fold arrives as a **struct** type parameter, exactly like the hashers, so `Combine` inlines instead of costing a virtual call per level.
563+
564+
```csharp
565+
// A live order book: the cheapest ask in any price band, while prices keep moving.
566+
var book = new SegmentTree<long, MinMonoid<long>>(new long[] { 105, 102, 108, 101, 110, 103 });
567+
568+
Console.WriteLine(book.Query(0, 4)); // 101 — cheapest in the first band
569+
Console.WriteLine(book.Aggregate); // 101 — cheapest overall
570+
571+
book[3] = 999; // that order was filled, O(log n)
572+
Console.WriteLine(book.Query(0, 4)); // 102 — refolded
573+
574+
// Any monoid works. Write a struct with an Identity and an associative Combine:
575+
public readonly struct GcdMonoid : IMonoid<uint>
576+
{
577+
public uint Identity => 0; // gcd(0, a) == a
578+
public uint Combine(uint left, uint right)
579+
{
580+
while (right != 0) (left, right) = (right, left % right);
581+
return left;
582+
}
583+
}
584+
```
585+
586+
</details>
587+
558588
<details>
559589
<summary><b>Construct from an existing collection</b></summary>
560590

@@ -623,6 +653,7 @@ Each type buys a different tradeoff. Find your workload below; if it isn't here,
623653
| **Look a string key up from a `ReadOnlySpan<char>`** you already hold (route dispatch, header lookup, parse-then-map) without allocating a `string` per probe | span overloads on `FrozenCelerityDictionary` / `FrozenCeleritySet` / `CelerityDictionary<string, …>` / `CeleritySet<string, …>` / `Trie<TValue>` | `TryGetValue(ReadOnlySpan<char>, …)` / `ContainsKey` / `Contains` probe the table directly, deleting the `new string(span)` allocation and copy per lookup. Available whenever the hasher implements `ISpanHashProvider` — every built-in `String*Hasher` does. Same results as the `string` overloads (ordinal comparison); an empty span means `""`, never the `null` key. See [span-keyed lookups](docs/api/collections.md#span-keyed-lookups). |
624654
| **Sorted keys** — you need the entries in comparer order, or the ordered questions a hash table cannot answer: smallest / largest key, "first key at or after *x*", "every key in `[a, b)`" (time-series by timestamp, order books, LSM-style memtables, sweep-line events, interval endpoints) | `BTreeDictionary<TKey, TValue>` / `BTreeSet<T>` | B-tree with up to 31 keys per node in flat arrays: a lookup visits `log₃₂(n)` nodes instead of chasing `log₂(n)` pointers (~4 cache misses instead of ~20 at `n = 1M`), an in-order walk streams contiguous arrays rather than successor pointers, and allocation is one node per 31 entries instead of one object per entry. The BCL has no B-tree: `SortedDictionary<,>` / `SortedSet<>` are red-black trees, `SortedList<,>` is `O(n)` per middle insert, and `OrderedDictionary<,>` (.NET 9) is *insertion*-ordered, not sorted. Wins on the **interleaved insert + lookup + range-scan** load; for a few dozen entries a `SortedList<,>` is hard to beat, and if you never need order a hash table answers in `O(1)`. |
625655
| **Prefix / range sums over a sequence you keep mutating** — running aggregates, rank / order-statistics counters (inversions, "how many ≤ x seen"), cumulative-frequency tables | `FenwickTree<T>` | Binary Indexed Tree (`T : INumber<T>`): **point update** and **prefix / range sum** both `O(log n)`, in one array with no per-node overhead. The BCL has no prefix-sum structure; a plain array forces `O(n)` per query (recompute the slice) *or* `O(n)` per update (fix the suffix). Wins precisely when updates and partial-sum queries interleave. If the data is immutable after build, a one-shot precomputed prefix-sum array answers in `O(1)` with less code; if you only update and never query a partial sum, a raw array is simpler. |
656+
| **Range min / max / gcd / bit-mask over a sequence you keep mutating** — sliding-window extrema over a live history, "cheapest offer in this price band" over an order book, per-window capability masks, or any other **associative fold with no inverse** | `SegmentTree<T, TMonoid>` | Point update and range query both `O(log n)`, in one flat array of `2n` cells. `FenwickTree<T>` cannot answer these at all: it computes a range as the *difference* of two prefix folds, so the operation must be invertible. Five folds ship (`Sum` / `Min` / `Max` / `BitwiseAnd` / `BitwiseOr`) and any associative one you write is a field-free struct; non-commutative folds are safe. The BCL has no range-aggregate structure, so the alternative is an `O(n)` scan per query — **14.8× faster** on interleaved update + range-min at 100k, **81×** on a query batch, but only **1.4×** at 1k, where scanning a contiguous array is cache-friendly. If the fold is addition use `FenwickTree<T>` (half the memory); if the sequence never changes after build, a sparse table or a prefix array answers in `O(1)`. Range *updates* are not supported — that needs lazy propagation, a different contract. |
626657
| **Set algebra over two lists you already hold in sorted order** — intersect / union / diff sorted ID, row-id or posting lists, or just ask how many values they share (inverted indexes, cohort intersection, join-key pre-filters) | `SortedSpan.Intersect` / `Union` / `Except` / `IntersectCount` / `Overlaps` (in `Celerity.Primitives`) | Not a collection — static set algebra over spans. A two-cursor merge exploits the ordering the data already has, so it touches each element once and writes into caller-owned memory: **4.2× faster than `HashSet<int>` at 1M × 1M and 0 bytes allocated against 17.9 MB**, and **257× faster** on the asymmetric 1k × 10M shape where it gallops. `IntersectCount` / `Overlaps` need no buffer at all. ⚠️ **Both spans must be sorted ascending** — unsorted input silently returns a wrong answer. If your data is not already sorted, sorting it first to use this is usually a loss; reach for a set instead. See [sorted-span set algebra](docs/api/utilities.md#sortedspan-sorted-span-set-algebra). |
627658
| Need a stable iteration order or multi-threaded access | `BTreeDictionary<,>` / `BTreeSet<>` for sorted order, `Trie<TValue>` for ordered string keys; BCL `ConcurrentDictionary<,>` for concurrency | Celerity is single-threaded, and the **hash-based** collections leave iteration order unspecified. The ordered collections do promise order by contract: the B-trees iterate in comparer order, `Trie<TValue>` in ascending ordinal key order. |
628659

0 commit comments

Comments
 (0)