Skip to content

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 dim and embed(texts); defaults to the offline :class:~poincare_memory.embed.HashingEmbedder.

None
curvature float

Positive curvature magnitude c of the ball.

1.0
decay DecayPolicy | None

Temporal policy; defaults to :class:~poincare_memory.temporal.NoDecay.

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 cone queries exact for the declared hierarchy but collapses semantic directions onto a few rays; off by default.

False
cone_k float

The K constant of entailment cones (Ganea et al. 2018).

DEFAULT_K
metric Metric

Default distance for search; see above.

'shell'
time_coef float

Score weights; see :func:poincare_memory.retrieval.combine.

1.0
depth_bias float

Score weights; see :func:poincare_memory.retrieval.combine.

1.0
clock Callable[[], datetime] | None

Callable returning "now"; override in tests or simulations.

None

points property

points

All points as an (n, dim) array in insertion order.

ancestors

ancestors(id)

Ancestors from the nearest parent up to the root.

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.

link(child, parent)

Re-parent child (and re-place its subtree).

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]).

tree

tree(root=None, max_text=60)

ASCII tree of the hierarchy (all roots, or below root).

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]).

save

save(path)

Persist records, points and configuration to a SQLite file.

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 c; the space has constant sectional curvature -c and the ball has Euclidean radius 1/sqrt(c).

1.0

radius property

radius

Euclidean radius of the ball.

project

project(x)

Clip points to lie strictly inside the ball (dtype-dependent margin).

is_inside

is_inside(x)

Boolean mask: which points lie inside the open ball.

conformal_factor

conformal_factor(x)

lambda_x = 2 / (1 - c ||x||^2), the metric's conformal factor.

mobius_add

mobius_add(x, y)

Möbius addition x ⊕_c y (non-commutative, non-associative).

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).

expmap0

expmap0(v)

Exponential map at the origin: tangent vector → point in the ball.

logmap0

logmap0(y)

Logarithmic map at the origin: point in the ball → tangent vector.

expmap

expmap(x, v)

Exponential map at x: x ⊕_c (tanh(sqrt(c) λ_x ||v|| / 2) v / (sqrt(c) ||v||)).

logmap

logmap(x, y)

Logarithmic map at x (inverse of :meth:expmap).

geodesic

geodesic(x, y, t)

Point at fraction t along the geodesic from x (t=0) to y (t=1).

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).

from_lorentz

from_lorentz(X)

Inverse of :meth:to_lorentz.

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_inner

lorentz_inner(X, Y)

Lorentzian inner product along the last axis.

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.

embed

embed(texts)

Return an array of shape (len(texts), dim).

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.

at_depth

at_depth(X, depth)

Points with the direction of X and hyperbolic distance depth from the origin.

expmap0

expmap0(X, scale=1.0)

Alternative: exponential map of the (scaled) raw vectors; depth = 2 * scale * ||x|| (the conformal factor at the origin is 2).

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.

children_map

children_map(edges)

Build parent -> [children] from (parent, child) edges and return the roots.

Raises ValueError on cycles or on a node with two parents.

depths

depths(children, root)

Depth of every node below root (root has depth 0).

transitive_closure

transitive_closure(children, root)

All (ancestor, descendant) pairs below root.

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( ((1 + ||x||^2) - ||x||^2 (1 + ||y||^2)) / (||x|| ||x - y|| sqrt(1 + ||x||^2 ||y||^2 - 2)) ) energy E(x, y) = max(0, Xi(x, y) - psi(x)) (0 means y is inside the cone)

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: IndexHNSWFlat with inner-product metric (pip install poincare-memory[ann]), approximate, for millions of points.

MIPSIndex

MIPSIndex(ball, backend='auto', hnsw_m=32, ef_search=64)

Maximum-inner-product index over Lorentz-mapped points.

build

build(points)

Index points (ball coordinates, shape (n, dim)).

query

query(point, k)

Indices and inner-product scores of the k nearest stored points to a ball point.

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).

to_networkx

to_networkx(memory)

networkx.DiGraph with node attributes (requires networkx).

poincare_memory.viz.html.render_html

render_html(
    memory, title=None, at_time=None, layout="auto"
)

poincare_memory.viz.mpl.plot_memory

plot_memory(memory, ax=None, labels=False, at_time=None)