diff --git a/.github/workflows/pnl-ci.yml b/.github/workflows/pnl-ci.yml
index cdb91a970bb..fd8c5517be8 100644
--- a/.github/workflows/pnl-ci.yml
+++ b/.github/workflows/pnl-ci.yml
@@ -163,7 +163,7 @@ jobs:
- name: Test with pytest
timeout-minutes: 180
- run: pytest --junit-xml=tests_out.xml --verbosity=0 -n logical ${{ matrix.extra-args }}
+ run: pytest --junit-xml=tests_out.xml --verbosity=0 -n logical --capture=sys -o console_output_style=count ${{ matrix.extra-args }}
- name: Upload test results
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml
index a0a74cb200c..2f21bba3b52 100644
--- a/.github/workflows/test-release.yml
+++ b/.github/workflows/test-release.yml
@@ -72,6 +72,11 @@ jobs:
python-version: [3.7, 3.8, 3.9, '3.10', 3.11, 3.12]
os: [ubuntu-latest, macos-latest, windows-latest]
dist: [wheel, sdist]
+ exclude:
+ # 3.7 is broken on macos-11,
+ # https://github.com/actions/virtual-environments/issues/4230
+ - python-version: '3.7'
+ os: macos-latest
runs-on: ${{ matrix.os }}
needs: [create-python-dist]
@@ -128,7 +133,7 @@ jobs:
- name: Upload test results
uses: actions/upload-artifact@v4
with:
- name: test-results-${{ matrix.os }}-${{ matrix.python-version }}
+ name: test-results-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.dist }}
path: tests_out.xml
retention-days: 30
if: success() || failure()
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Environment.py b/Scripts/Environment.py
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Environment.py
rename to Scripts/Environment.py
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/DeclanParams.py b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/DeclanParams.py
similarity index 84%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/DeclanParams.py
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/DeclanParams.py
index 7209121c186..c2dbbbf8180 100644
--- a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/DeclanParams.py
+++ b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/DeclanParams.py
@@ -39,10 +39,10 @@ def calc_prob(em_preds, test_ys):
# Names:
name = "EGO Model CSW",
+ em_name = "EM",
state_input_layer_name = "STATE",
previous_state_layer_name = "PREVIOUS STATE",
context_layer_name = 'CONTEXT',
- em_name = "EM",
prediction_layer_name = "PREDICTION",
# Structural
@@ -50,10 +50,10 @@ def calc_prob(em_preds, test_ys):
previous_state_d = 11, # length of state vector
context_d = 11, # length of context vector
memory_capacity = ALL, # number of entries in EM memory; ALL=> match to number of stims
- # memory_init = (0,.0001), # Initialize memory with random values in interval
- memory_init = None, # Initialize with zeros
- concatenate_queries = False,
- # concatenate_queries = True,
+ memory_init = (0,.0001), # Initialize memory with random values in interval
+ # memory_init = None, # Initialize with zeros
+ # concatenate_queries = False,
+ concatenate_queries = True,
# environment
# curriculum_type = 'Interleaved',
@@ -63,20 +63,23 @@ def calc_prob(em_preds, test_ys):
# Processing
integration_rate = .69, # rate at which state is integrated into new context
- # state_weight = 1, # weight of the state used during memory retrieval
+ # state_weight =normalize_field_weightsnormalize_field_weights 1, # weight of the state used during memory retrieval
# context_weight = 1, # weight of the context used during memory retrieval
- state_weight = .5, # weight of the state used during memory retrieval
+ previous_state_weight = .5, # weight of the state used during memory retrieval
context_weight = .5, # weight of the context used during memory retrieval
+ state_weight = None, # weight of the state used during memory retrieval
# normalize_field_weights = False, # whether to normalize the field weights during memory retrieval
normalize_field_weights = True, # whether to normalize the field weights during memory retrieval
+ normalize_memories = False, # whether to normalize the memory during memory retrieval
+ # normalize_memories = True, # whether to normalize the memory during memory retrieval
# softmax_temperature = None, # temperature of the softmax used during memory retrieval (smaller means more argmax-like
softmax_temperature = .1, # temperature of the softmax used during memory retrieval (smaller means more argmax-like
# softmax_temperature = ADAPTIVE, # temperature of the softmax used during memory retrieval (smaller means more argmax-like
# softmax_temperature = CONTROL, # temperature of the softmax used during memory retrieval (smaller means more argmax-like
# softmax_threshold = None, # threshold used to mask out small values in softmax
softmax_threshold = .001, # threshold used to mask out small values in softmax
- enable_learning=[False, False, True], # Enable learning for PREDICTION (STATE) but not CONTEXT or PREVIOUS STATE
- learn_field_weights = False,
+ # target_fields=[True, False, False], # Enable learning for PREDICTION (STATE) but not CONTEXT or PREVIOUS STATE
+ enable_learning = True,
loss_spec = Loss.BINARY_CROSS_ENTROPY,
# loss_spec = Loss.MSE,
learning_rate = .5,
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/EGO CSW Model (using RNN).py b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/EGO CSW Model (using RNN).py
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/EGO CSW Model (using RNN).py
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/EGO CSW Model (using RNN).py
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/EGO CSW Model.py b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/EGO CSW Model.py
similarity index 90%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/EGO CSW Model.py
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/EGO CSW Model.py
index 18d3ba419b0..f8703e3cab3 100644
--- a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/EGO CSW Model.py
+++ b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/EGO CSW Model.py
@@ -174,7 +174,7 @@
'CONTEXT',
'PREVIOUS STATE'],
start=0)
-state_retrieval_weight = 0
+state_retrieval_weight = None
RANDOM_WEIGHTS_INITIALIZATION=RandomMatrix(center=0.0, range=0.1) # Matrix spec used to initialize all Projections
if is_numeric_scalar(model_params['softmax_temperature']): # translate to gain of softmax retrieval function
@@ -194,7 +194,7 @@ def construct_model(model_name:str=model_params['name'],
state_size:int=model_params['state_d'],
# Previous state
- previous_state_input_name:str=model_params['previous_state_layer_name'],
+ previous_state_name:str=model_params['previous_state_layer_name'],
# Context representation (learned):
context_name:str=model_params['context_layer_name'],
@@ -205,12 +205,15 @@ def construct_model(model_name:str=model_params['name'],
em_name:str=model_params['em_name'],
retrieval_softmax_gain=retrieval_softmax_gain,
retrieval_softmax_threshold=model_params['softmax_threshold'],
- state_retrieval_weight:Union[float,int]=state_retrieval_weight,
- previous_state_retrieval_weight:Union[float,int]=model_params['state_weight'],
+ # state_retrieval_weight:Union[float,int]=state_retrieval_weight,
+ # previous_state_retrieval_weight:Union[float,int]=model_params['state_weight'],
+ state_retrieval_weight:Union[float,int]=model_params['state_weight'],
+ previous_state_retrieval_weight:Union[float,int]=model_params['previous_state_weight'],
context_retrieval_weight:Union[float,int]=model_params['context_weight'],
normalize_field_weights = model_params['normalize_field_weights'],
+ normalize_memories = model_params['normalize_memories'],
concatenate_queries = model_params['concatenate_queries'],
- learn_field_weights = model_params['learn_field_weights'],
+ enable_learning = model_params['enable_learning'],
memory_capacity = memory_capacity,
memory_init=model_params['memory_init'],
@@ -219,7 +222,7 @@ def construct_model(model_name:str=model_params['name'],
# Learning
loss_spec=model_params['loss_spec'],
- enable_learning=model_params['enable_learning'],
+ # target_fields=model_params['target_fields'],
learning_rate = model_params['learning_rate'],
device=model_params['device']
@@ -233,7 +236,7 @@ def construct_model(model_name:str=model_params['name'],
# ----------------------------------------------------------------------------------------------------------------
state_input_layer = ProcessingMechanism(name=state_input_name, input_shapes=state_size)
- previous_state_layer = ProcessingMechanism(name=previous_state_input_name, input_shapes=state_size)
+ previous_state_layer = ProcessingMechanism(name=previous_state_name, input_shapes=state_size)
# context_layer = ProcessingMechanism(name=context_name, input_shapes=context_size)
context_layer = TransferMechanism(name=context_name,
input_shapes=context_size,
@@ -241,15 +244,27 @@ def construct_model(model_name:str=model_params['name'],
integrator_mode=True,
integration_rate=integration_rate)
+
+
em = EMComposition(name=em_name,
memory_template=[[0] * state_size, # state
[0] * state_size, # previous state
[0] * state_size], # context
memory_fill=memory_init,
memory_capacity=memory_capacity,
+ normalize_memories=False,
memory_decay_rate=0,
softmax_gain=retrieval_softmax_gain,
softmax_threshold=retrieval_softmax_threshold,
+ fields = {state_input_name: {FIELD_WEIGHT: state_retrieval_weight,
+ LEARN_FIELD_WEIGHT: False,
+ TARGET_FIELD: True},
+ previous_state_name: {FIELD_WEIGHT:previous_state_retrieval_weight,
+ LEARN_FIELD_WEIGHT: False,
+ TARGET_FIELD: False},
+ context_name: {FIELD_WEIGHT:context_retrieval_weight,
+ LEARN_FIELD_WEIGHT: False,
+ TARGET_FIELD: False}},
# Input Nodes:
# field_names=[state_input_name,
# previous_state_input_name,
@@ -259,19 +274,20 @@ def construct_model(model_name:str=model_params['name'],
# previous_state_retrieval_weight,
# context_retrieval_weight
# ),
- field_names=[previous_state_input_name,
- context_name,
- state_input_name,
- ],
- field_weights=(previous_state_retrieval_weight,
- context_retrieval_weight,
- state_retrieval_weight,
- ),
+ # field_names=[previous_state_input_name,
+ # context_name,
+ # state_input_name,
+ # ],
+ # field_weights=(previous_state_retrieval_weight,
+ # context_retrieval_weight,
+ # state_retrieval_weight,
+ # ),
normalize_field_weights=normalize_field_weights,
+ normalize_memories=normalize_memories,
concatenate_queries=concatenate_queries,
- learn_field_weights=learn_field_weights,
- learning_rate=learning_rate,
enable_learning=enable_learning,
+ learning_rate=learning_rate,
+ # target_fields=target_fields,
device=device
)
@@ -311,7 +327,7 @@ def construct_model(model_name:str=model_params['name'],
em]
previous_state_to_em_pathway = [previous_state_layer,
MappingProjection(sender=previous_state_layer,
- receiver=em.nodes[previous_state_input_name+QUERY],
+ receiver=em.nodes[previous_state_name+QUERY],
matrix=IDENTITY_MATRIX,
learnable=False),
em]
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Environment.py b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Environment.py
new file mode 100644
index 00000000000..124de532c83
--- /dev/null
+++ b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Environment.py
@@ -0,0 +1,54 @@
+import numpy as np
+import torch
+from torch.utils.data import dataset
+from random import randint
+
+def one_hot_encode(labels, num_classes):
+ """
+ One hot encode labels and convert to tensor.
+ """
+ return torch.tensor((np.arange(num_classes) == labels[..., None]).astype(float),dtype=torch.float32)
+
+class DeterministicCSWDataset(dataset.Dataset):
+ def __init__(self, n_samples_per_context, contexts_to_load) -> None:
+ super().__init__()
+ raw_xs = np.array([
+ [[9,1,3,5,7],[9,2,4,6,8]],
+ [[10,1,4,5,8],[10,2,3,6,7]]
+ ])
+
+ item_indices = np.random.choice(raw_xs.shape[1],sum(n_samples_per_context),replace=True)
+ task_names = [0,1] # Flexible so these can be renamed later
+ task_indices = [task_names.index(name) for name in contexts_to_load]
+
+ context_indices = np.repeat(np.array(task_indices),n_samples_per_context)
+ self.xs = one_hot_encode(raw_xs[context_indices,item_indices],11)
+
+ self.xs = self.xs.reshape((-1,11))
+ self.ys = torch.cat([self.xs[1:],one_hot_encode(np.array([0]),11)],dim=0)
+ context_indices = np.repeat(np.array(task_indices),[x*5 for x in n_samples_per_context])
+ self.contexts = one_hot_encode(context_indices, len(task_names))
+
+ # Remove the last transition since there's no next state available
+ self.xs = self.xs[:-1]
+ self.ys = self.ys[:-1]
+ self.contexts = self.contexts[:-1]
+
+ def __len__(self):
+ return len(self.xs)
+
+ def __getitem__(self, idx):
+ return self.xs[idx], self.contexts[idx], self.ys[idx]
+
+def generate_dataset(condition='Blocked'):
+ # Generate the dataset for either the blocked or interleaved condition
+ if condition=='Blocked':
+ contexts_to_load = [0,1,0,1] + [randint(0,1) for _ in range(40)]
+ n_samples_per_context = [40,40,40,40] + [1]*40
+ elif condition == 'Interleaved':
+ contexts_to_load = [0,1]*80 + [randint(0,1) for _ in range(40)]
+ n_samples_per_context = [1]*160 + [1]*40
+ else:
+ raise ValueError(f'Unknown dataset condition: {condition}')
+
+ return DeterministicCSWDataset(n_samples_per_context, contexts_to_load)
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model (PyTorch).pdf b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model (PyTorch).pdf
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model (PyTorch).pdf
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model (PyTorch).pdf
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model (basic).pdf b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model (basic).pdf
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model (basic).pdf
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model (basic).pdf
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model (learning and store).pdf b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model (learning and store).pdf
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model (learning and store).pdf
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model (learning and store).pdf
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model (learning).pdf b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model (learning).pdf
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model (learning).pdf
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model (learning).pdf
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model - EM (with PNL learning).pdf b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model - EM (with PNL learning).pdf
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model - EM (with PNL learning).pdf
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model - EM (with PNL learning).pdf
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model - EM.pdf b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model - EM.pdf
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO CSW Model - EM.pdf
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO CSW Model - EM.pdf
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO Paper Figure.jpg b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO Paper Figure.jpg
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EGO Paper Figure.jpg
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EGO Paper Figure.jpg
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EMComposition (example BIG).pdf b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EMComposition (example BIG).pdf
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/Figures/EMComposition (example BIG).pdf
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/Figures/EMComposition (example BIG).pdf
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/ScriptControl.py b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/ScriptControl.py
similarity index 93%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/ScriptControl.py
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/ScriptControl.py
index 43016886d3a..f61ec5f75d4 100644
--- a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/ScriptControl.py
+++ b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/ScriptControl.py
@@ -3,8 +3,8 @@
# Settings for running script:
-MODEL_PARAMS = 'TestParams'
-# MODEL_PARAMS = 'DeclanParams'
+# MODEL_PARAMS = 'TestParams'
+MODEL_PARAMS = 'DeclanParams'
CONSTRUCT_MODEL = True # THIS MUST BE SET TO True to run the script
DISPLAY_MODEL = ( # Only one of the following can be uncommented:
@@ -13,7 +13,7 @@
# # 'show_pytorch': True, # show pytorch graph of model
# 'show_learning': True,
# # 'show_nested_args': {'show_learning': pnl.ALL},
- # 'show_projections_not_in_composition': True,
+ # # 'show_projections_not_in_composition': True,
# # 'show_nested': {'show_node_structure': True},
# # 'exclude_from_gradient_calc_style': 'dashed'# show target mechanisms for learning
# # 'show_node_structure': True # show detailed view of node structures and projections
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/TestParams.py b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/TestParams.py
similarity index 86%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/TestParams.py
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/TestParams.py
index e9893eff726..2ba7073f178 100644
--- a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/TestParams.py
+++ b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/TestParams.py
@@ -1,14 +1,16 @@
from psyneulink.core.llvm import ExecutionMode
from psyneulink.core.globals.keywords import ALL, ADAPTIVE, CONTROL, CPU, Loss, MPS, OPTIMIZATION_STEP, RUN, TRIAL
+
+
model_params = dict(
# Names:
name = "EGO Model CSW",
+ em_name = "EM",
state_input_layer_name = "STATE",
previous_state_layer_name = "PREVIOUS STATE",
context_layer_name = 'CONTEXT',
- em_name = "EM",
prediction_layer_name = "PREDICTION",
# Structural
@@ -20,7 +22,6 @@
# memory_init = None, # Initialize with zeros
concatenate_queries = False,
# concatenate_queries = True,
-
# environment
# curriculum_type = 'Interleaved',
curriculum_type = 'Blocked',
@@ -33,18 +34,19 @@
context_weight = 1, # weight of the context used during memory retrieval
# normalize_field_weights = False, # whether to normalize the field weights during memory retrieval
normalize_field_weights = True, # whether to normalize the field weights during memory retrieval
+ normalize_memories = False, # whether to normalize the memory during memory retrieval
# softmax_temperature = None, # temperature of the softmax used during memory retrieval (smaller means more argmax-like
softmax_temperature = .1, # temperature of the softmax used during memory retrieval (smaller means more argmax-like
# softmax_temperature = ADAPTIVE, # temperature of the softmax used during memory retrieval (smaller means more argmax-like
# softmax_temperature = CONTROL, # temperature of the softmax used during memory retrieval (smaller means more argmax-like
# softmax_threshold = None, # threshold used to mask out small values in softmax
softmax_threshold = .001, # threshold used to mask out small values in softmax
- enable_learning=[True, False, False], # Enable learning for PREDICTION (STATE) but not CONTEXT or PREVIOUS STATE
- # enable_learning=[True, True, True]
- # enable_learning=True,
- # enable_learning=False,
- learn_field_weights = True,
- # learn_field_weights = False,
+ # target_fields=[True, False, False], # Enable learning for PREDICTION (STATE) but not CONTEXT or PREVIOUS STATE
+ # target_fields=[True, True, True]
+ # target_fields=True,
+ # target_fields=False,
+ enable_learning = True,
+ # enable_learning = False,
loss_spec = Loss.BINARY_CROSS_ENTROPY,
# loss_spec = Loss.CROSS_ENTROPY,
# loss_spec = Loss.MSE,
@@ -53,8 +55,8 @@
synch_weights = RUN,
synch_values = RUN,
synch_results = RUN,
- execution_mode = ExecutionMode.Python,
- # execution_mode = ExecutionMode.PyTorch,
+ # execution_mode = ExecutionMode.Python,
+ execution_mode = ExecutionMode.PyTorch,
device = CPU,
# device = MPS,
)
diff --git a/Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/__init__.py b/Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/__init__.py
similarity index 100%
rename from Scripts/Models (Under Development)/EGO/Using EMComposition/CSW/__init__.py
rename to Scripts/Models (Under Development)/EGO/Using EMComposition/Coffee Shop World/__init__.py
diff --git a/broken_trans_deps.txt b/broken_trans_deps.txt
index f7b44a2bbe0..820cffb769e 100644
--- a/broken_trans_deps.txt
+++ b/broken_trans_deps.txt
@@ -34,6 +34,10 @@ cattrs != 23.2.1, != 23.2.2
# https://github.com/beartype/beartype/issues/324
beartype != 0.17.1; python_version == '3.9'
+# coverage 7.6.5 is broken
+# https://github.com/nedbat/coveragepy/issues/1891
+coverage != 7.6.5
+
# The following need at least sphinx-5 without indicating it in dependencies:
# * sphinxcontrib-applehelp >=1.0.8,
# * sphinxcontrib-devhelp >=1.0.6,
diff --git a/dev_requirements.txt b/dev_requirements.txt
index 15e000ee8dd..f5e01b75621 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -1,11 +1,11 @@
jupyter<1.1.2
packaging<25.0
-pytest<8.3.4
+pytest<8.3.5
pytest-benchmark<5.1.1
pytest-cov<6.0.1
pytest-forked<1.7.0
pytest-helpers-namespace<2021.12.30
-pytest-profiling<1.8.1
+pytest-profiling<1.8.2
pytest-pycodestyle<2.5.0
pytest-pydocstyle<2.5.0
pytest-xdist>=3.2.0, <3.7.0
diff --git a/docs/source/CombinationFunctions.rst b/docs/source/CombinationFunctions.rst
deleted file mode 100644
index 31a55947cc0..00000000000
--- a/docs/source/CombinationFunctions.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-CombinationFunctions
-====================
-
-.. toctree::
- :maxdepth: 3
-
-.. automodule:: psyneulink.core.components.functions.combinationfunctions
- :members: Concatenate, Rearrange, Reduce, LinearCombination, CombineMeans, PredictionErrorDeltaFunction
- :private-members:
- :exclude-members: Parameters
-
diff --git a/docs/source/Core.rst b/docs/source/Core.rst
index 292689884fd..ea1f1b7105d 100644
--- a/docs/source/Core.rst
+++ b/docs/source/Core.rst
@@ -57,8 +57,6 @@ Core
- `NonStatefulFunctions`
- - `CombinationFunctions`
-
- `DistributionFunctions`
- `LearningFunctions`
@@ -71,6 +69,8 @@ Core
- `TransferFunctions`
+ - `TransformFunctions`
+
- `StatefulFunctions`
- `IntegratorFunctions`
diff --git a/docs/source/NonStatefulFunctions.rst b/docs/source/NonStatefulFunctions.rst
index 55ad922776c..f69c780ad63 100644
--- a/docs/source/NonStatefulFunctions.rst
+++ b/docs/source/NonStatefulFunctions.rst
@@ -8,10 +8,11 @@ Functions that do *not* depend on a previous value.
.. toctree::
:maxdepth: 1
- CombinationFunctions
+
DistributionFunctions
LearningFunctions
ObjectiveFunctions
OptimizationFunctions
SelectionFunctions
- TransferFunctions
\ No newline at end of file
+ TransferFunctions
+ TransformFunctions
\ No newline at end of file
diff --git a/docs/source/TransformFunctions.rst b/docs/source/TransformFunctions.rst
new file mode 100644
index 00000000000..70cd2194ad7
--- /dev/null
+++ b/docs/source/TransformFunctions.rst
@@ -0,0 +1,11 @@
+TransformFunctions
+==================
+
+.. toctree::
+ :maxdepth: 3
+
+.. automodule:: psyneulink.core.components.functions.transformfunctions
+ :members: Concatenate, Rearrange, Reduce, LinearCombination, CombineMeans, MatrixTransform, PredictionErrorDeltaFunction
+ :private-members:
+ :exclude-members: Parameters
+
diff --git a/docs/source/_static/EMComposition_Example_fig.svg b/docs/source/_static/EMComposition_Example_fig.svg
index f3a5662f21e..7456c5d2b38 100644
--- a/docs/source/_static/EMComposition_Example_fig.svg
+++ b/docs/source/_static/EMComposition_Example_fig.svg
@@ -1,56 +1,94 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/docs/source/_static/EMComposition_field_weights_different.pdf b/docs/source/_static/EMComposition_field_weights_different.pdf
new file mode 100644
index 00000000000..97ebdb43148
Binary files /dev/null and b/docs/source/_static/EMComposition_field_weights_different.pdf differ
diff --git a/docs/source/_static/EMComposition_field_weights_different.svg b/docs/source/_static/EMComposition_field_weights_different.svg
index 94aab6b6a7c..eeb15badcc4 100644
--- a/docs/source/_static/EMComposition_field_weights_different.svg
+++ b/docs/source/_static/EMComposition_field_weights_different.svg
@@ -1,103 +1,209 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/docs/source/_static/EMComposition_field_weights_equal_fig.svg b/docs/source/_static/EMComposition_field_weights_equal_fig.svg
index dfa96297ffb..a093260a155 100644
--- a/docs/source/_static/EMComposition_field_weights_equal_fig.svg
+++ b/docs/source/_static/EMComposition_field_weights_equal_fig.svg
@@ -1,104 +1,209 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/psyneulink/core/components/component.py b/psyneulink/core/components/component.py
index fa00fbf1be4..23d1cfbf8ac 100644
--- a/psyneulink/core/components/component.py
+++ b/psyneulink/core/components/component.py
@@ -528,14 +528,14 @@
Context, ContextError, ContextFlags, INITIALIZATION_STATUS_FLAGS, _get_time, handle_external_context
from psyneulink.core.globals.mdf import MDFSerializable
from psyneulink.core.globals.keywords import \
- CONTEXT, CONTROL_PROJECTION, DEFERRED_INITIALIZATION, EXECUTE_UNTIL_FINISHED, \
+ CONTEXT, CONTROL_PROJECTION, DEFERRED_INITIALIZATION, DETERMINISTIC, EXECUTE_UNTIL_FINISHED, \
FUNCTION, FUNCTION_PARAMS, INIT_FULL_EXECUTE_METHOD, INPUT_PORTS, \
LEARNING, LEARNING_PROJECTION, MATRIX, MAX_EXECUTIONS_BEFORE_FINISHED, \
MODEL_SPEC_ID_PSYNEULINK, MODEL_SPEC_ID_METADATA, \
MODEL_SPEC_ID_INPUT_PORTS, MODEL_SPEC_ID_OUTPUT_PORTS, \
MODEL_SPEC_ID_MDF_VARIABLE, \
MODULATORY_SPEC_KEYWORDS, NAME, OUTPUT_PORTS, OWNER, PARAMS, PREFS_ARG, \
- RESET_STATEFUL_FUNCTION_WHEN, INPUT_SHAPES, VALUE, VARIABLE, SHARED_COMPONENT_TYPES
+ RANDOM, RESET_STATEFUL_FUNCTION_WHEN, INPUT_SHAPES, VALUE, VARIABLE, SHARED_COMPONENT_TYPES
from psyneulink.core.globals.log import LogCondition
from psyneulink.core.globals.parameters import \
Defaults, SharedParameter, Parameter, ParameterAlias, ParameterError, ParametersBase, check_user_specified, copy_parameter_value, is_array_like
@@ -931,6 +931,9 @@ class Component(MDFSerializable, metaclass=ComponentsMeta):
componentType = None
standard_constructor_args = {EXECUTE_UNTIL_FINISHED, FUNCTION_PARAMS, MAX_EXECUTIONS_BEFORE_FINISHED, RESET_STATEFUL_FUNCTION_WHEN, INPUT_SHAPES}
+ deprecated_constructor_args = {
+ 'size': 'input_shapes',
+ }
# helper attributes for MDF model spec
_model_spec_id_parameters = 'parameters'
@@ -1388,6 +1391,9 @@ def _get_compilation_state(self):
if cost_functions.DURATION not in cost_functions:
blacklist.add('duration_cost_fct')
+ if getattr(self, "mode", None) == DETERMINISTIC and getattr(self, "tie", None) != RANDOM:
+ whitelist.remove('random_state')
+
# Drop previous_value from MemoryFunctions
if hasattr(self.parameters, 'duplicate_keys'):
blacklist.add("previous_value")
@@ -1505,13 +1511,20 @@ def _get_compilation_params(self):
"retain_torch_trained_outputs", "retain_torch_targets", "retain_torch_losses"
"torch_trained_outputs", "torch_targets", "torch_losses",
# should be added to relevant _gen_llvm_function... when aug:
- # OneHot:
- 'abs_val', 'indicator',
# SoftMax:
'mask_threshold', 'adapt_scale', 'adapt_base', 'adapt_entropy_weighting',
# LCAMechanism
"mask"
}
+
+ # OneHot:
+ # * runtime abs_val and indicator are only used in deterministic mode.
+ # * random_state and seed are only used in RANDOM tie resolution.
+ if getattr(self, "mode", None) != DETERMINISTIC:
+ blacklist.update(['abs_val', 'indicator'])
+ elif getattr(self, "tie", None) != RANDOM:
+ blacklist.add("seed")
+
# Mechanism's need few extra entries:
# * matrix -- is never used directly, and is flatened below
# * integration_rate -- shape mismatch with param port input
@@ -2150,8 +2163,11 @@ def alias_conflicts(alias, passed_name):
conflicting_aliases = []
unused_constructor_args = {}
+ deprecated_args = {}
for p in self.parameters:
if p.name in illegal_passed_args:
+ # p must have a constructor_argument, because otherwise
+ # p.name would not be in illegal_passed_args
assert p.constructor_argument is not None
unused_constructor_args[p.name] = p.constructor_argument
@@ -2164,6 +2180,12 @@ def alias_conflicts(alias, passed_name):
if alias_conflicts(p, passed_name):
conflicting_aliases.append((p.source.name, passed_name, p.name))
+ for arg in illegal_passed_args:
+ try:
+ deprecated_args[arg] = self.deprecated_constructor_args[arg]
+ except KeyError:
+ continue
+
# raise constructor arg errors
if len(unused_constructor_args) > 0:
raise create_illegal_argument_error([
@@ -2171,6 +2193,13 @@ def alias_conflicts(alias, passed_name):
for arg, constr_arg in unused_constructor_args.items()
])
+ # raise deprecated argument errors
+ if len(deprecated_args) > 0:
+ raise create_illegal_argument_error([
+ f"'{arg}' is deprecated. Use '{new_arg}' instead"
+ for arg, new_arg in deprecated_args.items()
+ ])
+
# raise generic illegal argument error
unknown_args = illegal_passed_args.difference(unused_constructor_args)
if len(unknown_args) > 0:
diff --git a/psyneulink/core/components/functions/nonstateful/objectivefunctions.py b/psyneulink/core/components/functions/nonstateful/objectivefunctions.py
index 4a7d890028c..d68512493b1 100644
--- a/psyneulink/core/components/functions/nonstateful/objectivefunctions.py
+++ b/psyneulink/core/components/functions/nonstateful/objectivefunctions.py
@@ -143,7 +143,7 @@ class Stability(ObjectiveFunction):
length of array for which stability is calculated.
matrix : list, np.ndarray, function keyword, or MappingProjection : default HOLLOW_MATRIX
- weight matrix from each element of `variable ` to each other; if a matrix other
+ weight matrix from each element of `variable ` to each other; if a matrix other
than HOLLOW_MATRIX is assigned, it is convolved with HOLLOW_MATRIX to eliminate self-connections from the
stability calculation.
@@ -351,8 +351,8 @@ def _instantiate_attributes_before_function(self, function=None, context=None):
if isinstance(matrix, MappingProjection):
matrix = matrix._parameter_ports[MATRIX]
- elif isinstance(matrix, ParameterPort):
- pass
+ # elif isinstance(matrix, ParameterPort):
+ # pass
else:
matrix = get_matrix(matrix, size, size)
@@ -364,9 +364,13 @@ def _instantiate_attributes_before_function(self, function=None, context=None):
self.defaults.variable]
if self.metric == ENTROPY:
- self.metric_fct = Distance(default_variable=default_variable, metric=CROSS_ENTROPY, normalize=self.parameters.normalize.default_value)
+ self.metric_fct = Distance(default_variable=default_variable,
+ metric=CROSS_ENTROPY,
+ normalize=self.parameters.normalize.default_value)
elif self.metric in DISTANCE_METRICS._set():
- self.metric_fct = Distance(default_variable=default_variable, metric=self.metric, normalize=self.parameters.normalize.default_value)
+ self.metric_fct = Distance(default_variable=default_variable,
+ metric=self.metric,
+ normalize=self.parameters.normalize.default_value)
else:
assert False, "Unknown metric"
@@ -462,6 +466,8 @@ def _function(self,
# MODIFIED 6/12/19 END
matrix = self._get_current_parameter_value(MATRIX, context)
+ if matrix is None:
+ matrix = self.matrix
current = variable
@@ -538,7 +544,7 @@ class Energy(Stability):
length of array for which energy is calculated.
matrix : list, np.ndarray, or matrix keyword
- weight matrix from each element of `variable ` to each other; if a matrix other
+ weight matrix from each element of `variable ` to each other; if a matrix other
than INVERSE_HOLLOW_MATRIX is assigned, it is convolved with HOLLOW_MATRIX to eliminate self-connections from
the energy calculation.
@@ -566,7 +572,7 @@ def __init__(self,
default_variable=None,
input_shapes=None,
normalize:bool=None,
- # transfer_fct=None,
+ transfer_fct=None,
matrix=None,
params=None,
owner=None,
@@ -575,20 +581,20 @@ def __init__(self,
super().__init__(
default_variable=default_variable,
input_shapes=input_shapes,
- metric=ENERGY,
- matrix=matrix,
- # transfer_fct=transfer_fct,
- normalize=normalize,
- params=params,
- owner=owner,
- prefs=prefs)
+ metric=ENERGY,
+ matrix=matrix,
+ transfer_fct=transfer_fct,
+ normalize=normalize,
+ params=params,
+ owner=owner,
+ prefs=prefs)
class Entropy(Stability):
"""
Entropy( \
default_variable=None, \
- input_shapes=None, \
+ input_shapes=None, \
matrix=INVERSE_HOLLOW_MATRIX, \
transfer_fct=None \
normalize=False, \
@@ -648,7 +654,7 @@ class Entropy(Stability):
length of array for which energy is calculated.
matrix : list, np.ndarray, or matrix keyword
- weight matrix from each element of `variable ` to each other; if a matrix other
+ weight matrix from each element of `variable ` to each other; if a matrix other
than INVERSE_HOLLOW_MATRIX is assigned, it is convolved with HOLLOW_MATRIX to eliminate self-connections from
the entropy calculation.
@@ -674,7 +680,9 @@ class Entropy(Stability):
@check_user_specified
def __init__(self,
default_variable=None,
+ input_shapes=None,
normalize:bool=None,
+ matrix=None,
transfer_fct=None,
params=None,
owner=None,
@@ -682,13 +690,14 @@ def __init__(self,
super().__init__(
default_variable=default_variable,
- # matrix=matrix,
- metric=ENTROPY,
- transfer_fct=transfer_fct,
- normalize=normalize,
- params=params,
- owner=owner,
- prefs=prefs)
+ input_shapes=input_shapes,
+ metric=ENTROPY,
+ matrix=matrix,
+ transfer_fct=transfer_fct,
+ normalize=normalize,
+ params=params,
+ owner=owner,
+ prefs=prefs)
class Distance(ObjectiveFunction):
diff --git a/psyneulink/core/components/functions/nonstateful/selectionfunctions.py b/psyneulink/core/components/functions/nonstateful/selectionfunctions.py
index fb73085de62..defd01e050b 100644
--- a/psyneulink/core/components/functions/nonstateful/selectionfunctions.py
+++ b/psyneulink/core/components/functions/nonstateful/selectionfunctions.py
@@ -187,7 +187,7 @@ class OneHot(SelectionFunction):
First (possibly only) item specifies a template for the array to be transformed; if `mode ` is
*PROB* then a 2nd item must be included that is a probability distribution with same length as 1st item.
- mode : DETERMINISITC, PROB, PROB_INDICATOR,
+ mode : DETERMINISTiC, PROB, PROB_INDICATOR,
ARG_MAX, ARG_MAX_ABS, ARG_MAX_INDICATOR, ARG_MAX_ABS_INDICATOR,
ARG_MIN, ARG_MIN_ABS, ARG_MIN_INDICATOR, ARG_MIN_ABS_INDICATOR,
MAX_VAL, MAX_ABS_VAL, MAX_INDICATOR, MAX_ABS_INDICATOR,
@@ -237,7 +237,7 @@ class OneHot(SelectionFunction):
distribution, each element of which specifies the probability for selecting the corresponding element of the
1st item.
- mode : DETERMINISITC, PROB, PROB_INDICATOR,
+ mode : DETERMINISTIC, PROB, PROB_INDICATOR,
ARG_MAX, ARG_MAX_ABS, ARG_MAX_INDICATOR, ARG_MAX_ABS_INDICATOR,
ARG_MIN, ARG_MIN_ABS, ARG_MIN_INDICATOR, ARG_MIN_ABS_INDICATOR,
MAX_VAL, MAX_ABS_VAL, MAX_INDICATOR, MAX_ABS_INDICATOR,
@@ -421,86 +421,25 @@ def _validate_params(self, request_set, target_set=None, context=None):
f"cannot be specified.")
def _gen_llvm_function_body(self, ctx, builder, params, state, arg_in, arg_out, *, tags:frozenset):
- best_idx_ptr = builder.alloca(ctx.int32_ty)
- builder.store(best_idx_ptr.type.pointee(0), best_idx_ptr)
-
if self.mode in {PROB, PROB_INDICATOR}:
+
sum_ptr = builder.alloca(ctx.float_ty)
builder.store(sum_ptr.type.pointee(-0.0), sum_ptr)
- random_draw_ptr = builder.alloca(ctx.float_ty)
rand_state_ptr = ctx.get_random_state_ptr(builder, self, state, params)
rng_f = ctx.get_uniform_dist_function_by_state(rand_state_ptr)
+ random_draw_ptr = builder.alloca(rng_f.args[-1].type.pointee)
builder.call(rng_f, [rand_state_ptr, random_draw_ptr])
random_draw = builder.load(random_draw_ptr)
prob_in = builder.gep(arg_in, [ctx.int32_ty(0), ctx.int32_ty(1)])
arg_in = builder.gep(arg_in, [ctx.int32_ty(0), ctx.int32_ty(0)])
- with pnlvm.helpers.array_ptr_loop(builder, arg_in, "search") as (b1, idx):
- best_idx = b1.load(best_idx_ptr)
- best_ptr = b1.gep(arg_in, [ctx.int32_ty(0), best_idx])
-
- current_ptr = b1.gep(arg_in, [ctx.int32_ty(0), idx])
- current = b1.load(current_ptr)
-
- if self.mode not in {PROB, PROB_INDICATOR}:
- fabs = ctx.get_builtin("fabs", [current.type])
-
- is_first = b1.icmp_unsigned("==", idx, idx.type(0))
-
- # Allow the first element to win the comparison
- prev_best = b1.select(is_first, best_ptr.type.pointee(float("NaN")), b1.load(best_ptr))
-
- if self.mode == ARG_MAX:
- cmp_op = ">"
- cmp_prev = prev_best
- cmp_curr = current
- val = current
-
- elif self.mode == ARG_MAX_ABS:
- cmp_op = ">"
- cmp_prev = b1.call(fabs, [prev_best])
- cmp_curr = b1.call(fabs, [current])
- val = b1.call(fabs, [current])
-
- elif self.mode == ARG_MAX_INDICATOR:
- cmp_op = ">"
- cmp_prev = prev_best
- cmp_curr = current
- val = current.type(1.0)
-
- elif self.mode == ARG_MAX_ABS_INDICATOR:
- cmp_op = ">"
- cmp_prev = b1.call(fabs, [prev_best])
- cmp_curr = b1.call(fabs, [current])
- val = current.type(1.0)
-
- elif self.mode == ARG_MIN:
- cmp_op = "<"
- cmp_prev = prev_best
- cmp_curr = current
- val = current
-
- elif self.mode == ARG_MIN_ABS:
- cmp_op = "<"
- cmp_prev = b1.call(fabs, [prev_best])
- cmp_curr = b1.call(fabs, [current])
- val = b1.call(fabs, [current])
-
- elif self.mode == ARG_MIN_INDICATOR:
- cmp_op = "<"
- cmp_prev = prev_best
- cmp_curr = current
- val = current.type(1.0)
-
- elif self.mode == ARG_MIN_ABS_INDICATOR:
- cmp_op = "<"
- cmp_prev = b1.call(fabs, [prev_best])
- cmp_curr = b1.call(fabs, [current])
- val = current.type(1.0)
-
- elif self.mode in {PROB, PROB_INDICATOR}:
+ with pnlvm.helpers.array_ptr_loop(builder, arg_in, "search") as (b1, idx):
+
+ current_ptr = b1.gep(arg_in, [ctx.int32_ty(0), idx])
+ current = b1.load(current_ptr)
+
# Update prefix sum
current_prob_ptr = b1.gep(prob_in, [ctx.int32_ty(0), idx])
sum_old = b1.load(sum_ptr)
@@ -511,27 +450,125 @@ def _gen_llvm_function_body(self, ctx, builder, params, state, arg_in, arg_out,
new_above = b1.fcmp_ordered("<", random_draw, sum_new)
cond = b1.and_(new_above, old_below)
- cmp_prev = current.type(1.0)
- cmp_curr = b1.select(cond, cmp_prev, cmp_prev.type(0.0))
- cmp_op = "=="
if self.mode == PROB:
val = current
else:
val = current.type(1.0)
- else:
- assert False, "Unsupported mode in LLVM: {} for OneHot Function".format(self.mode)
- prev_res_ptr = b1.gep(arg_out, [ctx.int32_ty(0), best_idx])
- cur_res_ptr = b1.gep(arg_out, [ctx.int32_ty(0), idx])
+ write_val = b1.select(cond, val, val.type(0.0))
+ cur_res_ptr = b1.gep(arg_out, [ctx.int32_ty(0), idx])
+ builder.store(write_val, cur_res_ptr)
+
+ return builder
+
+ elif self.mode == DETERMINISTIC:
+ direction = self.direction
+ tie = self.tie
+ abs_val_ptr = ctx.get_param_or_state_ptr(builder, self, self.parameters.abs_val, param_struct_ptr=params)
+ indicator_ptr = ctx.get_param_or_state_ptr(builder, self, self.parameters.indicator, param_struct_ptr=params)
+
+ abs_val = builder.load(abs_val_ptr)
+ is_abs_val = builder.fcmp_unordered("!=", abs_val, abs_val.type(0))
+
+ indicator = builder.load(indicator_ptr)
+ is_indicator = builder.fcmp_unordered("!=", indicator, indicator.type(0))
+
+ else:
+ direction, abs_val, indicator, tie = self._parse_mode(self.mode)
+ is_abs_val = ctx.bool_ty(abs_val)
+ is_indicator = ctx.bool_ty(indicator)
+
+ num_extremes_ptr = builder.alloca(ctx.int32_ty)
+ builder.store(num_extremes_ptr.type.pointee(0), num_extremes_ptr)
+
+ extreme_val_ptr = builder.alloca(ctx.float_ty)
+ builder.store(extreme_val_ptr.type.pointee(float("NaN")), extreme_val_ptr)
+
+ fabs_f = ctx.get_builtin("fabs", [extreme_val_ptr.type.pointee])
+
+ with pnlvm.helpers.recursive_iterate_arrays(ctx, builder, arg_in, loop_id="count_extremes") as (loop_builder, current_ptr):
+
+ current = loop_builder.load(current_ptr)
+ current_abs = loop_builder.call(fabs_f, [current])
+ current = builder.select(is_abs_val, current_abs, current)
+
+ old_extreme = loop_builder.load(extreme_val_ptr)
+ cmp_op = ">" if direction == MAX else "<"
+ is_new_extreme = loop_builder.fcmp_unordered(cmp_op, current, old_extreme)
+
+ with loop_builder.if_then(is_new_extreme):
+ loop_builder.store(current, extreme_val_ptr)
+ loop_builder.store(num_extremes_ptr.type.pointee(1), num_extremes_ptr)
+
+ is_old_extreme = loop_builder.fcmp_ordered("==", current, old_extreme)
+ with loop_builder.if_then(is_old_extreme):
+ extreme_count = loop_builder.load(num_extremes_ptr)
+ extreme_count = loop_builder.add(extreme_count, extreme_count.type(1))
+ loop_builder.store(extreme_count, num_extremes_ptr)
+
+
+ if tie == FIRST:
+ extreme_start = num_extremes_ptr.type.pointee(0)
+ extreme_stop = num_extremes_ptr.type.pointee(1)
+
+ elif tie == LAST:
+ extreme_stop = builder.load(num_extremes_ptr)
+ extreme_start = builder.sub(extreme_stop, extreme_stop.type(1))
+
+ elif tie == ALL:
+ extreme_start = num_extremes_ptr.type.pointee(0)
+ extreme_stop = builder.load(num_extremes_ptr)
+
+ elif tie == RANDOM:
+ rand_state_ptr = ctx.get_random_state_ptr(builder, self, state, params)
+ rand_f = ctx.get_rand_int_function_by_state(rand_state_ptr)
+ random_draw_ptr = builder.alloca(rand_f.args[-1].type.pointee)
+ num_extremes = builder.load(num_extremes_ptr)
+
+ builder.call(rand_f, [rand_state_ptr, ctx.int32_ty(0), num_extremes, random_draw_ptr])
+
+ extreme_start = builder.load(random_draw_ptr)
+ extreme_start = builder.trunc(extreme_start, ctx.int32_ty)
+ extreme_stop = builder.add(extreme_start, extreme_start.type(1))
- # Make sure other elements are zeroed
- builder.store(cur_res_ptr.type.pointee(0), cur_res_ptr)
+ else:
+ assert False, "Unknown tie resolution: {}".format(tie)
+
+
+ extreme_val = builder.load(extreme_val_ptr)
+ extreme_write_val = builder.select(is_indicator, extreme_val.type(1), extreme_val)
+ next_extreme_ptr = builder.alloca(num_extremes_ptr.type.pointee)
+ builder.store(next_extreme_ptr.type.pointee(0), next_extreme_ptr)
+
+ pnlvm.helpers.printf(ctx,
+ builder,
+ "{} replacing extreme values of %e from <%u,%u) out of %u\n".format(self.name),
+ extreme_val,
+ extreme_start,
+ extreme_stop,
+ builder.load(num_extremes_ptr),
+ tags={"one_hot"})
+
+ with pnlvm.helpers.recursive_iterate_arrays(ctx, builder, arg_in, arg_out, loop_id="mark_extremes") as (loop_builder, current_ptr, out_ptr):
+ current = loop_builder.load(current_ptr)
+ current_abs = loop_builder.call(fabs_f, [current])
+ current = builder.select(is_abs_val, current_abs, current)
+
+ is_extreme = loop_builder.fcmp_ordered("==", current, extreme_val)
+ current_extreme_idx = loop_builder.load(next_extreme_ptr)
+
+ with loop_builder.if_then(is_extreme):
+ next_extreme_idx = loop_builder.add(current_extreme_idx, current_extreme_idx.type(1))
+ loop_builder.store(next_extreme_idx, next_extreme_ptr)
- cmp_res = builder.fcmp_unordered(cmp_op, cmp_curr, cmp_prev)
- with builder.if_then(cmp_res):
- builder.store(prev_res_ptr.type.pointee(0), prev_res_ptr)
- builder.store(val, cur_res_ptr)
- builder.store(idx, best_idx_ptr)
+ is_after_start = loop_builder.icmp_unsigned(">=", current_extreme_idx, extreme_start)
+ is_before_stop = loop_builder.icmp_unsigned("<", current_extreme_idx, extreme_stop)
+
+ should_write_extreme = loop_builder.and_(is_extreme, is_after_start)
+ should_write_extreme = loop_builder.and_(should_write_extreme, is_before_stop)
+
+ write_value = loop_builder.select(should_write_extreme, extreme_write_val, extreme_write_val.type(0))
+ loop_builder.store(write_value, out_ptr)
return builder
@@ -641,6 +678,9 @@ def _parse_mode(self, mode):
indicator = True
tie = ALL
+ else:
+ assert False, f"Unknown mode: {mode}"
+
return direction, abs_val, indicator, tie
def _function(self,
@@ -693,65 +733,62 @@ def _function(self,
random_value = random_state.uniform()
chosen_item = next(element for element in cum_sum if element > random_value)
chosen_in_cum_sum = np.where(cum_sum == chosen_item, 1, 0)
- if mode is PROB:
+ if mode == PROB:
result = v * chosen_in_cum_sum
else:
result = np.ones_like(v) * chosen_in_cum_sum
+
# chosen_item = np.random.choice(v, 1, p=prob_dist)
# one_hot_indicator = np.where(v == chosen_item, 1, 0)
# return v * one_hot_indicator
return result
- elif mode is not DETERMINISTIC:
+ elif mode != DETERMINISTIC:
direction, abs_val, indicator, tie = self._parse_mode(mode)
- # if np.array(variable).ndim != 1:
- # raise FunctionError(f"If {MODE} for {self.__class__.__name__} {Function.__name__} is not set to "
- # f"'PROB' or 'PROB_INDICATOR', variable must be a 1d array: {variable}.")
-
array = variable
- max = None
- min = None
-
if abs_val is True:
- array = np.absolute(array)
+ array = np.absolute(variable)
if direction == MAX:
- max = np.max(array)
- if max == -np.inf:
- warnings.warn(f"Array passed to {self.name} of {self.owner.name} "
- f"is all -inf.")
+ extreme_val = np.max(array)
+ if extreme_val == -np.inf:
+ warnings.warn(f"Array passed to {self.name} of {self.owner.name} is all -inf.")
+
+ elif direction == MIN:
+ extreme_val = np.min(array)
+ if extreme_val == np.inf:
+ warnings.warn(f"Array passed to {self.name} of {self.owner.name} is all inf.")
+
else:
- min = np.min(array)
- if min == np.inf:
- warnings.warn(f"Array passed to {self.name} of {self.owner.name} "
- f"is all inf.")
+ assert False, f"Unknown direction: '{direction}'."
- extreme_val = max if direction == MAX else min
+ extreme_indices = np.where(array == extreme_val)
+
+ num_indices = len(extreme_indices[0])
+ assert all(len(idx) == num_indices for idx in extreme_indices)
+
+ if tie == FIRST:
+ selected_idx = 0
+
+ elif tie == LAST:
+ selected_idx = -1
+
+ elif tie == RANDOM:
+ random_state = self._get_current_parameter_value("random_state", context)
+ selected_idx = random_state.randint(num_indices)
+
+ elif tie == ALL:
+ selected_idx = slice(num_indices)
- if tie == ALL:
- if direction == MAX:
- result = np.where(array == max, max, -np.inf)
- else:
- result = np.where(array == min, min, np.inf)
else:
- if tie == FIRST:
- index = np.min(np.where(array == extreme_val))
- elif tie == LAST:
- index = np.max(np.where(array == extreme_val))
- elif tie == RANDOM:
- index = np.random.choice(np.where(array == extreme_val))
- else:
- assert False, f"PROGRAM ERROR: Unrecognized value for 'tie' in OneHot function: '{tie}'."
- result = np.zeros_like(array)
- result[index] = extreme_val
-
- if indicator is True:
- result = np.where(result == extreme_val, 1, result)
- if max is not None:
- result = np.where(result == -np.inf, 0, result)
- if min is not None:
- result = np.where(result == np.inf, 0, result)
+ assert False, f"PROGRAM ERROR: Unrecognized value for 'tie' in OneHot function: '{tie}'."
+
+
+ set_indices = tuple(index[selected_idx] for index in extreme_indices)
+
+ result = np.zeros_like(variable)
+ result[set_indices] = 1 if indicator else extreme_val
return self.convert_output_type(result)
diff --git a/psyneulink/core/components/functions/nonstateful/transformfunctions.py b/psyneulink/core/components/functions/nonstateful/transformfunctions.py
index bd0403bfcf5..99733aad2bc 100644
--- a/psyneulink/core/components/functions/nonstateful/transformfunctions.py
+++ b/psyneulink/core/components/functions/nonstateful/transformfunctions.py
@@ -1628,20 +1628,48 @@ class MatrixTransform(TransformFunction): # -----------------------------------
Matrix transform of `variable `.
- `function ` returns dot product of variable with matrix:
+ `function ` returns a matrix transform of `variable `
+ based on the **operation** argument.
- .. math::
- variable \\bullet matrix
+ **operation** = *DOT_PRODUCT*:
- If *DOT_PRODUCT* is specified as the **operation*, the result is the dot product of `variable
- ` and `matrix `; if *L0* is specified, the result is the
- difference between `variable ` and `matrix ` (see
- `operation ` for additional details).
+ Returns the dot (inner) product of `variable ` and `matrix `:
- If **normalize** is True, the result is normalized by the product of the norms of the variable and matrix:
+ .. math::
+ {variable} \\bullet |matrix|
+
+ If **normalize** =True, the result is normalized by the product of the norms of the variable and matrix:
+
+ .. math::
+ \\frac{variable \\bullet matrix}{\\|variable\\| \\cdot \\|matrix\\|}
+
+ .. note::
+ For **normalize** =True, the result is the same as the cosine of the angle between pairs of vectors.
+
+ **operation** = *L0*:
+
+ Returns the absolute value of the difference between `variable ` and `matrix
+ `:
+
+ .. math::
+ |variable - matrix|
+
+ If **normalize** =True, the result is normalized by the norm of the sum of differences between the variable and
+ matrix, which is then subtracted from 1:
+
+ .. math::
+ 1 - \\frac{|variable - matrix|}{\\|variable - matrix\\|}
+
+ .. note::
+ For **normalize** =True, the result has the same effect as the normalized *DOT_PRODUCT* operation,
+ with more similar pairs of vectors producing larger values (closer to 1).
+
+ .. warning::
+ For **normalize** =False, the result is smaller (closer to 0) for more similar pairs of vectors,
+ which is **opposite** the effect of the *DOT_PRODUCT* and normalized *L0* operations. If the desired
+ result is that more similar pairs of vectors produce larger values, set **normalize** =True or
+ use the *DOT_PRODUCT* operation.
- .. math::
- \\frac{variable \\bullet matrix}{\\|variable\\| \\cdot \\|matrix\\|}
COMMENT: [CONVERT TO FIGURE]
----------------------------------------------------------------------------------------------------------
@@ -1679,7 +1707,7 @@ class MatrixTransform(TransformFunction): # -----------------------------------
specifies matrix used to transform `variable `
(see `matrix ` for specification details).
- When MatrixTransform is the `function ` of a projection:
+ When MatrixTransform is the `function ` of a projection:
- the matrix specification must be compatible with the variables of the `sender `
and `receiver `
@@ -1795,15 +1823,6 @@ class Parameters(TransformFunction.Parameters):
normalize = Parameter(False)
bounds = None
- # def is_matrix_spec(m):
- # if m is None:
- # return True
- # if m in MATRIX_KEYWORD_VALUES:
- # return True
- # if isinstance(m, (list, np.ndarray, types.FunctionType)):
- # return True
- # return False
-
@check_user_specified
@beartype
def __init__(self,
@@ -1833,25 +1852,6 @@ def __init__(self,
skip_log=True,
)
- # def _validate_variable(self, variable, context=None):
- # """Insure that variable passed to MatrixTransform is a max 2D array
- #
- # :param variable: (max 2D array)
- # :param context:
- # :return:
- # """
- # variable = super()._validate_variable(variable, context)
- #
- # # Check that variable <= 2D
- # try:
- # if not variable.ndim <= 2:
- # raise FunctionError("variable ({0}) for {1} must be a numpy.ndarray of dimension at most 2".format(variable, self.__class__.__name__))
- # except AttributeError:
- # raise FunctionError("PROGRAM ERROR: variable ({0}) for {1} should be a numpy.ndarray".
- # format(variable, self.__class__.__name__))
- #
- # return variable
-
def _validate_params(self, request_set, target_set=None, context=None):
"""Validate params and assign to targets
@@ -2013,15 +2013,6 @@ def _validate_params(self, request_set, target_set=None, context=None):
self.name,
self.owner_name,
MATRIX_KEYWORD_NAMES))
-
- # operation param
- elif param_name == OPERATION:
- if param_value == L0 and NORMALIZE in param_set and param_set[NORMALIZE]:
- raise FunctionError(f"The 'operation' parameter for the {self.name} function of "
- f"{self.owner_name} is set to 'L0', so the 'normalize' parameter "
- f"should not be set to True "
- f"(normalization is not needed, and can cause a divide by zero error). "
- f"Set 'normalize' to False or change 'operation' to 'DOT_PRODUCT'.")
else:
continue
@@ -2176,7 +2167,7 @@ def diff_with_normalization(vector, matrix):
if normalize:
return diff_with_normalization
else:
- return lambda x, y: torch.sum((1 - torch.abs(x - y)),axis=0)
+ return lambda x, y: torch.sum(torch.abs(x - y),axis=0)
else:
from psyneulink.library.compositions.autodiffcomposition import AutodiffCompositionError
@@ -2224,10 +2215,11 @@ def _function(self,
result = np.dot(vector, matrix)
elif operation == L0:
- normalization = 1
if normalize:
- normalization = np.sum(np.abs(vector - matrix))
- result = np.sum(((1 - np.abs(vector - matrix)) / normalization),axis=0)
+ normalization = np.sum(np.abs(vector - matrix)) or 1
+ result = np.sum((1 - (np.abs(vector - matrix)) / normalization),axis=0)
+ else:
+ result = np.sum((np.abs(vector - matrix)),axis=0)
return self.convert_output_type(result)
diff --git a/psyneulink/core/components/functions/stateful/memoryfunctions.py b/psyneulink/core/components/functions/stateful/memoryfunctions.py
index cd97cdb4189..7466f968abf 100644
--- a/psyneulink/core/components/functions/stateful/memoryfunctions.py
+++ b/psyneulink/core/components/functions/stateful/memoryfunctions.py
@@ -2372,6 +2372,7 @@ def _gen_llvm_function_body(self, ctx, builder, params, state, arg_in, arg_out,
max_entries = len(vals_ptr.type.pointee)
entries = builder.load(count_ptr)
entries = pnlvm.helpers.uint_min(builder, entries, max_entries)
+
# The call to random function needs to be after check to match python
with builder.if_then(retr_rand):
rand_ptr = builder.alloca(ctx.float_ty)
@@ -2385,53 +2386,41 @@ def _gen_llvm_function_body(self, ctx, builder, params, state, arg_in, arg_out,
with builder.if_then(retr, likely=True):
# Determine distances
distance_f = ctx.import_llvm_function(self.distance_function)
- distance_params, distance_state = ctx.get_param_or_state_ptr(builder, self, "distance_function", param_struct_ptr=params, state_struct_ptr=state)
+ distance_params, distance_state = ctx.get_param_or_state_ptr(builder,
+ self,
+ "distance_function",
+ param_struct_ptr=params,
+ state_struct_ptr=state)
distance_arg_in = builder.alloca(distance_f.args[2].type.pointee)
- builder.store(builder.load(var_key_ptr),
- builder.gep(distance_arg_in, [ctx.int32_ty(0),
- ctx.int32_ty(0)]))
+ builder.store(builder.load(var_key_ptr), builder.gep(distance_arg_in, [ctx.int32_ty(0), ctx.int32_ty(0)]))
selection_arg_in = builder.alloca(pnlvm.ir.ArrayType(distance_f.args[3].type.pointee, max_entries))
with pnlvm.helpers.for_loop_zero_inc(builder, entries, "distance_loop") as (b, idx):
compare_ptr = b.gep(keys_ptr, [ctx.int32_ty(0), idx])
- b.store(b.load(compare_ptr),
- b.gep(distance_arg_in, [ctx.int32_ty(0), ctx.int32_ty(1)]))
+ b.store(b.load(compare_ptr), b.gep(distance_arg_in, [ctx.int32_ty(0), ctx.int32_ty(1)]))
distance_arg_out = b.gep(selection_arg_in, [ctx.int32_ty(0), idx])
- b.call(distance_f, [distance_params, distance_state,
- distance_arg_in, distance_arg_out])
-
- # MODIFIED 10/13/24 NEW:
- # IMPLEMENTATION NOTE:
- # REPLACE MIN_VAL with ARG_MIN and MIN_INDICATOR with ARG_MIN_INDICATOR
- # until the MIN_XXX args are implemented in LLVM
- # since, at present, the tests don't seem to distinguish between these (i.e., return of multiple values;
- # should add tests that do so once MIN_VAL and related args are implemented in LLVM)
- if isinstance(self.selection_function, OneHot):
- mode = self.selection_function.mode
- if mode == MIN_VAL:
- self.selection_function.mode = ARG_MIN
- elif mode == MIN_INDICATOR:
- self.selection_function.mode = ARG_MIN_INDICATOR
- # MODIFIED 10/13/24 END
+ b.call(distance_f, [distance_params, distance_state, distance_arg_in, distance_arg_out])
+
selection_f = ctx.import_llvm_function(self.selection_function)
- selection_params, selection_state = ctx.get_param_or_state_ptr(builder, self, "selection_function", param_struct_ptr=params, state_struct_ptr=state)
+ selection_params, selection_state = ctx.get_param_or_state_ptr(builder,
+ self,
+ "selection_function",
+ param_struct_ptr=params,
+ state_struct_ptr=state)
selection_arg_out = builder.alloca(selection_f.args[3].type.pointee)
- builder.call(selection_f, [selection_params, selection_state,
- selection_arg_in, selection_arg_out])
+ builder.call(selection_f, [selection_params, selection_state, selection_arg_in, selection_arg_out])
# Find the selected index
selected_idx_ptr = builder.alloca(ctx.int32_ty)
builder.store(ctx.int32_ty(0), selected_idx_ptr)
- with pnlvm.helpers.for_loop_zero_inc(builder, entries, "distance_loop") as (b,idx):
+ with pnlvm.helpers.for_loop_zero_inc(builder, entries, "selection_loop") as (b, idx):
selection_val = b.load(b.gep(selection_arg_out, [ctx.int32_ty(0), idx]))
non_zero = b.fcmp_ordered('!=', selection_val, selection_val.type(0))
with b.if_then(non_zero):
b.store(idx, selected_idx_ptr)
selected_idx = builder.load(selected_idx_ptr)
- selected_key = builder.load(builder.gep(keys_ptr, [ctx.int32_ty(0),
- selected_idx]))
- selected_val = builder.load(builder.gep(vals_ptr, [ctx.int32_ty(0),
- selected_idx]))
+ selected_key = builder.load(builder.gep(keys_ptr, [ctx.int32_ty(0), selected_idx]))
+ selected_val = builder.load(builder.gep(vals_ptr, [ctx.int32_ty(0), selected_idx]))
builder.store(selected_key, out_key_ptr)
builder.store(selected_val, out_val_ptr)
diff --git a/psyneulink/core/components/mechanisms/mechanism.py b/psyneulink/core/components/mechanisms/mechanism.py
index e261ce5eb32..5bc3beeede2 100644
--- a/psyneulink/core/components/mechanisms/mechanism.py
+++ b/psyneulink/core/components/mechanisms/mechanism.py
@@ -2518,17 +2518,26 @@ def execute(self,
# UPDATE VARIABLE and InputPort(s)
# Executing or simulating Composition, so get input by updating input_ports
- if (input is None
- and (context.execution_phase is not ContextFlags.IDLE)
- and any(p.path_afferents for p in self.input_ports)):
+ if (
+ input is None
+ and (
+ (
+ context.execution_phase is not ContextFlags.IDLE
+ and any(p.path_afferents for p in self.input_ports)
+ )
+ or any(p.default_input is not None for p in self.input_ports)
+ )
+ ):
variable = self._update_input_ports(runtime_port_params[INPUT_PORT_PARAMS], context)
- # Direct call to execute Mechanism with specified input, so assign input to Mechanism's input_ports
else:
+ # Direct call to execute Mechanism with specified input, so assign input to Mechanism's input_ports
if context.source & ContextFlags.COMMAND_LINE:
context.execution_phase = ContextFlags.PROCESSING
if input is not None:
input = convert_all_elements_to_np_array(input)
+
+ # No input was specified, so use Mechanism's default variable
if input is None:
input = self.defaults.variable
# FIX: this input value is sent to input CIMs when compositions are nested
@@ -3197,12 +3206,41 @@ def _gen_llvm_function_reset(self, ctx, builder, m_base_params, m_state, m_arg_i
reinit_in = builder.alloca(reinit_func.args[2].type.pointee, name="reinit_in")
reinit_out = builder.alloca(reinit_func.args[3].type.pointee, name="reinit_out")
- reinit_base_params, reinit_state = ctx.get_param_or_state_ptr(builder, self, "function", param_struct_ptr=m_base_params, state_struct_ptr=m_state)
- reinit_params, builder = self._gen_llvm_param_ports_for_obj(
- self.function, reinit_base_params, ctx, builder, m_base_params, m_state, m_arg_in)
+ reinit_base_params, reinit_state = ctx.get_param_or_state_ptr(builder,
+ self,
+ "function",
+ param_struct_ptr=m_base_params,
+ state_struct_ptr=m_state)
+ reinit_params, builder = self._gen_llvm_param_ports_for_obj(self.function,
+ reinit_base_params,
+ ctx,
+ builder,
+ m_base_params,
+ m_state,
+ m_arg_in)
builder.call(reinit_func, [reinit_params, reinit_state, reinit_in, reinit_out])
+ if hasattr(self, "integrator_function") and getattr(self, "integrator_mode", False):
+ reinit_func = ctx.import_llvm_function(self.integrator_function, tags=tags)
+ reinit_in = builder.alloca(reinit_func.args[2].type.pointee, name="integrator_reinit_in")
+ reinit_out = builder.alloca(reinit_func.args[3].type.pointee, name="integrator_reinit_out")
+
+ reinit_base_params, reinit_state = ctx.get_param_or_state_ptr(builder,
+ self,
+ "integrator_function",
+ param_struct_ptr=m_base_params,
+ state_struct_ptr=m_state)
+ reinit_params, builder = self._gen_llvm_param_ports_for_obj(self.integrator_function,
+ reinit_base_params,
+ ctx,
+ builder,
+ m_base_params,
+ m_state,
+ m_arg_in)
+
+ builder.call(reinit_func, [reinit_params, reinit_state, reinit_in, reinit_out])
+
return builder
def _gen_llvm_function(self, *, extra_args=[], ctx:pnlvm.LLVMBuilderContext, tags:frozenset):
@@ -3212,9 +3250,11 @@ def _gen_llvm_function(self, *, extra_args=[], ctx:pnlvm.LLVMBuilderContext, tag
Mechanisms need to support "is_finished" execution variant (used by scheduling conditions)
on top of the variants supported by Component.
"""
+
+ # Call parent "_gen_llvm_function", this should result in calling
+ # "_gen_llvm_function_body" below
if "is_finished" not in tags:
- return super()._gen_llvm_function(extra_args=extra_args, ctx=ctx,
- tags=tags)
+ return super()._gen_llvm_function(extra_args=extra_args, ctx=ctx, tags=tags)
# Keep all 4 standard arguments to ease invocation
args = [ctx.get_param_struct_type(self).as_pointer(),
diff --git a/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py b/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py
index fb3003db099..72ec6ed3221 100644
--- a/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py
+++ b/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py
@@ -1452,7 +1452,7 @@ def _instantiate_objective_mechanism(self, input_ports=None, context=None):
if not isinstance(monitor_for_control, list):
monitor_for_control = [monitor_for_control]
- # If objective_mechanism is used to specify OutputPorts to be monitored (legacy feature)
+ # If objective_mechanism arg is used to specify OutputPorts to be monitored (legacy feature)
# move them to monitor_for_control
if isinstance(self.objective_mechanism, list):
monitor_for_control.extend(self.objective_mechanism)
diff --git a/psyneulink/core/components/ports/inputport.py b/psyneulink/core/components/ports/inputport.py
index ce123b7118c..8fdcb4a2501 100644
--- a/psyneulink/core/components/ports/inputport.py
+++ b/psyneulink/core/components/ports/inputport.py
@@ -713,7 +713,8 @@ class InputPort(Port_Base):
is executed and its variable is assigned None. If *default_input* is assigned *DEFAULT_VARIABLE*, then the
`default value ` for the InputPort's `variable ` is used as its value.
This is useful for assignment to a Mechanism that needs a constant (i.e., fixed value) as the input to its
- `function `.
+ `function ` (such as a `bias unit ` in an
+ `AutodiffComposition`).
.. note::
If `default_input ` is assigned *DEFAULT_VARIABLE*, then its `internal_only
diff --git a/psyneulink/core/components/ports/port.py b/psyneulink/core/components/ports/port.py
index 3388fb3417b..d238a3db4f7 100644
--- a/psyneulink/core/components/ports/port.py
+++ b/psyneulink/core/components/ports/port.py
@@ -2102,14 +2102,14 @@ def set_projection_value(projection, value, context):
def _execute(self, variable=None, context=None, runtime_params=None):
if variable is None:
+ if hasattr(self, DEFAULT_INPUT) and self.default_input == DEFAULT_VARIABLE:
+ return copy_parameter_value(self.defaults.variable)
+
variable = self._get_variable_from_projections(context)
# if the fallback is also None
# return None, so that this port is ignored
- # KDM 8/2/19: double check the relevance of this branch
if variable is None:
- if hasattr(self, DEFAULT_INPUT) and self.default_input == DEFAULT_VARIABLE:
- return copy_parameter_value(self.defaults.variable)
return None
return super()._execute(
diff --git a/psyneulink/core/compositions/composition.py b/psyneulink/core/compositions/composition.py
index 08f5e91e2bd..15362e90979 100644
--- a/psyneulink/core/compositions/composition.py
+++ b/psyneulink/core/compositions/composition.py
@@ -2912,7 +2912,7 @@ def input_function(env, result):
from psyneulink.core.components.mechanisms.modulatory.modulatorymechanism import ModulatoryMechanism_Base
from psyneulink.core.components.mechanisms.processing.compositioninterfacemechanism import CompositionInterfaceMechanism
from psyneulink.core.components.mechanisms.processing.objectivemechanism import ObjectiveMechanism
-from psyneulink.core.components.mechanisms.processing.processingmechanism import ProcessingMechanism
+from psyneulink.core.components.mechanisms.processing.processingmechanism import ProcessingMechanism, ProcessingMechanism_Base
from psyneulink.core.components.ports.inputport import InputPort, InputPortError
from psyneulink.core.components.ports.modulatorysignals.controlsignal import ControlSignal
from psyneulink.core.components.ports.modulatorysignals.learningsignal import LearningSignal
@@ -2939,7 +2939,8 @@ def input_function(env, result):
INPUT, INPUT_PORTS, INPUTS, INPUT_CIM_NAME, \
LEARNABLE, LEARNED_PROJECTIONS, LEARNING_FUNCTION, LEARNING_MECHANISM, LEARNING_MECHANISMS, LEARNING_PATHWAY, \
LEARNING_SIGNAL, Loss, \
- MATRIX, MAYBE, MODEL_SPEC_ID_METADATA, MONITOR, MONITOR_FOR_CONTROL, NAME, NESTED, NO_CLAMP, NODE, NODES, \
+ MATRIX, MAYBE, MODEL_SPEC_ID_METADATA, MONITOR, MONITOR_FOR_CONTROL, MULTIPLICATIVE_PARAM, \
+ NAME, NESTED, NO_CLAMP, NODE, NODES, \
OBJECTIVE_MECHANISM, ONLINE, ONLY, OUTCOME, OUTPUT, OUTPUT_CIM_NAME, OUTPUT_MECHANISM, OUTPUT_PORTS, OWNER_VALUE, \
PARAMETER, PARAMETER_CIM_NAME, PORT, \
PROCESSING_PATHWAY, PROJECTION, PROJECTIONS, PROJECTION_TYPE, PROJECTION_PARAMS, PULSE_CLAMP, RECEIVER, \
@@ -3369,7 +3370,7 @@ class NodeRole(enum.Enum):
BIAS
A `Node ` for which one or more of its `InputPorts ` is assigned
*DEFAULT_VARIABLE* as its `default_input ` (which provides it a prespecified
- input that is constant across executions). Such a node can also be assigned as an `INPUT` and/or `ORIGIN`,
+ input that is constant across executions). Such a node can also be assigned as an `INPUT` and/or `ORIGIN`,
if it receives input from outside the Composition and/or does not receive any `Projections ` from
other Nodes within the Composition, respectively. This role cannot be modified programmatically.
@@ -4368,6 +4369,7 @@ def add_node(self, node, required_roles=None, context=None):
else:
self._pre_existing_pathway_components[NODES].append(node)
+ # Aux components are being added by Composition, even if main Node is being added was from COMMAND_LINE
invalid_aux_components = self._add_node_aux_components(node, context=context)
# Implement required_roles
@@ -4639,13 +4641,9 @@ def _add_required_node_role(self, node, role, context=None):
raise CompositionError('Invalid NodeRole: {0}'.format(role))
# Disallow assignment of NodeRoles by user that are not programmitically modifiable:
- # FIX 4/25/20 [JDC]: CHECK IF ROLE OR EQUIVALENT STATUS HAS ALREADY BEEN ASSIGNED AND, IF SO, ISSUE WARNING
+ # FIX 4/25/20 [JDC] - CHECK IF ROLE OR EQUIVALENT STATUS HAS ALREADY BEEN ASSIGNED AND, IF SO, ISSUE WARNING
if context.source == ContextFlags.COMMAND_LINE:
- if role in {NodeRole.CONTROL_OBJECTIVE, NodeRole.CONTROLLER_OBJECTIVE}:
- # raise CompositionError(f"{role} cannot be directly assigned to an {ObjectiveMechanism.__name__};"
- # # f"assign 'CONTROL' to 'role' argument of consructor for {node} of {self.name}")
- # f"try assigning {node} to '{OBJECTIVE_MECHANISM}' argument of "
- # f"the constructor for the desired {ControlMechanism.__name__}.")
+ if role in {NodeRole.CONTROL_OBJECTIVE, NodeRole.CONTROLLER_OBJECTIVE} and not node.control_mechanism:
warnings.warn(f"{role} should be assigned with caution to {self.name}. "
f"{ObjectiveMechanism.__name__}s are generally constructed automatically by a "
f"{ControlMechanism.__name__}, or assigned to it in the '{OBJECTIVE_MECHANISM}' "
@@ -5116,7 +5114,7 @@ def _add_node_aux_components(self, node, context=None):
# ignore these for now and try to activate them again during every call to _analyze_graph
# and, at runtime, if there are still any invalid aux_components left, issue a warning
projections = []
- # Add all "nodes" to the composition first (in case projections reference them)
+ # Add all Nodes to the Composition first (in case Projections reference them)
for i, component in enumerate(node.aux_components):
if isinstance(component, (Mechanism, Composition)):
if isinstance(component, Composition):
@@ -5137,10 +5135,10 @@ def _add_node_aux_components(self, node, context=None):
"specification (True or False).".format(component, node.name))
elif isinstance(component[0], (Mechanism, Composition)):
if isinstance(component[1], NodeRole):
- self.add_node(node=component[0], required_roles=component[1])
+ self.add_node(node=component[0], required_roles=component[1], context=context)
elif isinstance(component[1], list):
if isinstance(component[1][0], NodeRole):
- self.add_node(node=component[0], required_roles=component[1])
+ self.add_node(node=component[0], required_roles=component[1], context=context)
else:
raise CompositionError("Invalid Component specification ({}) in {}'s aux_components. "
"If a tuple is used to specify a Mechanism or Composition, then "
@@ -9803,6 +9801,15 @@ def get_controller(comp):
return total_cost
+ def _get_modulable_mechanisms(self):
+ modulated_mechanisms = []
+ for mech in [m for m in self.nodes if (isinstance(m, ProcessingMechanism_Base) and
+ not (isinstance(m, ObjectiveMechanism)
+ and self.get_roles_for_node(m) != NodeRole.CONTROL)
+ and hasattr(m.function, MULTIPLICATIVE_PARAM))]:
+ modulated_mechanisms.append(mech)
+ return modulated_mechanisms
+
# endregion CONTROL
# ******************************************************************************************************************
@@ -12873,7 +12880,19 @@ def do_gradient_optimization(self, retain_in_pnl_options, context, optimization_
pass
@handle_external_context(fallback_most_recent=True)
- def reset(self, values=None, include_unspecified_nodes=True, context=NotImplemented):
+ def reset(self, values=None, include_unspecified_nodes=True, clear_results=False, context=NotImplemented):
+ """Reset all stateful functions in the Composition to their initial values.
+
+ If **values** is provided, the `previous_value ` of the corresponding
+ `stateful functions ` are set to the values specified. If a value is not provided for a
+ given node, the `previous_value ` is set to the value of its `initializer
+ `.
+
+ If **include_unspecified_nodes** is False, then all nodes must have corresponding reset values.
+ The `DEFAULT` keyword can be used in lieu of a numerical value to reset a node's value to its default.
+
+ If **clear_results** is True, the `results ` attribute is set to an empty list.
+ """
if not values:
values = {}
@@ -12883,30 +12902,33 @@ def reset(self, values=None, include_unspecified_nodes=True, context=NotImplemen
reset_val = values.get(node)
node.reset(reset_val, context=context)
+ if clear_results:
+ self.parameters.results._set([], context)
+
@handle_external_context(fallback_most_recent=True)
def initialize(self, values=None, include_unspecified_nodes=True, context=None):
- """
- Initializes the values of nodes within cycles. If `include_unspecified_nodes` is True and a value is
- provided for a given node, the node will be initialized to that value. If `include_unspecified_nodes` is
- True and a value is not provided, the node will be initialized to its default value. If
- `include_unspecified_nodes` is False, then all nodes must have corresponding initialization values. The
- `DEFAULT` keyword can be used in lieu of a numerical value to reset a node's value to its default.
+ """Initialize the values of nodes within cycles.
+ If `include_unspecified_nodes` is True and a value is provided for a given node, the node is initialized to
+ that value. If `include_unspecified_nodes` is True and a value is not provided, the node is initialized to
+ its default value. If `include_unspecified_nodes` is False, then all nodes must have corresponding
+ initialization values. The `DEFAULT` keyword can be used in lieu of a numerical value to reset a node's value
+ to its default.
- If a context is not provided, the most recent context under which the Composition has executed will be used.
+ If a context is not provided, the most recent context under which the Composition has executed is used.
- Arguments
- ----------
- values: Dict { Node: Node Value }
- A dictionary contaning key-value pairs of Nodes and initialization values. Nodes within cycles that are
- not included in this dict will be initialized to their default values.
+ Arguments
+ ----------
+ values: Dict { Node: Node Value }
+ A dictionary containing key-value pairs of Nodes and initialization values. Nodes within cycles that are
+ not included in this dict are initialized to their default values.
- include_unspecified_nodes: bool
- Specifies whether all nodes within cycles should be initialized or only ones specified in the provided
- values dictionary.
+ include_unspecified_nodes: bool
+ Specifies whether all nodes within cycles should be initialized or only ones specified in the provided
+ values dictionary.
- context: Context
- The context under which the nodes should be initialized. context will be set to
- self.most_recent_execution_context if one is not specified.
+ context: Context
+ The context under which the nodes should be initialized. context are set to
+ self.most_recent_execution_context if one is not specified.
"""
# comp must be initialized from context before cycle values are initialized
diff --git a/psyneulink/core/compositions/showgraph.py b/psyneulink/core/compositions/showgraph.py
index 1c766592f2d..6d48d9e7bee 100644
--- a/psyneulink/core/compositions/showgraph.py
+++ b/psyneulink/core/compositions/showgraph.py
@@ -2708,10 +2708,12 @@ def get_index_of_node_in_G_body(node, node_type: Literal['MECHANISM', 'Projectio
raise ShowGraphError(f"Bad arg in call to {composition.name}.show_graph: '{output_fmt}'.")
except ShowGraphError as e:
- raise ShowGraphError(str(e.error_value))
-
- except:
- raise ShowGraphError(f"Problem displaying graph for {composition.name}")
+ # raise ShowGraphError(str(e.error_value))
+ raise ShowGraphError(str(e.error_value)) from e
+ # except:
+ # raise ShowGraphError(f"Problem displaying graph for {composition.name}")
+ except Exception as e:
+ raise ShowGraphError(f"Problem displaying graph for {composition.name}: {e}") from e
def _is_composition_controller(self, mech, context, enclosing_comp=None):
# FIX 6/12/20: REPLACE WITH TEST FOR NodeRole.CONTROLLER ONCE THAT IS IMPLEMENTED
diff --git a/psyneulink/core/globals/keywords.py b/psyneulink/core/globals/keywords.py
index 415ed2d7c2e..7543fda313d 100644
--- a/psyneulink/core/globals/keywords.py
+++ b/psyneulink/core/globals/keywords.py
@@ -118,6 +118,7 @@
'PROCESS_EXECUTE', 'PROCESS_INIT', 'PROCESSES', 'PROCESSES_DIM', 'PROCESSING', 'PROCESSING_MECHANISM',
'PROCESSING_PATHWAY', 'PRODUCT', 'PROGRESS_BAR_CHAR', 'PROJECTION', 'PROJECTION_DIRECTION', 'PROJECTION_PARAMS',
'PROJECTION_RECEIVER', 'PROJECTION_SENDER', 'PROJECTION_TYPE', 'PROJECTIONS', 'PROJECTION_COMPONENT_CATEGORY',
+ 'PNL',
'QUOTIENT', 'RANDOM', 'RANDOM_CONNECTIVITY_MATRIX', 'RATE', 'RATIO', 'REARRANGE_FUNCTION', 'RECEIVER',
'RECEIVER_ARG', 'RECURRENT_TRANSFER_MECHANISM', 'REDUCE_FUNCTION', 'REFERENCE_VALUE', 'RESET',
'RESET_STATEFUL_FUNCTION_WHEN', 'RELU_FUNCTION', 'REST', 'RESULT', 'RESULT', 'ROLES', 'RL_FUNCTION', 'RUN',
@@ -143,7 +144,6 @@
from psyneulink._typing import Literal
-
#region ----------------------------------------- MATRICES -----------------------------------------------------------
class MatrixKeywords:
@@ -462,6 +462,7 @@ class Loss(Enum):
#region --------------------------------------------- GENERAL ----------------------------------------------------
# General
+PNL = 'psyneulink'
ON = True
OFF = False
diff --git a/psyneulink/core/globals/parameters.py b/psyneulink/core/globals/parameters.py
index 1d2bfeae32d..f79ce831aaa 100644
--- a/psyneulink/core/globals/parameters.py
+++ b/psyneulink/core/globals/parameters.py
@@ -1516,7 +1516,7 @@ def set(self, value, context=None, override=False, skip_history=False, skip_log=
if isinstance(value, Component):
owner = self._owner._owner
if value not in owner._parameter_components:
- if not owner.is_initializing:
+ if owner.initialization_status == ContextFlags.INITIALIZED:
value._initialize_from_context(context)
owner._parameter_components.add(value)
diff --git a/psyneulink/core/llvm/builder_context.py b/psyneulink/core/llvm/builder_context.py
index 0dcb6bae853..7fcd4224cdb 100644
--- a/psyneulink/core/llvm/builder_context.py
+++ b/psyneulink/core/llvm/builder_context.py
@@ -210,29 +210,46 @@ def init_builtins(self):
if "time_stat" in debug_env:
print("Time to setup PNL builtins: {}".format(finish - start))
+ def get_rand_int_function_by_state(self, state):
+ if len(state.type.pointee) == 5:
+ return self.import_llvm_function("__pnl_builtin_mt_rand_int32_bounded")
+
+ elif len(state.type.pointee) == 7:
+ # we have different versions based on selected FP precision
+ return self.import_llvm_function("__pnl_builtin_philox_rand_int32_bounded")
+
+ else:
+ assert False, "Unknown PRNG type!"
+
def get_uniform_dist_function_by_state(self, state):
if len(state.type.pointee) == 5:
return self.import_llvm_function("__pnl_builtin_mt_rand_double")
+
elif len(state.type.pointee) == 7:
# we have different versions based on selected FP precision
return self.import_llvm_function("__pnl_builtin_philox_rand_{}".format(str(self.float_ty)))
+
else:
assert False, "Unknown PRNG type!"
def get_binomial_dist_function_by_state(self, state):
if len(state.type.pointee) == 5:
return self.import_llvm_function("__pnl_builtin_mt_rand_binomial")
+
elif len(state.type.pointee) == 7:
return self.import_llvm_function("__pnl_builtin_philox_rand_binomial")
+
else:
assert False, "Unknown PRNG type!"
def get_normal_dist_function_by_state(self, state):
if len(state.type.pointee) == 5:
return self.import_llvm_function("__pnl_builtin_mt_rand_normal")
+
elif len(state.type.pointee) == 7:
# Normal exists only for self.float_ty
return self.import_llvm_function("__pnl_builtin_philox_rand_normal")
+
else:
assert False, "Unknown PRNG type!"
diff --git a/psyneulink/core/llvm/builtins.py b/psyneulink/core/llvm/builtins.py
index 20920ccf59e..f965c819ad4 100644
--- a/psyneulink/core/llvm/builtins.py
+++ b/psyneulink/core/llvm/builtins.py
@@ -17,8 +17,10 @@
def _setup_builtin_func_builder(ctx, name, args, *, return_type=ir.VoidType()):
- builder = ctx.create_llvm_function(args, None, _BUILTIN_PREFIX + name,
- return_type=return_type)
+ if not name.startswith(_BUILTIN_PREFIX):
+ name = _BUILTIN_PREFIX + name
+
+ builder = ctx.create_llvm_function(args, None, name, return_type=return_type)
# Add noalias attribute
for a in builder.function.args:
@@ -656,10 +658,11 @@ def _setup_mt_rand_init(ctx, state_ty, init_scalar):
return builder.function
-def _setup_mt_rand_integer(ctx, state_ty):
+def _setup_mt_rand_int32(ctx, state_ty):
int64_ty = ir.IntType(64)
+
# Generate random number generator function.
- # It produces random 32bit numberin a 64bit word
+ # It produces random 32bit number in a 64bit word
builder = _setup_builtin_func_builder(ctx, "mt_rand_int32", (state_ty.as_pointer(), int64_ty.as_pointer()))
state, out = builder.function.args
@@ -758,6 +761,42 @@ def _setup_mt_rand_integer(ctx, state_ty):
return builder.function
+def _setup_rand_bounded_int32(ctx, state_ty, gen_int32):
+
+ out_ty = gen_int32.args[1].type.pointee
+ builder = _setup_builtin_func_builder(ctx, gen_int32.name + "_bounded", (state_ty.as_pointer(), ctx.int32_ty, ctx.int32_ty, out_ty.as_pointer()))
+ state, lower, upper, out_ptr = builder.function.args
+
+ rand_range_excl = builder.sub(upper, lower)
+ rand_range_excl = builder.zext(rand_range_excl, out_ty)
+
+ range_leading_zeros = builder.ctlz(rand_range_excl, ctx.bool_ty(1))
+ mask = builder.lshr(range_leading_zeros.type(-1), range_leading_zeros)
+
+ loop_block = builder.append_basic_block("bounded_loop_block")
+ out_block = builder.append_basic_block("bounded_out_block")
+
+ builder.branch(loop_block)
+
+ # Loop:
+ # do:
+ # r = random() & mask
+ # while r >= limit
+ builder.position_at_end(loop_block)
+
+ builder.call(gen_int32, [state, out_ptr])
+ val = builder.load(out_ptr)
+ val = builder.and_(val, mask)
+
+ is_above_limit = builder.icmp_unsigned(">=", val, rand_range_excl)
+ builder.cbranch(is_above_limit, loop_block, out_block)
+
+ builder.position_at_end(out_block)
+ offset = builder.zext(lower, val.type)
+ result = builder.add(val, offset)
+ builder.store(result, out_ptr)
+ builder.ret_void()
+
def _setup_mt_rand_float(ctx, state_ty, gen_int):
"""
Mersenne Twister double prcision random number generation.
@@ -892,8 +931,9 @@ def setup_mersenne_twister(ctx):
init_scalar = _setup_mt_rand_init_scalar(ctx, state_ty)
_setup_mt_rand_init(ctx, state_ty, init_scalar)
- gen_int = _setup_mt_rand_integer(ctx, state_ty)
- gen_float = _setup_mt_rand_float(ctx, state_ty, gen_int)
+ gen_int32 = _setup_mt_rand_int32(ctx, state_ty)
+ _setup_rand_bounded_int32(ctx, state_ty, gen_int32)
+ gen_float = _setup_mt_rand_float(ctx, state_ty, gen_int32)
_setup_mt_rand_normal(ctx, state_ty, gen_float)
_setup_rand_binomial(ctx, state_ty, gen_float, prefix="mt")
@@ -1138,6 +1178,88 @@ def _setup_philox_rand_int32(ctx, state_ty, gen_int64):
return builder.function
+def _setup_rand_lemire_int32(ctx, state_ty, gen_int32):
+ """
+ Uses Lemire's algorithm - https://arxiv.org/abs/1805.10941
+ As implemented in Numpy to match Numpy results.
+ """
+
+ out_ty = gen_int32.args[1].type.pointee
+ builder = _setup_builtin_func_builder(ctx, gen_int32.name + "_bounded", (state_ty.as_pointer(), out_ty, out_ty, out_ty.as_pointer()))
+ state, lower, upper, out_ptr = builder.function.args
+
+ rand_range_excl = builder.sub(upper, lower)
+ rand_range_excl_64 = builder.zext(rand_range_excl, ir.IntType(64))
+ rand_range = builder.sub(rand_range_excl, rand_range_excl.type(1))
+
+
+ builder.call(gen_int32, [state, out_ptr])
+ val = builder.load(out_ptr)
+
+ is_full_range = builder.icmp_unsigned("==", rand_range, rand_range.type(0xffffffff))
+ with builder.if_then(is_full_range):
+ builder.ret_void()
+
+ val64 = builder.zext(val, rand_range_excl_64.type)
+ m = builder.mul(val64, rand_range_excl_64)
+
+ # Store current result as output. It will be overwritten below if needed.
+ out_val = builder.lshr(m, m.type(32))
+ out_val = builder.trunc(out_val, out_ptr.type.pointee)
+ out_val = builder.add(out_val, lower)
+ builder.store(out_val, out_ptr)
+
+ leftover = builder.and_(m, m.type(0xffffffff))
+
+ is_good = builder.icmp_unsigned(">=", leftover, rand_range_excl_64)
+ with builder.if_then(is_good):
+ builder.ret_void()
+
+ # Apply rejection sampling
+ leftover_ptr = builder.alloca(leftover.type)
+ builder.store(leftover, leftover_ptr)
+
+ rand_range_64 = builder.zext(rand_range, ir.IntType(64))
+ threshold = builder.sub(rand_range_64, rand_range_64.type(0xffffffff))
+ threshold = builder.urem(threshold, rand_range_excl_64)
+
+ cond_block = builder.append_basic_block("bounded_cond_block")
+ loop_block = builder.append_basic_block("bounded_loop_block")
+ out_block = builder.append_basic_block("bounded_out_block")
+
+ builder.branch(cond_block)
+
+ # Condition: leftover < threshold
+ builder.position_at_end(cond_block)
+ leftover = builder.load(leftover_ptr)
+ do_next = builder.icmp_unsigned("<", leftover, threshold)
+ builder.cbranch(do_next, loop_block, out_block)
+
+ # Loop block:
+ # m = ((uint64_t)next_uint32(bitgen_state)) * rng_excl;
+ # leftover = m & 0xffffffff
+ # result = m >> 32
+ builder.position_at_end(loop_block)
+ builder.call(gen_int32, [state, out_ptr])
+
+ val = builder.load(out_ptr)
+ val64 = builder.zext(val, rand_range_excl_64.type)
+ m = builder.mul(val64, rand_range_excl_64)
+
+ leftover = builder.and_(m, m.type(0xffffffff))
+ builder.store(leftover, leftover_ptr)
+
+ out_val = builder.lshr(m, m.type(32))
+ out_val = builder.trunc(out_val, out_ptr.type.pointee)
+ out_val = builder.add(out_val, lower)
+ builder.store(out_val, out_ptr)
+ builder.branch(cond_block)
+
+
+ builder.position_at_end(out_block)
+ builder.ret_void()
+
+
def _setup_philox_rand_double(ctx, state_ty, gen_int64):
# Generate random float number generator function
double_ty = ir.DoubleType()
@@ -2087,6 +2209,7 @@ def setup_philox(ctx):
_setup_rand_binomial(ctx, state_ty, gen_double, prefix="philox")
gen_int32 = _setup_philox_rand_int32(ctx, state_ty, gen_int64)
+ _setup_rand_lemire_int32(ctx, state_ty, gen_int32)
gen_float = _setup_philox_rand_float(ctx, state_ty, gen_int32)
_setup_philox_rand_normal(ctx, state_ty, gen_float, gen_int32, _wi_float_data, _ki_i32_data, _fi_float_data)
_setup_rand_binomial(ctx, state_ty, gen_float, prefix="philox")
diff --git a/psyneulink/core/llvm/helpers.py b/psyneulink/core/llvm/helpers.py
index c3dc3336bfe..7d7b7df10a2 100644
--- a/psyneulink/core/llvm/helpers.py
+++ b/psyneulink/core/llvm/helpers.py
@@ -377,23 +377,23 @@ def array_from_shape(shape, element_ty):
array_ty = ir.ArrayType(array_ty, dim)
return array_ty
-def recursive_iterate_arrays(ctx, builder, u, *args):
+@contextmanager
+def recursive_iterate_arrays(ctx, builder, *args, loop_id="recursive_iteration"):
"""Recursively iterates over all elements in scalar arrays of the same shape"""
- assert isinstance(u.type.pointee, ir.ArrayType), "Can only iterate over arrays!"
+
+ assert len(args) > 0, "Need at least one array to iterate over!"
+ assert all(isinstance(arr.type.pointee, ir.ArrayType) for arr in args), "Can only iterate over arrays!"
+
+ u = args[0]
assert all(len(u.type.pointee) == len(v.type.pointee) for v in args), "Tried to iterate over differing lengths!"
- with array_ptr_loop(builder, u, "recursive_iteration") as (b, idx):
- u_ptr = b.gep(u, [ctx.int32_ty(0), idx])
- arg_ptrs = (b.gep(v, [ctx.int32_ty(0), idx]) for v in args)
- if is_scalar(u_ptr):
- yield (u_ptr, *arg_ptrs)
- else:
- yield from recursive_iterate_arrays(ctx, b, u_ptr, *arg_ptrs)
-# TODO: Remove this function. Can be replaced by `recursive_iterate_arrays`
-def call_elementwise_operation(ctx, builder, x, operation, output_ptr):
- """Recurse through an array structure and call operation on each scalar element of the structure. Store result in output_ptr"""
- for (inp_ptr, out_ptr) in recursive_iterate_arrays(ctx, builder, x, output_ptr):
- builder.store(operation(ctx, builder, builder.load(inp_ptr)), out_ptr)
+ with array_ptr_loop(builder, u, loop_id) as (b, idx):
+ arg_ptrs = tuple(b.gep(arr, [ctx.int32_ty(0), idx]) for arr in args)
+ if is_scalar(arg_ptrs[0]):
+ yield (b, *arg_ptrs)
+ else:
+ with recursive_iterate_arrays(ctx, b, *arg_ptrs) as (b, *nested_args):
+ yield (b, *nested_args)
def printf(ctx, builder, fmt, *args, tags:set):
diff --git a/psyneulink/library/components/mechanisms/modulatory/control/agt/lccontrolmechanism.py b/psyneulink/library/components/mechanisms/modulatory/control/agt/lccontrolmechanism.py
index 8d696aec8ef..1acc55e1e31 100644
--- a/psyneulink/library/components/mechanisms/modulatory/control/agt/lccontrolmechanism.py
+++ b/psyneulink/library/components/mechanisms/modulatory/control/agt/lccontrolmechanism.py
@@ -56,11 +56,11 @@
.. _LCControlMechanism_ObjectiveMechanism_Creation:
*ObjectiveMechanism and Monitored OutputPorts*
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the **objective_mechanism** argument is specified then, as with a standard ControlMechanism, the specified
`ObjectiveMechanism` is assigned to its `objective_mechanism ` attribute. The
-`value ` of the ObjectiveMechanism's *OUTCOME* `OutputPort` must be a scalar (that is used as the
+`value ` of the ObjectiveMechanism's *OUTCOME* `OutputPort` must be a scalar, that is used as the
input to the LCControlMechanism's `function ` to drive its `phasic response
`. An ObjectiveMechanism can also be constructed automatically, by specifying
**objective_mechanism** as True; that is assigned a `CombineMeans` Function as its `function
@@ -82,17 +82,17 @@
` of the Mechanism's `function `. Therefore, any Mechanism
specified for control by an LCControlMechanism must be either a `ProcessingMechanism`, or a Mechanism that uses as its
`function ` a class of `Function ` that implements a `multiplicative_param
-`. The **modulate_mechanisms** argument must be either a list of such Mechanisms, or
-a `Composition` (to modulate all of the `ProcessingMechanisms ` in a Composition -- see below).
-see below). If a Mechanism specified in the **modulated_mechanisms** argument does not implement a multiplicative_param,
-it is ignored. A `ControlProjection` is automatically created that projects from the LCControlMechanism to the
+`. If a Mechanism specified in the **modulated_mechanisms** argument does not implement a
+multiplicative_param, it is ignored. The **modulate_mechanisms** argument must be either a list of suitable Mechanisms,
+or a `Composition` (to modulate all of the `ProcessingMechanisms ` in a Composition -- see below).
+A `ControlProjection` is automatically created that projects from the LCControlMechanism to the
`ParameterPort` for the `multiplicative_param ` of every Mechanism specified in the
-**modulated_mechanisms** argument. The Mechanisms modulated by an LCControlMechanism are listed in its
+**modulated_mechanisms** argument. The Mechanisms modulated by an LCControlMechanism are listed in its
`modulated_mechanisms ` attribute).
-If `Composition` is assigned as the value of **modulate_mechanisms**, then the LCControlMechanism will modulate all
+If a `Composition` is assigned as the value of **modulate_mechanisms**, then the LCControlMechanism will modulate all
of the `ProcessingMechanisms` in that Composition, with the exception of any `ObjectiveMechanism`\\s that are assigned
-a the `objective_mechanism ` of another `ControlMechanism`. Note that only the
+as the `objective_mechanism ` of another `ControlMechanism`. Note that only the
Mechanisms that already belong to that Composition are included at the time the LCControlMechanism is constructed.
Therefore, to include *all* Mechanisms in the Composition at the time it is run, the LCControlMechanism should be
constructed and `added to the Composition using the Composition's `add_node ` method) after all
@@ -119,7 +119,7 @@
ObjectiveMechanism
^^^^^^^^^^^^^^^^^^
-If an ObjectiveMechanism is `automatically created for an
+If an ObjectiveMechanism is `automatically created ` for an
LCControlMechanism, it receives its inputs from the `OutputPort(s) ` specified the
**monitor_for_control** argument of the LCControlMechanism constructor, or the **montiored_output_ports** argument
of the LCControlMechanism's `ObjectiveMechanism `. By default, the
@@ -312,14 +312,18 @@
from psyneulink._typing import Optional, Union, Iterable
from psyneulink.core import llvm as pnlvm
+from psyneulink.core.components.functions import FunctionError
+from psyneulink.core.components.functions.nonstateful.transformfunctions import CombineMeans
from psyneulink.core.components.functions.stateful.integratorfunctions import FitzHughNagumoIntegrator
-from psyneulink.core.components.mechanisms.modulatory.control.controlmechanism import ControlMechanism, ControlMechanismError
-from psyneulink.core.components.mechanisms.processing.objectivemechanism import ObjectiveMechanism
+from psyneulink.core.components.mechanisms.modulatory.control.controlmechanism import (
+ ControlMechanism, ControlMechanismError)
+from psyneulink.core.components.mechanisms.processing.objectivemechanism import (
+ ObjectiveMechanism, ObjectiveMechanismError)
from psyneulink.core.components.projections.modulatory.controlprojection import ControlProjection
from psyneulink.core.components.shellclasses import Mechanism
from psyneulink.core.components.ports.outputport import OutputPort
-from psyneulink.core.globals.keywords import \
- INIT_EXECUTE_METHOD_ONLY, MULTIPLICATIVE_PARAM, NAME, OWNER_VALUE, PORT_TYPE, PROJECTIONS, VARIABLE
+from psyneulink.core.globals.keywords import (ALL, INIT_EXECUTE_METHOD_ONLY, MULTIPLICATIVE_PARAM,
+ OBJECTIVE_MECHANISM, OWNER_VALUE, PROJECTIONS,VARIABLE, SUM)
from psyneulink.core.globals.parameters import Parameter, ParameterAlias, check_user_specified
from psyneulink.core.globals.preferences.basepreferenceset import ValidPrefSet
from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel
@@ -394,8 +398,10 @@ class LCControlMechanism(ControlMechanism):
modulated_mechanisms : List[`Mechanism `] or *ALL*
specifies the Mechanisms to be modulated by the LCControlMechanism. If it is a list, every item must be a
Mechanism with a `function ` that implements a `multiplicative_param
- `; alternatively the keyword *ALL* can be used to specify all of the
- `ProcessingMechanisms ` in the Composition(s) to which the LCControlMechanism belongs.
+ `; alternatively a `Composition` can be specified, in which case the
+ LCControlMechanism will modulate all of the suitable `ProcessingMechanisms ` in the
+ Composition that have been added to it up to the point at which the LCControlMechanism is constructed (see
+ `Mechanisms to Modulate ` for additional information).
initial_w_FitzHughNagumo : float : default 0.0
sets `initial_w ` on the LCControlMechanism's `FitzHughNagumoIntegrator
@@ -517,6 +523,12 @@ class LCControlMechanism(ControlMechanism):
` to parametrize the contribution made to its output by each of the values that
it monitors (see `ObjectiveMechanism Function `).
+ objective_mechanism : ObjectiveMechanism
+ `ObjectiveMechanism` that monitors and evaluates the values specified in the LCControlMechanism's
+ **objective_mechanism** argument, and transmits the result to the LCControlMechanism's *OUTCOME*
+ `input_port `, that drives its phasic response
+ (see `LCControlMechanism_ObjectiveMechanism` for additional details).
+
function : FitzHughNagumoIntegrator
takes the LCControlMechanism's `input ` and generates its response
` under
@@ -544,9 +556,10 @@ class LCControlMechanism(ControlMechanism):
` attribute.
modulated_mechanisms : List[Mechanism]
- list of `Mechanisms ` modulated by the LCControlMechanism.
+ list of `Mechanisms ` modulated by the LCControlMechanism (see `Mechanisms to Modulate
+ ` for additional information).
- initial_w_FitzHughNagumo : float : default 0.0
+ initial_w_FitzHughNagumo : float : default 0.0
sets `initial_w ` on the LCControlMechanism's `FitzHughNagumoIntegrator
` function
@@ -648,7 +661,7 @@ class LCControlMechanism(ControlMechanism):
value : ndarray
contains four values; the first is the `control_allocation `,
- followed by the three values are the `w`, `v`, and `x` terms returned by the LCControlMechanism's
+ followed by the three values -- the `w`, `v`, and `x` terms -- returned by the LCControlMechanism's
`FitzHughNagumoIntegrator` function.
.. note::
@@ -718,7 +731,7 @@ class Parameters(ControlMechanism.Parameters):
def __init__(self,
default_variable=None,
default_allocation: Optional[Union[int, float, list, np.ndarray]] = None,
- objective_mechanism: Optional[Union[ObjectiveMechanism, list]] = None,
+ objective_mechanism: Optional[Union[ObjectiveMechanism, list, bool]] = True,
monitor_for_control: Optional[Union[Iterable, Mechanism, OutputPort]] = None,
modulated_mechanisms=None,
modulation: Optional[str] = None,
@@ -785,12 +798,9 @@ def __init__(self,
def _validate_params(self, request_set, target_set=None, context=None):
"""Validate modulated_mechanisms argument.
-
- Validate that **modulated_mechanisms** is either a Composition or a list of eligible Mechanisms .
- Eligible Mechanisms are ones with a `function ` that has a multiplicative_param.
-
+ Validate that **modulated_mechanisms** is either a Composition or a list of eligible Mechanisms;
+ eligible Mechanisms are ones with a `function ` that has a multiplicative_param.
"""
-
super()._validate_params(request_set=request_set,
target_set=target_set,
context=context)
@@ -799,26 +809,38 @@ def _validate_params(self, request_set, target_set=None, context=None):
spec = target_set[MODULATED_MECHANISMS]
from psyneulink.core.compositions.composition import Composition
- if isinstance(spec, Composition):
+ if isinstance(spec, Composition) or spec is ALL:
pass
else:
if not isinstance(spec, list):
spec = [spec]
for mech in spec:
if not isinstance(mech, Mechanism):
- raise LCControlMechanismError("The specification of the {} argument for {} "
- "contained an item ({}) that is not a Mechanism.".
- format(repr(MODULATED_MECHANISMS), self.name, mech))
+ raise LCControlMechanismError(f"The specification of the {repr(MODULATED_MECHANISMS)} "
+ f"argument for {self.name} contained an item ({mech}) "
+ f"that is not a Mechanism.")
elif not hasattr(mech.function, MULTIPLICATIVE_PARAM):
raise LCControlMechanismError(f"The specification of the {repr(MODULATED_MECHANISMS)} "
f"argument for {self.name} contained a Mechanism ({mech}) "
f"that does not have a {repr(MULTIPLICATIVE_PARAM)}.")
+ def _instantiate_objective_mechanism(self, input_ports=None, context=None):
+ """Instantiate ObjectiveMechanism with CombineMeans as its function then call super()
+ """
+ # If objective_mechanism is specified, use it; otherwise, instantiate one
+ if not self.objective_mechanism or self.objective_mechanism is True:
+ try:
+ self.objective_mechanism = ObjectiveMechanism(function=CombineMeans(operation=SUM),
+ name=self.name + '_ObjectiveMechanism')
+ except (ObjectiveMechanismError, FunctionError) as e:
+ raise ObjectiveMechanismError(f"Error creating {OBJECTIVE_MECHANISM} for {self.name}: {e}")
+ super()._instantiate_objective_mechanism(input_ports=input_ports, context=context)
+
def _instantiate_output_ports(self, context=None):
- """Override to insure that ControlSignals are instantiated even thought self.control is not specified
- LCControlMechanism automatically assigns ControlSignals, so no control specification is needed in constructor
- super._instantiate_output_ports does not call _instantiate_control_signals if self.control is not specified;
- so, override is needed to insure _instantiate_control_signals is called
+ """Override to ensure that ControlSignals are instantiated even though self.control is not specified.
+ LCControlMechanism automatically assigns ControlSignals, so no control specification is needed in constructor;
+ super()._instantiate_output_ports does not call _instantiate_control_signals if self.control is not specified;
+ so, override is needed to ensure _instantiate_control_signals is called.
"""
self._register_control_signal_type(context=None)
self._instantiate_control_signals(context=context)
@@ -829,26 +851,19 @@ def _instantiate_output_ports(self, context=None):
self.aux_components.extend(self.control_projections)
def _instantiate_control_signals(self, context=None):
- """Instantiate ControlSignals and assign ControlProjections to Mechanisms in self.modulated_mechanisms
-
- If **modulated_mechanisms** argument of constructor was specified as *ALL*, assign all ProcessingMechanisms
+ """Instantiate ControlSignals and assign ControlProjections to Mechanisms in self.modulated_mechanisms.
+ If **modulated_mechanisms** argument of constructor is specified as *ALL*, assign all ProcessingMechanisms
in Compositions to which LCControlMechanism belongs to self.modulated_mechanisms.
Instantiate ControlSignal with Projection to the ParameterPort for the multiplicative_param of every
Mechanism listed in self.modulated_mechanisms.
"""
- from psyneulink.core.components.mechanisms.processing.processingmechanism import ProcessingMechanism_Base
- # A Composition is specified for modulated_mechanisms, so assign all Processing Mechanisms in composition
- # to its modulated_mechanisms attribute
+ # A Composition is specified for modulated_mechanisms,
+ # so assign all Processing Mechanisms in Composition to its modulated_mechanisms attribute
from psyneulink.core.compositions.composition import Composition, NodeRole
+ # FIX: 11/27/24 - NEED TO HANDLE "ALL" HERE, BY DEFERRING UNTIL ADDED TO COMPOSITION
if isinstance(self.modulated_mechanisms, Composition):
- comp = self.modulated_mechanisms
- self.modulated_mechanisms = []
- for mech in [m for m in comp.nodes if (isinstance(m, ProcessingMechanism_Base) and
- not (isinstance(m, ObjectiveMechanism)
- and comp.get_roles_for_node(m) != NodeRole.CONTROL)
- and hasattr(m.function, MULTIPLICATIVE_PARAM))]:
- self.modulated_mechanisms.append(mech)
+ self.modulated_mechanisms = self.modulated_mechanisms._get_modulable_mechanisms()
# Get the name of the multiplicative_param of each Mechanism in self.modulated_mechanisms
if self.modulated_mechanisms:
diff --git a/psyneulink/library/components/mechanisms/modulatory/learning/EMstoragemechanism.py b/psyneulink/library/components/mechanisms/modulatory/learning/EMstoragemechanism.py
index f9d296eef87..f828ca9b689 100644
--- a/psyneulink/library/components/mechanisms/modulatory/learning/EMstoragemechanism.py
+++ b/psyneulink/library/components/mechanisms/modulatory/learning/EMstoragemechanism.py
@@ -642,7 +642,7 @@ def _validate_params(self, request_set, target_set=None, context=None):
f"a list or 2d np.array containing entries that have the same shape "
f"({memory_matrix.shape}) as an entry (row) in 'memory_matrix' arg.")
- # Ensure the number of fields is equal to the numbder of items in variable
+ # Ensure the number of fields is equal to the number of items in variable
if FIELDS in request_set:
fields = request_set[FIELDS]
if len(fields) != len(self.variable):
@@ -672,13 +672,14 @@ def _validate_params(self, request_set, target_set=None, context=None):
f"in its variable ({len(self.variable)}).")
# Ensure shape of learning_signals matches shapes of matrices for match nodes (i.e., either keys or concatenate)
+ key_indices = [i for i, field_type in enumerate(field_types) if field_type == 1]
for i, learning_signal in enumerate(learning_signals[:num_match_fields]):
learning_signal_shape = learning_signal.parameters.matrix._get(context).shape
if concatenate_queries:
memory_matrix_field_shape = np.array([np.concatenate(row, dtype=object).flatten()
for row in memory_matrix[:,0:num_keys]]).T.shape
else:
- memory_matrix_field_shape = np.array(memory_matrix[:,i].tolist()).T.shape
+ memory_matrix_field_shape = np.array(memory_matrix[:,key_indices[i]].tolist()).T.shape
assert learning_signal_shape == memory_matrix_field_shape, \
f"The shape ({learning_signal_shape}) of the matrix for the Projection {learning_signal.name} " \
f"used to specify learning signal {i} of {self.name} does not match the shape " \
diff --git a/psyneulink/library/components/mechanisms/processing/transfer/recurrenttransfermechanism.py b/psyneulink/library/components/mechanisms/processing/transfer/recurrenttransfermechanism.py
index ff58048f456..b05f2d77859 100644
--- a/psyneulink/library/components/mechanisms/processing/transfer/recurrenttransfermechanism.py
+++ b/psyneulink/library/components/mechanisms/processing/transfer/recurrenttransfermechanism.py
@@ -193,10 +193,11 @@
from psyneulink.core import llvm as pnlvm
from psyneulink.core.components.component import _get_parametervalue_attr
+from psyneulink.core.components.functions.nonstateful.transferfunctions import Linear
from psyneulink.core.components.functions.nonstateful.transformfunctions import LinearCombination
from psyneulink.core.components.functions.function import Function, get_matrix
from psyneulink.core.components.functions.nonstateful.learningfunctions import Hebbian
-from psyneulink.core.components.functions.nonstateful.objectivefunctions import Stability
+from psyneulink.core.components.functions.nonstateful.objectivefunctions import Stability, Energy, Entropy
from psyneulink.core.components.functions.stateful.integratorfunctions import AdaptiveIntegrator
from psyneulink.core.components.functions.userdefinedfunction import UserDefinedFunction
from psyneulink.core.components.mechanisms.mechanism import Mechanism_Base, MechanismError
@@ -210,7 +211,8 @@
from psyneulink.core.components.projections.pathway.mappingprojection import MappingProjection
from psyneulink.core.globals.context import handle_external_context
from psyneulink.core.globals.keywords import \
- AUTO, ENERGY, ENTROPY, HETERO, HOLLOW_MATRIX, INPUT_PORT, MATRIX, NAME, RECURRENT_TRANSFER_MECHANISM, RESULT
+ (AUTO, ENERGY, ENTROPY, FUNCTION, HETERO, HOLLOW_MATRIX, INPUT_PORT,
+ MATRIX, NAME, RECURRENT_TRANSFER_MECHANISM, RESULT)
from psyneulink.core.globals.parameters import Parameter, SharedParameter, check_user_specified, copy_parameter_value
from psyneulink.core.globals.preferences.basepreferenceset import ValidPrefSet
from psyneulink.core.globals.registry import register_instance, remove_instance_from_registry
@@ -243,7 +245,6 @@
ENTROPY_OUTPUT_PORT_NAME=ENTROPY
-
class RecurrentTransferError(MechanismError):
pass
@@ -518,13 +519,13 @@ class RecurrentTransferMechanism(TransferMechanism):
*ENERGY* : float
the energy of the elements in the LCAMechanism's `value `,
- calculated using the `Stability` Function using the `ENERGY` metric.
+ calculated using the `Stability` Function with the `ENERGY` metric.
.. _LCAMechanism_ENTROPY:
*ENTROPY* : float
the entropy of the elements in the LCAMechanism's `value `,
- calculated using the `Stability` Function using the `ENTROPY ` metric.
+ calculated using the `Stability` Function with the `ENTROPY ` metric.
Returns
-------
@@ -533,6 +534,11 @@ class RecurrentTransferMechanism(TransferMechanism):
"""
componentType = RECURRENT_TRANSFER_MECHANISM
+ standard_output_ports = TransferMechanism.standard_output_ports.copy()
+ standard_output_ports.extend([{NAME:ENERGY_OUTPUT_PORT_NAME}, {NAME:ENTROPY_OUTPUT_PORT_NAME}])
+ standard_output_port_names = TransferMechanism.standard_output_port_names.copy()
+ standard_output_port_names.extend([ENERGY_OUTPUT_PORT_NAME, ENTROPY_OUTPUT_PORT_NAME])
+
class Parameters(TransferMechanism.Parameters):
"""
Attributes
@@ -637,11 +643,6 @@ class Parameters(TransferMechanism.Parameters):
)
recurrent_projection = Parameter(None, stateful=False, loggable=False, structural=True)
- standard_output_ports = TransferMechanism.standard_output_ports.copy()
- standard_output_ports.extend([{NAME:ENERGY_OUTPUT_PORT_NAME}, {NAME:ENTROPY_OUTPUT_PORT_NAME}])
- standard_output_port_names = TransferMechanism.standard_output_port_names.copy()
- standard_output_port_names.extend([ENERGY_OUTPUT_PORT_NAME, ENTROPY_OUTPUT_PORT_NAME])
-
@check_user_specified
@beartype
def __init__(self,
@@ -952,9 +953,20 @@ def _instantiate_attributes_after_function(self, context=None):
"""
from psyneulink.library.components.projections.pathway.autoassociativeprojection import AutoAssociativeProjection
+ matrix = self.parameters.matrix._get(context)
+
+ # Now that matrix and default_variable size are known,
+ # instantiate functions for ENERGY and ENTROPY standard_output_ports
+ if ENERGY_OUTPUT_PORT_NAME in self.output_ports:
+ energy_idx = self.standard_output_port_names.index(ENERGY_OUTPUT_PORT_NAME)
+ self.standard_output_ports[energy_idx][FUNCTION] = Energy(self.defaults.variable,
+ matrix=matrix)
+ if ENTROPY_OUTPUT_PORT_NAME in self.output_ports:
+ energy_idx = self.standard_output_port_names.index(ENTROPY_OUTPUT_PORT_NAME)
+ self.standard_output_ports[energy_idx][FUNCTION] = Entropy(self.defaults.variable)
+
super()._instantiate_attributes_after_function(context=context)
- matrix = self.parameters.matrix._get(context)
# (7/19/17 CW) this line of code is now questionable, given the changes to matrix and the recurrent projection
if isinstance(matrix, AutoAssociativeProjection):
self.recurrent_projection = matrix
@@ -974,23 +986,6 @@ def _instantiate_attributes_after_function(self, context=None):
if self.learning_enabled:
self.configure_learning(context=context)
- if ENERGY_OUTPUT_PORT_NAME in self.output_ports.names:
- energy = Stability(self.defaults.variable[0],
- metric=ENERGY,
- transfer_fct=self.function,
- matrix=self.recurrent_projection._parameter_ports[MATRIX])
- self.output_ports[ENERGY_OUTPUT_PORT_NAME]._calculate = energy.function
-
- if ENTROPY_OUTPUT_PORT_NAME in self.output_ports.names:
- if self.function.bounds == (0,1) or self.clip == (0,1):
- entropy = Stability(self.defaults.variable[0],
- metric=ENTROPY,
- transfer_fct=self.function,
- matrix=self.recurrent_projection._parameter_ports[MATRIX])
- self.output_ports[ENTROPY_OUTPUT_PORT_NAME]._calculate = entropy.function
- else:
- del self.output_ports[ENTROPY_OUTPUT_PORT_NAME]
-
def _update_parameter_ports(self, runtime_params=None, context=None):
for port in self._parameter_ports:
# (8/2/17 CW) because the auto and hetero params are solely used by the AutoAssociativeProjection
diff --git a/psyneulink/library/compositions/autodiffcomposition.py b/psyneulink/library/compositions/autodiffcomposition.py
index 5ce5f1eb188..003b43db767 100644
--- a/psyneulink/library/compositions/autodiffcomposition.py
+++ b/psyneulink/library/compositions/autodiffcomposition.py
@@ -110,10 +110,17 @@
AutodiffComposition does not (currently) support the *automatic* construction of separate bias parameters.
Thus, when constructing a model using an AutodiffComposition that corresponds to one in PyTorch, the `bias
` parameter of PyTorch modules should be set
-to `False`. Trainable biases *can* be specified explicitly in an AutodiffComposition by including a
-TransferMechanism that projects to the relevant Mechanism (i.e., implementing that layer of the network to
-receive the biases) using a `MappingProjection` with a `matrix ` parameter that
-implements a diagnoal matrix with values corresponding to the initial value of the biases.
+to `False`.
+
+ .. hint::
+ Trainable biases *can* be specified explicitly in an AutodiffComposition by including a `ProcessingMechanism`
+ that projects to the relevant Mechanism (i.e., implementing that layer of the network to receive the biases)
+ using a `MappingProjection` with a `matrix ` parameter that implements a diagnoal
+ matrix with values corresponding to the initial value of the biases, and setting the `default_input
+ ` Parameter of one of the ProcessingMechanism's `input_ports
+ ` to *DEFAULT_VARIABLE*, and its `default_variable `
+ equal to 1. ProcessingMechanisms configured in this way are assigned `NodeRole` `BIAS`, and the MappingProjection
+ is subject to learning.
.. _AutodiffComposition_Nesting:
@@ -951,8 +958,9 @@ def create_pathway(node)->list:
return pathways
- # Construct a pathway for each INPUT Node (except the TARGET Node)
- pathways = [pathway for node in self.get_nodes_by_role(NodeRole.INPUT)
+ # Construct a pathway for each INPUT Node (including BIAS Nodes), except the TARGET Node)
+ pathways = [pathway
+ for node in (self.get_nodes_by_role(NodeRole.INPUT) + self.get_nodes_by_role(NodeRole.BIAS))
if node not in self.get_nodes_by_role(NodeRole.TARGET)
for pathway in _get_pytorch_backprop_pathway(node)]
@@ -1055,8 +1063,7 @@ def _get_loss(self, loss_spec):
# and therefore requires a wrapper function to properly package inputs.
return lambda x, y: nn.CrossEntropyLoss()(torch.atleast_2d(x), torch.atleast_2d(y.type(x.type())))
elif loss_spec == Loss.BINARY_CROSS_ENTROPY:
- if version.parse(torch.version.__version__) >= version.parse('1.12.0'):
- return nn.BCELoss()
+ return nn.BCELoss()
elif loss_spec == Loss.L1:
return nn.L1Loss(reduction='sum')
elif loss_spec == Loss.NLL:
@@ -1118,7 +1125,7 @@ def autodiff_forward(self, inputs, targets,
trial_loss = 0
for i in range(len(curr_tensors_for_trained_outputs[component])):
trial_loss += self.loss_function(curr_tensors_for_trained_outputs[component][i],
- curr_target_tensors_for_trained_outputs[component][i])
+ curr_target_tensors_for_trained_outputs[component][i])
pytorch_rep.minibatch_loss += trial_loss
pytorch_rep.minibatch_loss_count += 1
diff --git a/psyneulink/library/compositions/emcomposition.py b/psyneulink/library/compositions/emcomposition.py
index 46acce0308d..c76aab16a44 100644
--- a/psyneulink/library/compositions/emcomposition.py
+++ b/psyneulink/library/compositions/emcomposition.py
@@ -7,240 +7,8 @@
# ********************************************* EMComposition *************************************************
-#
-# TODO:
-# - QUESTION:
-# - SHOULD differential of SoftmaxGainControl Node be included in learning?
-# - SHOULD MEMORY DECAY OCCUR IF STORAGE DOES NOT? CURRENTLY IT DOES NOT (SEE EMStorage Function)
-
-# - FIX: Concatenation:
-# - LLVM for function and derivative
-# - Add Concatenate to pytorchcreator_function
-# - Deal with matrix assignment in LearningProjection LINE 643
-# - Reinstate test for execution of Concatenate with learning in test_emcomposition (currently commented out)
-# - FIX: Softmax Gain Control:
-# Test if it current works (they are added to Composition but not in BackProp processing pathway)
-# Does backprop have to run through this if not learnable?
-# If so, need to add PNL Function, with derivative and LLVM and Pytorch implementations
-# - FIX: WRITE MORE TESTS FOR EXECUTION, WARNINGS, AND ERROR MESSAGES
-# - learning (with and without learning field weights
-# - 3d tuple with first entry != memory_capacity if specified
-# - list with number of entries > memory_capacity if specified
-# - input is added to the correct row of the matrix for each key and value for
-# for non-contiguous keys (e.g, field_weights = [1,0,1]))
-# - explicitly that storage occurs after retrieval
-# - FIX: WARNING NOT OCCURRING FOR Normalize ON ZEROS WITH MULTIPLE ENTRIES (HAPPENS IF *ANY* KEY IS EVER ALL ZEROS)
-# - FIX: IMPLEMENT LearningMechanism FOR RETRIEVAL WEIGHTS:
-# - what is learning_update: AFTER doing? Use for scheduling execution of storage_node?
-# ?? implement derivative for concatenate
-# - FIX: implement add_storage_pathway to handle addition of storage_node as learning mechanism
-# - in "_create_storage_learning_components()" assign "learning_update" arg
-# as BEORE OR DURING instead of AFTER (assigned to learning_enabled arg of LearningMechanism)
-# - FIX: Add StorageMechanism LearningProjections to Composition? -> CAUSES TEST FAILURES; NEEDS INVESTIGATION
-# - FIX: Thresholded version of SoftMax gain (per Kamesh)
-# - FIX: DEAL WITH INDEXING IN NAMES FOR NON-CONTIGUOUS KEYS AND VALUES (reorder to keep all keys together?)
-# - FIX: _import_composition:
-# - MOVE LearningProjections
-# - MOVE Condition? (e.g., AllHaveRun) (OR PUT ON MECHANISM?)
-# - FIX: IMPLEMENT _integrate_into_composition METHOD THAT CALLS _import_composition ON ANOTHER COMPOSITION
-# - AND TRANSFERS RELEVANT ATTRIBUTES (SUCH AS MEMORY, query_input_nodeS, ETC., POSSIBLY APPENDING NAMES)
-# - FIX: ADD Option to suppress field_weights when computing norm for weakest entry in EMStorageMechanism
-# - FIX: GENERATE ANIMATION w/ STORAGE (uses Learning but not in usual way)
-# - IMPLEMENT use OF multiple inheritance of EMComposition from AutoDiff and Composition
-
-# - FIX: DOCUMENTATION:
-# - enable_learning vs. learning_field_weights
-# - USE OF EMStore.storage_location (NONE => LOCAL, SPECIFIED => GLOBAL)
-# - define "keys" and "values" explicitly
-# - define "key weights" explicitly as field_weights for all non-zero values
-# - make it clear that full size of memory is initialized (rather than "filling up" w/ use)
-# - write examples for run()
-# - FIX: ADD NOISE
-# - FIX: ?ADD add_memory() METHOD FOR STORING W/O RETRIEVAL, OR JUST ADD retrieval_prob AS modulable Parameter
-# - FIX: CONFIDENCE COMPUTATION (USING SIGMOID ON DOT PRODUCTS) AND REPORT THAT (EVEN ON FIRST CALL)
-# - FIX: ALLOW SOFTMAX SPEC TO BE A DICT WITH PARAMETERS FOR _get_softmax_gain() FUNCTION
-# MISC:
-# - WRITE TESTS FOR INPUT_PORT and MATRIX SPECS CORRECT IN LATEST BRANCHs
-# - ACCESSIBILITY OF DISTANCES (SEE BELOW): MAKE IT A LOGGABLE PARAMETER (I.E., WITH APPROPRIATE SETTER)
-# ADD COMPILED VERSION OF NORMED LINEAR_COMBINATION FUNCTION TO LinearCombination FUNCTION: dot / (norm a * norm b)
-# - DECAY WEIGHTS BY:
-# ? 1-SOFTMAX / N (WHERE N = NUMBER OF ITEMS IN MEMORY)
-# or
-# 1/N (where N=number of items in memory, and thus gets smaller as N gets
-# larger) on each storage (to some asymptotic minimum value), and store the new memory to the unit with the
-# smallest weights (randomly selected among “ties" [i.e., within epsilon of each other]), I think we have a
-# mechanism that can adaptively use its limited capacity as sensibly as possible, by re-cycling the units
-# that have the least used memories.
-# - MAKE "_store_memory" METHOD USE LEARNING INSTEAD OF ASSIGNMENT
-# - make LearningMechanism that, instead of error, simply adds relevant input to weights (with all others = 0)
-# - (relationship to Steven's Hebbian / DPP model?):
-
-# - ADD ADDITIONAL PARAMETERS FROM CONTENTADDRESSABLEMEMORY FUNCTION
-# - ADAPTIVE TEMPERATURE: KAMESH FOR FORMULA
-# - ADD MEMORY_DECAY TO ContentAddressableMemory FUNCTION (and compiled version by Samyak)
-# - MAKE memory_template A CONSTRUCTOR ARGUMENT FOR default_variable
-
-# - FIX: PSYNEULINK:
-# - TESTS:
-# - WRITE TESTS FOR DriftOnASphere variable = scalar, 2d vector or 1d vector of correct and incorrect lengths
-# - WRITE TESTS FOR LEARNING WITH LinearCombination of 1, 2 and 3 inputs
-#
-# - COMPILATION:
-# - Remove CIM projections on import to another composition
-# - Autodiff support for IdentityFunction
-# - MatrixTransform to add normalization
-# - _store() method to assign weights to memory
-# - LLVM problem with ComparatorMechanism
-#
-# - pytorchcreator_function:
-# SoftMax implementation: torch.nn.Softmax(dim=0) is not getting passed correctly
-# Implement LinearCombination
-# - MatrixTransform Function:
-#
-# - LEARNING - Backpropagation LearningFunction / LearningMechanism
-# - DOCUMENTATION:
-# - weight_change_matrix = gradient (result of delta rule) * learning_rate
-# - ERROR_SIGNAL is OPTIONAL (only implemented when there is an error_source specified)
-# - Backprop: (related to above?) handle call to constructor with default_variable = None
-# - WRITE TESTS FOR USE OF COVARIATES AND RELATED VIOLATIONS: (see ScratchPad)
-# - Use of LinearCombination with PRODUCT in output_source
-# - Use of LinearCombination with PRODUCT in InputPort of output_source
-# - Construction of LearningMechanism with Backprop:
-# - MappingProjection / LearningMechanism:
-# - Add learning_rate parameter to MappingProjection (if learnable is True)
-# - Refactor LearningMechanism to use MappingProjection learning_rate specification if present
-# - CHECK FOR EXISTING LM ASSERT IN pytests
-#
-# - AutodiffComposition:
-# - replace handling / flattening of nested compositions with Pytorch.add_module (which adds "child" modules)
-# - Check that error occurs for adding a controller to an AutodiffComposition
-# - Check that if "epochs" is not in input_dict for Autodiff, then:
-# - set to num_trials as default,
-# - leave it to override num_trials if specified (add this to DOCUMENTATION)
-# - Input construction has to be:
-# - same for Autodiff in Python mode and PyTorch mode
-# (NOTE: used to be that autodiff could get left in Python mode
-# so only where tests for Autodiff happened did it branch)
-# - AND different from Composition (in Python mode)
-# - support use of pathway argument in Autodff
-# - the following format doesn't work for LLVM (see test_identicalness_of_input_types:
-# xor = pnl.AutodiffComposition(nodes=[input_layer,hidden_layer,output_layer])
-# xor.add_projections([input_to_hidden_wts, hidden_to_output_wts])
-# - DOCUMENTATION: execution_mode=ExecutionMode.Python allowed
-# - Add warning of this on initial call to learn()
-#
-# - Composition:
-# - Add default_execution_mode attribute to allow nested Compositions to be executed in
-# different model than outer Composition
-# - _validate_input_shapes_and_expand_for_all_trials: consolidate with get_input_format()
-# - Generalize treatment of FEEDBACK specification:
- # - FIX: ADD TESTS FOR FEEDBACK TUPLE SPECIFICATION OF Projection, DIRECT SPECIFICATION IN CONSTRUCTOR
-# - FIX: why aren't FEEDBACK_SENDER and FEEDBACK_RECEIVER roles being assigned when feedback is specified?
-# - add property that keeps track of warnings that have been issued, and suppresses repeats if specified
-# - add property of Composition that lists it cycles
-# - Add warning if termination_condition is trigged (and verbosePref is set)
-# - Addition of projections to a ControlMechanism seems too dependent on the order in which the
-# the ControlMechanism is constructed with respect to its afferents (if it comes before one,
-# the projection to it (i.e., for monitoring) does not get added to the Composition
-# - - IMPLEMENTATION OF LEARNING: NEED ERROR IF TRY TO CALL LEARN ON A COMPOSITION THAT HAS NO LEARNING MECHANISMS
-# INCLUDING IN PYTHON MODE?? OR JUST ALLOW IT TO CONSTRUCT THE PATHWAY AUTOMATICALLY?
-# - Change size argument in constructor to use standard numpy shape format if tupe, and PNL format if list
-# - Write convenience Function for returning current time from context
-# - requires it be called from execution within aComposition, error otherwise)
-# - takes argument for time scale (e.g., TimeScale.TRIAL, TimeScale.RUN, etc.)
-# - Add TimeMechanism for which this is the function, and can be configured to report at a timescale
-# - Add Composition.run_status attribute assigned a context flag, with is_preparing property that checks it
-# (paralleling handling of is_initializing)
-# - Allow set of lists as specification for pathways in Composition
-# - Add support for set notation in add_backpropagation_learning_pathway (to match add_linear_processing_pathway)
-# see ScratchPad: COMPOSITION 2 INPUTS UNNESTED VERSION: MANY-TO-MANY
-# - Make sure that shadow inputs (see InputPort_Shadow_Inputs) uses the same matrix as shadowed input.
-# - composition.add_backpropagation_learning_pathway(): support use of set notation for multiple nodes that
-# project to a single one.
-# - add LearningProjections executed in EXECUTION_PHASE to self.projections
-# and then remove MODIFIED 8/1/23 in _check_for_unused_projections
-# - Why can't verbosePref be set directly on a composition?
-# - Composition.add_nodes():
-# - should check, on each call to add_node, to see if one that has a releavantprojection and, if so, add it.
-# - Allow [None] as argument and treat as []
-# - IF InputPort HAS default_input = DEFAULT_VARIABLE,
-# THEN IT SHOULD BE IGNORED AS AN INPUT NODE IN A COMPOSITION
-# - Add use of dict in pathways specification to map outputs from a set to inputs of another set
-# (including nested comps)
-#
-# - ShowGraph: (show_graph)
-# - don't show INPUT/OUTPUT Nodes for nested Comps in green/red
-# (as they don't really receive input or generate output on a run
-# - show feedback projections as pink (shouldn't that already be the case?)
-# - add mode for showing projections as diamonds without show_learning (e.g., "show_projections")
-# - figure out how to get storage_node to show without all other learning stuff
-# - show 'operation' parameter for LinearCombination in show_node_structure=ALL
-# - specify set of nodes to show and only show those
-# - fix: show_learning=ALL (or merge from EM branch)
-#
-# - ControlMechanism
-# - refactor ControlMechanism per notes of 11/3/21, including:
-# FIX: 11/3/21 - MOVE _parse_monitor_specs TO HERE FROM ObjectiveMechanism
-# - EpisodicMemoryMechanism:
-# - make storage_prob and retrieval_prob parameters linked to function
-# - make distance_field_weights a parameter linked to function
-#
-# - LinearCombination Function:
-# - finish adding derivative (for if exponents are specified)
-# - remove properties (use getter and setter for Parameters)
-#
-# - ContentAddressableMemory Function:
-# - rename "cue" -> "query"
-# - add field_weights as parameter of EM, and make it a shared_parameter ?as well as a function_parameter?
-
-# - DDM:
-# - make reset_stateful_function_when a Parameter and arg in constructor
-# and align with reset Parameter of IntegratorMechanism)
-#
-# - FIX: BUGS:
-# - composition:
-# - If any MappingProjection is specified from nested node to outer node,
-# then direct projections are instantiated to the output_CIM of the outer comp, and the
-# nested comp is treated as OUTPUT Node of outer comp even if all its projections are to nodes in outer comp
-# LOOK IN add_projections? for nested comps
-# - composition (?add_backpropagation_learning_pathway?):
-# THIS FAILS:
-# comp = Composition(name='a_outer')
-# comp.add_backpropagation_learning_pathway([input_1, hidden_1, output_1])
-# comp.add_backpropagation_learning_pathway([input_1, hidden_1, output_2])
-# BUT THE FOLLOWING WORKS (WITH IDENTICAL show_graph(show_learning=True)):
-# comp = Composition(name='a_outer')
-# comp.add_backpropagation_learning_pathway([input_1, hidden_1, output_1])
-# comp.add_backpropagation_learning_pathway([hidden_1, output_2])
-# - show_graph(): QUIRK (BUT NOT BUG?):
-# SHOWS TWO PROJECTIONS FROM a_inner.input_CIM -> hidden_x:
-# ?? BECAUSE hidden_x HAS TWO input_ports SINCE ITS FUNCTION IS LinearCombination?
-# a_inner = AutodiffComposition([hidden_x],name='a_inner')
-# a_outer = AutodiffComposition([[input_1, a_inner, output_1],
-# [a_inner, output_2]],
-# a_outer.show_graph(show_cim=True)
-
-# -LearningMechanism / Backpropagation LearningFunction:
-# - Construction of LearningMechanism on its own fails; e.g.:
-# lm = LearningMechanism(learning_rate=.01, learning_function=BackPropagation())
-# causes the following error:
-# TypeError("Logistic.derivative() missing 1 required positional argument: 'self'")
-# - Adding GatingMechanism after Mechanisms they gate fails to implement gating projections
-# (example: reverse order of the following in _construct_pathways
-# self.add_nodes(self.softmax_nodes)
-# self.add_nodes(self.field_weight_nodes)
-# - add Normalize as option
-# - Anytime a row's norm is 0, replace with 1s
-# - WHY IS Concatenate NOT WORKING AS FUNCTION OF AN INPUTPORT (WASN'T THAT USED IN CONTEXT OF BUFFER?
-# SEE NOTES TO KATHERINE
-#
-# - TESTS
-# For duplicate Projections (e.g., assign a Mechanism in **monitor** of ControlMechanism
-# and use comp.add_projection(MappingProjection(mointored, control_mech) -> should generate a duplicate
-# then search for other instances of the same error message
"""
-
Contents
--------
@@ -248,17 +16,18 @@
- `Organization `
- `Operation `
* `EMComposition_Creation`
- - `Fields `
+ - `Memory `
- `Capacity `
+ - `Fields `
- `Storage and Retrieval `
- `Learning `
* `EMComposition_Structure`
- `Input `
- - `Memory `
+ - `Memory `
- `Output `
* `EMComposition_Execution`
- `Processing `
- - `Learning `
+ - `Learning `
* `EMComposition_Examples`
- `Memory Template and Fill `
- `Field Weights `
@@ -269,27 +38,36 @@
Overview
--------
-The EMComposition implements a configurable, content-addressable form of episodic, or eternal memory, that emulates
+The EMComposition implements a configurable, content-addressable form of episodic (or external) memory. It emulates
an `EpisodicMemoryMechanism` -- reproducing all of the functionality of its `ContentAddressableMemory` `Function` --
-in the form of an `AutodiffComposition` that is capable of learning how to differentially weight different cues used
-for retrieval,, and that adds the capability for `memory_decay `. Its `memory
-` is configured using two arguments of its constructor: **memory_template** argument, that defines
-how each entry in `memory ` is structured (the number of fields in each entry and the length
-of each field); and **field_weights** argument, that defines which fields are used as cues for retrieval, i.e., "keys",
-including whether and how they are differentially weighted in the match process used for retrieval); and which
-fields are treated as "values" that are stored retrieved, but not used by the match process. The inputs to an
-EMComposition, corresponding to each key ("query") and value field are assigned to each of its `INPUT `
-`Nodes ` (listed in its `query_input_nodes ` and `value_input_nodes
-` attributes, respectively), and the retrieved values are represented as `OUTPUT
-` `Nodes ` of the EMComposition. The `memory ` can be
-accessed using its `memory ` attribute.
+in the form of an `AutodiffComposition`. This allows it to backpropagate error signals based retrieved values to
+it inputs, and learn how to differentially weight cues (queries) used for retrieval. It also adds the capability for
+`memory_decay `. In these respects, it implements a variant of a `Modern Hopfield
+Network `_, as well as some of the features of a `Transformer
+`_
+
+The `memory ` of an EMComposition is configured using two arguments of its constructor:
+the **memory_template** argument, that defines the overall structure of its `memory ` (the
+number of fields in each entry, the length of each field, and the number of entries); and **fields** argument, that
+defines which fields are used as cues for retrieval (i.e., as "keys"), including whether and how they are weighted in
+the match process used for retrieval, which fields are treated as "values" that are stored retrieved but not used by
+the match process, and which are involved in learning. The inputs to an EMComposition, corresponding to its keys and
+values, are assigned to each of its `INPUT ` `Nodes `: inputs to be matched to keys
+(i.e., used as "queries") are assigned to its `query_input_nodes `; and the remaining
+inputs assigned to it `value_input_nodes `. When the EMComposition is executed, the
+retrieved values for all fields are returned as the result, and recorded in its `results `
+attribute. The value for each field is assigned as the `value ` of its `OUTPUT `
+`Nodes `. The input is then stored in its `memory `, with a probability
+determined by its `storage_prob ` `Parameter`, and all previous memories decayed by its
+`memory_decay_rate `. The `memory ` can be accessed using its
+`memory ` Parameter.
.. technical_note::
- The memories of an EMComposition are actually stored in the `matrix ` attribute of a
- set of `MappingProjections ` (see `note below `). The `memory
- ` attribute compiles and formats these as a single 3d array, the rows of which (axis 0)
- are each entry, the columns of which (axis 1) are the fields of each entry, and the items of which (axis 2)
- are the values of each field (see `EMComposition_Memory` for additional details).
+ The memories of an EMComposition are actually stored in the `matrix ` `Parameter`
+ of a set of `MappingProjections ` (see `note below `). The
+ `memory ` Parameter compiles and formats these as a single 3d array, the rows of which
+ (axis 0) are each entry, the columns of which (axis 1) are the fields of each entry, and the items of which
+ (axis 2) are the values of each field (see `EMComposition_Memory_Configuration` for additional details).
.. _EMComposition_Organization:
@@ -300,14 +78,14 @@
*Entries and Fields*. Each entry in memory can have an arbitrary number of fields, and each field can have an arbitrary
length. However, all entries must have the same number of fields, and the corresponding fields must all have the same
length across entries. Each field is treated as a separate "channel" for storage and retrieval, and is associated with
-its own corresponding input (key or value) and output (retrieved value) `Node ` some or all of
+its own corresponding input (key or value) and output (retrieved value) `Node `, some or all of
which can be used to compute the similarity of the input (key) to entries in memory, that is used for retreieval.
Fields can be differentially weighted to determine the influence they have on retrieval, using the `field_weights
-` parameter (see `retrieval ` below). The number and
-shape of the fields in each entry is specified in the **memory_template** argument of the EMComposition's constructor
-(see `memory_template `). Which fields treated as keys (i.e., matched against queries during
-retrieval) and which are treated as values (i.e., retrieved but not used for matching retrieval) is specified in the
-**field_weights** argument of the EMComposition's constructor (see `field_weights `).
+` parameter (see `retrieval ` below). The number and shape
+of the fields in each entry is specified in the **memory_template** argument of the EMComposition's constructor (see
+`memory_template `). Which fields treated as keys (i.e., matched against queries
+during retrieval) and which are treated as values (i.e., retrieved but not used for matching retrieval) is specified in
+the **field_weights** argument of the EMComposition's constructor (see `field_weights `).
.. _EMComposition_Operation:
@@ -315,39 +93,46 @@
*Retrieval.* The values retrieved from `memory ` (one for each field) are based
on the relative similarity of the keys to the entries in memory, computed as the distance of each key and the
-values in the corresponding field for each entry in memory. By default, normalized dot products (comparable to cosine
-similarity) are used to compute the similarity of each query to each key in memory. These distances are then
-weighted by the corresponding `field_weights ` for each field (if specified) and then
-summed, and the sum is softmaxed to produce a softmax distribution over the entries in memory. That is then used to
-generate a softmax-weighted average of the retrieved values across all fields, which is returned as the `result
-` of the EMComposition's `execution ` (an EMComposition can also be
-configured to return the entry with the lowest distance weighted by field, however then it is not compatible
-with learning; see `softmax_choice `).
+values in the corresponding field for each entry in memory. By default, for queries and keys that are vectors,
+normalized dot products (comparable to cosine similarity) are used to compute the similarity of each query to each
+key in memory; and if they are scalars the L0 norm is used. These distances are then weighted by the corresponding
+`field_weights ` for each field (if specified) and then summed, and the sum is softmaxed
+to produce a softmax distribution over the entries in memory. That is then used to generate a softmax-weighted average
+of the retrieved values across all fields, which is returned as the `result ` of the EMComposition's
+`execution ` (an EMComposition can also be configured to return the exact entry with the lowest
+distance (weighted by field), however then it is not compatible with learning; see `softmax_choice
+`).
COMMENT:
TBD DISTANCE ATTRIBUTES:
- The distances used for the last retrieval is stored in XXXX and the distances of each of their corresponding fields
+ The distance used for the last retrieval is stored in XXXX, and the distances of each of their corresponding fields
(weighted by `distance_field_weights `), are returned in XXX,
respectively.
COMMENT
-*Storage.* The `inputs ` to the EMComposition's fields are stored in `memory
-` after each execution, with a probability determined by `storage_prob
-`. If `memory_decay_rate ` is specified, then the `memory
-` is decayed by that amount after each execution. If `memory_capacity
-` has been reached, then each new memory replaces the weakest entry (i.e., the one
-with the smallest norm across all of its fields) in `memory `.
+*Storage.* The `inputs ` to the EMComposition's fields are stored
+in `memory ` after each execution, with a probability determined by `storage_prob
+`. If `memory_decay_rate ` is specified, then
+the `memory ` is decayed by that amount after each execution. If `memory_capacity
+` has been reached, then each new memory replaces the weakest entry
+(i.e., the one with the smallest norm across all of its fields) in `memory `.
.. _EMComposition_Creation:
Creation
--------
-An EMComposition is created by calling its constructor, that takes the following arguments:
+An EMComposition is created by calling its constructor. There are four major elements that can be configured:
+the structure of its `memory ; the fields ` for the entries
+in memory; how `storage and retrieval ` operate; and whether and how `learning
+` is carried out.
- .. _EMComposition_Fields:
+.. _EMComposition_Memory_Specification:
-*Field Specification*
+*Memory Specification*
+~~~~~~~~~~~~~~~~~~~~~~
+
+These arguments are used to specify the shape and number of memory entries.
.. _EMComposition_Memory_Template:
@@ -392,18 +177,6 @@
zeros, and **memory_fill** is specified, then the matrix is filled with the value specified in **memory_fill**;
otherwise, zeros are used to fill all entries.
-.. _EMComposition_Memory_Capacity:
-
-*Memory Capacity*
-
-* **memory_capacity**: specifies the number of items that can be stored in the EMComposition's memory; when
- `memory_capacity ` is reached, each new entry overwrites the weakest entry (i.e., the
- one with the smallest norm across all of its fields) in `memory `. If `memory_template
- ` is specified as a 3-item tuple or 3d list or array (see above), then that is used
- to determine `memory_capacity ` (if it is specified and conflicts with either of those
- an error is generated). Otherwise, it can be specified using a numerical value, with a default of 1000. The
- `memory_capacity ` cannot be modified once the EMComposition has been constructed.
-
.. _EMComposition_Memory_Fill:
* **memory_fill**: specifies the value used to fill the `memory `, based on the shape specified
@@ -418,51 +191,131 @@
This can be ignored, as it does not affect the results of execution, but it can be averted by specifying
`memory_fill ` to use small random values (e.g., ``memory_fill=(0,.001)``).
+.. _EMComposition_Memory_Capacity:
+
+* **memory_capacity**: specifies the number of items that can be stored in the EMComposition's memory; when
+ `memory_capacity ` is reached, each new entry overwrites the weakest entry (i.e., the
+ one with the smallest norm across all of its fields) in `memory `. If `memory_template
+ ` is specified as a 3-item tuple or 3d list or array (see above), then that is used
+ to determine `memory_capacity ` (if it is specified and conflicts with either of those
+ an error is generated). Otherwise, it can be specified using a numerical value, with a default of 1000. The
+ `memory_capacity ` cannot be modified once the EMComposition has been constructed.
+
+.. _EMComposition_Fields:
+
+*Fields*
+~~~~~~~~
+
+These arguments are used to specify the names of the fields in a memory entry, which are used for its keys and values,
+how keys are weighted for retrieval, whether those weights are learned, and which fields are used for computing error
+that is propagated through the EMComposition.
+
+.. _EMComposition_Field_Specification_Dict:
+
+* **fields**: a dict that specifies the names of the fields and their attributes. There must be an entry for each
+ field specified in the **memory_template**, and must have the following format:
+
+ * *key*: a string that specifies the name of the field.
+
+ * *value*: a dict or tuple with three entries; if a dict, the key to each entry must be the keyword specified below,
+ and if a tuple, the entries must appear in the following order:
+
+ - *FIELD_WEIGHT* `specification ` - value must be a scalar or None. If it is a scalar,
+ the field is treated as a `retrieval key ` in `memory ` that
+ is weighted by that value during retrieval; if None, it is treated as a value in `memory `
+ and the field cannot be reconfigured later.
+
+ - *LEARN_FIELD_WEIGHT* `specification ` - value must be a boolean or a float;
+ if False, the field_weight for that field is not learned; if True, the field weight is learned using the
+ EMComposition's `learning_rate `; if a float, that is used as its learning_rate.
+
+ - *TARGET_FIELD* `specification ` - value must be a boolean; if True, the value of the
+ `retrieved_node ` for that field conrtributes to the error computed during learning
+ and backpropagated through the EMComposition (see `Backpropagation of `);
+ if False, the retrieved value for that field does not contribute to the error; however, its field_weight can still
+ be learned if that is specfified in `learn_field_weight `.
+
+ .. _note:
+ The **fields** argument is provided as a convenient and reliable way of specifying field attributes;
+ the dict itself is not retained as a `Parameter` or attribute of the EMComposition.
+
+ The specifications provided in the **fields** argument are assigned to the corresponding Parameters of
+ the EMComposition which, alternatively, can be specified individually using the **field_names**, **field_weights**,
+ **learn_field_weights** and **target_fields** arguments of the EMComposition's constructor, as described below.
+ However, these and the **fields** argument cannot both be used together; doing so raises an error.
+
+.. _EMComposition_Field_Names:
+
+* **field_names**: a list specifies names that can be assigned to the fields. The number of names specified must match
+ the number of fields specified in the memory_template. If specified, the names are used to label the nodes of the
+ EMComposition; otherwise, the fields are labeled generically as "Key 0", "Key 1", and "Value 1", "Value 2", etc..
+
.. _EMComposition_Field_Weights:
-* **field_weights**: specifies which fields are used as keys, and how they are weighted during retrieval. The
- number of entries specified must match the number of fields specified in **memory_template** (i.e., the size of
- of its first dimension (axis 0)). All non-zero entries must be positive; these designate *keys* -- fields
- that are used to match queries against entries in memory for retrieval (see `Match memories by field
- `). Entries of 0 designate *values* -- fields that are ignored during the matching
- process, but the values of which are retrieved and assigned as the `value ` of the
- corresponding `retrieved_node `. This distinction between keys and value corresponds
+* **field_weights**: specifies which fields are used as keys, and how they are weighted during retrieval. Fields
+ designated as keys used to match inputs (queries) against entries in memory for retrieval (see `Match memories by
+ field `); entries designated as *values* are ignored during the matching process, but
+ their values in memory are retrieved and assigned as the `value ` of the corresponding
+ `retrieved_node `. This distinction between keys and value corresponds
to the format of a standard "dictionary," though in that case only a single key and value are allowed, whereas
- here there can be one or more keys and any number of values; if all fields are keys, this implements a full form of
- content-addressable memory. If **learn_field_weights** is True (and `enable_learning`
- is either True or a list with True for at least one entry), then the field_weights can be modified during training
- (this functions similarly to the attention head of a Transformer model, although at present the field can only be
- scalar values rather than vecdtors); if **learn_field_weights** is False, then the field_weights are fixed.
- The following options can be used to specify **field_weights**:
-
- * *None* (the default): all fields except the last are treated as keys, and are weighted equally for retrieval,
- while the last field is treated as a value field;
-
- * *single entry*: all fields are treated as keys (i.e., used for retrieval) and weighted equally for retrieval.
- if `normalize_field_weights ` is True, the value is ignored and all
- of keys are weighted by 1 / number of keys (i.e., normalized), whereas if `normalize_field_weights
- ` is False, then the value specified is used to weight the retrieval of
- every keys.
-
- * *multiple non-zero entries*: If all entries are identical, the value is ignored and the corresponding keys
- are weighted equally for retrieval; if the non-zero entries are non-identical, they are used to weight the
- corresponding fields during retrieval (see `Weight fields `). In either case,
- the remaining fields (with zero weights) are treated as value fields.
+ in an EMComposition there can be one or more keys and any number of values; if all fields are keys, this implements a
+ full form of content-addressable memory. The following options can be used to specify **field_weights**:
-.. _EMComposition_Normalize_Field_Weights:
+ * *None* (the default): all fields except the last are treated as keys, and are assigned a weight of 1,
+ while the last field is treated as a value field (same as assiging it None in a list or tuple (see below).
-* **normalize_field_weights**: specifies whether the `field_weights ` are normalized
- or their raw values are used. If True, the `field_weights ` are normalized so that
- they sum to 1.0, and are used to weight (i.e., multiply) the corresponding fields during retrieval (see `Weight
- fields `). If False, the raw values of the `field_weights `
- are used to weight the retrieved value of each field. This setting is ignored if **field_weights**
- is None or `concatenate_queries ` is in effect.
+ * *scalar*: all fields are treated as keys (i.e., used for retrieval) and weighted equally for retrieval. If
+ `normalize_field_weights ` is True, the value is divided by the number
+ of keys, whereas if `normalize_field_weights ` is False, then the value
+ specified is used to weight the retrieval of all keys with that value.
+
+ .. note::
+ At present these have the same result, since the `SoftMax` function is used to normalize the match between
+ queries and keys. However, other retrieval functions could be used in the future that would be affected by
+ the value of the `field_weights `. Therefore, it is recommended to leave
+ `normalize_field_weights ` set to True (the default) to ensure that
+ the `field_weights ` are normalized to sum to 1.0.
+
+ * *list or tuple*: the number of entries must match the number of fields specified in **memory_template**, and
+ all entries must be either 0, a positive scalar value, or None. If all entries are identical, they are treated
+ as if a single value was specified (see above); if the entries are non-identical, any entries that are not None
+ are used to weight the corresponding fields during retrieval (see `Weight fields `),
+ including those that are 0 (though these will not be used in the retrieval process unless/until they are changed
+ to a positive value). If `normalize_field_weights ` is True, all non-None
+ entries are normalized so that they sum to 1.0; if False, the raw values are used to weight the retrieval of
+ the corresponding fields. All entries of None are treated as value fields, are not assigned a `field_weight_node
+ `, and are ignored during retrieval. These *cannot be modified* after the
+ EMComposition has been constructed (see note below).
+
+ .. _EMComposition_Field_Weights_Change_Note:
+
+ .. note::
+ The field_weights can be modified after the EMComposition has been constructed, by assigning a new set of weights
+ to its `field_weights ` `Parameter`. However, only field_weights associated with
+ key fields (i.e., that were initially assigned non-zero field_weights) can be modified; the weights for value
+ fields (i.e., ones that were initially assigned a field_weight of None) cannot be modified, and doing so raises
+ an error. If a field that will be used initially as a value may later need to be used as a key, it should be
+ assigned a `field_weight ` of 0 at construction (rather than None), which can then
+ later be changed as needed.
+
+ .. technical_note::
+ The reason that field_weights can be modified only for keys is that `field_weight_nodes
+ ` are constructed only for keys, since ones for values would have no effect
+ on the retrieval process and therefore are uncecessary (and can be misleading).
-.. _EMComposition_Field_Names:
-* **field_names**: specifies names that can be assigned to the fields. The number of names specified must
- match the number of fields specified in the memory_template. If specified, the names are used to label the
- nodes of the EMComposition. If not specified, the fields are labeled generically as "Key 0", "Key 1", etc..
+* **learn_field_weights**: if **enable_learning** is True, this specifies which field_weights are subject to learning,
+ and optionally the `learning_rate ` for each (see `learn_field_weights
+ ` below for details of specification).
+
+.. _EMComposition_Normalize_Field_Weights:
+
+* **normalize_field_weights**: specifies whether the `field_weights ` are normalized or
+ their raw values are used. If True, the value of all non-None `field_weights ` are
+ normalized so that they sum to 1.0, and the normalized values are used to weight (i.e., multiply) the corresponding
+ fields during retrieval (see `Weight fields `). If False, the raw values of the
+ `field_weights ` are used to weight the retrieved value of each field. This setting
+ is ignored if **field_weights** is None or `concatenate_queries ` is True.
.. _EMComposition_Concatenate_Queries:
@@ -486,27 +339,20 @@
are always preserved, even when `concatenate_queries ` is True, so that
separate inputs can be provided for each key, and the value of each key can be retrieved separately.
-.. _EMComposition_Memory_Decay_Rate
-
-* **memory_decay_rate**: specifies the rate at which items in the EMComposition's memory decay; the default rate
- is *AUTO*, which sets it to 1 / `memory_capacity `, such that the oldest memories
- are the most likely to be replaced when `memory_capacity ` is reached. If
- **memory_decay_rate** is set to 0 None or False, then memories do not decay and, when `memory_capacity
- ` is reached, the weakest memories are replaced, irrespective of order of entry.
-
.. _EMComposition_Retrieval_Storage:
*Retrieval and Storage*
+~~~~~~~~~~~~~~~~~~~~~~~
-* **storage_prob** : specifies the probability that the inputs to the EMComposition will be stored as an item in
+* **storage_prob**: specifies the probability that the inputs to the EMComposition will be stored as an item in
`memory ` on each execution.
-* **normalize_memories** : specifies whether queries and keys in memory are normalized before computing their dot
+* **normalize_memories**: specifies whether queries and keys in memory are normalized before computing their dot
products.
.. _EMComposition_Softmax_Gain:
-* **softmax_gain** : specifies the gain (inverse temperature) used for softmax normalizing the combined distances
+* **softmax_gain**: specifies the gain (inverse temperature) used for softmax normalizing the combined distances
used for retrieval (see `EMComposition_Execution` below). The following options can be used:
* numeric value: the value is used as the gain of the `SoftMax` Function for the EMComposition's
@@ -531,7 +377,7 @@
.. _EMComposition_Softmax_Choice:
-* **softmax_choice** : specifies how the `SoftMax` Function of the EMComposition's `softmax_node
+* **softmax_choice**: specifies how the `SoftMax` Function of the EMComposition's `softmax_node
` is used, with the combined distances, to generate a retrieved item;
the following are the options that can be used and the retrieved value they produce:
@@ -545,7 +391,7 @@
.. warning::
Use of the *ARG_MAX* and *PROBABILISTIC* options is not compatible with learning, as these implement a discrete
choice and thus are not differentiable. Constructing an EMComposition with **softmax_choice** set to either of
- these options and **enable_learning** set to True (or a list with any True entries) will generate a warning, and
+ these options and **learn_field_weights** set to True (or a list with any True entries) will generate a warning, and
calling the EMComposition's `learn ` method will generate an error; it must be changed to
*WEIGHTED_AVG* to execute learning.
@@ -554,37 +400,91 @@
passed as *ARG_MAX_INDICATOR*; and *PROBALISTIC* is passed as *PROB_INDICATOR*; the other SoftMax options are
not currently supported.
+.. _EMComposition_Memory_Decay_Rate:
+
+* **memory_decay_rate**: specifies the rate at which items in the EMComposition's memory decay; the default rate
+ is *AUTO*, which sets it to 1 / `memory_capacity `, such that the oldest memories
+ are the most likely to be replaced when `memory_capacity ` is reached. If
+ **memory_decay_rate** is set to 0 None or False, then memories do not decay and, when `memory_capacity
+ ` is reached, the weakest memories are replaced, irrespective of order of entry.
+
+.. _EMComposition_Purge_by_Weight:
+
+* **purge_by_field_weight**: specifies whether `field_weights ` are used in determining
+ which memory entry is replaced when a new memory is `stored `. If True, the norm of each
+ entry is multiplied by its `field_weight