lex-enactive-cognition 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-enactive-cognition.gemspec +29 -0
- data/lib/legion/extensions/enactive_cognition/client.rb +23 -0
- data/lib/legion/extensions/enactive_cognition/helpers/enaction_engine.rb +116 -0
- data/lib/legion/extensions/enactive_cognition/helpers/sensorimotor_loop.rb +118 -0
- data/lib/legion/extensions/enactive_cognition/runners/enactive_cognition.rb +120 -0
- data/lib/legion/extensions/enactive_cognition/version.rb +9 -0
- data/lib/legion/extensions/enactive_cognition.rb +14 -0
- data/spec/legion/extensions/enactive_cognition/client_spec.rb +19 -0
- data/spec/legion/extensions/enactive_cognition/helpers/enaction_engine_spec.rb +181 -0
- data/spec/legion/extensions/enactive_cognition/helpers/sensorimotor_loop_spec.rb +184 -0
- data/spec/legion/extensions/enactive_cognition/runners/enactive_cognition_spec.rb +214 -0
- data/spec/spec_helper.rb +20 -0
- metadata +74 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bbed0f46a25d2c4adf962bc34b11afb26c788647d1df2d5a6576ff2adb37fe10
|
|
4
|
+
data.tar.gz: 32fc5bbcf961735ed8c9817bba4be8e558835fb25ac0392375b464d9a65fdef0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2999787b0454927f7cae93ce1f84831fdd0b21dfe4e3742a13b16820acaaf55ff4f7bd56fd730b930f4f3f06b2e1ed85bc80326059a3365b09c70673d2a301d3
|
|
7
|
+
data.tar.gz: c93bbe503a804b7b4bb8f16dd9c3e2aa05aec38a00ffe69dbe086ca1e73d990efb58298e9ae71222d56fdb1bd9815ab57aaf1f0bea4093b24dfd08c4d81c4da4
|
data/Gemfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/enactive_cognition/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-enactive-cognition'
|
|
7
|
+
spec.version = Legion::Extensions::EnactiveCognition::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Enactive Cognition'
|
|
12
|
+
spec.description = "Varela's enactivism: cognition through action-perception loops, sensorimotor contingencies, and structural coupling"
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-enactive-cognition'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-enactive-cognition'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-enactive-cognition'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-enactive-cognition'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-enactive-cognition/issues'
|
|
22
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-enactive-cognition.gemspec Gemfile]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/enactive_cognition/helpers/sensorimotor_loop'
|
|
4
|
+
require 'legion/extensions/enactive_cognition/helpers/enaction_engine'
|
|
5
|
+
require 'legion/extensions/enactive_cognition/runners/enactive_cognition'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module EnactiveCognition
|
|
10
|
+
class Client
|
|
11
|
+
include Runners::EnactiveCognition
|
|
12
|
+
|
|
13
|
+
def initialize(**)
|
|
14
|
+
@enaction_engine = Helpers::EnactionEngine.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :enaction_engine
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module EnactiveCognition
|
|
6
|
+
module Helpers
|
|
7
|
+
class EnactionEngine
|
|
8
|
+
attr_reader :couplings
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@couplings = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_coupling(action:, perception:, domain:, loop_type: :sensorimotor)
|
|
15
|
+
prune_to_limit
|
|
16
|
+
|
|
17
|
+
loop = SensorimotorLoop.new(
|
|
18
|
+
action: action,
|
|
19
|
+
perception: perception,
|
|
20
|
+
domain: domain,
|
|
21
|
+
loop_type: loop_type
|
|
22
|
+
)
|
|
23
|
+
@couplings[loop.id] = loop
|
|
24
|
+
loop
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def execute_action(coupling_id:, actual_perception:)
|
|
28
|
+
loop = @couplings[coupling_id]
|
|
29
|
+
return { success: false, reason: :not_found } unless loop
|
|
30
|
+
|
|
31
|
+
result = loop.execute!(actual_perception: actual_perception)
|
|
32
|
+
{
|
|
33
|
+
success: true,
|
|
34
|
+
coupling_id: coupling_id,
|
|
35
|
+
match: result[:match],
|
|
36
|
+
coupling_strength: result[:coupling_strength],
|
|
37
|
+
prediction_accuracy: result[:prediction_accuracy],
|
|
38
|
+
coupling_label: loop.coupling_label
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def adapt_coupling(coupling_id:, new_perception:)
|
|
43
|
+
loop = @couplings[coupling_id]
|
|
44
|
+
return { success: false, reason: :not_found } unless loop
|
|
45
|
+
|
|
46
|
+
loop.adapt_perception!(new_perception: new_perception)
|
|
47
|
+
{ success: true, coupling_id: coupling_id, new_perception: new_perception }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def find_action_for(perception:)
|
|
51
|
+
best = @couplings.values
|
|
52
|
+
.select(&:coupled?)
|
|
53
|
+
.select { |lp| lp.perception.to_s == perception.to_s }
|
|
54
|
+
.max_by(&:coupling_strength)
|
|
55
|
+
return nil unless best
|
|
56
|
+
|
|
57
|
+
best
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def coupled_loops
|
|
61
|
+
@couplings.values.select(&:coupled?)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def by_domain(domain:)
|
|
65
|
+
@couplings.values.select { |lp| lp.domain.to_s == domain.to_s }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def by_type(loop_type:)
|
|
69
|
+
@couplings.values.select { |lp| lp.loop_type == loop_type }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def strongest_couplings(limit: 5)
|
|
73
|
+
@couplings.values.sort_by { |lp| -lp.coupling_strength }.first(limit)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def overall_coupling
|
|
77
|
+
return 0.0 if @couplings.empty?
|
|
78
|
+
|
|
79
|
+
total = @couplings.values.sum(&:coupling_strength)
|
|
80
|
+
total / @couplings.size
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def decay_all
|
|
84
|
+
@couplings.each_value(&:decay!)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def prune_decoupled
|
|
88
|
+
@couplings.delete_if { |_id, lp| lp.coupling_strength < SensorimotorLoop::COUPLING_FLOOR + 0.05 }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def count
|
|
92
|
+
@couplings.size
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_h
|
|
96
|
+
{
|
|
97
|
+
coupling_count: @couplings.size,
|
|
98
|
+
coupled_count: coupled_loops.size,
|
|
99
|
+
overall_coupling: overall_coupling.round(4),
|
|
100
|
+
strongest: strongest_couplings(limit: 3).map(&:to_h)
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def prune_to_limit
|
|
107
|
+
return unless @couplings.size >= SensorimotorLoop::MAX_COUPLINGS
|
|
108
|
+
|
|
109
|
+
weakest = @couplings.values.sort_by(&:coupling_strength).first(10)
|
|
110
|
+
weakest.each { |lp| @couplings.delete(lp.id) }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module EnactiveCognition
|
|
8
|
+
module Helpers
|
|
9
|
+
class SensorimotorLoop
|
|
10
|
+
MAX_COUPLINGS = 200
|
|
11
|
+
MAX_ACTIONS = 500
|
|
12
|
+
MAX_PERCEPTIONS = 500
|
|
13
|
+
MAX_HISTORY = 300
|
|
14
|
+
|
|
15
|
+
DEFAULT_COUPLING_STRENGTH = 0.5
|
|
16
|
+
COUPLING_FLOOR = 0.0
|
|
17
|
+
COUPLING_CEILING = 1.0
|
|
18
|
+
|
|
19
|
+
REINFORCEMENT_RATE = 0.1
|
|
20
|
+
DECOUPLING_RATE = 0.15
|
|
21
|
+
|
|
22
|
+
PREDICTION_ACCURACY_THRESHOLD = 0.6
|
|
23
|
+
|
|
24
|
+
COUPLING_DECAY = 0.02
|
|
25
|
+
STALE_THRESHOLD = 120
|
|
26
|
+
|
|
27
|
+
COUPLING_LABELS = {
|
|
28
|
+
(0.8..) => :entrained,
|
|
29
|
+
(0.6...0.8) => :coupled,
|
|
30
|
+
(0.4...0.6) => :forming,
|
|
31
|
+
(0.2...0.4) => :weak,
|
|
32
|
+
(..0.2) => :decoupled
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
LOOP_TYPES = %i[sensorimotor cognitive social].freeze
|
|
36
|
+
|
|
37
|
+
attr_reader :id, :action, :perception, :domain, :loop_type,
|
|
38
|
+
:coupling_strength, :prediction_accuracy,
|
|
39
|
+
:execution_count, :accurate_predictions,
|
|
40
|
+
:created_at, :last_executed_at
|
|
41
|
+
|
|
42
|
+
def initialize(action:, perception:, domain:, loop_type: :sensorimotor)
|
|
43
|
+
@id = SecureRandom.uuid
|
|
44
|
+
@action = action
|
|
45
|
+
@perception = perception
|
|
46
|
+
@domain = domain
|
|
47
|
+
@loop_type = LOOP_TYPES.include?(loop_type) ? loop_type : :sensorimotor
|
|
48
|
+
@coupling_strength = DEFAULT_COUPLING_STRENGTH
|
|
49
|
+
@execution_count = 0
|
|
50
|
+
@accurate_predictions = 0
|
|
51
|
+
@prediction_accuracy = 0.0
|
|
52
|
+
@created_at = Time.now.utc
|
|
53
|
+
@last_executed_at = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def execute!(actual_perception:)
|
|
57
|
+
@execution_count += 1
|
|
58
|
+
@last_executed_at = Time.now.utc
|
|
59
|
+
match = actual_perception.to_s == @perception.to_s
|
|
60
|
+
|
|
61
|
+
if match
|
|
62
|
+
@accurate_predictions += 1
|
|
63
|
+
@coupling_strength = (@coupling_strength + REINFORCEMENT_RATE).clamp(COUPLING_FLOOR, COUPLING_CEILING)
|
|
64
|
+
else
|
|
65
|
+
@coupling_strength = (@coupling_strength - DECOUPLING_RATE).clamp(COUPLING_FLOOR, COUPLING_CEILING)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
@prediction_accuracy = @accurate_predictions.to_f / @execution_count
|
|
69
|
+
{ match: match, coupling_strength: @coupling_strength, prediction_accuracy: @prediction_accuracy }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def coupled?
|
|
73
|
+
coupling_strength >= 0.6
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def coupling_label
|
|
77
|
+
COUPLING_LABELS.each do |range, label|
|
|
78
|
+
return label if range.cover?(coupling_strength)
|
|
79
|
+
end
|
|
80
|
+
:decoupled
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def adapt_perception!(new_perception:)
|
|
84
|
+
@perception = new_perception
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def decay!
|
|
88
|
+
@coupling_strength = (@coupling_strength - COUPLING_DECAY).clamp(COUPLING_FLOOR, COUPLING_CEILING)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def stale?
|
|
92
|
+
return false if @last_executed_at.nil?
|
|
93
|
+
|
|
94
|
+
(Time.now.utc - @last_executed_at) > STALE_THRESHOLD
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def to_h
|
|
98
|
+
{
|
|
99
|
+
id: @id,
|
|
100
|
+
action: @action,
|
|
101
|
+
perception: @perception,
|
|
102
|
+
domain: @domain,
|
|
103
|
+
loop_type: @loop_type,
|
|
104
|
+
coupling_strength: @coupling_strength,
|
|
105
|
+
coupling_label: coupling_label,
|
|
106
|
+
prediction_accuracy: @prediction_accuracy,
|
|
107
|
+
execution_count: @execution_count,
|
|
108
|
+
accurate_predictions: @accurate_predictions,
|
|
109
|
+
coupled: coupled?,
|
|
110
|
+
created_at: @created_at,
|
|
111
|
+
last_executed_at: @last_executed_at
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module EnactiveCognition
|
|
6
|
+
module Runners
|
|
7
|
+
module EnactiveCognition
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def create_sensorimotor_coupling(action:, perception:, domain:, loop_type: :sensorimotor, **)
|
|
12
|
+
type = loop_type.is_a?(Symbol) ? loop_type : loop_type.to_sym
|
|
13
|
+
loop = enaction_engine.create_coupling(
|
|
14
|
+
action: action,
|
|
15
|
+
perception: perception,
|
|
16
|
+
domain: domain,
|
|
17
|
+
loop_type: type
|
|
18
|
+
)
|
|
19
|
+
Legion::Logging.debug "[enactive_cognition] created coupling id=#{loop.id[0..7]} " \
|
|
20
|
+
"action=#{action} domain=#{domain} type=#{type}"
|
|
21
|
+
{ success: true, coupling: loop.to_h }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute_enactive_action(coupling_id:, actual_perception:, **)
|
|
25
|
+
result = enaction_engine.execute_action(
|
|
26
|
+
coupling_id: coupling_id,
|
|
27
|
+
actual_perception: actual_perception
|
|
28
|
+
)
|
|
29
|
+
unless result[:success]
|
|
30
|
+
Legion::Logging.debug "[enactive_cognition] execute failed: #{coupling_id[0..7]} not found"
|
|
31
|
+
return result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Legion::Logging.debug "[enactive_cognition] executed #{coupling_id[0..7]} " \
|
|
35
|
+
"match=#{result[:match]} label=#{result[:coupling_label]}"
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def adapt_sensorimotor_coupling(coupling_id:, new_perception:, **)
|
|
40
|
+
result = enaction_engine.adapt_coupling(
|
|
41
|
+
coupling_id: coupling_id,
|
|
42
|
+
new_perception: new_perception
|
|
43
|
+
)
|
|
44
|
+
unless result[:success]
|
|
45
|
+
Legion::Logging.debug "[enactive_cognition] adapt failed: #{coupling_id[0..7]} not found"
|
|
46
|
+
return result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Legion::Logging.info "[enactive_cognition] adapted #{coupling_id[0..7]} new_perception=#{new_perception}"
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def find_action_for_perception(perception:, **)
|
|
54
|
+
loop = enaction_engine.find_action_for(perception: perception)
|
|
55
|
+
unless loop
|
|
56
|
+
Legion::Logging.debug "[enactive_cognition] no coupled action found for perception=#{perception}"
|
|
57
|
+
return { found: false, perception: perception }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
Legion::Logging.debug "[enactive_cognition] found action=#{loop.action} for perception=#{perception}"
|
|
61
|
+
{ found: true, action: loop.action, coupling: loop.to_h }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def coupled_sensorimotor_loops(**)
|
|
65
|
+
loops = enaction_engine.coupled_loops
|
|
66
|
+
Legion::Logging.debug "[enactive_cognition] coupled loops count=#{loops.size}"
|
|
67
|
+
{ loops: loops.map(&:to_h), count: loops.size }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def domain_couplings(domain:, **)
|
|
71
|
+
loops = enaction_engine.by_domain(domain: domain)
|
|
72
|
+
Legion::Logging.debug "[enactive_cognition] domain=#{domain} loops=#{loops.size}"
|
|
73
|
+
{ domain: domain, loops: loops.map(&:to_h), count: loops.size }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def strongest_couplings(limit: 5, **)
|
|
77
|
+
loops = enaction_engine.strongest_couplings(limit: limit)
|
|
78
|
+
Legion::Logging.debug "[enactive_cognition] strongest couplings limit=#{limit} found=#{loops.size}"
|
|
79
|
+
{ loops: loops.map(&:to_h), count: loops.size }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def overall_enactive_coupling(**)
|
|
83
|
+
strength = enaction_engine.overall_coupling
|
|
84
|
+
label = coupling_label_for(strength)
|
|
85
|
+
Legion::Logging.debug "[enactive_cognition] overall_coupling=#{strength.round(3)} label=#{label}"
|
|
86
|
+
{ overall_coupling: strength, coupling_label: label }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def update_enactive_cognition(decay: false, prune: false, **)
|
|
90
|
+
enaction_engine.decay_all if decay
|
|
91
|
+
enaction_engine.prune_decoupled if prune
|
|
92
|
+
Legion::Logging.debug "[enactive_cognition] update decay=#{decay} prune=#{prune} " \
|
|
93
|
+
"remaining=#{enaction_engine.count}"
|
|
94
|
+
{ success: true, decay: decay, prune: prune, coupling_count: enaction_engine.count }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def enactive_cognition_stats(**)
|
|
98
|
+
stats = enaction_engine.to_h
|
|
99
|
+
Legion::Logging.debug "[enactive_cognition] stats couplings=#{stats[:coupling_count]} " \
|
|
100
|
+
"coupled=#{stats[:coupled_count]}"
|
|
101
|
+
{ success: true, stats: stats }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def enaction_engine
|
|
107
|
+
@enaction_engine ||= Helpers::EnactionEngine.new
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def coupling_label_for(strength)
|
|
111
|
+
Helpers::SensorimotorLoop::COUPLING_LABELS.each do |range, label|
|
|
112
|
+
return label if range.cover?(strength)
|
|
113
|
+
end
|
|
114
|
+
:decoupled
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/enactive_cognition/version'
|
|
4
|
+
require 'legion/extensions/enactive_cognition/helpers/sensorimotor_loop'
|
|
5
|
+
require 'legion/extensions/enactive_cognition/helpers/enaction_engine'
|
|
6
|
+
require 'legion/extensions/enactive_cognition/runners/enactive_cognition'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module EnactiveCognition
|
|
11
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/enactive_cognition/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::EnactiveCognition::Client do
|
|
6
|
+
it 'responds to all runner methods' do
|
|
7
|
+
client = described_class.new
|
|
8
|
+
expect(client).to respond_to(:create_sensorimotor_coupling)
|
|
9
|
+
expect(client).to respond_to(:execute_enactive_action)
|
|
10
|
+
expect(client).to respond_to(:adapt_sensorimotor_coupling)
|
|
11
|
+
expect(client).to respond_to(:find_action_for_perception)
|
|
12
|
+
expect(client).to respond_to(:coupled_sensorimotor_loops)
|
|
13
|
+
expect(client).to respond_to(:domain_couplings)
|
|
14
|
+
expect(client).to respond_to(:strongest_couplings)
|
|
15
|
+
expect(client).to respond_to(:overall_enactive_coupling)
|
|
16
|
+
expect(client).to respond_to(:update_enactive_cognition)
|
|
17
|
+
expect(client).to respond_to(:enactive_cognition_stats)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/enactive_cognition/helpers/sensorimotor_loop'
|
|
4
|
+
require 'legion/extensions/enactive_cognition/helpers/enaction_engine'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::EnactiveCognition::Helpers::EnactionEngine do
|
|
7
|
+
subject(:engine) { described_class.new }
|
|
8
|
+
|
|
9
|
+
let(:coupling) do
|
|
10
|
+
engine.create_coupling(action: 'grasp', perception: 'grip', domain: 'motor')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '#create_coupling' do
|
|
14
|
+
it 'returns a SensorimotorLoop instance' do
|
|
15
|
+
expect(coupling).to be_a(Legion::Extensions::EnactiveCognition::Helpers::SensorimotorLoop)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'stores the coupling by id' do
|
|
19
|
+
expect(engine.couplings[coupling.id]).to eq(coupling)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'increments coupling count' do
|
|
23
|
+
engine.create_coupling(action: 'push', perception: 'moved', domain: 'motor')
|
|
24
|
+
expect(engine.count).to eq(1)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'defaults loop_type to :sensorimotor' do
|
|
28
|
+
expect(coupling.loop_type).to eq(:sensorimotor)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'accepts explicit loop_type' do
|
|
32
|
+
lp = engine.create_coupling(action: 'think', perception: 'clarity', domain: 'cog', loop_type: :cognitive)
|
|
33
|
+
expect(lp.loop_type).to eq(:cognitive)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#execute_action' do
|
|
38
|
+
it 'returns success: true on match' do
|
|
39
|
+
result = engine.execute_action(coupling_id: coupling.id, actual_perception: 'grip')
|
|
40
|
+
expect(result[:success]).to be true
|
|
41
|
+
expect(result[:match]).to be true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns success: true on mismatch' do
|
|
45
|
+
result = engine.execute_action(coupling_id: coupling.id, actual_perception: 'slip')
|
|
46
|
+
expect(result[:success]).to be true
|
|
47
|
+
expect(result[:match]).to be false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'returns success: false for missing coupling' do
|
|
51
|
+
result = engine.execute_action(coupling_id: 'nonexistent', actual_perception: 'grip')
|
|
52
|
+
expect(result[:success]).to be false
|
|
53
|
+
expect(result[:reason]).to eq(:not_found)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'includes coupling_label in result' do
|
|
57
|
+
result = engine.execute_action(coupling_id: coupling.id, actual_perception: 'grip')
|
|
58
|
+
expect(result[:coupling_label]).to be_a(Symbol)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe '#adapt_coupling' do
|
|
63
|
+
it 'updates the coupling perception' do
|
|
64
|
+
engine.adapt_coupling(coupling_id: coupling.id, new_perception: 'firm_grip')
|
|
65
|
+
expect(engine.couplings[coupling.id].perception).to eq('firm_grip')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'returns success: true' do
|
|
69
|
+
result = engine.adapt_coupling(coupling_id: coupling.id, new_perception: 'firm_grip')
|
|
70
|
+
expect(result[:success]).to be true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'returns success: false for missing coupling' do
|
|
74
|
+
result = engine.adapt_coupling(coupling_id: 'bad', new_perception: 'x')
|
|
75
|
+
expect(result[:success]).to be false
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe '#find_action_for' do
|
|
80
|
+
before do
|
|
81
|
+
5.times { engine.execute_action(coupling_id: coupling.id, actual_perception: 'grip') }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns the loop when it is coupled and perception matches' do
|
|
85
|
+
result = engine.find_action_for(perception: 'grip')
|
|
86
|
+
if coupling.coupled?
|
|
87
|
+
expect(result).not_to be_nil
|
|
88
|
+
expect(result.action).to eq('grasp')
|
|
89
|
+
else
|
|
90
|
+
expect(result).to be_nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'returns nil when perception does not match any coupled loop' do
|
|
95
|
+
result = engine.find_action_for(perception: 'unknown_perception')
|
|
96
|
+
expect(result).to be_nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe '#coupled_loops' do
|
|
101
|
+
it 'returns empty when nothing is coupled' do
|
|
102
|
+
engine.create_coupling(action: 'act', perception: 'per', domain: 'dom')
|
|
103
|
+
expect(engine.coupled_loops).to be_empty
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'returns loops above coupling threshold' do
|
|
107
|
+
c2 = engine.create_coupling(action: 'act2', perception: 'per2', domain: 'dom2')
|
|
108
|
+
allow(c2).to receive(:coupled?).and_return(true)
|
|
109
|
+
expect(engine.coupled_loops).to include(c2)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe '#by_domain' do
|
|
114
|
+
it 'returns loops in the specified domain' do
|
|
115
|
+
engine.create_coupling(action: 'act', perception: 'per', domain: 'motor')
|
|
116
|
+
engine.create_coupling(action: 'speak', perception: 'heard', domain: 'social')
|
|
117
|
+
expect(engine.by_domain(domain: 'motor').size).to eq(1)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe '#by_type' do
|
|
122
|
+
it 'returns loops of the specified type' do
|
|
123
|
+
engine.create_coupling(action: 'act', perception: 'per', domain: 'dom', loop_type: :cognitive)
|
|
124
|
+
engine.create_coupling(action: 'act2', perception: 'per2', domain: 'dom2', loop_type: :social)
|
|
125
|
+
expect(engine.by_type(loop_type: :cognitive).size).to eq(1)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe '#strongest_couplings' do
|
|
130
|
+
it 'returns at most limit couplings sorted by strength desc' do
|
|
131
|
+
3.times { |i| engine.create_coupling(action: "a#{i}", perception: "p#{i}", domain: 'dom') }
|
|
132
|
+
result = engine.strongest_couplings(limit: 2)
|
|
133
|
+
expect(result.size).to be <= 2
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe '#overall_coupling' do
|
|
138
|
+
it 'returns 0.0 when no couplings exist' do
|
|
139
|
+
expect(engine.overall_coupling).to eq(0.0)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'returns mean coupling strength' do
|
|
143
|
+
engine.create_coupling(action: 'a', perception: 'p', domain: 'd')
|
|
144
|
+
expect(engine.overall_coupling).to eq(
|
|
145
|
+
Legion::Extensions::EnactiveCognition::Helpers::SensorimotorLoop::DEFAULT_COUPLING_STRENGTH
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe '#decay_all' do
|
|
151
|
+
it 'reduces coupling strength of all loops' do
|
|
152
|
+
lp = engine.create_coupling(action: 'a', perception: 'p', domain: 'd')
|
|
153
|
+
before = lp.coupling_strength
|
|
154
|
+
engine.decay_all
|
|
155
|
+
expect(lp.coupling_strength).to be < before
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
describe '#prune_decoupled' do
|
|
160
|
+
it 'removes very weak couplings' do
|
|
161
|
+
lp = engine.create_coupling(action: 'a', perception: 'p', domain: 'd')
|
|
162
|
+
100.times { lp.execute!(actual_perception: 'miss') }
|
|
163
|
+
count_before = engine.count
|
|
164
|
+
engine.prune_decoupled
|
|
165
|
+
expect(engine.count).to be <= count_before
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
describe '#to_h' do
|
|
170
|
+
it 'includes expected keys' do
|
|
171
|
+
result = engine.to_h
|
|
172
|
+
expect(result).to include(:coupling_count, :coupled_count, :overall_coupling, :strongest)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'rounds overall_coupling to 4 decimal places' do
|
|
176
|
+
engine.create_coupling(action: 'a', perception: 'p', domain: 'd')
|
|
177
|
+
result = engine.to_h
|
|
178
|
+
expect(result[:overall_coupling]).to be_a(Float)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/enactive_cognition/helpers/sensorimotor_loop'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::EnactiveCognition::Helpers::SensorimotorLoop do
|
|
6
|
+
subject(:loop_obj) do
|
|
7
|
+
described_class.new(action: 'reach', perception: 'contact', domain: 'motor')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe '#initialize' do
|
|
11
|
+
it 'assigns a uuid id' do
|
|
12
|
+
expect(loop_obj.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'sets default coupling strength' do
|
|
16
|
+
expect(loop_obj.coupling_strength).to eq(described_class::DEFAULT_COUPLING_STRENGTH)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'defaults loop_type to :sensorimotor' do
|
|
20
|
+
expect(loop_obj.loop_type).to eq(:sensorimotor)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'accepts valid loop types' do
|
|
24
|
+
cognitive = described_class.new(action: 'think', perception: 'insight', domain: 'cog', loop_type: :cognitive)
|
|
25
|
+
expect(cognitive.loop_type).to eq(:cognitive)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'falls back to :sensorimotor for invalid loop_type' do
|
|
29
|
+
bad = described_class.new(action: 'x', perception: 'y', domain: 'z', loop_type: :invalid)
|
|
30
|
+
expect(bad.loop_type).to eq(:sensorimotor)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'starts with zero execution count' do
|
|
34
|
+
expect(loop_obj.execution_count).to eq(0)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'starts with zero accurate_predictions' do
|
|
38
|
+
expect(loop_obj.accurate_predictions).to eq(0)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#execute!' do
|
|
43
|
+
context 'when actual matches expected perception' do
|
|
44
|
+
it 'increments execution_count' do
|
|
45
|
+
loop_obj.execute!(actual_perception: 'contact')
|
|
46
|
+
expect(loop_obj.execution_count).to eq(1)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'increments accurate_predictions' do
|
|
50
|
+
loop_obj.execute!(actual_perception: 'contact')
|
|
51
|
+
expect(loop_obj.accurate_predictions).to eq(1)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'strengthens coupling' do
|
|
55
|
+
before = loop_obj.coupling_strength
|
|
56
|
+
loop_obj.execute!(actual_perception: 'contact')
|
|
57
|
+
expect(loop_obj.coupling_strength).to be > before
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'returns match: true' do
|
|
61
|
+
result = loop_obj.execute!(actual_perception: 'contact')
|
|
62
|
+
expect(result[:match]).to be true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'does not exceed COUPLING_CEILING' do
|
|
66
|
+
20.times { loop_obj.execute!(actual_perception: 'contact') }
|
|
67
|
+
expect(loop_obj.coupling_strength).to be <= described_class::COUPLING_CEILING
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context 'when actual does not match expected perception' do
|
|
72
|
+
it 'weakens coupling' do
|
|
73
|
+
before = loop_obj.coupling_strength
|
|
74
|
+
loop_obj.execute!(actual_perception: 'no_contact')
|
|
75
|
+
expect(loop_obj.coupling_strength).to be < before
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'returns match: false' do
|
|
79
|
+
result = loop_obj.execute!(actual_perception: 'no_contact')
|
|
80
|
+
expect(result[:match]).to be false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'does not fall below COUPLING_FLOOR' do
|
|
84
|
+
20.times { loop_obj.execute!(actual_perception: 'no_contact') }
|
|
85
|
+
expect(loop_obj.coupling_strength).to be >= described_class::COUPLING_FLOOR
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'does not increment accurate_predictions' do
|
|
89
|
+
loop_obj.execute!(actual_perception: 'no_contact')
|
|
90
|
+
expect(loop_obj.accurate_predictions).to eq(0)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'updates prediction_accuracy' do
|
|
95
|
+
3.times { loop_obj.execute!(actual_perception: 'contact') }
|
|
96
|
+
loop_obj.execute!(actual_perception: 'miss')
|
|
97
|
+
expect(loop_obj.prediction_accuracy).to eq(0.75)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'sets last_executed_at' do
|
|
101
|
+
loop_obj.execute!(actual_perception: 'contact')
|
|
102
|
+
expect(loop_obj.last_executed_at).not_to be_nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe '#coupled?' do
|
|
107
|
+
it 'returns false at default strength (0.5)' do
|
|
108
|
+
expect(loop_obj.coupled?).to be false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'returns true when strength reaches 0.6' do
|
|
112
|
+
loop_obj.execute!(actual_perception: 'contact')
|
|
113
|
+
loop_obj.execute!(actual_perception: 'contact')
|
|
114
|
+
if loop_obj.coupling_strength >= 0.6
|
|
115
|
+
expect(loop_obj.coupled?).to be true
|
|
116
|
+
else
|
|
117
|
+
expect(loop_obj.coupled?).to be false
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe '#coupling_label' do
|
|
123
|
+
it 'returns :forming at default strength 0.5' do
|
|
124
|
+
expect(loop_obj.coupling_label).to eq(:forming)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'returns :entrained at strength 0.9' do
|
|
128
|
+
allow(loop_obj).to receive(:coupling_strength).and_return(0.9)
|
|
129
|
+
expect(loop_obj.coupling_label).to eq(:entrained)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'returns :decoupled at strength 0.1' do
|
|
133
|
+
allow(loop_obj).to receive(:coupling_strength).and_return(0.1)
|
|
134
|
+
expect(loop_obj.coupling_label).to eq(:decoupled)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'returns :weak at strength 0.25' do
|
|
138
|
+
allow(loop_obj).to receive(:coupling_strength).and_return(0.25)
|
|
139
|
+
expect(loop_obj.coupling_label).to eq(:weak)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'returns :coupled at strength 0.7' do
|
|
143
|
+
allow(loop_obj).to receive(:coupling_strength).and_return(0.7)
|
|
144
|
+
expect(loop_obj.coupling_label).to eq(:coupled)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
describe '#adapt_perception!' do
|
|
149
|
+
it 'updates the expected perception' do
|
|
150
|
+
loop_obj.adapt_perception!(new_perception: 'soft_contact')
|
|
151
|
+
expect(loop_obj.perception).to eq('soft_contact')
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe '#decay!' do
|
|
156
|
+
it 'reduces coupling_strength by COUPLING_DECAY' do
|
|
157
|
+
before = loop_obj.coupling_strength
|
|
158
|
+
loop_obj.decay!
|
|
159
|
+
expect(loop_obj.coupling_strength).to eq((before - described_class::COUPLING_DECAY).clamp(0.0, 1.0))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'does not fall below COUPLING_FLOOR' do
|
|
163
|
+
100.times { loop_obj.decay! }
|
|
164
|
+
expect(loop_obj.coupling_strength).to be >= described_class::COUPLING_FLOOR
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe '#to_h' do
|
|
169
|
+
it 'includes all expected keys' do
|
|
170
|
+
hash = loop_obj.to_h
|
|
171
|
+
expect(hash).to include(
|
|
172
|
+
:id, :action, :perception, :domain, :loop_type,
|
|
173
|
+
:coupling_strength, :coupling_label, :prediction_accuracy,
|
|
174
|
+
:execution_count, :accurate_predictions, :coupled,
|
|
175
|
+
:created_at, :last_executed_at
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'reflects current coupling_label' do
|
|
180
|
+
hash = loop_obj.to_h
|
|
181
|
+
expect(hash[:coupling_label]).to eq(loop_obj.coupling_label)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/enactive_cognition/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::EnactiveCognition::Runners::EnactiveCognition do
|
|
6
|
+
let(:client) { Legion::Extensions::EnactiveCognition::Client.new }
|
|
7
|
+
|
|
8
|
+
def make_coupling(action: 'press', perception: 'click', domain: 'motor', loop_type: :sensorimotor)
|
|
9
|
+
client.create_sensorimotor_coupling(
|
|
10
|
+
action: action,
|
|
11
|
+
perception: perception,
|
|
12
|
+
domain: domain,
|
|
13
|
+
loop_type: loop_type
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '#create_sensorimotor_coupling' do
|
|
18
|
+
it 'returns success: true' do
|
|
19
|
+
result = make_coupling
|
|
20
|
+
expect(result[:success]).to be true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'includes coupling hash with uuid id' do
|
|
24
|
+
result = make_coupling
|
|
25
|
+
expect(result[:coupling][:id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'reflects the given action and perception' do
|
|
29
|
+
result = make_coupling(action: 'push', perception: 'moved')
|
|
30
|
+
expect(result[:coupling][:action]).to eq('push')
|
|
31
|
+
expect(result[:coupling][:perception]).to eq('moved')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'accepts string loop_type and converts to symbol' do
|
|
35
|
+
result = client.create_sensorimotor_coupling(
|
|
36
|
+
action: 'x',
|
|
37
|
+
perception: 'y',
|
|
38
|
+
domain: 'z',
|
|
39
|
+
loop_type: 'cognitive'
|
|
40
|
+
)
|
|
41
|
+
expect(result[:coupling][:loop_type]).to eq(:cognitive)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#execute_enactive_action' do
|
|
46
|
+
let(:coupling_id) { make_coupling[:coupling][:id] }
|
|
47
|
+
|
|
48
|
+
it 'returns success: true on matching perception' do
|
|
49
|
+
result = client.execute_enactive_action(coupling_id: coupling_id, actual_perception: 'click')
|
|
50
|
+
expect(result[:success]).to be true
|
|
51
|
+
expect(result[:match]).to be true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'returns success: true on mismatching perception' do
|
|
55
|
+
result = client.execute_enactive_action(coupling_id: coupling_id, actual_perception: 'silence')
|
|
56
|
+
expect(result[:success]).to be true
|
|
57
|
+
expect(result[:match]).to be false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'returns success: false for unknown coupling_id' do
|
|
61
|
+
result = client.execute_enactive_action(coupling_id: 'bad-id', actual_perception: 'click')
|
|
62
|
+
expect(result[:success]).to be false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'includes coupling_label in result' do
|
|
66
|
+
result = client.execute_enactive_action(coupling_id: coupling_id, actual_perception: 'click')
|
|
67
|
+
expect(result[:coupling_label]).to be_a(Symbol)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '#adapt_sensorimotor_coupling' do
|
|
72
|
+
let(:coupling_id) { make_coupling[:coupling][:id] }
|
|
73
|
+
|
|
74
|
+
it 'returns success: true' do
|
|
75
|
+
result = client.adapt_sensorimotor_coupling(coupling_id: coupling_id, new_perception: 'soft_click')
|
|
76
|
+
expect(result[:success]).to be true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'returns new_perception in result' do
|
|
80
|
+
result = client.adapt_sensorimotor_coupling(coupling_id: coupling_id, new_perception: 'soft_click')
|
|
81
|
+
expect(result[:new_perception]).to eq('soft_click')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns success: false for missing coupling' do
|
|
85
|
+
result = client.adapt_sensorimotor_coupling(coupling_id: 'missing', new_perception: 'x')
|
|
86
|
+
expect(result[:success]).to be false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '#find_action_for_perception' do
|
|
91
|
+
it 'returns found: false when nothing is coupled' do
|
|
92
|
+
result = client.find_action_for_perception(perception: 'click')
|
|
93
|
+
expect(result[:found]).to be false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'returns found: true when a coupled loop matches' do
|
|
97
|
+
id = make_coupling[:coupling][:id]
|
|
98
|
+
5.times { client.execute_enactive_action(coupling_id: id, actual_perception: 'click') }
|
|
99
|
+
result = client.find_action_for_perception(perception: 'click')
|
|
100
|
+
# may or may not be coupled depending on starting strength + reinforcement
|
|
101
|
+
if result[:found]
|
|
102
|
+
expect(result[:action]).to eq('press')
|
|
103
|
+
else
|
|
104
|
+
expect(result[:found]).to be false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe '#coupled_sensorimotor_loops' do
|
|
110
|
+
it 'returns loops and count' do
|
|
111
|
+
result = client.coupled_sensorimotor_loops
|
|
112
|
+
expect(result).to include(:loops, :count)
|
|
113
|
+
expect(result[:loops]).to be_an(Array)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'count is zero initially' do
|
|
117
|
+
expect(client.coupled_sensorimotor_loops[:count]).to eq(0)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe '#domain_couplings' do
|
|
122
|
+
before { make_coupling(domain: 'motor') }
|
|
123
|
+
|
|
124
|
+
it 'returns loops in the domain' do
|
|
125
|
+
result = client.domain_couplings(domain: 'motor')
|
|
126
|
+
expect(result[:domain]).to eq('motor')
|
|
127
|
+
expect(result[:count]).to eq(1)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'returns zero for unknown domain' do
|
|
131
|
+
result = client.domain_couplings(domain: 'unknown')
|
|
132
|
+
expect(result[:count]).to eq(0)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe '#strongest_couplings' do
|
|
137
|
+
before { 3.times { |i| make_coupling(action: "act#{i}", perception: "per#{i}", domain: 'dom') } }
|
|
138
|
+
|
|
139
|
+
it 'returns loops array' do
|
|
140
|
+
result = client.strongest_couplings(limit: 2)
|
|
141
|
+
expect(result[:loops].size).to be <= 2
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'uses default limit of 5' do
|
|
145
|
+
result = client.strongest_couplings
|
|
146
|
+
expect(result[:loops].size).to be <= 5
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe '#overall_enactive_coupling' do
|
|
151
|
+
it 'returns overall_coupling and coupling_label' do
|
|
152
|
+
result = client.overall_enactive_coupling
|
|
153
|
+
expect(result).to include(:overall_coupling, :coupling_label)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'returns 0.0 when no couplings exist' do
|
|
157
|
+
expect(client.overall_enactive_coupling[:overall_coupling]).to eq(0.0)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'returns :decoupled label when no couplings' do
|
|
161
|
+
expect(client.overall_enactive_coupling[:coupling_label]).to eq(:decoupled)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'returns :forming label at default strength' do
|
|
165
|
+
make_coupling
|
|
166
|
+
result = client.overall_enactive_coupling
|
|
167
|
+
expect(result[:coupling_label]).to eq(:forming)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
describe '#update_enactive_cognition' do
|
|
172
|
+
before { make_coupling }
|
|
173
|
+
|
|
174
|
+
it 'returns success: true' do
|
|
175
|
+
result = client.update_enactive_cognition
|
|
176
|
+
expect(result[:success]).to be true
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'decays couplings when decay: true' do
|
|
180
|
+
before_strength = client.strongest_couplings(limit: 1)[:loops].first[:coupling_strength]
|
|
181
|
+
client.update_enactive_cognition(decay: true)
|
|
182
|
+
after_strength = client.strongest_couplings(limit: 1)[:loops].first[:coupling_strength]
|
|
183
|
+
expect(after_strength).to be < before_strength
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'reflects coupling_count in result' do
|
|
187
|
+
result = client.update_enactive_cognition
|
|
188
|
+
expect(result[:coupling_count]).to be >= 1
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
describe '#enactive_cognition_stats' do
|
|
193
|
+
it 'returns success: true' do
|
|
194
|
+
result = client.enactive_cognition_stats
|
|
195
|
+
expect(result[:success]).to be true
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it 'includes stats hash' do
|
|
199
|
+
result = client.enactive_cognition_stats
|
|
200
|
+
expect(result[:stats]).to include(:coupling_count, :coupled_count, :overall_coupling)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'reports zero coupling_count when empty' do
|
|
204
|
+
result = client.enactive_cognition_stats
|
|
205
|
+
expect(result[:stats][:coupling_count]).to eq(0)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'increments coupling_count after creating a coupling' do
|
|
209
|
+
make_coupling
|
|
210
|
+
result = client.enactive_cognition_stats
|
|
211
|
+
expect(result[:stats][:coupling_count]).to eq(1)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
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/enactive_cognition'
|
|
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,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-enactive-cognition
|
|
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: 'Varela''s enactivism: cognition through action-perception loops, sensorimotor
|
|
27
|
+
contingencies, and structural coupling'
|
|
28
|
+
email:
|
|
29
|
+
- matthewdiverson@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- Gemfile
|
|
35
|
+
- lex-enactive-cognition.gemspec
|
|
36
|
+
- lib/legion/extensions/enactive_cognition.rb
|
|
37
|
+
- lib/legion/extensions/enactive_cognition/client.rb
|
|
38
|
+
- lib/legion/extensions/enactive_cognition/helpers/enaction_engine.rb
|
|
39
|
+
- lib/legion/extensions/enactive_cognition/helpers/sensorimotor_loop.rb
|
|
40
|
+
- lib/legion/extensions/enactive_cognition/runners/enactive_cognition.rb
|
|
41
|
+
- lib/legion/extensions/enactive_cognition/version.rb
|
|
42
|
+
- spec/legion/extensions/enactive_cognition/client_spec.rb
|
|
43
|
+
- spec/legion/extensions/enactive_cognition/helpers/enaction_engine_spec.rb
|
|
44
|
+
- spec/legion/extensions/enactive_cognition/helpers/sensorimotor_loop_spec.rb
|
|
45
|
+
- spec/legion/extensions/enactive_cognition/runners/enactive_cognition_spec.rb
|
|
46
|
+
- spec/spec_helper.rb
|
|
47
|
+
homepage: https://github.com/LegionIO/lex-enactive-cognition
|
|
48
|
+
licenses:
|
|
49
|
+
- MIT
|
|
50
|
+
metadata:
|
|
51
|
+
homepage_uri: https://github.com/LegionIO/lex-enactive-cognition
|
|
52
|
+
source_code_uri: https://github.com/LegionIO/lex-enactive-cognition
|
|
53
|
+
documentation_uri: https://github.com/LegionIO/lex-enactive-cognition
|
|
54
|
+
changelog_uri: https://github.com/LegionIO/lex-enactive-cognition
|
|
55
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-enactive-cognition/issues
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '3.4'
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 3.6.9
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: LEX Enactive Cognition
|
|
74
|
+
test_files: []
|