API reference¶
Memory¶
poincare_memory.store.memory.Memory ¶
Memory(
embedder=None,
curvature=1.0,
decay=None,
base_depth=1.0,
depth_step=0.5,
inherit=0.25,
cone_constrained=False,
cone_k=DEFAULT_K,
metric="shell",
time_coef=1.0,
depth_bias=0.0,
clock=None,
)
Hierarchy-aware, time-aware memory for agents and RAG.
Each memory gets a point in the Poincaré ball. Its direction comes from
the text embedding (optionally pulled toward its parent's direction); its
depth (hyperbolic distance from the origin) comes from its level in the
hierarchy you declare with parent=. Structural queries (subtree,
ancestors, siblings, cone) and the temporal policy are geometric
and explicit, and every memory can be inspected visually.
Scoring. search ranks memories by
-distance + depth_bias * depth + time_coef * log(time_weight).
With metric="shell" (default) distance is the hyperbolic distance
between the query and the memory after both are moved to the reference
shell at base_depth: it measures semantic (angular) mismatch in
hyperbolic units and is comparable across levels. With
metric="hyperbolic" it is the raw distance to the memory's actual point,
which strongly favours shallow, general memories (a root at depth 1 is close
to everything) and is the right choice only when the embedder itself is
hyperbolic (e.g. Hierarchy Transformers) or when generality is wanted. Use
depth_bias to prefer specific (> 0) or general (< 0) memories
explicitly.
Thread safety. All mutating and searching methods take an internal
re-entrant lock, so a Memory can be shared between threads; the
embedder you pass must be safe to call from several threads if you do.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embedder
|
Embedder | None
|
Anything with |
None
|
curvature
|
float
|
Positive curvature magnitude |
1.0
|
decay
|
DecayPolicy | None
|
Temporal policy; defaults to :class: |
None
|
base_depth
|
float
|
Hyperbolic radius of root memories and the extra radius per level. |
1.0
|
depth_step
|
float
|
Hyperbolic radius of root memories and the extra radius per level. |
1.0
|
inherit
|
float
|
How much of the parent's direction a child inherits (0 = none, 1 = all). |
0.25
|
cone_constrained
|
bool
|
Force children inside the parent's entailment cone. Makes |
False
|
cone_k
|
float
|
The |
DEFAULT_K
|
metric
|
Metric
|
Default distance for |
'shell'
|
time_coef
|
float
|
Score weights; see :func: |
1.0
|
depth_bias
|
float
|
Score weights; see :func: |
1.0
|
clock
|
Callable[[], datetime] | None
|
Callable returning "now"; override in tests or simulations. |
None
|
stats ¶
stats(at_time=None)
Summary numbers: counts, levels, depths, timestamps, mean temporal weight.
add ¶
add(
text,
parent=None,
t=None,
id=None,
meta=None,
valid_from=None,
valid_until=None,
)
Store a memory; returns its id.
add_many ¶
add_many(items)
Store many memories with one embedding call.
Each item is a dict of add keyword arguments; parents must precede
their children in the iteration order. Returns the ids in order.
update ¶
update(
id,
text=None,
meta=None,
t=None,
valid_from=_UNSET,
valid_until=_UNSET,
)
Change a memory in place. New text re-embeds it and re-places its subtree;
meta replaces the metadata dict; valid_from / valid_until may be set to None.
embed_query ¶
embed_query(query, depth=None)
Point for a free-text query (direction from the embedder, depth given or base_depth).
build_index ¶
build_index(backend='auto')
Build (or rebuild) the shell-metric nearest-neighbour index over all memories.
search ¶
search(
query=None,
k=5,
mode="nearest",
root=None,
at_time=None,
filters=None,
metric=None,
query_depth=None,
touch=True,
use_index=None,
oversample=8,
)
Rank memories by -distance + depth_bias*depth + time_coef*log(time_weight).
mode restricts the candidates: nearest (all), subtree/children/
ancestors/siblings of root, or cone (points inside the
entailment cone of root). query may be omitted for structural modes,
in which case candidates are ranked by time weight and depth only.
metric overrides the memory's default (see the class docstring);
query_depth only matters for metric="hyperbolic" (default base_depth).
use_index: with True (or None once :meth:build_index has been
called) a nearest + shell search first takes the k * oversample
closest memories from the index, then scores them exactly with the temporal
policy and filters. The index is rebuilt automatically after changes.
table ¶
table(at_time=None)
Plain rows: id, text, parent, level, depth, time, weight, accesses, meta.
to_dataframe ¶
to_dataframe(at_time=None)
pandas.DataFrame of :meth:table (pip install poincare-memory[pandas]).
inspect ¶
inspect(
path=None, open_browser=False, title=None, layout="auto"
)
Self-contained interactive HTML (disk, table, tree). Returns the HTML; writes it if path.
layout="structural" draws the stored hierarchy as a radial tree with exact
depths (default when parents exist); "projection" draws the semantic directions.
plot ¶
plot(ax=None, labels=False, at_time=None)
Matplotlib Poincaré-disk plot (pip install poincare-memory[viz]).
load
classmethod
¶
load(path, embedder=None, decay=None, clock=None)
Load a file written by :meth:save.
The hashing embedder and the built-in decay policies are restored
automatically; for other embedders pass the same embedder again.
to_json ¶
to_json(path=None, indent=None)
Portable JSON (config, records, points, embeddings). Returns the text; writes it if path.
from_json
classmethod
¶
from_json(source, embedder=None, decay=None, clock=None)
Inverse of :meth:to_json; source is a path or the JSON text itself.
poincare_memory.store.records.MemoryRecord
dataclass
¶
MemoryRecord(
id,
text,
parent_id=None,
t=utcnow(),
meta=dict(),
valid_from=None,
valid_until=None,
access_count=0,
last_accessed=None,
)
One stored memory. Positions live in the :class:~poincare_memory.Memory that owns it.
poincare_memory.store.records.SearchResult
dataclass
¶
SearchResult(
record,
score,
distance,
time_weight,
depth,
cone_energy=None,
)
Geometry¶
poincare_memory.geometry.poincare.PoincareBall
dataclass
¶
PoincareBall(curvature=1.0)
Poincaré ball of curvature -curvature.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
curvature
|
float
|
Positive number |
1.0
|
conformal_factor ¶
conformal_factor(x)
lambda_x = 2 / (1 - c ||x||^2), the metric's conformal factor.
mobius_scalar_mul ¶
mobius_scalar_mul(r, x)
Möbius scalar multiplication r ⊗_c x (scales hyperbolic distance from 0 by r).
distance ¶
distance(x, y)
Geodesic distance via the gyrovector formula.
d_c(x, y) = (2 / sqrt(c)) artanh(sqrt(c) || (-x) ⊕_c y ||).
distance_arcosh ¶
distance_arcosh(x, y)
Geodesic distance via the arcosh formula (paper eq. 3, generalised to curvature c).
d_c(x, y) = (1 / sqrt(c)) arcosh(1 + 2c ||x - y||^2 / ((1 - c||x||^2)(1 - c||y||^2))).
distance_to_origin ¶
distance_to_origin(x)
d_c(0, x) = (2 / sqrt(c)) artanh(sqrt(c) ||x||) — the depth coordinate.
pairwise_distance ¶
pairwise_distance(x, y)
All-pairs distances: x of shape (n, d), y of shape (m, d) → (n, m).
expmap ¶
expmap(x, v)
Exponential map at x: x ⊕_c (tanh(sqrt(c) λ_x ||v|| / 2) v / (sqrt(c) ||v||)).
point_at ¶
point_at(direction, hyperbolic_radius)
Point at hyperbolic distance hyperbolic_radius from the origin along direction.
set_depth ¶
set_depth(x, hyperbolic_radius)
Move x radially so that its distance from the origin equals hyperbolic_radius.
to_lorentz ¶
to_lorentz(x)
Map ball points to the hyperboloid {X : <X,X>_L = -1/c, X_0 > 0}.
Returns an array with one extra leading coordinate (the time coordinate).
egrad2rgrad ¶
egrad2rgrad(x, grad)
Rescale a Euclidean gradient to the Riemannian gradient: ((1 - c||x||^2)^2 / 4) grad.
retraction ¶
retraction(x, v)
First-order retraction proj(x + v) used by Riemannian SGD (Nickel & Kiela 2017).
random_points ¶
random_points(n, dim, max_hyperbolic_radius=2.0, seed=None)
Uniform-in-direction points with hyperbolic radius uniform in [0, max_hyperbolic_radius].
poincare_memory.geometry.lorentz ¶
Lorentz (hyperboloid) model helpers.
The hyperboloid of curvature -c is {X in R^{d+1} : <X, X>_L = -1/c, X_0 > 0}
with the Lorentzian inner product <X, Y>_L = -X_0 Y_0 + sum_i X_i Y_i.
Why it matters for retrieval: the geodesic distance is
d(X, Y) = (1/sqrt(c)) arcosh(-c <X, Y>_L), a decreasing function of the
Lorentzian inner product. Negating the time coordinate of every stored vector
turns <X, Y>_L into an ordinary dot product, so any maximum-inner-product
(dot-product) ANN index returns exact hyperbolic nearest neighbours. This is
the trick used by hyperbolic RAG systems and documented by vector-database
vendors; no custom metric is needed.
lorentz_distance ¶
lorentz_distance(X, Y, curvature=1.0)
Geodesic distance on the hyperboloid of curvature -curvature.
to_mips_vectors ¶
to_mips_vectors(X)
Negate the time coordinate so that dot(to_mips_vectors(X), Y) == <X, Y>_L.
Store to_mips_vectors(points) in a dot-product index and query with the
raw hyperboloid coordinates of the query point; the largest dot products are
the hyperbolic nearest neighbours.
mips_scores ¶
mips_scores(stored_mips, query)
Dot-product scores (larger = closer) between MIPS-prepared stored vectors and a query.
Embedders¶
poincare_memory.embed.base ¶
Embedder protocol and dependency-free implementations.
Embedder ¶
Bases: Protocol
Anything that turns texts into fixed-size Euclidean vectors.
HashingEmbedder
dataclass
¶
HashingEmbedder(dim=256, ngram_range=(3, 5), seed=0)
Deterministic, dependency-free text embedder (word + character n-gram hashing).
It is not a semantic model; it is a stable, fast stand-in that makes the
library usable offline and in tests. Swap in :class:SentenceTransformerEmbedder
or any callable for real semantics.
Algorithm (stable across versions and ports): lower-case the text; tokens are
[\w']+; features are "w:" + token and "c:" + ngram for character
n-grams of "#" + token + "#" with lengths in ngram_range; each feature is
hashed with keyed BLAKE2b (digest 8 bytes, key = 4-byte little-endian seed);
index = first 4 bytes as little-endian integer mod dim; sign = +1 if the
fifth byte is odd else -1; the row is L2-normalised.
CallableEmbedder
dataclass
¶
CallableEmbedder(fn, dim)
Wrap any fn(texts) -> array as an :class:Embedder.
SentenceTransformerEmbedder ¶
SentenceTransformerEmbedder(
model_name="sentence-transformers/all-MiniLM-L6-v2",
**kwargs,
)
Sentence-Transformers wrapper (pip install poincare-memory[embed]).
poincare_memory.embed.projection.BallProjector
dataclass
¶
BallProjector(ball)
Place L2-normalised Euclidean vectors in the ball at a chosen hyperbolic depth.
Hierarchy¶
poincare_memory.hierarchy.sarkar ¶
Training-free tree embedding by Sarkar's combinatorial construction.
Sarkar (2011) showed that any tree embeds in the hyperbolic plane with
arbitrarily low distortion: put the root at the origin, place its children on
a circle of hyperbolic radius tau at evenly spaced angles, then for every
child move it to the origin by an isometry, place its own children on a circle
around it while avoiding the direction of its parent, and move everything back.
Sala, De Sa, Gu & Ré (ICML 2018) analysed the construction and extended it to
higher dimensions by spreading child directions on a sphere.
This module implements both from the papers. Larger tau lowers distortion
but pushes deep nodes toward the boundary; :func:max_safe_depth tells you how
deep a tree can go before floating-point projection starts compressing it.
max_safe_depth ¶
max_safe_depth(ball, tau, dtype=np.float64)
Largest total hyperbolic radius representable before the ball margin clips points.
Returns the number of tau-length steps from the origin that stay strictly
inside (1 - eps) / sqrt(c).
spread_directions ¶
spread_directions(
k, dim, avoid=None, seed=0, iterations=200
)
k unit vectors in R^dim spread apart (and away from avoid if given).
In two dimensions the angles are exact: children fill the circle evenly, with the parent's direction occupying one slot. In higher dimensions a seeded repulsion on the sphere (a Thomson-style relaxation) spreads the directions.
sarkar_layout ¶
sarkar_layout(
edges,
ball=None,
dim=2,
tau=1.0,
edge_length=None,
root=None,
seed=0,
)
Embed a rooted tree given as (parent, child) edges.
Every child is placed at hyperbolic distance tau * edge_length[(parent, child)]
from its parent (edge_length defaults to 1). Returns node -> point.
A forest is handled by treating the listed root (or the single root) as
the origin; pass a virtual root edge set if you have several roots.
poincare_memory.hierarchy.rsgd ¶
Poincaré embeddings trained with Riemannian SGD (Nickel & Kiela, NeurIPS 2017), in NumPy.
Use this when the hierarchy is noisy, partial, or a DAG rather than a tree; for a
clean tree :func:~poincare_memory.hierarchy.sarkar.sarkar_layout is exact and
instantaneous. A Euclidean twin with the same loss is provided as a baseline for
benchmarks. Implemented from the paper; no third-party code was consulted.
fit_poincare_embeddings ¶
fit_poincare_embeddings(
edges,
dim=10,
epochs=100,
lr=0.1,
negatives=10,
burn_in=10,
burn_in_lr_factor=0.1,
batch_size=64,
seed=0,
ball=None,
)
Learn points for every node so related pairs are close in the Poincaré ball.
edges are (u, v) related pairs (typically the transitive closure of a
hierarchy). Returns node -> point (scaled to the curvature of ball)
and a training report.
fit_euclidean_embeddings ¶
fit_euclidean_embeddings(
edges,
dim=10,
epochs=100,
lr=0.05,
negatives=10,
batch_size=64,
seed=0,
)
Same loss and sampling as :func:fit_poincare_embeddings, Euclidean distance (baseline).
poincare_memory.hierarchy.evaluation ¶
Standard hierarchy-embedding evaluation: reconstruction MAP and mean rank.
reconstruction ¶
reconstruction(points, closure, distance)
Reconstruction protocol of Nickel & Kiela (2017).
For every node u with related nodes R(u): the mean rank of each
related node counts only unrelated nodes that are closer to u (so hub
nodes with many descendants are not penalised for their own descendants),
and the average precision is computed over the ranking of all other nodes.
poincare_memory.hierarchy.tree ¶
Small rooted-tree utilities shared by the layout and training code.
Temporal policies¶
poincare_memory.temporal.policies ¶
Named temporal weighting policies.
The paper describes "time-weighted embeddings" that favour recent information
while keeping established knowledge reachable. Here that idea is a pluggable,
testable policy that maps a memory's age (and optionally its access history or
validity window) to a weight in (0, 1]. Weights enter the retrieval score
as time_coef * log(weight), so a weight of 1 is neutral.
Forms implemented from the literature:
- exponential half-life (the 0.995^hours recency of Generative Agents,
Park et al. 2023, is HalfLife(hours=138.3));
- Ebbinghaus forgetting exp(-t / S) with stability that grows with
accesses (MemoryBank, Zhong et al. 2024);
- validity windows (bi-temporal facts as in Graphiti/Zep).
DecayPolicy ¶
Bases: Protocol
weight ¶
weight(now, times, access_counts, valid_from, valid_until)
Weights in [MIN_WEIGHT, 1] for each memory.
HalfLife
dataclass
¶
HalfLife(hours=24 * 30)
w = 0.5 ** (age_hours / hours); memories older than the future (negative age) get weight 1.
Ebbinghaus
dataclass
¶
Ebbinghaus(stability_hours=24 * 7, boost_per_access=0.5)
w = exp(-age_hours / S) with S = stability_hours * (1 + boost_per_access * accesses).
ValidityWindow
dataclass
¶
ValidityWindow(outside_weight=MIN_WEIGHT)
Weight 1 while valid_from <= now <= valid_until, outside_weight otherwise.
Missing bounds are open. Combine with another policy via :class:Product.
Product
dataclass
¶
Product(*policies)
Elementwise product of several policies (e.g. Product(HalfLife(72), ValidityWindow())).
generative_agents_recency ¶
generative_agents_recency()
The 0.995 ** hours recency used by Generative Agents, as a half-life (≈138.3 h).
Retrieval¶
poincare_memory.retrieval.scoring ¶
Combining geometry and time into one retrieval score.
combine ¶
combine(
distances,
time_weights=None,
depths=None,
time_coef=1.0,
depth_bias=0.0,
)
score = -distance + depth_bias * depth + time_coef * log(time_weight).
Larger is better. depth_bias > 0 prefers specific (deep) memories,
< 0 prefers general ones; time_coef scales how strongly the temporal
policy counts (1.0 means a half-life halving of the weight costs ln 2
in score, the same as moving ln 2 ≈ 0.69 further away in the ball).
poincare_memory.retrieval.cones ¶
Hyperbolic entailment cones (Ganea, Bécigneul & Hofmann, ICML 2018).
Every point x in the ball (outside a small central region of radius K)
owns a geodesically convex cone opening toward the boundary. "y lies in the
cone of x" is the geometric form of "x entails / is more general than
y". With depth encoded by the norm, cones give ancestor and descendant
queries that plain nearest-neighbour search cannot express.
Formulas (unit ball; points are scaled by sqrt(c) first):
half-aperture psi(x) = arcsin(K (1 - ||x||^2) / ||x||)
angle at x Xi(x, y) = arccos( (
cone_energy ¶
cone_energy(x, y, ball, K=DEFAULT_K)
max(0, Xi(x, y) - psi(x)); zero when y is inside the entailment cone of x.
Index¶
poincare_memory.index ¶
Nearest-neighbour indexes for the reference-shell metric.
Semantic search in :class:~poincare_memory.Memory compares the query with every
memory on the root shell. For large stores that comparison is offloaded to a
maximum-inner-product index: shell points are mapped to the Lorentz model and
their time coordinate negated (:func:~poincare_memory.geometry.to_mips_vectors),
after which the largest dot products are exactly the hyperbolic nearest
neighbours. Two backends:
numpy: exact, vectorised, fine to a few hundred thousand points;faiss:IndexHNSWFlatwith inner-product metric (pip install poincare-memory[ann]), approximate, for millions of points.
MIPSIndex ¶
MIPSIndex(ball, backend='auto', hnsw_m=32, ef_search=64)
Visualisation¶
poincare_memory.viz.export ¶
Export a memory as JSON / NetworkX and project points to the 2-D disk for drawing.
disk_coordinates ¶
disk_coordinates(points, ball)
2-D Poincaré-disk coordinates for drawing.
In 2-D the points are returned unchanged (scaled to the unit disk). In higher dimensions the directions are reduced with an uncentred SVD of the tangent vectors at the origin (so the origin stays the origin) and every point keeps its exact hyperbolic depth; angles are approximate, depths are not.
structural_coordinates ¶
structural_coordinates(memory)
2-D disk coordinates for inspection: angles from the hierarchy, radii from the true depths.
Every memory keeps its exact hyperbolic distance from the origin (the paper's depth coordinate); its angle comes from a radial tree layout in which each subtree owns an angular wedge proportional to its number of leaves, so siblings spread evenly and subtrees do not overlap. This is a drawing, not an isometric embedding: use the real points for distances.
to_dict ¶
to_dict(memory, at_time=None, layout='auto')
Serialisable snapshot. layout: "structural" (tree layout), "projection" (semantic
directions), or "auto" (structural when any parent links exist).
poincare_memory.viz.html.render_html ¶
render_html(
memory, title=None, at_time=None, layout="auto"
)