lex-feature-binding 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d449f9894bc3dc97a55225d9f6a6878a05fc4a73fae9641bf9825119188e8862
4
+ data.tar.gz: a9459e1caf0817290a4c96ecd8c4b3afee36f8b308b1d00f828f30bc697515b1
5
+ SHA512:
6
+ metadata.gz: ac2f5a591b2c72076d2870b3601591569121b08b94d2400a32e8f44155b1f0eb7f2d406f440f1991a1d2e78c58406d0e7d9d278bdb3f0d253652fb49638b8d8d
7
+ data.tar.gz: 85d294af88ba119e6282609b25c2afe7e5100511aac34b6a12c8731f8278284e6ac56953a8d5e975b0d7e1b73d426e33c0992b5bb471cea8d371f8dead9759bc
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # lex-feature-binding
2
+
3
+ Feature binding modeling for the LegionIO brain-modeled cognitive architecture.
4
+
5
+ ## What It Does
6
+
7
+ Implements the cognitive binding problem — how separate features from different processing streams are combined into unified object percepts. Registers unbound feature sets across visual, auditory, semantic, spatial, temporal, and affective dimensions, then binds them into coherent bound objects. Detects binding conflicts when feature dimensions contradict each other, and manages a vividness-based object store with decay.
8
+
9
+ Based on Treisman's Feature Integration Theory.
10
+
11
+ ## Usage
12
+
13
+ ```ruby
14
+ client = Legion::Extensions::FeatureBinding::Client.new
15
+
16
+ # Register incoming features from multiple sensory streams
17
+ client.register_features(
18
+ features: {
19
+ semantic: 'incoming HTTP request',
20
+ temporal: :recent,
21
+ affective: :neutral,
22
+ spatial: :network_boundary
23
+ }
24
+ )
25
+ # => { success: true, feature_set_id: "...", dimensions_present: [:semantic, :temporal, :affective, :spatial],
26
+ # ready_to_bind: true }
27
+
28
+ # Bind into a unified object percept
29
+ client.bind_features(feature_set_id: '...', binding_mode: :automatic)
30
+ # => { success: true, object_id: "...", coherence: 0.75, bound: true, conflicts: [] }
31
+
32
+ # Check for feature conflicts before binding
33
+ client.detect_conflicts(feature_set_id: '...')
34
+ # => { conflicts: [], conflict_count: 0, severity: :none }
35
+
36
+ # Attend to an object to boost its coherence
37
+ client.attend_to_object(object_id: '...')
38
+ # => { coherence_boost: 0.2, new_coherence: 0.95 }
39
+
40
+ # View current bound objects
41
+ client.bound_objects
42
+ # => { objects: [...sorted by vividness], count: 5 }
43
+
44
+ # Overall binding quality
45
+ client.binding_quality
46
+ # => { avg_coherence: 0.72, coherence_label: :coherent, conflict_rate: 0.05 }
47
+
48
+ # Periodic tick: decay vividness, archive faded objects
49
+ client.update_feature_binding
50
+ ```
51
+
52
+ ## Development
53
+
54
+ ```bash
55
+ bundle install
56
+ bundle exec rspec
57
+ bundle exec rubocop
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT
@@ -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 FeatureBinding
8
+ module Actor
9
+ class Decay < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::FeatureBinding::Runners::FeatureBinding
12
+ end
13
+
14
+ def runner_function
15
+ 'update_feature_binding'
16
+ end
17
+
18
+ def time
19
+ 30
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/feature_binding/helpers/constants'
4
+ require 'legion/extensions/feature_binding/helpers/feature'
5
+ require 'legion/extensions/feature_binding/helpers/bound_object'
6
+ require 'legion/extensions/feature_binding/helpers/binding_field'
7
+ require 'legion/extensions/feature_binding/runners/feature_binding'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module FeatureBinding
12
+ class Client
13
+ include Runners::FeatureBinding
14
+
15
+ def initialize(field: nil, **)
16
+ @field = field || Helpers::BindingField.new
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :field
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FeatureBinding
6
+ module Helpers
7
+ class BindingField
8
+ include Constants
9
+
10
+ attr_reader :features, :objects, :binding_history
11
+
12
+ def initialize
13
+ @features = {}
14
+ @objects = {}
15
+ @binding_history = []
16
+ @object_counter = 0
17
+ end
18
+
19
+ def register_feature(id:, dimension:, value:, source: :perception, salience: 0.5)
20
+ return @features[id] if @features.key?(id)
21
+ return nil if @features.size >= MAX_FEATURES
22
+
23
+ @features[id] = Feature.new(id: id, dimension: dimension, value: value, source: source, salience: salience)
24
+ end
25
+
26
+ def bind(feature_ids:, attention: false)
27
+ ids = Array(feature_ids).select { |fid| @features.key?(fid) }
28
+ return nil if ids.size < 2
29
+ return nil if @objects.size >= MAX_OBJECTS
30
+
31
+ @object_counter += 1
32
+ obj_id = :"object_#{@object_counter}"
33
+ strength = attention ? DEFAULT_BINDING_STRENGTH + ATTENTION_BOOST : DEFAULT_BINDING_STRENGTH
34
+
35
+ obj = BoundObject.new(id: obj_id, feature_ids: ids, binding_strength: strength)
36
+ obj.confirm if attention
37
+ @objects[obj_id] = obj
38
+
39
+ record_binding(obj_id, ids, attention)
40
+ obj
41
+ end
42
+
43
+ def attend(object_id:)
44
+ obj = @objects[object_id]
45
+ return nil unless obj
46
+
47
+ obj.strengthen
48
+ obj.confirm unless obj.confirmed?
49
+ obj
50
+ end
51
+
52
+ def unbind(object_id:)
53
+ obj = @objects.delete(object_id)
54
+ !obj.nil?
55
+ end
56
+
57
+ def features_of(object_id:)
58
+ obj = @objects[object_id]
59
+ return [] unless obj
60
+
61
+ obj.feature_ids.filter_map { |fid| @features[fid]&.to_h }
62
+ end
63
+
64
+ def objects_with_feature(feature_id:)
65
+ @objects.values.select { |o| o.includes_feature?(feature_id) }.map(&:to_h)
66
+ end
67
+
68
+ def illusory_conjunctions
69
+ @objects.values.select { |o| o.state == :illusory }.map(&:to_h)
70
+ end
71
+
72
+ def unbound_features
73
+ bound_ids = @objects.values.flat_map(&:feature_ids).uniq
74
+ @features.values.reject { |f| bound_ids.include?(f.id) }.map(&:to_h)
75
+ end
76
+
77
+ def search_by_dimension(dimension:, value: nil)
78
+ matches = @features.values.select { |f| f.dimension == dimension }
79
+ matches = matches.select { |f| f.value == value } if value
80
+ matches.map(&:to_h)
81
+ end
82
+
83
+ def decay_all
84
+ @features.each_value(&:decay)
85
+ @features.reject! { |_, f| f.faded? }
86
+
87
+ @objects.each_value(&:decay)
88
+ @objects.reject! { |_, o| o.dissolved? }
89
+ end
90
+
91
+ def feature_count
92
+ @features.size
93
+ end
94
+
95
+ def object_count
96
+ @objects.size
97
+ end
98
+
99
+ def to_h
100
+ {
101
+ features: @features.size,
102
+ objects: @objects.size,
103
+ confirmed_objects: @objects.values.count(&:confirmed?),
104
+ illusory_count: @objects.values.count { |o| o.state == :illusory },
105
+ unbound_features: unbound_features.size,
106
+ history_size: @binding_history.size
107
+ }
108
+ end
109
+
110
+ private
111
+
112
+ def record_binding(obj_id, feature_ids, attended)
113
+ @binding_history << { object_id: obj_id, features: feature_ids, attended: attended, at: Time.now.utc }
114
+ @binding_history.shift while @binding_history.size > MAX_BINDING_HISTORY
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FeatureBinding
6
+ module Helpers
7
+ class BoundObject
8
+ include Constants
9
+
10
+ attr_reader :id, :feature_ids, :bound_at, :confirmed_at
11
+ attr_accessor :binding_strength
12
+
13
+ def initialize(id:, feature_ids:, binding_strength: DEFAULT_BINDING_STRENGTH)
14
+ @id = id
15
+ @feature_ids = Array(feature_ids).uniq
16
+ @binding_strength = binding_strength.to_f.clamp(0.0, 1.0)
17
+ @bound_at = Time.now.utc
18
+ @confirmed_at = nil
19
+ end
20
+
21
+ def confirm
22
+ @confirmed_at = Time.now.utc
23
+ @binding_strength = [@binding_strength + ATTENTION_BOOST, 1.0].min
24
+ end
25
+
26
+ def confirmed?
27
+ !@confirmed_at.nil?
28
+ end
29
+
30
+ def strengthen(amount = ATTENTION_BOOST)
31
+ @binding_strength = [@binding_strength + amount, 1.0].min
32
+ end
33
+
34
+ def decay
35
+ @binding_strength = [@binding_strength - BINDING_DECAY, 0.0].max
36
+ end
37
+
38
+ def dissolved?
39
+ @binding_strength <= BINDING_STRENGTH_FLOOR
40
+ end
41
+
42
+ def state
43
+ if @binding_strength >= BINDING_CONFIRMATION_THRESHOLD && confirmed?
44
+ :bound
45
+ elsif @binding_strength >= ILLUSORY_CONJUNCTION_THRESHOLD
46
+ :tentative
47
+ elsif @binding_strength > BINDING_STRENGTH_FLOOR
48
+ :illusory
49
+ else
50
+ :unbound
51
+ end
52
+ end
53
+
54
+ def feature_count
55
+ @feature_ids.size
56
+ end
57
+
58
+ def includes_feature?(feature_id)
59
+ @feature_ids.include?(feature_id)
60
+ end
61
+
62
+ def strength_label
63
+ STRENGTH_LABELS.each { |range, lbl| return lbl if range.cover?(@binding_strength) }
64
+ :dissolving
65
+ end
66
+
67
+ def to_h
68
+ {
69
+ id: @id,
70
+ features: @feature_ids.dup,
71
+ feature_count: feature_count,
72
+ binding_strength: @binding_strength.round(4),
73
+ strength_label: strength_label,
74
+ state: state,
75
+ state_label: BINDING_STATE_LABELS[state],
76
+ confirmed: confirmed?,
77
+ bound_at: @bound_at,
78
+ confirmed_at: @confirmed_at
79
+ }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FeatureBinding
6
+ module Helpers
7
+ module Constants
8
+ MAX_FEATURES = 200
9
+ MAX_BINDINGS = 100
10
+ MAX_OBJECTS = 50
11
+ MAX_BINDING_HISTORY = 200
12
+
13
+ BINDING_STRENGTH_FLOOR = 0.05
14
+ BINDING_DECAY = 0.01
15
+ BINDING_ALPHA = 0.15
16
+ ATTENTION_BOOST = 0.3
17
+ DEFAULT_BINDING_STRENGTH = 0.3
18
+
19
+ ILLUSORY_CONJUNCTION_THRESHOLD = 0.3
20
+ BINDING_CONFIRMATION_THRESHOLD = 0.6
21
+ FEATURE_SALIENCE_FLOOR = 0.05
22
+
23
+ FEATURE_DIMENSIONS = %i[
24
+ shape color size motion texture location
25
+ pitch volume timbre temporal_pattern
26
+ semantic syntactic pragmatic
27
+ valence arousal familiarity
28
+ ].freeze
29
+
30
+ BINDING_STATE_LABELS = {
31
+ bound: 'features unified into coherent object',
32
+ tentative: 'binding forming, not yet confirmed',
33
+ illusory: 'incorrect binding (conjunction error)',
34
+ unbound: 'features floating in pre-attentive field'
35
+ }.freeze
36
+
37
+ STRENGTH_LABELS = {
38
+ (0.8..) => :solid,
39
+ (0.6...0.8) => :firm,
40
+ (0.4...0.6) => :forming,
41
+ (0.2...0.4) => :weak,
42
+ (..0.2) => :dissolving
43
+ }.freeze
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FeatureBinding
6
+ module Helpers
7
+ class Feature
8
+ include Constants
9
+
10
+ attr_reader :id, :dimension, :value, :source, :detected_at
11
+ attr_accessor :salience
12
+
13
+ def initialize(id:, dimension:, value:, source: :perception, salience: 0.5)
14
+ @id = id
15
+ @dimension = dimension
16
+ @value = value
17
+ @source = source
18
+ @salience = salience.to_f.clamp(0.0, 1.0)
19
+ @detected_at = Time.now.utc
20
+ end
21
+
22
+ def salient?
23
+ @salience > FEATURE_SALIENCE_FLOOR
24
+ end
25
+
26
+ def decay
27
+ @salience = [@salience - BINDING_DECAY, 0.0].max
28
+ end
29
+
30
+ def faded?
31
+ @salience <= FEATURE_SALIENCE_FLOOR
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ id: @id,
37
+ dimension: @dimension,
38
+ value: @value,
39
+ source: @source,
40
+ salience: @salience.round(4),
41
+ detected_at: @detected_at
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FeatureBinding
6
+ module Runners
7
+ module FeatureBinding
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def register_feature(id:, dimension:, value:, source: :perception, salience: 0.5, **)
12
+ Legion::Logging.debug "[feature_binding] register: id=#{id} dim=#{dimension} val=#{value}"
13
+ feature = field.register_feature(id: id, dimension: dimension, value: value, source: source,
14
+ salience: salience)
15
+ if feature
16
+ { success: true, feature: feature.to_h, total_features: field.feature_count }
17
+ else
18
+ { success: false, reason: :limit_reached }
19
+ end
20
+ end
21
+
22
+ def bind_features(feature_ids:, attention: false, **)
23
+ Legion::Logging.debug "[feature_binding] bind: features=#{feature_ids} attention=#{attention}"
24
+ obj = field.bind(feature_ids: feature_ids, attention: attention)
25
+ if obj
26
+ { success: true, object: obj.to_h, total_objects: field.object_count }
27
+ else
28
+ { success: false, reason: :insufficient_features }
29
+ end
30
+ end
31
+
32
+ def attend_object(object_id:, **)
33
+ Legion::Logging.debug "[feature_binding] attend: object=#{object_id}"
34
+ obj = field.attend(object_id: object_id.to_sym)
35
+ if obj
36
+ { success: true, object: obj.to_h }
37
+ else
38
+ { success: false, reason: :not_found }
39
+ end
40
+ end
41
+
42
+ def unbind_object(object_id:, **)
43
+ Legion::Logging.debug "[feature_binding] unbind: object=#{object_id}"
44
+ removed = field.unbind(object_id: object_id.to_sym)
45
+ { success: removed }
46
+ end
47
+
48
+ def features_of_object(object_id:, **)
49
+ features = field.features_of(object_id: object_id.to_sym)
50
+ Legion::Logging.debug "[feature_binding] features_of: object=#{object_id} count=#{features.size}"
51
+ { success: true, features: features, count: features.size }
52
+ end
53
+
54
+ def objects_with(feature_id:, **)
55
+ objects = field.objects_with_feature(feature_id: feature_id)
56
+ Legion::Logging.debug "[feature_binding] objects_with: feature=#{feature_id} count=#{objects.size}"
57
+ { success: true, objects: objects, count: objects.size }
58
+ end
59
+
60
+ def illusory_conjunctions(**)
61
+ illusions = field.illusory_conjunctions
62
+ Legion::Logging.debug "[feature_binding] illusory: #{illusions.size}"
63
+ { success: true, illusory: illusions, count: illusions.size }
64
+ end
65
+
66
+ def unbound_features(**)
67
+ unbound = field.unbound_features
68
+ Legion::Logging.debug "[feature_binding] unbound: #{unbound.size}"
69
+ { success: true, unbound: unbound, count: unbound.size }
70
+ end
71
+
72
+ def search_features(dimension:, value: nil, **)
73
+ results = field.search_by_dimension(dimension: dimension, value: value)
74
+ Legion::Logging.debug "[feature_binding] search: dim=#{dimension} found=#{results.size}"
75
+ { success: true, results: results, count: results.size }
76
+ end
77
+
78
+ def update_feature_binding(**)
79
+ Legion::Logging.debug '[feature_binding] tick'
80
+ field.decay_all
81
+ { success: true, features: field.feature_count, objects: field.object_count }
82
+ end
83
+
84
+ def feature_binding_stats(**)
85
+ Legion::Logging.debug '[feature_binding] stats'
86
+ { success: true, stats: field.to_h }
87
+ end
88
+
89
+ private
90
+
91
+ def field
92
+ @field ||= Helpers::BindingField.new
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module FeatureBinding
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/feature_binding/version'
4
+ require 'legion/extensions/feature_binding/helpers/constants'
5
+ require 'legion/extensions/feature_binding/helpers/feature'
6
+ require 'legion/extensions/feature_binding/helpers/bound_object'
7
+ require 'legion/extensions/feature_binding/helpers/binding_field'
8
+ require 'legion/extensions/feature_binding/runners/feature_binding'
9
+ require 'legion/extensions/feature_binding/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module FeatureBinding
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-feature-binding
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: Treisman's Feature Integration Theory for LegionIO — attention as glue
27
+ binding features into unified percepts
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - README.md
35
+ - lib/legion/extensions/feature_binding.rb
36
+ - lib/legion/extensions/feature_binding/actors/decay.rb
37
+ - lib/legion/extensions/feature_binding/client.rb
38
+ - lib/legion/extensions/feature_binding/helpers/binding_field.rb
39
+ - lib/legion/extensions/feature_binding/helpers/bound_object.rb
40
+ - lib/legion/extensions/feature_binding/helpers/constants.rb
41
+ - lib/legion/extensions/feature_binding/helpers/feature.rb
42
+ - lib/legion/extensions/feature_binding/runners/feature_binding.rb
43
+ - lib/legion/extensions/feature_binding/version.rb
44
+ homepage: https://github.com/LegionIO/lex-feature-binding
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ homepage_uri: https://github.com/LegionIO/lex-feature-binding
49
+ source_code_uri: https://github.com/LegionIO/lex-feature-binding
50
+ documentation_uri: https://github.com/LegionIO/lex-feature-binding/blob/master/README.md
51
+ changelog_uri: https://github.com/LegionIO/lex-feature-binding/blob/master/CHANGELOG.md
52
+ bug_tracker_uri: https://github.com/LegionIO/lex-feature-binding/issues
53
+ rubygems_mfa_required: 'true'
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.6.9
69
+ specification_version: 4
70
+ summary: LegionIO feature binding extension
71
+ test_files: []