Skip to content

Add Thousand Brains Theory and predictive coding components - #69

Open
soroushdeimi wants to merge 14 commits into
cnrl:mainfrom
soroushdeimi:features/sush
Open

Add Thousand Brains Theory and predictive coding components#69
soroushdeimi wants to merge 14 commits into
cnrl:mainfrom
soroushdeimi:features/sush

Conversation

@soroushdeimi

Copy link
Copy Markdown

What this adds

Two related groups of behaviors, plus tests and some fixes to existing code
that turned up along the way.

Thousand Brains / HTM

  • Grid cells and displacement cells for allocentric reference frames
  • Active dendrites with segment-based NMDA-style computation
  • Temporal Memory (HTM sequence learning) and Spatial Pooler
  • An SDR type with overlap, union, intersection and similarity measures
  • Inter-column voting and consensus

Predictive coding

  • Prediction and error units, precision weighting, top-down prediction
  • Free energy tracking and a learning rule that descends it
  • Predictive synapse types and a hierarchy builder for the canonical
    L4 / L2-3 / L5 / L6 microcircuit

All new behaviors are registered in conex/nn/priority.py, so they compose
with prioritize_behaviors() like the existing ones. Priorities follow each
group's data flow: encoding runs ahead of the dendrites, predictive coding
sits between dendrite structure and computation in the order
prediction -> error -> precision -> free energy, and learning and readout
run after Fire.

Fixes to existing code

  • Local2dSTDP.compute_dw built its post-synaptic spike term from
    pre_spike. Conv2dSTDP already used post_spike — this was a
    copy-paste slip that made the LTP term wrong, and raised a shape error
    whenever source and destination sizes differed.
  • CorticalLayerConnection.connect_dst tested self.dst twice instead of
    self.src and self.dst, so it could try to build synapses with no source.
  • CorticalColumn.save_helper and its loader indexed sub_structures for
    both layers and layer connections, which mixed the two up on reload.
    Layers are now keyed by tag.
  • InputLayer.__repr__ and OutputLayer.__repr__ chained conditional
    expressions whose precedence made them return the wrong string.

Public API

Package __init__ files now declare __all__. The leaf packages list what
their modules define; behaviors, nn and the top-level package build
theirs by aggregating from their children, so the lists cannot drift out of
sync with the code. Internal imports (Behavior, torch, typing names) are
no longer re-exported. from conex import * gives 142 names.

Tests

114 new tests, CPU only, about a second to run:

  • test_sdr.py — overlap, union, intersection, difference, subsample,
    Jaccard and threshold matching, UnionSDR membership
  • test_htm.py — spatial pooler sparsity, stability and permanence bounds;
    temporal memory bursting, winner selection, anomaly score, and the drop in
    anomaly once a sequence repeats
  • test_grid_cells.py — activation bounds, determinism, module scales, path
    integration, and periodicity on the hexagonal lattice (the shortest
    translation that returns all three wave vectors is 2*lambda/sqrt(3) at
    theta+30 degrees)
  • test_predictive_coding.py — error computation, precision weighting,
    prediction smoothing, free energy terms
  • test_learning.py — regression tests for the Local2dSTDP fix; all six
    fail if the update reads pre_spike again
  • test_exports.py — every public name resolves from conex, and every
    Behavior in the package has a priority entry

tests/conftest.py has a shared network builder. Behaviors across CoNeX read
network.dt, so test networks need TimeResolution attached.

Known gaps

Worth flagging before review:

  • Apart from ActiveDendriteComputation, the new behaviors do not read or
    write neurons.spikes / neurons.I. They are driven by explicit method
    calls and by attributes the caller sets (sp_input, tm_input_columns),
    so for now they sit alongside the spiking pipeline rather than inside it.
    Wiring them in is the obvious follow-up.
  • @dataclass(slots=True) in sdr.py and active_dendrites.py needs
    Python 3.10, but setup.py still declares python_requires=">=3.8".
  • nn/utils/precision.py calls torch.amp.GradScaler, which is torch 2.3+,
    while requirements.txt pins torch==1.13.1. It also targets
    torch.nn.Module, so it does not apply to CoNeX behaviors yet.
  • SDR.overlap assumes the index tensor holds no duplicates; the
    constructor does not deduplicate.
  • Unrelated to this branch, but conex.helpers.transforms.masks imports
    torchvision and it is not in install_requires, so import conex fails
    on a clean install. Happy to fix that here or separately.

New modules:
- Grid cells and displacement cells for reference frames
- Active dendrites with NMDA nonlinearity
- Temporal Memory (HTM sequence learning)
- Spatial Pooler (SDR encoding)
- SDR operations (overlap, union, encoding)
- Column voting for inter-column consensus

Bug fixes:
- Fix connect_dst condition in CorticalLayerConnection
- Fix build_helper key reconstruction in CorticalColumn
- Fix Local2dSTDP using wrong spike variable

Improvements:
- Improved __repr__ methods across structure classes
- Updated README with Thousand Brains documentation
- Add __all__ to all 10 __init__.py modules for explicit exports
- Add docstrings to all module init files
- Create conex/nn/utils/precision.py with:
  - PrecisionMode enum (FULL, HALF, BFLOAT16, MIXED)
  - PrecisionConfig dataclass for configuration
  - MixedPrecisionManager for autocast and gradient scaling
  - Hardware support detection utilities
  - Optimal precision auto-configuration
- Fix incorrect __all__ entries to match actual class names
Implements predictive processing based on Free Energy Principle:

Core Behaviors (conex/behaviors/neurons/predictive_coding.py):
- PredictionUnit: Generates predictions about expected input
- ErrorUnit: Computes prediction errors (surprise)
- PrecisionWeighting: Adaptive uncertainty estimation
- TopDownPrediction: Higher-to-lower layer predictions
- PredictiveCodingLearning: Weight updates via error minimization
- FreeEnergyMinimization: Tracks variational free energy
- HierarchicalPredictiveCoding: Multi-level processing

Predictive Synapses (conex/behaviors/synapses/predictive.py):
- FeedbackPredictionSynapse: Top-down predictions
- FeedforwardErrorSynapse: Bottom-up errors
- LateralPredictionSynapse: Same-level context
- PredictiveConnection: Factory for connections

High-Level Structures (conex/nn/structure/predictive_hierarchy.py):
- PredictiveHierarchy: Multi-level hierarchy builder
- PredictiveCorticalColumn: Canonical microcircuit (L4/L2-3/L5/L6)

References:
- Rao & Ballard (1999): Predictive coding in visual cortex
- Friston (2005): Free Energy Principle
- Bastos et al. (2012): Canonical microcircuits
The 25 behaviors added for Thousand Brains Theory and predictive coding
had no entry in priority.py, so prioritize_behaviors() raised a bare
KeyError for every one of them.

Placement follows the data flow of each group:
- encoding ahead of the dendrites (SDREncoder, SpatialPooler,
  TemporalMemory, grid and location modules)
- predictive coding between dendrite structure and computation, ordered
  prediction -> error -> precision -> free energy
- learning and readout after Fire (PredictiveCodingLearning, SDRClassifier)
- predictive synapses alongside their dendritic-input and learning peers

Existing priority values are unchanged. prioritize_behaviors() now names
the offending class instead of raising a bare KeyError.
The __all__ lists added to the package __init__ files were missing most
of the existing API. Because conex/__init__.py re-exports through star
imports, a short __all__ in an intermediate package stops those names
from binding on conex at all, which breaks explicit imports too.

`from conex import *` had dropped to 11 names. LIF, Fire, KWTA,
SimpleSTDP, WeightInitializer and 11 others were no longer reachable as
conex.<name>, Example/test/mnist.py could not import replicate or
save_structure, and the newly added SDR, GridCellModule, TemporalMemory
and SpatialPooler were unreachable as well.

Leaf packages now list everything their modules define; behaviors, nn
and the top-level package build __all__ from their children so the two
cannot drift apart again. Internal imports (Behavior, torch, typing
names) stay unexported.

`from conex import *` now yields 142 names.
The Thousand Brains and predictive coding work landed without tests. This
adds 114, all runnable on CPU in about a second.

- test_exports.py: every legacy and new name resolves from conex, __all__
  has no duplicates and matches `import *`, and every Behavior in the
  package has a priority entry. These pin the two regressions above.
- test_sdr.py: overlap, union, intersection, difference, subsample and
  the Jaccard and threshold measures, plus UnionSDR membership.
- test_htm.py: spatial pooler sparsity, stability and permanence bounds;
  temporal memory bursting, winner selection, anomaly score and the drop
  in anomaly once a sequence repeats.
- test_grid_cells.py: activation bounds and determinism, module scales,
  path integration, and periodicity on the hexagonal lattice, where the
  shortest translation is 2*lambda/sqrt(3) at theta+30 degrees.
- test_predictive_coding.py: error, precision weighting, prediction
  smoothing and the free energy terms.
- test_learning.py: regression tests for the Local2dSTDP fix. All six
  fail if the update reads pre_spike again.
- conftest.py: shared network builder. Behaviors read network.dt, so
  test networks need TimeResolution.
@saeedark
saeedark self-requested a review August 2, 2026 12:38
@saeedark

saeedark commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Thank you for This PR.

As it's quite lengthy and covers multiple avenues, its review takes some time. On the positive note, CoNeX should finally enjoy from wildcard imports :)

Was any Agent/LLM involved in this PR?

@soroushdeimi

Copy link
Copy Markdown
Author

Thanks for taking a look no rush, I know it's a big diff.

The Thousand Brains and predictive coding work is mine, from December last year. I had actually discussed these changes with Dr. Tabesh back then, but I got the impression he wasn't very enthusiastic about my contribution, so I ended up shelving the idea. Today, while organizing my files, I came across the code again. I thought it held up well and decided it was worth opening a PR in case it could be of some help.

The three commits at the top of the branch are more recent. I had been using the library myself and kept hitting the export and priority problems. It seemed a shame to leave them as is, so I sat down this week to fix them properly, which included the __all__ restructuring, the priority table entries, and adding the tests.

I was also planning to add a CI workflow nothing currently runs the tests on a PR, which seemed worth fixing if the project is going to grow.

Happy to walk through any part of it. If it'd be easier to review.

@saeedark

saeedark commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

I really don't like to change minimum version of python. As far as I can tell, you only used @dataclass(slots=True) for configuration classes. I don't think they are essential. please either remove config classes or explain why I'm wrong?

(note: in case of DendriteSegment behavior you also need to change __init__ method)

@saeedark

saeedark commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Please add your name and email address as author on top of following files:

  • conex/behaviors/network/voting.py
  • conex/behaviors/neurons/active_dendrites.py
  • conex/behaviors/neurons/grid_cells.py
  • conex/behaviors/neurons/predictive_coding.py
  • conex/behaviors/neurons/sdr.py
  • conex/behaviors/neurons/sequence_memory.py
  • conex/behaviors/neurons/spatial_pooler.py
  • conex/behaviors/synapses/predictive.py
  • conex/nn/structure/predictive_hierarchy.py
  • conex/nn/utils/precision.py

(basically all the new behavior files + precision + structure)

Adds an Author line to the module docstring of each new file, as requested
in review. Docstring-only change, no behavior touched.
@DataClass(slots=True) requires Python 3.10, while setup.py declares
python_requires=">=3.8". It was used on two config classes,
DendriticSegmentConfig and SDRConfig, where it bought nothing: both are
short-lived parameter bundles built once when a behavior is constructed,
never in the simulation loop.

vermin puts the package minimum back at 3.8, down from 3.10, and these two
decorators were its only violations. The config classes themselves stay —
plain dataclasses are 3.7+, and NamedTuple configs were never affected.
@soroushdeimi

Copy link
Copy Markdown
Author

Done dropped slots=True from DendriticSegmentConfig and SDRConfig
It was a two-line change and the package minimum is back at 3.8.

I kept the config classes themselves for now, since they turned out not to
be the version problem only two of the six had slots at all.
TemporalMemoryConfig and SpatialPoolerConfig are NamedTuples, and
PredictiveCodingConfig and PrecisionConfig are plain dataclasses, all of
which are fine on 3.8.

if you'd still rather see them go on design grounds, I'm happy to
do it I think you have a point, just for a different reason than the
version. The current pattern has a real bug: init unpacks the config
into super().init(...) and also forwards **kwargs, so

TemporalMemory(n_columns=100)
TypeError: got multiple values for keyword argument 'n_columns'

Same for SpatialPooler and DendriticSegment. Right now they can only be
configured by building a whole config object, which is the opposite of how
every other behavior in CoNeX works. Moving to plain keyword arguments would
fix that by construction. It touches ~100 references across the package and
the tests though, so I'd rather do it as its own PR than grow this one up
to you.

@saeedark

saeedark commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Done dropped slots=True from DendriticSegmentConfig and SDRConfig It was a two-line change and the package minimum is back at 3.8.

I kept the config classes themselves for now, since they turned out not to be the version problem only two of the six had slots at all. TemporalMemoryConfig and SpatialPoolerConfig are NamedTuples, and PredictiveCodingConfig and PrecisionConfig are plain dataclasses, all of which are fine on 3.8.

if you'd still rather see them go on design grounds, I'm happy to do it I think you have a point, just for a different reason than the version. The current pattern has a real bug: init unpacks the config into super().init(...) and also forwards **kwargs, so

TemporalMemory(n_columns=100)
TypeError: got multiple values for keyword argument 'n_columns'

Same for SpatialPooler and DendriticSegment. Right now they can only be configured by building a whole config object, which is the opposite of how every other behavior in CoNeX works. Moving to plain keyword arguments would fix that by construction. It touches ~100 references across the package and the tests though, so I'd rather do it as its own PR than grow this one up to you.

Yes, please remove All the data classes. should be the list below?

  • conex/nn/utils/precision.py
  • conex/nn/structure/predictive_hierarchy.py
  • conex/behaviors/neurons/active_dendrites.py
  • conex/behaviors/neurons/sdr.py

and corresponding test?

@soroushdeimi

Copy link
Copy Markdown
Author

Before I start the previous commit already fixed the version issue, so I want to check that removing the classes is still what you want.

vermin on the package, before and after dropping slots=True:

with @dataclass(slots=True):   Minimum required versions: 3.10
current branch:                Minimum required versions: 3.8

So 3.8 works today with the config classes still in place. slots was the only
3.10 construct everything else is either 3.8-compatible or sits in
annotations behind from __future__ import annotations.

That makes removing them a design decision rather than a compatibility one.
I'm happy to do it if you want the consistency with the rest of the codebase;
I just didn't want to run a large refactor on the assumption it was still
needed for 3.8.

If you do want it there is five dataclasses total:

active_dendrites.py       DendriticSegmentConfig
predictive_coding.py      PredictiveCodingConfig   
sdr.py                    SDRConfig
predictive_hierarchy.py   HierarchyLevel
precision.py              PrecisionConfig

PredictiveCodingConfig is the bulk of the work: 60 references across the
behaviors, the predictive synapses, and PredictiveHierarchy, which passes it
down to every level.

And two questions:

  1. HierarchyLevel is a dataclass but not a config object it's the internal record for one level (size, name, layer, config). Dropping it means a tuple or a dict instead, which I think reads worse. In scope?

  2. TemporalMemoryConfig and SpatialPoolerConfig are NamedTuple rather than dataclass, so they're not on your list, but they serve the same role. In or out?

@saeedark

saeedark commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator
  1. Yes, use lists and dictionaries instead.
  2. Thank you for noticing, replace them too.

@saeedark

saeedark commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Okay. two more things, and I should be done.

  1. Please provide a working example in format of script/notebook in Example/numenta folder.
  2. move below files in a separate folder like this pattern:
    pattern:
    conex/behaviors/network/voting.py -> ../network/numenta/voting.py
  • conex/behaviors/network/voting.py
  • conex/behaviors/neurons/active_dendrites.py
  • conex/behaviors/neurons/grid_cells.py
  • conex/behaviors/neurons/predictive_coding.py
  • conex/behaviors/neurons/sdr.py
  • conex/behaviors/neurons/sequence_memory.py
  • conex/behaviors/neurons/spatial_pooler.py
  • conex/behaviors/synapses/predictive.py
  • conex/nn/structure/predictive_hierarchy.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants