Adaptive External Semantic Graph (AESG) - A persistent, self-organizing external memory layer for neural networks.
Project description
AESG — Adaptive External Semantic Graph
Version 0.3.0 · PyPI · GitHub · MIT License
AESG is a persistent external memory module for PyTorch models. It stores knowledge as a graph of concept nodes backed by memory-mapped files (mmap), which means the graph lives on disk and is accessed without full serialization into RAM. Retrieval works through spreading activation rather than nearest-neighbor search, propagating activation energy through the graph's edges.
It is designed to be attached to an existing architecture (Transformer, LSTM, CNN) as an nn.Module without modifying the base model's weights.
Installation
pip install aesg
Requirements: torch >= 1.10, numpy >= 1.20
How it works
When a query vector arrives, AESG:
- Finds the most relevant seed node in the graph
- Propagates activation energy outward through edges (spreading activation)
- Returns the activated subgraph as context
- Optionally creates a new node if the query is not well-explained by the current graph (novelty detection)
Periodically, a consolidation step ages all nodes, decays their relevance score, and prunes those that have not been activated above a configurable threshold.
Quick Start
from aesg.config import AESGConfig
from aesg.memory.controller import AESGMemory
import torch
config = AESGConfig(
vector_dim=128,
max_concepts=500_000,
spreading_activation_steps=3,
novelty_birth_threshold=3,
)
memory = AESGMemory(directory="./my_memory", config=config)
Usage Examples
Retrieving context from a query
import torch
from aesg.config import AESGConfig
from aesg.memory.controller import AESGMemory
config = AESGConfig(vector_dim=64, max_concepts=100_000)
memory = AESGMemory(directory="./memory_store", config=config)
query = torch.randn(64)
# Reset state between unrelated sequences
memory.reset_state()
context = memory.retrieve(query)
# context.concept_vectors: Tensor of activated concept embeddings
# context.activated_nodes: list of node indices
if context.concept_vectors is not None:
print(f"Retrieved {context.concept_vectors.shape[0]} concepts")
Attaching AESG to an LSTM
import torch
import torch.nn as nn
from aesg.config import AESGConfig
from aesg.memory.controller import AESGMemory
class LSTMWithMemory(nn.Module):
def __init__(self, input_dim=64, hidden_dim=64, output_dim=10):
super().__init__()
config = AESGConfig(vector_dim=hidden_dim, max_concepts=50_000)
self.memory = AESGMemory(directory="./lstm_memory", config=config)
self.lstm = nn.LSTM(input_dim, hidden_dim, batch_first=True)
self.head = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
# x: [B, T, input_dim]
self.memory.reset_state()
lstm_out, _ = self.lstm(x) # [B, T, H]
final_hidden = lstm_out[:, -1, :] # [B, H]
ctx = self.memory(final_hidden)
return self.head(final_hidden)
model = LSTMWithMemory()
x = torch.randn(4, 10, 64) # batch=4, seq_len=10
out = model(x)
print(out.shape) # [4, 10]
Attaching AESG to a Transformer encoder
import torch
import torch.nn as nn
from aesg.config import AESGConfig
from aesg.memory.controller import AESGMemory
class TransformerWithMemory(nn.Module):
def __init__(self, d_model=128, nhead=4, num_layers=2):
super().__init__()
encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead, batch_first=True)
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
config = AESGConfig(vector_dim=d_model, max_concepts=100_000)
self.memory = AESGMemory(directory="./transformer_memory", config=config)
self.projector = nn.Linear(d_model * 2, d_model)
def forward(self, src):
# src: [B, T, d_model]
encoded = self.encoder(src)
cls_token = encoded[:, 0, :] # Use first token as query
self.memory.reset_state()
ctx = self.memory(cls_token)
if ctx.concept_vectors is not None:
ctx_mean = ctx.concept_vectors.mean(dim=0, keepdim=True).expand(cls_token.size(0), -1)
output = self.projector(torch.cat([cls_token, ctx_mean], dim=1))
else:
output = cls_token
return output
model = TransformerWithMemory()
src = torch.randn(2, 8, 128)
out = model(src)
print(out.shape) # [2, 128]
Saving and loading the memory
from aesg.config import AESGConfig
from aesg.memory.controller import AESGMemory
# --- Session 1: build and save ---
memory = AESGMemory(directory="./persistent_memory", config=AESGConfig(vector_dim=64))
import torch
for _ in range(100):
memory.retrieve(torch.randn(64))
memory.save()
print(f"Saved {memory.storage.node_count} concepts")
# --- Session 2: reload and continue ---
memory2 = AESGMemory(directory="./persistent_memory")
print(f"Loaded {memory2.storage.node_count} concepts")
ctx = memory2.retrieve(torch.randn(64))
Triggering consolidation (pruning)
import torch
from aesg.config import AESGConfig
from aesg.memory.controller import AESGMemory
config = AESGConfig(
vector_dim=32,
max_concepts=1000,
survival_threshold_relevance=0.1, # Nodes below this relevance are pruned
survival_threshold_frequency=2, # Nodes accessed fewer than N times are pruned
)
memory = AESGMemory(directory="./mem_prune", config=config)
for i in range(3000):
memory.cognitive_engine.create_sensory_concept(torch.randn(32))
print(f"Before consolidation: {memory.storage.node_count} nodes")
memory.update_topology() # Runs consolidation + pruning
active = (memory.storage.nodes['is_active'][:memory.storage.node_count] == 1).sum()
print(f"Active after consolidation: {active}")
Reading the evolution log
from aesg.config import AESGConfig
from aesg.memory.controller import AESGMemory
import torch
memory = AESGMemory(directory="./mem_log", config=AESGConfig(vector_dim=16))
for _ in range(20):
memory.retrieve(torch.randn(16))
# Each event is a dict: timestamp, type, id1, id2, val
history = memory.logger.get_history()
for event in history[:5]:
print(event)
# {'timestamp': 1718728800, 'type': 'CREATE', 'id1': 4293871, 'id2': 0, 'val': 1.0}
Handling common errors
import torch
from aesg.config import AESGConfig
from aesg.memory.controller import AESGMemory
# Wrong vector dimension
config = AESGConfig(vector_dim=64)
memory = AESGMemory(directory="./mem_err", config=config)
try:
wrong_query = torch.randn(128) # dim mismatch
ctx = memory.retrieve(wrong_query)
except Exception as e:
print(f"Dimension mismatch: {e}")
# Safe pattern: always match query dim to config.vector_dim
query = torch.randn(config.vector_dim)
ctx = memory.retrieve(query)
# Loading from a non-existent directory (will initialize fresh)
memory_new = AESGMemory(directory="./brand_new_memory", config=config)
print(f"Fresh memory: {memory_new.storage.node_count} nodes") # 0
# Windows: always call memory.save() before deleting the directory,
# otherwise the mmap files remain locked by the process.
memory.save()
Configuration Reference
All parameters are set via AESGConfig. No values are hardcoded in the core.
from aesg.config import AESGConfig
config = AESGConfig(
# Core
vector_dim=256, # Embedding dimensionality
max_concepts=1_000_000, # Maximum active nodes
# Novelty detection
novelty_explanation_threshold=0.6, # Graph score below this triggers novelty check
novelty_birth_threshold=3, # How many times a novel input must persist to create a node
# Evolutionary pressure
survival_threshold_relevance=0.05, # Minimum relevance score to survive pruning
survival_threshold_frequency=5, # Minimum activation count to survive pruning
# Navigation
spreading_activation_steps=3, # Depth of activation propagation
spreading_activation_decay=0.8, # Energy decay per hop
region_facilitation_multiplier=1.5, # Bonus for intra-region traversal
)
Storage Format
The graph is stored as binary memory-mapped files in the specified directory:
my_memory/
├── nodes.aesg # Node records (mmap, struct array)
├── edges.aesg # Edge records (mmap, linked list)
├── meta.npy # Capacity and count metadata
└── evolution.aesglog # Binary event log (32 bytes/event)
Node struct fields: id, vector[D], created_at, modified_at, use_frequency, relevance, age, stability, region_id, is_active, head_edge_idx.
The graph does not need to be fully loaded into RAM — it is accessed via numpy.memmap with zero-copy reads.
Performance Notes
Benchmarks run on a single CPU core, 24 GB RAM, standard SSD, Windows 11. vector_dim=16 for insertion tests.
| Measurement | Result |
|---|---|
| Insertion throughput | ~1,600–1,940 nodes/s (pure Python loop) |
| Retrieve latency (1–8 hops) | 1.04–1.17 ms average |
| 1,000,000 nodes — RAM overhead | 81 MB |
| 1,000,000 nodes — disk size | 283 MB |
| Evolution log read (100k events) | 0.13 s |
Insertion throughput is bounded by the Python interpreter loop. The storage and retrieval layer itself has no algorithmic bottleneck for graph sizes in the millions.
License
MIT License — Copyright (c) 2026 bueormnew
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Author: bueormnew · dalusx64@gmail.com
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aesg-0.3.0.tar.gz.
File metadata
- Download URL: aesg-0.3.0.tar.gz
- Upload date:
- Size: 22.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f140abd5a034a055d8f6e656513fcb99a16bcbc54d80cbabee0ce4d8a6c3471
|
|
| MD5 |
ef0a1d46ecd04ac09ad5ee0336ba8164
|
|
| BLAKE2b-256 |
826d30cc5016298fd45549eed79253f9a2b2dd7887d2e815937fccf29c2f1ae1
|
Provenance
The following attestation bundles were made for aesg-0.3.0.tar.gz:
Publisher:
publish.yml on bueormnew/aesg
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aesg-0.3.0.tar.gz -
Subject digest:
1f140abd5a034a055d8f6e656513fcb99a16bcbc54d80cbabee0ce4d8a6c3471 - Sigstore transparency entry: 1862479242
- Sigstore integration time:
-
Permalink:
bueormnew/aesg@48898e6278332c705c31f924679dfaf14072efaf -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/bueormnew
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@48898e6278332c705c31f924679dfaf14072efaf -
Trigger Event:
release
-
Statement type:
File details
Details for the file aesg-0.3.0-py3-none-any.whl.
File metadata
- Download URL: aesg-0.3.0-py3-none-any.whl
- Upload date:
- Size: 23.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a49de133ef05417d9b6b509916603592285eb8e2f5cc04eae995e14bea5a1570
|
|
| MD5 |
5d19aa31a602f144a7f38c7947cc4002
|
|
| BLAKE2b-256 |
cfab05ef51c227297b2634a2e7d65a45096883e055497529619d5d944b18a8da
|
Provenance
The following attestation bundles were made for aesg-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on bueormnew/aesg
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aesg-0.3.0-py3-none-any.whl -
Subject digest:
a49de133ef05417d9b6b509916603592285eb8e2f5cc04eae995e14bea5a1570 - Sigstore transparency entry: 1862479342
- Sigstore integration time:
-
Permalink:
bueormnew/aesg@48898e6278332c705c31f924679dfaf14072efaf -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/bueormnew
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@48898e6278332c705c31f924679dfaf14072efaf -
Trigger Event:
release
-
Statement type: