Skip to content

Commit

Permalink
Component: rework size argument to accept and validate numpy shapes
Browse files Browse the repository at this point in the history
- size=int - single input item, with numpy shape (x,)
- size=iterable - one or more input items, each with respective numpy
shapes
- size containing float - no longer supported, because numpy rejects as
shape (prior behavior casted to int, because size as float isn't
defined)

ComponentError thrown if default_variable and size arguments are
both provided and conflict
  • Loading branch information
kmantel committed Oct 2, 2024
1 parent f35f6c9 commit 17e7917
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 312 deletions.
229 changes: 107 additions & 122 deletions psyneulink/core/components/component.py

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions psyneulink/core/components/mechanisms/mechanism.py
Original file line number Diff line number Diff line change
Expand Up @@ -1222,12 +1222,15 @@ class Mechanism_Base(Mechanism):
of its `function <Mechanism_Base.function>` if those are not specified. If it is not specified, then a
subclass-specific default is assigned (usually [[0]]).
size : int, list or np.ndarray of ints : default None
size : int, or Iterable of tuples or ints : default None
specifies default_variable as array(s) of zeros if **default_variable** is not passed as an argument;
if **default_variable** is specified, it takes precedence over the specification of **size**.
if **default_variable** is specified, it must be equivalent to
**size**.
For example, the following Mechanisms are equivalent::
my_mech = ProcessingMechanism(size = [3, 2])
my_mech = ProcessingMechanism(default_variable = [[0, 0, 0], [0, 0]])
When specified as an iterable, each element of **size** is used
as the size of the corresponding InputPort.
input_ports : str, list, dict, or np.ndarray : default None
specifies the InputPorts for the Mechanism; if it is not specified, a single InputPort is created
Expand Down
52 changes: 0 additions & 52 deletions psyneulink/core/components/ports/port.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,58 +1124,6 @@ def __init__(self,
if context.source == ContextFlags.COMMAND_LINE:
owner.add_ports([self])

def _handle_size(self, size, variable):
"""Overwrites the parent method in Component.py, because the variable of a Port
is generally 1D, rather than 2D as in the case of Mechanisms
"""
if size is not NotImplemented:

def checkAndCastInt(x):
if not isinstance(x, numbers.Number):
raise PortError("Size ({}) is not a number.".format(x))
if x < 1:
raise PortError("Size ({}) is not a positive number.".format(x))
try:
int_x = int(x)
except:
raise PortError(
"Failed to convert size argument ({}) for {} {} to an integer. For Ports, size "
"should be a number, which is an integer or can be converted to integer.".
format(x, type(self), self.name))
if int_x != x:
if hasattr(self, 'prefs') and hasattr(self.prefs, VERBOSE_PREF) and self.prefs.verbosePref:
warnings.warn("When size ({}) was cast to integer, its value changed to {}.".format(x, int_x))
return int_x

# region Convert variable to a 1D array, cast size to an integer
if size is not None:
size = checkAndCastInt(size)
try:
if variable is not None:
variable = np.atleast_1d(variable)
except:
raise PortError("Failed to convert variable (of type {}) to a 1D array.".format(type(variable)))
# endregion

# region if variable is None and size is not None, make variable a 1D array of zeros of length = size
if variable is None and size is not None:
try:
variable = np.zeros(size)
except:
raise ComponentError("variable (perhaps default_variable) was not specified, but PsyNeuLink "
"was unable to infer variable from the size argument, {}. size should be"
" an integer or able to be converted to an integer. Either size or "
"variable must be specified.".format(size))
#endregion

if variable is not None and size is not None: # try tossing this "if" check
# If they conflict, raise exception
if size != len(variable):
raise PortError("The size arg of {} ({}) conflicts with the length of its variable arg ({})".
format(self.name, size, variable))

return variable

def _validate_variable(self, variable, context=None):
"""Validate variable and return validated variable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,6 @@ def __init__(self,
self.target_start = 0
self._target_included = False
self.target_end = self.target_start + target_size
size = self.recurrent_size

default_variable = [np.zeros(input_size), np.zeros(self.recurrent_size)]
# Set InputPort sizes in _instantiate_input_ports,
Expand All @@ -1059,7 +1058,6 @@ def __init__(self,

super().__init__(
default_variable=default_variable,
size=size,
input_ports=input_ports,
combination_function=combination_function,
function=function,
Expand Down
4 changes: 2 additions & 2 deletions tests/composition/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,8 @@ def test_user_added_ports(self):
mech = ProcessingMechanism()
comp.add_node(mech)
# instantiate custom input and output ports
inp = InputPort(size=2)
out = OutputPort(size=2)
inp = InputPort()
out = OutputPort()

# NOTE: Adding ports to CIM from command line is currenlty disallowed
# # add custom input and output ports to CIM
Expand Down
25 changes: 3 additions & 22 deletions tests/mechanisms/test_ddm_mechanism.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,25 +529,6 @@ def test_DDM_size_int_inputs():

# INVALID INPUTS

# ------------------------------------------------------------------------------------------------
# TEST 1
# size = 0, check less-than-one error


def test_DDM_mech_size_zero():
with pytest.raises(ComponentError) as error_text:
T = DDM(
name='DDM',
size=0,
function=DriftDiffusionIntegrator(
noise=0.0,
rate=-5.0,
time_step_size=1.0
),
execute_until_finished=False,
)
assert "is not a positive number" in str(error_text.value)

# ------------------------------------------------------------------------------------------------
# TEST 2
# size = -1.0, check less-than-one error
Expand All @@ -557,15 +538,15 @@ def test_DDM_mech_size_negative_one():
with pytest.raises(ComponentError) as error_text:
T = DDM(
name='DDM',
size=-1.0,
size=-1,
function=DriftDiffusionIntegrator(
noise=0.0,
rate=-5.0,
time_step_size=1.0
),
execute_until_finished=False,
)
assert "is not a positive number" in str(error_text.value)
assert "negative dimensions" in str(error_text.value)

# ------------------------------------------------------------------------------------------------
# TEST 3
Expand All @@ -576,7 +557,7 @@ def test_DDM_size_too_large():
with pytest.raises(DDMError) as error_text:
T = DDM(
name='DDM',
size=3.0,
size=3,
function=DriftDiffusionIntegrator(
noise=0.0,
rate=-5.0,
Expand Down
153 changes: 43 additions & 110 deletions tests/mechanisms/test_transfer_mechanism.py
Original file line number Diff line number Diff line change
Expand Up @@ -998,48 +998,6 @@ def test_transfer_mech_size_int_inputs_floats(self):
# val = T.execute([Linear().execute(), NormalDist().execute(), Exponential().execute(), ExponentialDist().execute()])
# np.testing.assert_allclose(val, [[np.array([0.]), 0.4001572083672233, np.array([1.]), 0.7872011523172707]]

# ------------------------------------------------------------------------------------------------
# TEST 5
# size = float, check if variable is an array of zeros

@pytest.mark.mechanism
@pytest.mark.transfer_mechanism
def test_transfer_mech_size_float_inputs_check_var(self):
T = TransferMechanism(
name='T',
size=4.0,
)
np.testing.assert_array_equal(T.defaults.variable, [[0, 0, 0, 0]])
assert len(T.size == 1) and T.size[0] == 4.0 and isinstance(T.size[0], np.integer)

# ------------------------------------------------------------------------------------------------
# TEST 6
# size = float, variable = list of ints

@pytest.mark.mechanism
@pytest.mark.transfer_mechanism
def test_transfer_mech_size_float_inputs_ints(self):
T = TransferMechanism(
name='T',
size=4.0
)
val = T.execute([10, 10, 10, 10])
np.testing.assert_array_equal(val, [[10.0, 10.0, 10.0, 10.0]])

# ------------------------------------------------------------------------------------------------
# TEST 7
# size = float, variable = list of floats

@pytest.mark.mechanism
@pytest.mark.transfer_mechanism
def test_transfer_mech_size_float_inputs_floats(self):
T = TransferMechanism(
name='T',
size=4.0
)
val = T.execute([10.0, 10.0, 10.0, 10.0])
np.testing.assert_array_equal(val, [[10.0, 10.0, 10.0, 10.0]])

# ------------------------------------------------------------------------------------------------
# TEST 8
# size = float, variable = list of functions
Expand Down Expand Up @@ -1069,18 +1027,6 @@ def test_transfer_mech_size_list_of_ints(self):
assert len(T.defaults.variable) == 3 and len(T.defaults.variable[0]) == 2 and len(T.defaults.variable[1]) == 3 and len(T.defaults.variable[2]) == 4

# ------------------------------------------------------------------------------------------------
# TEST 10
# size = list of floats, check that variable is correct

@pytest.mark.mechanism
@pytest.mark.transfer_mechanism
def test_transfer_mech_size_list_of_floats(self):
T = TransferMechanism(
name='T',
size=[2., 3., 4.]
)
assert len(T.defaults.variable) == 3 and len(T.defaults.variable[0]) == 2 and len(T.defaults.variable[1]) == 3 and len(T.defaults.variable[2]) == 4

# note that this output under the Linear function is useless/odd, but the purpose of allowing this configuration
# is for possible user-defined functions that do use unusual shapes.

Expand All @@ -1089,7 +1035,7 @@ def test_transfer_mech_size_list_of_floats(self):
def test_transfer_mech_size_var_both_lists(self):
T = TransferMechanism(
name='T',
size=[2., 3.],
size=[2, 3],
default_variable=[[1, 2], [3, 4, 5]]
)
assert len(T.defaults.variable) == 2
Expand All @@ -1103,13 +1049,14 @@ def test_transfer_mech_size_var_both_lists(self):
@pytest.mark.mechanism
@pytest.mark.transfer_mechanism
def test_transfer_mech_size_scalar_var_2d(self):
T = TransferMechanism(
name='T',
size=2,
default_variable=[[1, 2], [3, 4]]
)
np.testing.assert_array_equal(T.defaults.variable, [[1, 2], [3, 4]])
assert len(T.size) == 2 and T.size[0] == 2 and T.size[1] == 2
with pytest.raises(
ComponentError, match=r'size and default_variable arguments.*conflict.*'
):
TransferMechanism(
name='T',
size=2,
default_variable=[[1, 2], [3, 4]]
)

# ------------------------------------------------------------------------------------------------
# TEST 13
Expand All @@ -1131,14 +1078,14 @@ def test_transfer_mech_var_2d_array(self):
@pytest.mark.mechanism
@pytest.mark.transfer_mechanism
def test_transfer_mech_var_1D_size_wrong(self):
T = TransferMechanism(
name='T',
default_variable=[1, 2, 3, 4],
size=2
)
np.testing.assert_array_equal(T.defaults.variable, [[1, 2, 3, 4]])
val = T.execute([10.0, 10.0, 10.0, 10.0])
np.testing.assert_array_equal(val, [[10.0, 10.0, 10.0, 10.0]])
with pytest.raises(
ComponentError, match=r'size and default_variable arguments.*conflict.*'
):
TransferMechanism(
name='T',
default_variable=[1, 2, 3, 4],
size=2
)

# ------------------------------------------------------------------------------------------------
# TEST 15
Expand All @@ -1147,14 +1094,14 @@ def test_transfer_mech_var_1D_size_wrong(self):
@pytest.mark.mechanism
@pytest.mark.transfer_mechanism
def test_transfer_mech_var_1D_size_wrong_2(self):
T = TransferMechanism(
name='T',
default_variable=[1, 2, 3, 4],
size=[2, 3, 4]
)
np.testing.assert_array_equal(T.defaults.variable, [[1, 2, 3, 4]])
val = T.execute([10.0, 10.0, 10.0, 10.0])
np.testing.assert_array_equal(val, [[10.0, 10.0, 10.0, 10.0]])
with pytest.raises(
ComponentError, match=r'size and default_variable arguments.*conflict.*'
):
TransferMechanism(
name='T',
default_variable=[1, 2, 3, 4],
size=[2, 3, 4]
)

# ------------------------------------------------------------------------------------------------
# TEST 16
Expand All @@ -1163,14 +1110,14 @@ def test_transfer_mech_var_1D_size_wrong_2(self):
@pytest.mark.mechanism
@pytest.mark.transfer_mechanism
def test_transfer_mech_size_var_incompatible1(self):
T = TransferMechanism(
name='T',
size=2,
default_variable=[[1, 2], [3, 4, 5]]
)
assert len(T.defaults.variable) == 2
np.testing.assert_array_equal(T.defaults.variable[0], [1, 2])
np.testing.assert_array_equal(T.defaults.variable[1], [3, 4, 5])
with pytest.raises(
ComponentError, match=r'size and default_variable arguments.*conflict.*'
):
TransferMechanism(
name='T',
size=2,
default_variable=[[1, 2], [3, 4, 5]]
)

# ------------------------------------------------------------------------------------------------
# TEST 17
Expand All @@ -1179,33 +1126,19 @@ def test_transfer_mech_size_var_incompatible1(self):
@pytest.mark.mechanism
@pytest.mark.transfer_mechanism
def test_transfer_mech_size_var_incompatible2(self):
T = TransferMechanism(
name='T',
size=[2, 2],
default_variable=[[1, 2], [3, 4, 5]]
)
assert len(T.defaults.variable) == 2
np.testing.assert_array_equal(T.defaults.variable[0], [1, 2])
np.testing.assert_array_equal(T.defaults.variable[1], [3, 4, 5])
with pytest.raises(
ComponentError, match=r'size and default_variable arguments.*conflict.*'
):
TransferMechanism(
name='T',
size=[2, 2],
default_variable=[[1, 2], [3, 4, 5]]
)

# ------------------------------------------------------------------------------------------------

# INVALID INPUTS

# ------------------------------------------------------------------------------------------------
# TEST 1
# size = 0, check less-than-one error

@pytest.mark.mechanism
@pytest.mark.transfer_mechanism
def test_transfer_mech_size_zero(self):
with pytest.raises(ComponentError) as error_text:
T = TransferMechanism(
name='T',
size=0,
)
assert "is not a positive number" in str(error_text.value)

# ------------------------------------------------------------------------------------------------
# TEST 2
# size = -1.0, check less-than-one error
Expand All @@ -1216,9 +1149,9 @@ def test_transfer_mech_size_negative_one(self):
with pytest.raises(ComponentError) as error_text:
T = TransferMechanism(
name='T',
size=-1.0,
size=-1,
)
assert "is not a positive number" in str(error_text.value)
assert "negative dimensions" in str(error_text.value)

# this test below and the (currently commented) test immediately after it _may_ be deprecated if we ever fix
# warnings to be no longer fatal. At the time of writing (6/30/17, CW), warnings are always fatal.
Expand Down

0 comments on commit 17e7917

Please sign in to comment.