|
|
|
@ -0,0 +1,491 @@
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using ZeroLevel.Services.Serialization;
|
|
|
|
|
|
|
|
|
|
namespace ZeroLevel.HNSW.Services.OPT
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// NSW graph
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal sealed class OptLayer<TItem>
|
|
|
|
|
: IBinarySerializable
|
|
|
|
|
{
|
|
|
|
|
private readonly NSWOptions<TItem> _options;
|
|
|
|
|
private readonly VectorSet<TItem> _vectors;
|
|
|
|
|
private readonly CompactBiDirectionalLinksSet _links;
|
|
|
|
|
internal SortedList<long, float> Links => _links.Links;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// There are links е the layer
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal bool HasLinks => (_links.Count > 0);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// HNSW layer
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="options">HNSW graph options</param>
|
|
|
|
|
/// <param name="vectors">General vector set</param>
|
|
|
|
|
internal OptLayer(NSWOptions<TItem> options, VectorSet<TItem> vectors)
|
|
|
|
|
{
|
|
|
|
|
_options = options;
|
|
|
|
|
_vectors = vectors;
|
|
|
|
|
_links = new CompactBiDirectionalLinksSet();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Adding new bidirectional link
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="q">New node</param>
|
|
|
|
|
/// <param name="p">The node with which the connection will be made</param>
|
|
|
|
|
/// <param name="qpDistance"></param>
|
|
|
|
|
/// <param name="isMapLayer"></param>
|
|
|
|
|
internal void AddBidirectionallConnections(int q, int p, float qpDistance, bool isMapLayer)
|
|
|
|
|
{
|
|
|
|
|
// поиск в ширину ближайших узлов к найденному
|
|
|
|
|
var nearest = _links.FindLinksForId(p).ToArray();
|
|
|
|
|
// если у найденного узла максимальное количество связей
|
|
|
|
|
// if │eConn│ > Mmax // shrink connections of e
|
|
|
|
|
if (nearest.Length >= (isMapLayer ? _options.M * 2 : _options.M))
|
|
|
|
|
{
|
|
|
|
|
// ищем связь с самой большой дистанцией
|
|
|
|
|
float distance = nearest[0].Item3;
|
|
|
|
|
int index = 0;
|
|
|
|
|
for (int ni = 1; ni < nearest.Length; ni++)
|
|
|
|
|
{
|
|
|
|
|
// Если осталась ссылка узла на себя, удаляем ее в первую очередь
|
|
|
|
|
if (nearest[ni].Item1 == nearest[ni].Item2)
|
|
|
|
|
{
|
|
|
|
|
index = ni;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (nearest[ni].Item3 > distance)
|
|
|
|
|
{
|
|
|
|
|
index = ni;
|
|
|
|
|
distance = nearest[ni].Item3;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// делаем перелинковку вставляя новый узел между найденными
|
|
|
|
|
var id1 = nearest[index].Item1;
|
|
|
|
|
var id2 = nearest[index].Item2;
|
|
|
|
|
_links.Relink(id1, id2, q, qpDistance, _options.Distance(_vectors[id2], _vectors[q]));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (nearest.Length == 1 && nearest[0].Item1 == nearest[0].Item2)
|
|
|
|
|
{
|
|
|
|
|
// убираем связи на самих себя
|
|
|
|
|
var id1 = nearest[0].Item1;
|
|
|
|
|
var id2 = nearest[0].Item2;
|
|
|
|
|
_links.Relink(id1, id2, q, qpDistance, _options.Distance(_vectors[id2], _vectors[q]));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// добавляем связь нового узла к найденному
|
|
|
|
|
_links.Add(q, p, qpDistance);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Adding a node with a connection to itself
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="q"></param>
|
|
|
|
|
internal void Append(int q)
|
|
|
|
|
{
|
|
|
|
|
_links.Add(q, q, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Implementation of https://arxiv.org/ftp/arxiv/papers/1603/1603.09320.pdf
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Algorithm 2
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="q">query element</param>
|
|
|
|
|
/// <param name="ep">enter points ep</param>
|
|
|
|
|
/// <returns>Output: ef closest neighbors to q</returns>
|
|
|
|
|
internal void KNearestAtLayer(int entryPointId, Func<int, float> targetCosts, BinaryHeap W, int ef)
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* v ← ep // set of visited elements
|
|
|
|
|
* C ← ep // set of candidates
|
|
|
|
|
* W ← ep // dynamic list of found nearest neighbors
|
|
|
|
|
* while │C│ > 0
|
|
|
|
|
* c ← extract nearest element from C to q
|
|
|
|
|
* f ← get furthest element from W to q
|
|
|
|
|
* if distance(c, q) > distance(f, q)
|
|
|
|
|
* break // all elements in W are evaluated
|
|
|
|
|
* for each e ∈ neighbourhood(c) at layer lc // update C and W
|
|
|
|
|
* if e ∉ v
|
|
|
|
|
* v ← v ⋃ e
|
|
|
|
|
* f ← get furthest element from W to q
|
|
|
|
|
* if distance(e, q) < distance(f, q) or │W│ < ef
|
|
|
|
|
* C ← C ⋃ e
|
|
|
|
|
* W ← W ⋃ e
|
|
|
|
|
* if │W│ > ef
|
|
|
|
|
* remove furthest element from W to q
|
|
|
|
|
* return W
|
|
|
|
|
*/
|
|
|
|
|
var v = new VisitedBitSet(_vectors.Count, _options.M);
|
|
|
|
|
// v ← ep // set of visited elements
|
|
|
|
|
v.Add(entryPointId);
|
|
|
|
|
|
|
|
|
|
var d = targetCosts(entryPointId);
|
|
|
|
|
// C ← ep // set of candidates
|
|
|
|
|
var C = new BinaryHeap();
|
|
|
|
|
C.Push(entryPointId, d);
|
|
|
|
|
// W ← ep // dynamic list of found nearest neighbors
|
|
|
|
|
W.Push(entryPointId, d);
|
|
|
|
|
|
|
|
|
|
// run bfs
|
|
|
|
|
while (C.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
// get next candidate to check and expand
|
|
|
|
|
var toExpand = C.PopNearest();
|
|
|
|
|
var farthestResult = W.Farthest;
|
|
|
|
|
if (toExpand.Item2 > farthestResult.Item2)
|
|
|
|
|
{
|
|
|
|
|
// the closest candidate is farther than farthest result
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// expand candidate
|
|
|
|
|
var neighboursIds = GetNeighbors(toExpand.Item1).ToArray();
|
|
|
|
|
for (int i = 0; i < neighboursIds.Length; ++i)
|
|
|
|
|
{
|
|
|
|
|
int neighbourId = neighboursIds[i];
|
|
|
|
|
if (!v.Contains(neighbourId))
|
|
|
|
|
{
|
|
|
|
|
// enqueue perspective neighbours to expansion list
|
|
|
|
|
farthestResult = W.Farthest;
|
|
|
|
|
|
|
|
|
|
var neighbourDistance = targetCosts(neighbourId);
|
|
|
|
|
if (W.Count < ef || neighbourDistance < farthestResult.Item2)
|
|
|
|
|
{
|
|
|
|
|
C.Push(neighbourId, neighbourDistance);
|
|
|
|
|
W.Push(neighbourId, neighbourDistance);
|
|
|
|
|
if (W.Count > ef)
|
|
|
|
|
{
|
|
|
|
|
W.PopFarthest();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
v.Add(neighbourId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
C.Clear();
|
|
|
|
|
v.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Algorithm 2
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="q">query element</param>
|
|
|
|
|
/// <param name="ep">enter points ep</param>
|
|
|
|
|
/// <returns>Output: ef closest neighbors to q</returns>
|
|
|
|
|
internal void KNearestAtLayer(int entryPointId, Func<int, float> targetCosts, BinaryHeap W, int ef, SearchContext context)
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* v ← ep // set of visited elements
|
|
|
|
|
* C ← ep // set of candidates
|
|
|
|
|
* W ← ep // dynamic list of found nearest neighbors
|
|
|
|
|
* while │C│ > 0
|
|
|
|
|
* c ← extract nearest element from C to q
|
|
|
|
|
* f ← get furthest element from W to q
|
|
|
|
|
* if distance(c, q) > distance(f, q)
|
|
|
|
|
* break // all elements in W are evaluated
|
|
|
|
|
* for each e ∈ neighbourhood(c) at layer lc // update C and W
|
|
|
|
|
* if e ∉ v
|
|
|
|
|
* v ← v ⋃ e
|
|
|
|
|
* f ← get furthest element from W to q
|
|
|
|
|
* if distance(e, q) < distance(f, q) or │W│ < ef
|
|
|
|
|
* C ← C ⋃ e
|
|
|
|
|
* W ← W ⋃ e
|
|
|
|
|
* if │W│ > ef
|
|
|
|
|
* remove furthest element from W to q
|
|
|
|
|
* return W
|
|
|
|
|
*/
|
|
|
|
|
var v = new VisitedBitSet(_vectors.Count, _options.M);
|
|
|
|
|
// v ← ep // set of visited elements
|
|
|
|
|
v.Add(entryPointId);
|
|
|
|
|
// C ← ep // set of candidates
|
|
|
|
|
var C = new BinaryHeap();
|
|
|
|
|
var d = targetCosts(entryPointId);
|
|
|
|
|
C.Push(entryPointId, d);
|
|
|
|
|
// W ← ep // dynamic list of found nearest neighbors
|
|
|
|
|
if (context.IsActiveNode(entryPointId))
|
|
|
|
|
{
|
|
|
|
|
W.Push(entryPointId, d);
|
|
|
|
|
}
|
|
|
|
|
// run bfs
|
|
|
|
|
while (C.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
// get next candidate to check and expand
|
|
|
|
|
var toExpand = C.PopNearest();
|
|
|
|
|
if (W.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
if (toExpand.Item2 > W.Farthest.Item2)
|
|
|
|
|
{
|
|
|
|
|
// the closest candidate is farther than farthest result
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// expand candidate
|
|
|
|
|
var neighboursIds = GetNeighbors(toExpand.Item1).ToArray();
|
|
|
|
|
for (int i = 0; i < neighboursIds.Length; ++i)
|
|
|
|
|
{
|
|
|
|
|
int neighbourId = neighboursIds[i];
|
|
|
|
|
if (!v.Contains(neighbourId))
|
|
|
|
|
{
|
|
|
|
|
// enqueue perspective neighbours to expansion list
|
|
|
|
|
var neighbourDistance = targetCosts(neighbourId);
|
|
|
|
|
if (context.IsActiveNode(neighbourId))
|
|
|
|
|
{
|
|
|
|
|
if (W.Count < ef || (W.Count > 0 && neighbourDistance < W.Farthest.Item2))
|
|
|
|
|
{
|
|
|
|
|
W.Push(neighbourId, neighbourDistance);
|
|
|
|
|
if (W.Count > ef)
|
|
|
|
|
{
|
|
|
|
|
W.PopFarthest();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (W.Count < ef)
|
|
|
|
|
{
|
|
|
|
|
C.Push(neighbourId, neighbourDistance);
|
|
|
|
|
}
|
|
|
|
|
v.Add(neighbourId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
C.Clear();
|
|
|
|
|
v.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Algorithm 2, modified for LookAlike
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="q">query element</param>
|
|
|
|
|
/// <param name="ep">enter points ep</param>
|
|
|
|
|
/// <returns>Output: ef closest neighbors to q</returns>
|
|
|
|
|
internal void KNearestAtLayer(BinaryHeap W, int ef, SearchContext context)
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* v ← ep // set of visited elements
|
|
|
|
|
* C ← ep // set of candidates
|
|
|
|
|
* W ← ep // dynamic list of found nearest neighbors
|
|
|
|
|
* while │C│ > 0
|
|
|
|
|
* c ← extract nearest element from C to q
|
|
|
|
|
* f ← get furthest element from W to q
|
|
|
|
|
* if distance(c, q) > distance(f, q)
|
|
|
|
|
* break // all elements in W are evaluated
|
|
|
|
|
* for each e ∈ neighbourhood(c) at layer lc // update C and W
|
|
|
|
|
* if e ∉ v
|
|
|
|
|
* v ← v ⋃ e
|
|
|
|
|
* f ← get furthest element from W to q
|
|
|
|
|
* if distance(e, q) < distance(f, q) or │W│ < ef
|
|
|
|
|
* C ← C ⋃ e
|
|
|
|
|
* W ← W ⋃ e
|
|
|
|
|
* if │W│ > ef
|
|
|
|
|
* remove furthest element from W to q
|
|
|
|
|
* return W
|
|
|
|
|
*/
|
|
|
|
|
// v ← ep // set of visited elements
|
|
|
|
|
var v = new VisitedBitSet(_vectors.Count, _options.M);
|
|
|
|
|
// C ← ep // set of candidates
|
|
|
|
|
var C = new BinaryHeap();
|
|
|
|
|
foreach (var ep in context.EntryPoints)
|
|
|
|
|
{
|
|
|
|
|
var neighboursIds = GetNeighbors(ep).ToArray();
|
|
|
|
|
for (int i = 0; i < neighboursIds.Length; ++i)
|
|
|
|
|
{
|
|
|
|
|
C.Push(ep, _links.Distance(ep, neighboursIds[i]));
|
|
|
|
|
}
|
|
|
|
|
v.Add(ep);
|
|
|
|
|
}
|
|
|
|
|
// W ← ep // dynamic list of found nearest neighbors
|
|
|
|
|
// run bfs
|
|
|
|
|
while (C.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
// get next candidate to check and expand
|
|
|
|
|
var toExpand = C.PopNearest();
|
|
|
|
|
if (W.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
if (toExpand.Item2 > W.Farthest.Item2)
|
|
|
|
|
{
|
|
|
|
|
// the closest candidate is farther than farthest result
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (context.IsActiveNode(toExpand.Item1))
|
|
|
|
|
{
|
|
|
|
|
if (W.Count < ef || W.Count == 0 || (W.Count > 0 && toExpand.Item2 < W.Farthest.Item2))
|
|
|
|
|
{
|
|
|
|
|
W.Push(toExpand.Item1, toExpand.Item2);
|
|
|
|
|
if (W.Count > ef)
|
|
|
|
|
{
|
|
|
|
|
W.PopFarthest();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (W.Count > ef)
|
|
|
|
|
{
|
|
|
|
|
while (W.Count > ef)
|
|
|
|
|
{
|
|
|
|
|
W.PopFarthest();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
foreach (var c in W)
|
|
|
|
|
{
|
|
|
|
|
C.Push(c.Item1, c.Item2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
while (C.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
// get next candidate to check and expand
|
|
|
|
|
var toExpand = C.PopNearest();
|
|
|
|
|
// expand candidate
|
|
|
|
|
var neighboursIds = GetNeighbors(toExpand.Item1).ToArray();
|
|
|
|
|
for (int i = 0; i < neighboursIds.Length; ++i)
|
|
|
|
|
{
|
|
|
|
|
int neighbourId = neighboursIds[i];
|
|
|
|
|
if (!v.Contains(neighbourId))
|
|
|
|
|
{
|
|
|
|
|
// enqueue perspective neighbours to expansion list
|
|
|
|
|
var neighbourDistance = _links.Distance(toExpand.Item1, neighbourId);
|
|
|
|
|
if (context.IsActiveNode(neighbourId))
|
|
|
|
|
{
|
|
|
|
|
if (W.Count < ef || (W.Count > 0 && neighbourDistance < W.Farthest.Item2))
|
|
|
|
|
{
|
|
|
|
|
W.Push(neighbourId, neighbourDistance);
|
|
|
|
|
if (W.Count > ef)
|
|
|
|
|
{
|
|
|
|
|
W.PopFarthest();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (W.Count < ef)
|
|
|
|
|
{
|
|
|
|
|
C.Push(neighbourId, neighbourDistance);
|
|
|
|
|
}
|
|
|
|
|
v.Add(neighbourId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
C.Clear();
|
|
|
|
|
v.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Algorithm 3
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal BinaryHeap SELECT_NEIGHBORS_SIMPLE(BinaryHeap W, int M)
|
|
|
|
|
{
|
|
|
|
|
var bestN = M;
|
|
|
|
|
if (W.Count > bestN)
|
|
|
|
|
{
|
|
|
|
|
while (W.Count > bestN)
|
|
|
|
|
{
|
|
|
|
|
W.PopFarthest();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return W;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Algorithm 4
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="q">base element</param>
|
|
|
|
|
/// <param name="C">candidate elements</param>
|
|
|
|
|
/// <param name="extendCandidates">flag indicating whether or not to extend candidate list</param>
|
|
|
|
|
/// <param name="keepPrunedConnections">flag indicating whether or not to add discarded elements</param>
|
|
|
|
|
/// <returns>Output: M elements selected by the heuristic</returns>
|
|
|
|
|
internal BinaryHeap SELECT_NEIGHBORS_HEURISTIC(Func<int, float> distance, BinaryHeap W, int M)
|
|
|
|
|
{
|
|
|
|
|
// R ← ∅
|
|
|
|
|
var R = new BinaryHeap();
|
|
|
|
|
// W ← C // working queue for the candidates
|
|
|
|
|
// if extendCandidates // extend candidates by their neighbors
|
|
|
|
|
if (_options.ExpandBestSelection)
|
|
|
|
|
{
|
|
|
|
|
var extendBuffer = new HashSet<int>();
|
|
|
|
|
// for each e ∈ C
|
|
|
|
|
foreach (var e in W)
|
|
|
|
|
{
|
|
|
|
|
var neighbors = GetNeighbors(e.Item1);
|
|
|
|
|
// for each e_adj ∈ neighbourhood(e) at layer lc
|
|
|
|
|
foreach (var e_adj in neighbors)
|
|
|
|
|
{
|
|
|
|
|
// if eadj ∉ W
|
|
|
|
|
if (extendBuffer.Contains(e_adj) == false)
|
|
|
|
|
{
|
|
|
|
|
extendBuffer.Add(e_adj);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// W ← W ⋃ eadj
|
|
|
|
|
foreach (var id in extendBuffer)
|
|
|
|
|
{
|
|
|
|
|
W.Push(id, distance(id));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wd ← ∅ // queue for the discarded candidates
|
|
|
|
|
var Wd = new BinaryHeap();
|
|
|
|
|
// while │W│ > 0 and │R│< M
|
|
|
|
|
while (W.Count > 0 && R.Count < M)
|
|
|
|
|
{
|
|
|
|
|
// e ← extract nearest element from W to q
|
|
|
|
|
var (e, ed) = W.PopNearest();
|
|
|
|
|
var (fe, fd) = R.PopFarthest();
|
|
|
|
|
|
|
|
|
|
// if e is closer to q compared to any element from R
|
|
|
|
|
if (R.Count == 0 ||
|
|
|
|
|
ed < fd)
|
|
|
|
|
{
|
|
|
|
|
// R ← R ⋃ e
|
|
|
|
|
R.Push(e, ed);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Wd ← Wd ⋃ e
|
|
|
|
|
Wd.Push(e, ed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// if keepPrunedConnections // add some of the discarded // connections from Wd
|
|
|
|
|
if (_options.KeepPrunedConnections)
|
|
|
|
|
{
|
|
|
|
|
// while │Wd│> 0 and │R│< M
|
|
|
|
|
while (Wd.Count > 0 && R.Count < M)
|
|
|
|
|
{
|
|
|
|
|
// R ← R ⋃ extract nearest element from Wd to q
|
|
|
|
|
var nearest = Wd.PopNearest();
|
|
|
|
|
R.Push(nearest.Item1, nearest.Item2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// return R
|
|
|
|
|
return R;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
private IEnumerable<int> GetNeighbors(int id) => _links.FindLinksForId(id).Select(d => d.Item2);
|
|
|
|
|
|
|
|
|
|
public void Serialize(IBinaryWriter writer)
|
|
|
|
|
{
|
|
|
|
|
_links.Serialize(writer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Deserialize(IBinaryReader reader)
|
|
|
|
|
{
|
|
|
|
|
_links.Deserialize(reader);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal Histogram GetHistogram(HistogramMode mode) => _links.CalculateHistogram(mode);
|
|
|
|
|
}
|
|
|
|
|
}
|