def coerce(self, conn, function): function = super().coerce(conn, function) if function is None: function_info = FunctionInfo(function=None, size=None) elif isinstance(function, FunctionInfo): function_info = function elif is_array_like(function): array = np.array(function, copy=False, dtype=np.float64) self.check_array(conn, array) function_info = FunctionInfo(function=array, size=array.shape[1]) elif callable(function): function_info = FunctionInfo( function=function, size=self.determine_size(conn, function)) # TODO: necessary? super().coerce(conn, function_info) else: raise ValidationError("Invalid connection function type %r " "(must be callable or array-like)" % type(function).__name__, attr=self.name, obj=conn) self.check_function_can_be_applied(conn, function_info) return function_info
def __init__(self, shape, init=1.0): super().__init__() self.shape = shape if is_array_like(init): init = np.asarray(init, dtype=rc.float_dtype) # check that the shape of init is compatible with the given shape # for this transform expected_shape = None if shape[0] != shape[1]: # init must be 2D if transform is not square expected_shape = shape elif init.ndim == 1: expected_shape = (shape[0], ) elif init.ndim >= 2: expected_shape = shape if expected_shape is not None and init.shape != expected_shape: raise ValidationError( "Shape of initial value %s does not match expected " "shape %s" % (init.shape, expected_shape), attr="init", ) self.init = init
def __init__(self, indices, data, shape): super().__init__() self.indices = indices self.shape = shape # if data is not a distribution if is_array_like(data): data = np.asarray(data) # convert scalars to vectors if data.size == 1: data = data.item() * np.ones(self.indices.shape[0], dtype=data.dtype) if data.ndim != 1 or data.shape[0] != self.indices.shape[0]: raise ValidationError( "Must be a vector of the same length as `indices`", attr="data", obj=self, ) self.data = data self._allocated = None self._dense = None
def coerce(self, conn, function): function = super().coerce(conn, function) if function is None: function_info = FunctionInfo(function=None, size=None) elif isinstance(function, FunctionInfo): function_info = function elif is_array_like(function): array = np.array(function, copy=False, dtype=np.float64) self.check_array(conn, array) function_info = FunctionInfo(function=array, size=array.shape[1]) elif callable(function): function_info = FunctionInfo(function=function, size=self.determine_size( conn, function)) # TODO: necessary? super().coerce(conn, function_info) else: raise ValidationError("Invalid connection function type %r " "(must be callable or array-like)" % type(function).__name__, attr=self.name, obj=conn) self.check_function_can_be_applied(conn, function_info) return function_info
def hashvalue(self, instance): d = self.__get__(instance, None) if d is None: return hash(d) return hash( tuple((k, array_hash(v) if is_array_like(v) else hash(v)) for k, v in d.items()))
def build_node(model, node): """Builds a `.Node` object into a model. The node build function is relatively simple. It involves creating input and output signals, and connecting them with an `.Operator` that depends on the type of ``node.output``. Parameters ---------- model : Model The model to build into. node : Node The node to build. Notes ----- Sets ``model.params[node]`` to ``None``. """ # input signal if not is_array_like(node.output) and node.size_in > 0: sig_in = Signal(shape=node.size_in, name="%s.in" % node) model.add_op(Reset(sig_in)) else: sig_in = None # Provide output if node.output is None: sig_out = sig_in elif isinstance(node.output, Process): sig_out = Signal(shape=node.size_out, name="%s.out" % node) model.build(node.output, sig_in, sig_out, mode="set") elif callable(node.output): sig_out = ( Signal(shape=node.size_out, name="%s.out" % node) if node.size_out > 0 else None ) model.add_op(SimPyFunc(output=sig_out, fn=node.output, t=model.time, x=sig_in)) elif is_array_like(node.output): sig_out = Signal(node.output.astype(rc.float_dtype), name="%s.out" % node) else: raise BuildError("Invalid node output type %r" % type(node.output).__name__) model.sig[node]["in"] = sig_in model.sig[node]["out"] = sig_out model.params[node] = None
def coerce(self, conn, transform): if transform is None: transform = NoTransform(conn.size_mid) elif is_array_like(transform) or isinstance(transform, Distribution): transform = Dense((conn.size_out, conn.size_mid), transform) if transform.size_in != conn.size_mid: if isinstance(transform, Dense) and ( transform.shape[0] == transform.shape[1] ): # we provide a different error message in this case; # the transform is not changing the dimensionality of the # signal, so the blame most likely lies with the function raise ValidationError( "Function output size is incorrect; should return a " "vector of size %d" % conn.size_mid, attr=self.name, obj=conn, ) else: raise ValidationError( "Transform input size (%d) not equal to %s output size " "(%d)" % (transform.size_in, type(conn.pre_obj).__name__, conn.size_mid), attr=self.name, obj=conn, ) if transform.size_out != conn.size_out: raise ValidationError( "Transform output size (%d) not equal to connection " "output size (%d)" % (transform.size_out, conn.size_out), attr=self.name, obj=conn, ) # we don't support repeated indices on 2D transforms because it makes # the matrix multiplication more complicated (we'd need to expand # the weight matrix for the duplicated rows/columns). it could be done # if there were a demand at some point. if isinstance(transform, Dense) and len(transform.init_shape) == 2: def repeated_inds(x): return not isinstance(x, slice) and np.unique(x).size != len(x) if repeated_inds(conn.pre_slice): raise ValidationError( "Input object selection has repeated indices", attr=self.name, obj=conn, ) if repeated_inds(conn.post_slice): raise ValidationError( "Output object selection has repeated indices", attr=self.name, obj=conn, ) return super().coerce(conn, transform)
def build_node(model, node): """Builds a `.Node` object into a model. The node build function is relatively simple. It involves creating input and output signals, and connecting them with an `.Operator` that depends on the type of ``node.output``. Parameters ---------- model : Model The model to build into. node : Node The node to build. Notes ----- Sets ``model.params[node]`` to ``None``. """ # input signal if not is_array_like(node.output) and node.size_in > 0: sig_in = Signal(np.zeros(node.size_in), name="%s.in" % node) model.add_op(Reset(sig_in)) else: sig_in = None # Provide output if node.output is None: sig_out = sig_in elif isinstance(node.output, Process): sig_out = Signal(np.zeros(node.size_out), name="%s.out" % node) model.build(node.output, sig_in, sig_out) elif callable(node.output): sig_out = (Signal(np.zeros(node.size_out), name="%s.out" % node) if node.size_out > 0 else None) model.add_op(SimPyFunc( output=sig_out, fn=node.output, t=model.time, x=sig_in)) elif is_array_like(node.output): sig_out = Signal(node.output, name="%s.out" % node) else: raise BuildError( "Invalid node output type %r" % type(node.output).__name__) model.sig[node]['in'] = sig_in model.sig[node]['out'] = sig_out model.params[node] = None
def __ufunc_power(base, exponent): """Operator that implements raising to some power with Gyrus operator(s). Currently only supports ``base ** exponent`` where ``exponent`` is not an Operator. That is, the Gyrus operator is the base. The same exponent is applied to each element of the base. """ if is_array_like(exponent): assert isinstance(base, Operator) return base.decode(lambda x: x**exponent) return NotImplemented
def assert_is_deepcopy(cp, original): assert cp is not original # ensures separate parameters for param in iter_params(cp): param_inst = getattr(cp, param) if isinstance(param_inst, (nengo.solvers.Solver, nengo.base.NengoObject)): assert_is_copy(param_inst, getattr(original, param)) elif is_array_like(param_inst): assert np.all(param_inst == getattr(original, param)) else: assert param_inst == getattr(original, param)
def coerce(self, instance, obj): # Do nothing if no object is given -- that's fine if obj is None: return None # Make sure obj is either callable or an array if (not callable(obj)) and (not is_array_like(obj)): raise nengo.exceptions.ValidationError( "connectivity must either be a Npost x Npre array or a callable", attr=self.name, obj=instance) # Convert the given array to the right format if is_array_like(obj): obj = np.array(obj, copy=False, dtype=np.float64) if obj.ndim != 2: raise nengo.exceptions.ValidationError( "connectivity must be a 2D array", attr=self.name, obj=instance) return obj
def build_neurons(model, neurontype, neurons, input_sig=None, output_sig=None): """Builds a `.NeuronType` object into a model. This function adds a `.SimNeurons` operator connecting the input current to the neural output signals, and handles any additional state variables defined within the neuron type. Parameters ---------- model : Model The model to build into. neurontype : NeuronType Neuron type to build. neuron : Neurons The neuron population object corresponding to the neuron type. Notes ----- Does not modify ``model.params[]`` and can therefore be called more than once with the same `.NeuronType` instance. """ input_sig = model.sig[neurons]["in"] if input_sig is None else input_sig output_sig = model.sig[neurons]["out"] if output_sig is None else output_sig dtype = input_sig.dtype n_neurons = neurons.size_in rng = np.random.RandomState(model.seeds[neurons.ensemble] + 1) state_init = neurontype.make_state(n_neurons, rng=rng, dtype=dtype) state = {} for key, init in state_init.items(): if key in model.sig[neurons]: raise BuildError( f"State name '{key}' overlaps with existing signal name") if is_array_like(init): model.sig[neurons][key] = Signal(initial_value=init, name=f"{neurons}.{key}") state[key] = model.sig[neurons][key] elif isinstance(init, np.random.RandomState): # Pass through RandomState instances state[key] = init else: raise BuildError( f"State '{key}' is of type '{type(init).__name__}'. Only array-likes " "and RandomStates are currently supported.") model.add_op( SimNeurons(neurons=neurontype, J=input_sig, output=output_sig, state=state))
def build_neurons(model, neurontype, neurons): """Builds a `.NeuronType` object into a model. This function adds a `.SimNeurons` operator connecting the input current to the neural output signals, and handles any additional state variables defined within the neuron type. Parameters ---------- model : Model The model to build into. neurontype : NeuronType Neuron type to build. neuron : Neurons The neuron population object corresponding to the neuron type. Notes ----- Does not modify ``model.params[]`` and can therefore be called more than once with the same `.NeuronType` instance. """ dtype = model.sig[neurons]["in"].dtype n_neurons = neurons.size_in rng = np.random.RandomState(model.seeds[neurons.ensemble] + 1) state_init = neurontype.make_state(n_neurons, rng=rng, dtype=dtype) state = {} for key, init in state_init.items(): if key in model.sig[neurons]: raise BuildError("State name %r overlaps with existing signal name" % key) if is_array_like(init): model.sig[neurons][key] = Signal( initial_value=init, name="%s.%s" % (neurons, key) ) state[key] = model.sig[neurons][key] elif isinstance(init, np.random.RandomState): # Pass through RandomState instances state[key] = init else: raise BuildError( "State %r is of type %r. Only array-likes and RandomStates are " "currently supported." % (key, type(init).__name__) ) model.sig[neurons]["out"] = ( state["spikes"] if neurontype.spiking else state["rates"] ) model.add_op( SimNeurons(neurons=neurontype, J=model.sig[neurons]["in"], state=state) )
def __ufunc_add(a, b): """Operator that implements element-wise addition with Gyrus operator(s). If either operand is a Gyrus fold, then it is replaced with its underlying array of Gyrus operators to follow NumPy's broadcasting rules for np.add. If both operands are Gyrus operators, then they are added together by Nengo transform(s) that add every dimension, element-wise. If one of the operands is like an array, then it is expected to be either 0D (scalar) or 1D (list). In the 0D case, the scalar is provided by a Nengo node, and broadcasted through Nengo transforms according to the shape of the other operand. In the 1D case, the list is expected to have the same number of elements as the size_out of each element in the other operand, such that the same vector can be added to every vector that is produced by the other operand. """ args = _broadcast_folds(a, b) if args is not None: return asoperator(np.add(*args)) elif isinstance(a, Operator) and isinstance(b, Operator): return reduce_transform([a, b], trs=[1, 1], axis=0) elif isinstance(b, Operator): a, b = b, a # At least one of the two operands has to be an Operator. assert isinstance(a, Operator) assert not isinstance(b, Operator) # due to first if statement if is_array_like(b): # This can be generalized to handle a wider variety of cases, # But, similar to __ufunc_multiply we are keeping the behaviour # as unambiguous as possible for now. b = np.asarray(b) if np.all(b == 0): return a elif b.ndim == 0: # Creates a single scalar Node and then transforms it # according to the size_out of each element in a. b = broadcast_scalar(b, size_out=a.size_out) elif b.ndim == 1: # Repeats the same Node for every element in a. if np.any(a.size_out != len(b)): raise TypeError( f"add size mismatch for a (operator) + b (array): " f"a.size_out ({a.size_out}) must match len(b) ({len(b)})") b = asoperator(np.broadcast_to(stimulus(b), shape=a.shape)) else: return NotImplemented assert isinstance(b, Operator) return np.add(a, b) return NotImplemented
def __ufunc_multiply(a, b): """Operator that implements element-wise multiplication with Gyrus operator(s). If either operand is a Gyrus fold, then it is replaced with its underlying array of Gyrus operators to follow NumPy's broadcasting rules for np.multiply. If both operands are Gyrus operators, then they are multiplied together by using ``gyrus.multiply``, which vectorizes an element-wise product network across both operands. If one of the operands is like an array, then it is expected to be either 0D (scalar) or 1D (list). In either case, the operand becomes a Nengo transform that is applied to each Gyrus operator to scale its outputs element-wise. """ # gyrus.multiply (used here and defined elsewhere) is a bit different from # the ufunc defined here, np.multiply. The former only supports multiplying two # operators. The latter delegates to the former (in the same way as __mul__), but # also delegates to transform to handle a wider variety of types. In particular, if # one of the two operands is not an operator, then it will be used as a transform on # the other operand. To make the semantics of this unambiguous, in a similar manner # to __ufunc_add, only 0D or 1D transforms are currently supported, such that the # transform is doing an element-wise multiplication on each element. args = _broadcast_folds(a, b) if args is not None: return asoperator(np.multiply(*args)) elif isinstance(a, Operator) and isinstance(b, Operator): return multiply(a, b) elif isinstance(b, Operator): a, b = b, a # At least one of the two operands has to be an Operator. assert isinstance(a, Operator) assert not isinstance(b, Operator) # due to first if statement if is_array_like(b): b = np.asarray(b) # Scalars (b.ndim == 0) are fine, as is, as they naturally work with # nengo.Connection. 1D arrays also work, as is, so long as their length is equal # to the number of input dimensions in Nengo, which will also be the number of # output dimensions. if np.all(b == 1): return a elif b.ndim == 1 and np.any(a.size_out != len(b)): raise TypeError( f"multiply size mismatch for a (operator) * b (array): " f"a.size_out ({a.size_out}) must match len(b) ({len(b)})") elif b.ndim >= 2: return NotImplemented return transform(a, tr=b)
def __init__(self, initial_state=None): super().__init__() self.initial_state = initial_state if self.initial_state is not None: for name, value in self.initial_state.items(): if name not in self.state: raise ValidationError( "State variable %r not recognized; should be one of %s" % (name, ", ".join(repr(k) for k in self.state)), attr="initial_state", obj=self, ) if not (isinstance(value, Distribution) or is_array_like(value)): raise ValidationError( "State variable %r must be a distribution or array-like" % (name,), attr="initial_state", obj=self, )
def coerce(self, node, output): output = super().coerce(node, output) size_in_set = node.size_in is not None node.size_in = node.size_in if size_in_set else 0 # --- Validate and set the new size_out if output is None: if node.size_out is not None: warnings.warn("'Node.size_out' is being overwritten with " "'Node.size_in' since 'Node.output=None'") node.size_out = node.size_in elif isinstance(output, Process): if not size_in_set: node.size_in = output.default_size_in if node.size_out is None: node.size_out = output.default_size_out elif callable(output): self.check_callable_args_list(node, output) # We trust user's size_out if set, because calling output # may have unintended consequences (e.g., network communication) if node.size_out is None: node.size_out = self.check_callable_output(node, output) elif is_array_like(output): # Make into correctly shaped numpy array before validation output = npext.array(output, min_dims=1, copy=False, dtype=rc.float_dtype) self.check_ndarray(node, output) if not np.all(np.isfinite(output)): raise ValidationError("Output value must be finite.", attr=self.name, obj=node) node.size_out = output.size else: raise ValidationError( "Invalid node output type %r" % type(output).__name__, attr=self.name, obj=node, ) return output
def coerce(self, node, output): output = super().coerce(node, output) size_in_set = node.size_in is not None node.size_in = node.size_in if size_in_set else 0 # --- Validate and set the new size_out if output is None: if node.size_out is not None: warnings.warn("'Node.size_out' is being overwritten with " "'Node.size_in' since 'Node.output=None'") node.size_out = node.size_in elif isinstance(output, Process): if not size_in_set: node.size_in = output.default_size_in if node.size_out is None: node.size_out = output.default_size_out elif callable(output): self.check_callable_args_list(node, output) # We trust user's size_out if set, because calling output # may have unintended consequences (e.g., network communication) if node.size_out is None: node.size_out = self.check_callable_output(node, output) elif is_array_like(output): # Make into correctly shaped numpy array before validation output = npext.array( output, min_dims=1, copy=False, dtype=np.float64) self.check_ndarray(node, output) if not np.all(np.isfinite(output)): raise ValidationError("Output value must be finite.", attr=self.name, obj=node) node.size_out = output.size else: raise ValidationError("Invalid node output type %r" % type(output).__name__, attr=self.name, obj=node) return output
def make_step(self, signals, dt, rng): src = signals[self.src] dst = signals[self.dst] src_slice = self.src_slice if self.src_slice is not None else Ellipsis dst_slice = self.dst_slice if self.dst_slice is not None else Ellipsis inc = self.inc # If there are repeated indices in dst_slice, special handling needed. repeats = False if npext.is_array_like(dst_slice): # copy because we might modify it dst_slice = np.array(dst_slice) if dst_slice.dtype.kind != "b": # get canonical, positive indices first dst_slice[dst_slice < 0] += len(dst) repeats = len(np.unique(dst_slice)) < len(dst_slice) if inc and repeats: def step_copy(): np.add.at(dst, dst_slice, src[src_slice]) elif inc: def step_copy(): dst[dst_slice] += src[src_slice] elif repeats: raise BuildError( f"{self}: Cannot have repeated indices in " "``dst_slice`` when copy is not an increment" ) else: def step_copy(): dst[dst_slice] = src[src_slice] return step_copy
def equal(a, b): if is_array_like(a) or is_array_like(b): return np.array_equal(a, b) else: return a == b
def equal(a, b): """Check if two (possibly array-like) objects are equal.""" if is_array_like(a) or is_array_like(b): return np.array_equal(a, b) else: return a == b