lex-hebbian-assembly 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +11 -0
- data/lex-hebbian-assembly.gemspec +32 -0
- data/lib/legion/extensions/hebbian_assembly/actors/decay.rb +41 -0
- data/lib/legion/extensions/hebbian_assembly/client.rb +25 -0
- data/lib/legion/extensions/hebbian_assembly/helpers/assembly.rb +78 -0
- data/lib/legion/extensions/hebbian_assembly/helpers/assembly_network.rb +186 -0
- data/lib/legion/extensions/hebbian_assembly/helpers/constants.rb +46 -0
- data/lib/legion/extensions/hebbian_assembly/helpers/unit.rb +90 -0
- data/lib/legion/extensions/hebbian_assembly/runners/hebbian_assembly.rb +90 -0
- data/lib/legion/extensions/hebbian_assembly/version.rb +9 -0
- data/lib/legion/extensions/hebbian_assembly.rb +17 -0
- data/spec/legion/extensions/hebbian_assembly/client_spec.rb +38 -0
- data/spec/legion/extensions/hebbian_assembly/helpers/assembly_network_spec.rb +142 -0
- data/spec/legion/extensions/hebbian_assembly/helpers/assembly_spec.rb +89 -0
- data/spec/legion/extensions/hebbian_assembly/helpers/unit_spec.rb +119 -0
- data/spec/legion/extensions/hebbian_assembly/runners/hebbian_assembly_spec.rb +109 -0
- data/spec/spec_helper.rb +20 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a7a9d7d72a5ff5d0f18795d1429a8db621b543021e9ccc316027110529a6f235
|
|
4
|
+
data.tar.gz: 30287fe026d3c978361f85bf5e43757946f0c1bb4ee28bb7472a7ae11d9df70a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fed78e7e6d176dad68a17b9239311bf932c7e1160978b511557e9836b66afea52a10d2ce57427d3608bd60a59a67d3595d5c15df59fe0fa29103b8fa9ca53a55
|
|
7
|
+
data.tar.gz: e00aa1d2f97b2c87e14db0109b1f6a0500f6b043793c7b587a53c18daa2e32ea31c04d050955d922fda134373ce27dc1baa58f735523daed59034cd8629e755b
|
data/Gemfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/hebbian_assembly/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-hebbian-assembly'
|
|
7
|
+
spec.version = Legion::Extensions::HebbianAssembly::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Hebbian Assembly'
|
|
12
|
+
spec.description = "Hebb's Cell Assembly Theory for brain-modeled agentic AI — neurons that fire " \
|
|
13
|
+
'together wire together, forming emergent assemblies that represent concepts, ' \
|
|
14
|
+
'enable pattern completion, and support associative recall through ' \
|
|
15
|
+
'co-activation-driven synaptic strengthening.'
|
|
16
|
+
spec.homepage = 'https://github.com/LegionIO/lex-hebbian-assembly'
|
|
17
|
+
spec.license = 'MIT'
|
|
18
|
+
spec.required_ruby_version = '>= 3.4'
|
|
19
|
+
|
|
20
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
21
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-hebbian-assembly'
|
|
22
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-hebbian-assembly'
|
|
23
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-hebbian-assembly'
|
|
24
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-hebbian-assembly/issues'
|
|
25
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
26
|
+
|
|
27
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
28
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-hebbian-assembly.gemspec Gemfile]
|
|
29
|
+
end
|
|
30
|
+
spec.require_paths = ['lib']
|
|
31
|
+
spec.add_development_dependency 'legion-gaia'
|
|
32
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/every'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module HebbianAssembly
|
|
8
|
+
module Actor
|
|
9
|
+
class Decay < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::HebbianAssembly::Runners::HebbianAssembly
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'update_hebbian'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
60
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run_now?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def use_runner?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_subtask?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def generate_task?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/hebbian_assembly/helpers/constants'
|
|
4
|
+
require 'legion/extensions/hebbian_assembly/helpers/unit'
|
|
5
|
+
require 'legion/extensions/hebbian_assembly/helpers/assembly'
|
|
6
|
+
require 'legion/extensions/hebbian_assembly/helpers/assembly_network'
|
|
7
|
+
require 'legion/extensions/hebbian_assembly/runners/hebbian_assembly'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module HebbianAssembly
|
|
12
|
+
class Client
|
|
13
|
+
include Runners::HebbianAssembly
|
|
14
|
+
|
|
15
|
+
def initialize(network: nil, **)
|
|
16
|
+
@network = network || Helpers::AssemblyNetwork.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :network
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module HebbianAssembly
|
|
6
|
+
module Helpers
|
|
7
|
+
class Assembly
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :id, :member_ids, :formed_at, :activation_count, :last_activated
|
|
11
|
+
attr_accessor :coherence
|
|
12
|
+
|
|
13
|
+
def initialize(id:, member_ids:)
|
|
14
|
+
@id = id
|
|
15
|
+
@member_ids = Array(member_ids).uniq
|
|
16
|
+
@coherence = 0.5
|
|
17
|
+
@formed_at = Time.now.utc
|
|
18
|
+
@activation_count = 0
|
|
19
|
+
@last_activated = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def activate
|
|
23
|
+
@activation_count += 1
|
|
24
|
+
@last_activated = Time.now.utc
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def consolidate
|
|
28
|
+
@coherence = [@coherence + CONSOLIDATION_BOOST, MAX_WEIGHT].min
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def decay
|
|
32
|
+
@coherence = [@coherence - WEIGHT_DECAY, 0.0].max
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def dissolving?
|
|
36
|
+
@coherence < ASSEMBLY_MIN_WEIGHT
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def member_count
|
|
40
|
+
@member_ids.size
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def includes?(unit_id)
|
|
44
|
+
@member_ids.include?(unit_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def state
|
|
48
|
+
if @last_activated && (Time.now.utc - @last_activated) < CO_ACTIVATION_WINDOW
|
|
49
|
+
:active
|
|
50
|
+
elsif @last_activated && (Time.now.utc - @last_activated) < (CO_ACTIVATION_WINDOW * 3)
|
|
51
|
+
:primed
|
|
52
|
+
elsif dissolving?
|
|
53
|
+
:dissolving
|
|
54
|
+
elsif @coherence >= ASSEMBLY_MIN_WEIGHT
|
|
55
|
+
:dormant
|
|
56
|
+
else
|
|
57
|
+
:forming
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
{
|
|
63
|
+
id: @id,
|
|
64
|
+
members: @member_ids.dup,
|
|
65
|
+
member_count: member_count,
|
|
66
|
+
coherence: @coherence.round(4),
|
|
67
|
+
state: state,
|
|
68
|
+
state_label: ASSEMBLY_STATE_LABELS[state],
|
|
69
|
+
activation_count: @activation_count,
|
|
70
|
+
formed_at: @formed_at,
|
|
71
|
+
last_activated: @last_activated
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module HebbianAssembly
|
|
6
|
+
module Helpers
|
|
7
|
+
class AssemblyNetwork
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :units, :assemblies, :activation_history
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@units = {}
|
|
14
|
+
@assemblies = {}
|
|
15
|
+
@activation_history = []
|
|
16
|
+
@assembly_counter = 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add_unit(id:, domain: :general)
|
|
20
|
+
return @units[id] if @units.key?(id)
|
|
21
|
+
return nil if @units.size >= MAX_UNITS
|
|
22
|
+
|
|
23
|
+
@units[id] = Unit.new(id: id, domain: domain)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def activate_unit(id:, level: 1.0)
|
|
27
|
+
ensure_unit(id)
|
|
28
|
+
unit = @units[id]
|
|
29
|
+
unit.activate(level: level)
|
|
30
|
+
record_activation(id)
|
|
31
|
+
hebbian_update(id)
|
|
32
|
+
detect_assemblies
|
|
33
|
+
unit
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def co_activate(ids:, level: 1.0)
|
|
37
|
+
ids = Array(ids)
|
|
38
|
+
ids.each do |id|
|
|
39
|
+
ensure_unit(id)
|
|
40
|
+
@units[id].activate(level: level)
|
|
41
|
+
record_activation(id)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
ids.combination(2).each do |a, b|
|
|
45
|
+
ensure_connection(a, b)
|
|
46
|
+
@units[a].strengthen(b)
|
|
47
|
+
@units[b].strengthen(a)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
detect_assemblies
|
|
51
|
+
ids.map { |id| @units[id].to_h }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def query_weight(from:, to:)
|
|
55
|
+
return 0.0 unless @units.key?(from)
|
|
56
|
+
|
|
57
|
+
@units[from].weight_to(to)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def query_assembly(id:)
|
|
61
|
+
@assemblies[id]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def assemblies_containing(unit_id:)
|
|
65
|
+
@assemblies.values.select { |a| a.includes?(unit_id) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def pattern_complete(partial_ids:)
|
|
69
|
+
partial = Array(partial_ids)
|
|
70
|
+
candidates = partial.flat_map { |uid| assemblies_containing(unit_id: uid) }.uniq(&:id)
|
|
71
|
+
return nil if candidates.empty?
|
|
72
|
+
|
|
73
|
+
best = candidates.max_by { |a| (partial & a.member_ids).size }
|
|
74
|
+
missing = best.member_ids - partial
|
|
75
|
+
{ assembly_id: best.id, known: partial & best.member_ids, predicted: missing, coherence: best.coherence }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def strongest_units(n = 10)
|
|
79
|
+
@units.values
|
|
80
|
+
.sort_by { |u| -u.activation_count }
|
|
81
|
+
.first(n)
|
|
82
|
+
.map(&:to_h)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def decay_all
|
|
86
|
+
@units.each_value(&:decay_weights)
|
|
87
|
+
@assemblies.each_value(&:decay)
|
|
88
|
+
@assemblies.reject! { |_, a| a.dissolving? }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def unit_count
|
|
92
|
+
@units.size
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def assembly_count
|
|
96
|
+
@assemblies.size
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def to_h
|
|
100
|
+
{
|
|
101
|
+
units: @units.size,
|
|
102
|
+
assemblies: @assemblies.size,
|
|
103
|
+
total_connections: @units.values.sum(&:connection_count),
|
|
104
|
+
history_size: @activation_history.size,
|
|
105
|
+
active_units: @units.values.count(&:active?)
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def ensure_unit(id)
|
|
112
|
+
add_unit(id: id) unless @units.key?(id)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def ensure_connection(a, b)
|
|
116
|
+
@units[a].connect(b) unless @units[a].connections.key?(b)
|
|
117
|
+
@units[b].connect(a) unless @units[b].connections.key?(a)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def record_activation(id)
|
|
121
|
+
@activation_history << { unit_id: id, at: Time.now.utc }
|
|
122
|
+
@activation_history.shift while @activation_history.size > MAX_ACTIVATION_HISTORY
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def hebbian_update(active_id)
|
|
126
|
+
recent_window = Time.now.utc - CO_ACTIVATION_WINDOW
|
|
127
|
+
recent_ids = @activation_history
|
|
128
|
+
.select { |h| h[:at] >= recent_window && h[:unit_id] != active_id }
|
|
129
|
+
.map { |h| h[:unit_id] }
|
|
130
|
+
.uniq
|
|
131
|
+
|
|
132
|
+
recent_ids.each do |other_id|
|
|
133
|
+
next unless @units.key?(other_id) && @units[other_id].active?
|
|
134
|
+
|
|
135
|
+
ensure_connection(active_id, other_id)
|
|
136
|
+
@units[active_id].strengthen(other_id)
|
|
137
|
+
@units[other_id].strengthen(active_id)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def detect_assemblies
|
|
142
|
+
strongly_connected = find_strongly_connected
|
|
143
|
+
return if strongly_connected.size < ASSEMBLY_THRESHOLD
|
|
144
|
+
|
|
145
|
+
existing = find_matching_assembly(strongly_connected)
|
|
146
|
+
existing ? reinforce_assembly(existing) : create_assembly(strongly_connected)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def find_strongly_connected
|
|
150
|
+
active_ids = @units.values.select(&:active?).map(&:id)
|
|
151
|
+
return [] if active_ids.size < ASSEMBLY_THRESHOLD
|
|
152
|
+
|
|
153
|
+
active_ids.select do |uid|
|
|
154
|
+
peers = active_ids - [uid]
|
|
155
|
+
peers.count { |p| @units[uid].weight_to(p) >= ASSEMBLY_MIN_WEIGHT } >= (ASSEMBLY_THRESHOLD - 1)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def find_matching_assembly(member_ids)
|
|
160
|
+
@assemblies.values.find do |a|
|
|
161
|
+
(member_ids - a.member_ids).empty? && (a.member_ids - member_ids).empty?
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def reinforce_assembly(assembly)
|
|
166
|
+
assembly.activate
|
|
167
|
+
assembly.consolidate
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def create_assembly(member_ids)
|
|
171
|
+
@assembly_counter += 1
|
|
172
|
+
id = :"assembly_#{@assembly_counter}"
|
|
173
|
+
@assemblies[id] = Assembly.new(id: id, member_ids: member_ids)
|
|
174
|
+
@assemblies[id].activate
|
|
175
|
+
prune_assemblies if @assemblies.size > MAX_ASSEMBLIES
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def prune_assemblies
|
|
179
|
+
weakest = @assemblies.min_by { |_, a| a.coherence }&.first
|
|
180
|
+
@assemblies.delete(weakest) if weakest
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module HebbianAssembly
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_UNITS = 500
|
|
9
|
+
MAX_ASSEMBLIES = 100
|
|
10
|
+
MAX_CONNECTIONS_PER_UNIT = 30
|
|
11
|
+
MAX_ACTIVATION_HISTORY = 200
|
|
12
|
+
|
|
13
|
+
DEFAULT_WEIGHT = 0.1
|
|
14
|
+
WEIGHT_FLOOR = 0.01
|
|
15
|
+
MAX_WEIGHT = 1.0
|
|
16
|
+
LEARNING_RATE = 0.05
|
|
17
|
+
WEIGHT_DECAY = 0.002
|
|
18
|
+
|
|
19
|
+
ACTIVATION_THRESHOLD = 0.3
|
|
20
|
+
CO_ACTIVATION_WINDOW = 5
|
|
21
|
+
ASSEMBLY_THRESHOLD = 3
|
|
22
|
+
ASSEMBLY_MIN_WEIGHT = 0.3
|
|
23
|
+
|
|
24
|
+
CONSOLIDATION_BOOST = 0.02
|
|
25
|
+
COMPETITION_FACTOR = 0.01
|
|
26
|
+
|
|
27
|
+
WEIGHT_LABELS = {
|
|
28
|
+
(0.8..) => :bonded,
|
|
29
|
+
(0.6...0.8) => :strong,
|
|
30
|
+
(0.4...0.6) => :moderate,
|
|
31
|
+
(0.2...0.4) => :weak,
|
|
32
|
+
(..0.2) => :nascent
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
ASSEMBLY_STATE_LABELS = {
|
|
36
|
+
active: 'assembly currently firing',
|
|
37
|
+
primed: 'assembly recently active',
|
|
38
|
+
dormant: 'assembly stable but inactive',
|
|
39
|
+
forming: 'assembly still consolidating',
|
|
40
|
+
dissolving: 'assembly losing coherence'
|
|
41
|
+
}.freeze
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module HebbianAssembly
|
|
6
|
+
module Helpers
|
|
7
|
+
class Unit
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :id, :domain, :connections, :activation_count, :last_activated
|
|
11
|
+
attr_accessor :activation_level
|
|
12
|
+
|
|
13
|
+
def initialize(id:, domain: :general)
|
|
14
|
+
@id = id
|
|
15
|
+
@domain = domain
|
|
16
|
+
@activation_level = 0.0
|
|
17
|
+
@connections = {}
|
|
18
|
+
@activation_count = 0
|
|
19
|
+
@last_activated = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def activate(level: 1.0)
|
|
23
|
+
@activation_level = level.to_f.clamp(0.0, 1.0)
|
|
24
|
+
@activation_count += 1
|
|
25
|
+
@last_activated = Time.now.utc
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def active?
|
|
29
|
+
@activation_level >= ACTIVATION_THRESHOLD
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def connect(other_id, weight: DEFAULT_WEIGHT)
|
|
33
|
+
return if @connections.size >= MAX_CONNECTIONS_PER_UNIT && !@connections.key?(other_id)
|
|
34
|
+
|
|
35
|
+
@connections[other_id] = weight.to_f.clamp(WEIGHT_FLOOR, MAX_WEIGHT)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def strengthen(other_id, amount: LEARNING_RATE)
|
|
39
|
+
return unless @connections.key?(other_id)
|
|
40
|
+
|
|
41
|
+
@connections[other_id] = [@connections[other_id] + amount, MAX_WEIGHT].min
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def weaken(other_id, amount: COMPETITION_FACTOR)
|
|
45
|
+
return unless @connections.key?(other_id)
|
|
46
|
+
|
|
47
|
+
@connections[other_id] = [@connections[other_id] - amount, WEIGHT_FLOOR].max
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def weight_to(other_id)
|
|
51
|
+
@connections[other_id] || 0.0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def weight_label(other_id)
|
|
55
|
+
w = weight_to(other_id)
|
|
56
|
+
WEIGHT_LABELS.each { |range, lbl| return lbl if range.cover?(w) }
|
|
57
|
+
:nascent
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def decay_weights
|
|
61
|
+
@connections.each do |k, w|
|
|
62
|
+
@connections[k] = [w - WEIGHT_DECAY, WEIGHT_FLOOR].max
|
|
63
|
+
end
|
|
64
|
+
@connections.reject! { |_, w| w <= WEIGHT_FLOOR }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def connection_count
|
|
68
|
+
@connections.size
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def strongest_connections(n = 5)
|
|
72
|
+
@connections.sort_by { |_, w| -w }.first(n).to_h
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_h
|
|
76
|
+
{
|
|
77
|
+
id: @id,
|
|
78
|
+
domain: @domain,
|
|
79
|
+
activation_level: @activation_level.round(4),
|
|
80
|
+
active: active?,
|
|
81
|
+
connections: @connections.size,
|
|
82
|
+
activation_count: @activation_count,
|
|
83
|
+
last_activated: @last_activated
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module HebbianAssembly
|
|
6
|
+
module Runners
|
|
7
|
+
module HebbianAssembly
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def activate_unit(id:, level: 1.0, domain: :general, **)
|
|
12
|
+
Legion::Logging.debug "[hebbian] activate: id=#{id} level=#{level}"
|
|
13
|
+
network.add_unit(id: id, domain: domain)
|
|
14
|
+
unit = network.activate_unit(id: id, level: level)
|
|
15
|
+
{ success: true, unit: unit.to_h, assemblies: network.assembly_count }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def co_activate_units(ids:, level: 1.0, **)
|
|
19
|
+
Legion::Logging.debug "[hebbian] co_activate: ids=#{ids}"
|
|
20
|
+
results = network.co_activate(ids: ids, level: level)
|
|
21
|
+
{ success: true, units: results, assemblies: network.assembly_count }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def query_weight(from:, to:, **)
|
|
25
|
+
w = network.query_weight(from: from, to: to)
|
|
26
|
+
label = Helpers::Constants::WEIGHT_LABELS.each { |range, l| break l if range.cover?(w) }
|
|
27
|
+
label = :nascent unless label.is_a?(Symbol)
|
|
28
|
+
Legion::Logging.debug "[hebbian] weight: #{from}->#{to} = #{w}"
|
|
29
|
+
{ success: true, from: from, to: to, weight: w.round(4), label: label }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def list_assemblies(**)
|
|
33
|
+
assemblies = network.assemblies.values.map(&:to_h)
|
|
34
|
+
Legion::Logging.debug "[hebbian] list_assemblies: #{assemblies.size}"
|
|
35
|
+
{ success: true, assemblies: assemblies, count: assemblies.size }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def query_assembly(id:, **)
|
|
39
|
+
asm = network.query_assembly(id: id.to_sym)
|
|
40
|
+
Legion::Logging.debug "[hebbian] query_assembly: id=#{id} found=#{!asm.nil?}"
|
|
41
|
+
if asm
|
|
42
|
+
{ success: true, assembly: asm.to_h }
|
|
43
|
+
else
|
|
44
|
+
{ success: false, reason: :not_found }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def pattern_complete(partial_ids:, **)
|
|
49
|
+
Legion::Logging.debug "[hebbian] pattern_complete: partial=#{partial_ids}"
|
|
50
|
+
result = network.pattern_complete(partial_ids: partial_ids)
|
|
51
|
+
if result
|
|
52
|
+
{ success: true, completion: result }
|
|
53
|
+
else
|
|
54
|
+
{ success: false, reason: :no_matching_assembly }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def strongest_units(limit: 10, **)
|
|
59
|
+
units = network.strongest_units(limit.to_i)
|
|
60
|
+
Legion::Logging.debug "[hebbian] strongest_units: #{units.size}"
|
|
61
|
+
{ success: true, units: units }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def assemblies_for(unit_id:, **)
|
|
65
|
+
asms = network.assemblies_containing(unit_id: unit_id).map(&:to_h)
|
|
66
|
+
Legion::Logging.debug "[hebbian] assemblies_for: unit=#{unit_id} count=#{asms.size}"
|
|
67
|
+
{ success: true, assemblies: asms, count: asms.size }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def update_hebbian(**)
|
|
71
|
+
Legion::Logging.debug '[hebbian] decay tick'
|
|
72
|
+
network.decay_all
|
|
73
|
+
{ success: true, units: network.unit_count, assemblies: network.assembly_count }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def hebbian_stats(**)
|
|
77
|
+
Legion::Logging.debug '[hebbian] stats'
|
|
78
|
+
{ success: true, stats: network.to_h }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def network
|
|
84
|
+
@network ||= Helpers::AssemblyNetwork.new
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/hebbian_assembly/version'
|
|
4
|
+
require 'legion/extensions/hebbian_assembly/helpers/constants'
|
|
5
|
+
require 'legion/extensions/hebbian_assembly/helpers/unit'
|
|
6
|
+
require 'legion/extensions/hebbian_assembly/helpers/assembly'
|
|
7
|
+
require 'legion/extensions/hebbian_assembly/helpers/assembly_network'
|
|
8
|
+
require 'legion/extensions/hebbian_assembly/runners/hebbian_assembly'
|
|
9
|
+
require 'legion/extensions/hebbian_assembly/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module HebbianAssembly
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::HebbianAssembly::Client do
|
|
4
|
+
subject(:client) { described_class.new }
|
|
5
|
+
|
|
6
|
+
it 'includes Runners::HebbianAssembly' do
|
|
7
|
+
expect(described_class.ancestors).to include(Legion::Extensions::HebbianAssembly::Runners::HebbianAssembly)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'supports full Hebbian learning lifecycle' do
|
|
11
|
+
# Repeatedly co-activate a pattern
|
|
12
|
+
10.times { client.co_activate_units(ids: %i[see dog bark]) }
|
|
13
|
+
|
|
14
|
+
# Assembly should form
|
|
15
|
+
assemblies = client.list_assemblies
|
|
16
|
+
expect(assemblies[:count]).to be >= 1
|
|
17
|
+
|
|
18
|
+
# Pattern completion: given "see" and "dog", predict "bark"
|
|
19
|
+
completion = client.pattern_complete(partial_ids: %i[see dog])
|
|
20
|
+
expect(completion[:success]).to be true
|
|
21
|
+
expect(completion[:completion][:predicted]).to include(:bark)
|
|
22
|
+
|
|
23
|
+
# Weights should be strong
|
|
24
|
+
weight = client.query_weight(from: :see, to: :dog)
|
|
25
|
+
expect(weight[:weight]).to be > 0.3
|
|
26
|
+
|
|
27
|
+
# Find assemblies containing :dog
|
|
28
|
+
dog_asms = client.assemblies_for(unit_id: :dog)
|
|
29
|
+
expect(dog_asms[:count]).to be >= 1
|
|
30
|
+
|
|
31
|
+
# Decay
|
|
32
|
+
client.update_hebbian
|
|
33
|
+
|
|
34
|
+
# Stats
|
|
35
|
+
stats = client.hebbian_stats
|
|
36
|
+
expect(stats[:stats][:units]).to eq(3)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::HebbianAssembly::Helpers::AssemblyNetwork do
|
|
4
|
+
subject(:network) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:constants) { Legion::Extensions::HebbianAssembly::Helpers::Constants }
|
|
7
|
+
|
|
8
|
+
describe '#add_unit' do
|
|
9
|
+
it 'adds a unit' do
|
|
10
|
+
unit = network.add_unit(id: :a)
|
|
11
|
+
expect(unit.id).to eq(:a)
|
|
12
|
+
expect(network.unit_count).to eq(1)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'returns existing unit on duplicate' do
|
|
16
|
+
first = network.add_unit(id: :a)
|
|
17
|
+
second = network.add_unit(id: :a)
|
|
18
|
+
expect(first).to equal(second)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'limits units' do
|
|
22
|
+
constants::MAX_UNITS.times { |i| network.add_unit(id: :"u_#{i}") }
|
|
23
|
+
expect(network.add_unit(id: :overflow)).to be_nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#activate_unit' do
|
|
28
|
+
it 'activates a unit' do
|
|
29
|
+
unit = network.activate_unit(id: :a, level: 0.8)
|
|
30
|
+
expect(unit.activation_level).to eq(0.8)
|
|
31
|
+
expect(unit.active?).to be true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'auto-creates unit if missing' do
|
|
35
|
+
network.activate_unit(id: :new_unit)
|
|
36
|
+
expect(network.unit_count).to eq(1)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'records activation history' do
|
|
40
|
+
network.activate_unit(id: :a)
|
|
41
|
+
expect(network.activation_history.size).to eq(1)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#co_activate' do
|
|
46
|
+
it 'activates multiple units' do
|
|
47
|
+
results = network.co_activate(ids: %i[a b c])
|
|
48
|
+
expect(results.size).to eq(3)
|
|
49
|
+
expect(network.unit_count).to eq(3)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'strengthens connections between co-activated units' do
|
|
53
|
+
network.co_activate(ids: %i[a b])
|
|
54
|
+
expect(network.query_weight(from: :a, to: :b)).to be > 0
|
|
55
|
+
expect(network.query_weight(from: :b, to: :a)).to be > 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'repeated co-activation increases weight' do
|
|
59
|
+
network.co_activate(ids: %i[a b])
|
|
60
|
+
first_weight = network.query_weight(from: :a, to: :b)
|
|
61
|
+
network.co_activate(ids: %i[a b])
|
|
62
|
+
expect(network.query_weight(from: :a, to: :b)).to be > first_weight
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe '#query_weight' do
|
|
67
|
+
it 'returns 0 for unconnected units' do
|
|
68
|
+
expect(network.query_weight(from: :a, to: :b)).to eq(0.0)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe 'assembly detection' do
|
|
73
|
+
it 'forms assembly from repeated co-activation of 3+ units' do
|
|
74
|
+
10.times { network.co_activate(ids: %i[a b c], level: 1.0) }
|
|
75
|
+
expect(network.assembly_count).to be >= 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'consolidates existing assembly on repeated co-activation' do
|
|
79
|
+
10.times { network.co_activate(ids: %i[a b c], level: 1.0) }
|
|
80
|
+
assembly = network.assemblies.values.first
|
|
81
|
+
first_coherence = assembly.coherence
|
|
82
|
+
5.times { network.co_activate(ids: %i[a b c], level: 1.0) }
|
|
83
|
+
expect(assembly.coherence).to be >= first_coherence
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe '#assemblies_containing' do
|
|
88
|
+
it 'finds assemblies containing a unit' do
|
|
89
|
+
10.times { network.co_activate(ids: %i[a b c], level: 1.0) }
|
|
90
|
+
asms = network.assemblies_containing(unit_id: :a)
|
|
91
|
+
expect(asms).not_to be_empty
|
|
92
|
+
expect(asms.first.includes?(:a)).to be true
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe '#pattern_complete' do
|
|
97
|
+
it 'completes partial pattern from assembly' do
|
|
98
|
+
10.times { network.co_activate(ids: %i[a b c], level: 1.0) }
|
|
99
|
+
result = network.pattern_complete(partial_ids: %i[a b])
|
|
100
|
+
expect(result).not_to be_nil
|
|
101
|
+
expect(result[:predicted]).to include(:c)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'returns nil when no matching assembly' do
|
|
105
|
+
expect(network.pattern_complete(partial_ids: [:unknown])).to be_nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe '#strongest_units' do
|
|
110
|
+
it 'returns most activated units' do
|
|
111
|
+
5.times { network.activate_unit(id: :frequent, level: 1.0) }
|
|
112
|
+
network.activate_unit(id: :rare, level: 1.0)
|
|
113
|
+
top = network.strongest_units(1)
|
|
114
|
+
expect(top.first[:id]).to eq(:frequent)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe '#decay_all' do
|
|
119
|
+
it 'decays unit weights' do
|
|
120
|
+
network.co_activate(ids: %i[a b])
|
|
121
|
+
before = network.query_weight(from: :a, to: :b)
|
|
122
|
+
network.decay_all
|
|
123
|
+
expect(network.query_weight(from: :a, to: :b)).to be < before
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'removes dissolving assemblies' do
|
|
127
|
+
10.times { network.co_activate(ids: %i[a b c], level: 1.0) }
|
|
128
|
+
network.assemblies.each_value { |a| a.coherence = 0.1 }
|
|
129
|
+
network.decay_all
|
|
130
|
+
expect(network.assembly_count).to eq(0)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe '#to_h' do
|
|
135
|
+
it 'returns stats' do
|
|
136
|
+
network.co_activate(ids: %i[a b c])
|
|
137
|
+
h = network.to_h
|
|
138
|
+
expect(h).to include(:units, :assemblies, :total_connections, :history_size, :active_units)
|
|
139
|
+
expect(h[:units]).to eq(3)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::HebbianAssembly::Helpers::Assembly do
|
|
4
|
+
subject(:assembly) { described_class.new(id: :asm_one, member_ids: %i[a b c]) }
|
|
5
|
+
|
|
6
|
+
let(:constants) { Legion::Extensions::HebbianAssembly::Helpers::Constants }
|
|
7
|
+
|
|
8
|
+
describe '#initialize' do
|
|
9
|
+
it 'sets attributes' do
|
|
10
|
+
expect(assembly.id).to eq(:asm_one)
|
|
11
|
+
expect(assembly.member_ids).to eq(%i[a b c])
|
|
12
|
+
expect(assembly.coherence).to eq(0.5)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'deduplicates members' do
|
|
16
|
+
asm = described_class.new(id: :dup, member_ids: %i[a a b])
|
|
17
|
+
expect(asm.member_ids).to eq(%i[a b])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '#activate' do
|
|
22
|
+
it 'increments activation count' do
|
|
23
|
+
assembly.activate
|
|
24
|
+
expect(assembly.activation_count).to eq(1)
|
|
25
|
+
expect(assembly.last_activated).to be_a(Time)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe '#consolidate' do
|
|
30
|
+
it 'increases coherence' do
|
|
31
|
+
before = assembly.coherence
|
|
32
|
+
assembly.consolidate
|
|
33
|
+
expect(assembly.coherence).to be > before
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#decay' do
|
|
38
|
+
it 'decreases coherence' do
|
|
39
|
+
before = assembly.coherence
|
|
40
|
+
assembly.decay
|
|
41
|
+
expect(assembly.coherence).to be < before
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#dissolving?' do
|
|
46
|
+
it 'returns false when coherent' do
|
|
47
|
+
expect(assembly.dissolving?).to be false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'returns true when coherence drops below threshold' do
|
|
51
|
+
assembly.coherence = 0.1
|
|
52
|
+
expect(assembly.dissolving?).to be true
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#includes?' do
|
|
57
|
+
it 'returns true for members' do
|
|
58
|
+
expect(assembly.includes?(:a)).to be true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'returns false for non-members' do
|
|
62
|
+
expect(assembly.includes?(:z)).to be false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe '#state' do
|
|
67
|
+
it 'returns :dormant for stable inactive assembly' do
|
|
68
|
+
expect(assembly.state).to eq(:dormant)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'returns :active when recently activated' do
|
|
72
|
+
assembly.activate
|
|
73
|
+
expect(assembly.state).to eq(:active)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'returns :dissolving when coherence is low' do
|
|
77
|
+
assembly.coherence = 0.1
|
|
78
|
+
expect(assembly.state).to eq(:dissolving)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe '#to_h' do
|
|
83
|
+
it 'returns hash with all fields' do
|
|
84
|
+
h = assembly.to_h
|
|
85
|
+
expect(h).to include(:id, :members, :member_count, :coherence, :state, :state_label, :activation_count)
|
|
86
|
+
expect(h[:member_count]).to eq(3)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::HebbianAssembly::Helpers::Unit do
|
|
4
|
+
subject(:unit) { described_class.new(id: :neuron_a, domain: :cognition) }
|
|
5
|
+
|
|
6
|
+
let(:constants) { Legion::Extensions::HebbianAssembly::Helpers::Constants }
|
|
7
|
+
|
|
8
|
+
describe '#initialize' do
|
|
9
|
+
it 'sets attributes' do
|
|
10
|
+
expect(unit.id).to eq(:neuron_a)
|
|
11
|
+
expect(unit.domain).to eq(:cognition)
|
|
12
|
+
expect(unit.activation_level).to eq(0.0)
|
|
13
|
+
expect(unit.activation_count).to eq(0)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '#activate' do
|
|
18
|
+
it 'sets activation level and increments count' do
|
|
19
|
+
unit.activate(level: 0.8)
|
|
20
|
+
expect(unit.activation_level).to eq(0.8)
|
|
21
|
+
expect(unit.activation_count).to eq(1)
|
|
22
|
+
expect(unit.last_activated).to be_a(Time)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'clamps activation' do
|
|
26
|
+
unit.activate(level: 1.5)
|
|
27
|
+
expect(unit.activation_level).to eq(1.0)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '#active?' do
|
|
32
|
+
it 'returns true when above threshold' do
|
|
33
|
+
unit.activate(level: 0.5)
|
|
34
|
+
expect(unit.active?).to be true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'returns false when below threshold' do
|
|
38
|
+
expect(unit.active?).to be false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#connect' do
|
|
43
|
+
it 'creates a connection' do
|
|
44
|
+
unit.connect(:neuron_b, weight: 0.3)
|
|
45
|
+
expect(unit.weight_to(:neuron_b)).to eq(0.3)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'clamps weight' do
|
|
49
|
+
unit.connect(:neuron_b, weight: 2.0)
|
|
50
|
+
expect(unit.weight_to(:neuron_b)).to eq(1.0)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'limits connections' do
|
|
54
|
+
constants::MAX_CONNECTIONS_PER_UNIT.times { |i| unit.connect(:"n_#{i}") }
|
|
55
|
+
unit.connect(:overflow)
|
|
56
|
+
expect(unit.connection_count).to eq(constants::MAX_CONNECTIONS_PER_UNIT)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#strengthen' do
|
|
61
|
+
it 'increases weight' do
|
|
62
|
+
unit.connect(:neuron_b, weight: 0.3)
|
|
63
|
+
unit.strengthen(:neuron_b)
|
|
64
|
+
expect(unit.weight_to(:neuron_b)).to eq(0.3 + constants::LEARNING_RATE)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'caps at MAX_WEIGHT' do
|
|
68
|
+
unit.connect(:neuron_b, weight: 0.98)
|
|
69
|
+
unit.strengthen(:neuron_b)
|
|
70
|
+
expect(unit.weight_to(:neuron_b)).to eq(constants::MAX_WEIGHT)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '#weaken' do
|
|
75
|
+
it 'decreases weight' do
|
|
76
|
+
unit.connect(:neuron_b, weight: 0.5)
|
|
77
|
+
unit.weaken(:neuron_b)
|
|
78
|
+
expect(unit.weight_to(:neuron_b)).to eq(0.5 - constants::COMPETITION_FACTOR)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe '#decay_weights' do
|
|
83
|
+
it 'decays all connections' do
|
|
84
|
+
unit.connect(:neuron_b, weight: 0.5)
|
|
85
|
+
unit.decay_weights
|
|
86
|
+
expect(unit.weight_to(:neuron_b)).to eq(0.5 - constants::WEIGHT_DECAY)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'prunes connections at floor' do
|
|
90
|
+
unit.connect(:neuron_b, weight: constants::WEIGHT_FLOOR + 0.001)
|
|
91
|
+
unit.decay_weights
|
|
92
|
+
expect(unit.connection_count).to eq(0)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe '#weight_label' do
|
|
97
|
+
it 'returns label for weight' do
|
|
98
|
+
unit.connect(:neuron_b, weight: 0.9)
|
|
99
|
+
expect(unit.weight_label(:neuron_b)).to eq(:bonded)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe '#strongest_connections' do
|
|
104
|
+
it 'returns top connections sorted by weight' do
|
|
105
|
+
unit.connect(:a, weight: 0.3)
|
|
106
|
+
unit.connect(:b, weight: 0.8)
|
|
107
|
+
unit.connect(:c, weight: 0.5)
|
|
108
|
+
top = unit.strongest_connections(2)
|
|
109
|
+
expect(top.keys).to eq(%i[b c])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe '#to_h' do
|
|
114
|
+
it 'returns hash' do
|
|
115
|
+
h = unit.to_h
|
|
116
|
+
expect(h).to include(:id, :domain, :activation_level, :active, :connections, :activation_count)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::HebbianAssembly::Runners::HebbianAssembly do
|
|
4
|
+
let(:client) { Legion::Extensions::HebbianAssembly::Client.new }
|
|
5
|
+
|
|
6
|
+
describe '#activate_unit' do
|
|
7
|
+
it 'activates a unit' do
|
|
8
|
+
result = client.activate_unit(id: :a, level: 0.8, domain: :cognition)
|
|
9
|
+
expect(result[:success]).to be true
|
|
10
|
+
expect(result[:unit][:id]).to eq(:a)
|
|
11
|
+
expect(result[:unit][:active]).to be true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe '#co_activate_units' do
|
|
16
|
+
it 'co-activates multiple units' do
|
|
17
|
+
result = client.co_activate_units(ids: %i[a b c])
|
|
18
|
+
expect(result[:success]).to be true
|
|
19
|
+
expect(result[:units].size).to eq(3)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '#query_weight' do
|
|
24
|
+
it 'returns weight between units' do
|
|
25
|
+
client.co_activate_units(ids: %i[a b])
|
|
26
|
+
result = client.query_weight(from: :a, to: :b)
|
|
27
|
+
expect(result[:success]).to be true
|
|
28
|
+
expect(result[:weight]).to be > 0
|
|
29
|
+
expect(result[:label]).to be_a(Symbol)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'returns 0 for unconnected' do
|
|
33
|
+
result = client.query_weight(from: :x, to: :y)
|
|
34
|
+
expect(result[:weight]).to eq(0.0)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#list_assemblies' do
|
|
39
|
+
it 'lists assemblies' do
|
|
40
|
+
10.times { client.co_activate_units(ids: %i[a b c]) }
|
|
41
|
+
result = client.list_assemblies
|
|
42
|
+
expect(result[:success]).to be true
|
|
43
|
+
expect(result[:count]).to be >= 1
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '#query_assembly' do
|
|
48
|
+
it 'queries a specific assembly' do
|
|
49
|
+
10.times { client.co_activate_units(ids: %i[a b c]) }
|
|
50
|
+
all = client.list_assemblies
|
|
51
|
+
id = all[:assemblies].first[:id]
|
|
52
|
+
result = client.query_assembly(id: id)
|
|
53
|
+
expect(result[:success]).to be true
|
|
54
|
+
expect(result[:assembly][:members]).to include(:a, :b, :c)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'returns not_found for unknown' do
|
|
58
|
+
result = client.query_assembly(id: :nonexistent)
|
|
59
|
+
expect(result[:success]).to be false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#pattern_complete' do
|
|
64
|
+
it 'completes partial pattern' do
|
|
65
|
+
10.times { client.co_activate_units(ids: %i[a b c]) }
|
|
66
|
+
result = client.pattern_complete(partial_ids: %i[a b])
|
|
67
|
+
expect(result[:success]).to be true
|
|
68
|
+
expect(result[:completion][:predicted]).to include(:c)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'returns failure when no match' do
|
|
72
|
+
result = client.pattern_complete(partial_ids: [:unknown])
|
|
73
|
+
expect(result[:success]).to be false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#strongest_units' do
|
|
78
|
+
it 'returns most activated units' do
|
|
79
|
+
5.times { client.activate_unit(id: :freq) }
|
|
80
|
+
client.activate_unit(id: :rare)
|
|
81
|
+
result = client.strongest_units(limit: 1)
|
|
82
|
+
expect(result[:units].first[:id]).to eq(:freq)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe '#assemblies_for' do
|
|
87
|
+
it 'finds assemblies for a unit' do
|
|
88
|
+
10.times { client.co_activate_units(ids: %i[a b c]) }
|
|
89
|
+
result = client.assemblies_for(unit_id: :a)
|
|
90
|
+
expect(result[:success]).to be true
|
|
91
|
+
expect(result[:count]).to be >= 1
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe '#update_hebbian' do
|
|
96
|
+
it 'runs decay tick' do
|
|
97
|
+
result = client.update_hebbian
|
|
98
|
+
expect(result[:success]).to be true
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe '#hebbian_stats' do
|
|
103
|
+
it 'returns stats' do
|
|
104
|
+
result = client.hebbian_stats
|
|
105
|
+
expect(result[:success]).to be true
|
|
106
|
+
expect(result[:stats]).to include(:units, :assemblies, :total_connections)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Logging
|
|
7
|
+
def self.debug(_msg); end
|
|
8
|
+
def self.info(_msg); end
|
|
9
|
+
def self.warn(_msg); end
|
|
10
|
+
def self.error(_msg); end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'legion/extensions/hebbian_assembly'
|
|
15
|
+
|
|
16
|
+
RSpec.configure do |config|
|
|
17
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
18
|
+
config.disable_monkey_patching!
|
|
19
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-hebbian-assembly
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: legion-gaia
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Hebb's Cell Assembly Theory for brain-modeled agentic AI — neurons that
|
|
27
|
+
fire together wire together, forming emergent assemblies that represent concepts,
|
|
28
|
+
enable pattern completion, and support associative recall through co-activation-driven
|
|
29
|
+
synaptic strengthening.
|
|
30
|
+
email:
|
|
31
|
+
- matthewdiverson@gmail.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- Gemfile
|
|
37
|
+
- lex-hebbian-assembly.gemspec
|
|
38
|
+
- lib/legion/extensions/hebbian_assembly.rb
|
|
39
|
+
- lib/legion/extensions/hebbian_assembly/actors/decay.rb
|
|
40
|
+
- lib/legion/extensions/hebbian_assembly/client.rb
|
|
41
|
+
- lib/legion/extensions/hebbian_assembly/helpers/assembly.rb
|
|
42
|
+
- lib/legion/extensions/hebbian_assembly/helpers/assembly_network.rb
|
|
43
|
+
- lib/legion/extensions/hebbian_assembly/helpers/constants.rb
|
|
44
|
+
- lib/legion/extensions/hebbian_assembly/helpers/unit.rb
|
|
45
|
+
- lib/legion/extensions/hebbian_assembly/runners/hebbian_assembly.rb
|
|
46
|
+
- lib/legion/extensions/hebbian_assembly/version.rb
|
|
47
|
+
- spec/legion/extensions/hebbian_assembly/client_spec.rb
|
|
48
|
+
- spec/legion/extensions/hebbian_assembly/helpers/assembly_network_spec.rb
|
|
49
|
+
- spec/legion/extensions/hebbian_assembly/helpers/assembly_spec.rb
|
|
50
|
+
- spec/legion/extensions/hebbian_assembly/helpers/unit_spec.rb
|
|
51
|
+
- spec/legion/extensions/hebbian_assembly/runners/hebbian_assembly_spec.rb
|
|
52
|
+
- spec/spec_helper.rb
|
|
53
|
+
homepage: https://github.com/LegionIO/lex-hebbian-assembly
|
|
54
|
+
licenses:
|
|
55
|
+
- MIT
|
|
56
|
+
metadata:
|
|
57
|
+
homepage_uri: https://github.com/LegionIO/lex-hebbian-assembly
|
|
58
|
+
source_code_uri: https://github.com/LegionIO/lex-hebbian-assembly
|
|
59
|
+
documentation_uri: https://github.com/LegionIO/lex-hebbian-assembly
|
|
60
|
+
changelog_uri: https://github.com/LegionIO/lex-hebbian-assembly
|
|
61
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-hebbian-assembly/issues
|
|
62
|
+
rubygems_mfa_required: 'true'
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '3.4'
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 3.6.9
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: LEX Hebbian Assembly
|
|
80
|
+
test_files: []
|