Raw File
testctrnn.py
#!/usr/bin/env python2.4
# *-* encoding: utf8
# 
# Copyright (c) 2006 Stian Soiland
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Author: Stian Soiland <stian@soiland.no>
# URL: http://soiland.no/i/src/
# License: MIT
#
"""Tests for ctrnn.py
"""

import unittest
import random

import ctrnn
import numpy

class TestTransfers(unittest.TestCase):
    def testIdentity(self):
        self.assertEqual(ctrnn.identity(1.5), 1.5)
        self.assertEqual(ctrnn.identity(-0.9), -0.9)
        self.assertEqual(ctrnn.identity(0.0), 0.0)

    def testStep(self):
        self.assertEqual(ctrnn.step(-0.3), 0.0)
        self.assertEqual(ctrnn.step(0.2), 0.0)
        self.assertEqual(ctrnn.step(0.4), 1.0)
        self.assertEqual(ctrnn.step(0.8), 1.0)
        self.assertEqual(ctrnn.step(1.8), 1.0)

    def testSigmoid(self):
        self.assertEqual(ctrnn.sigmoid(1000), 1.0)
        self.assertEqual(ctrnn.sigmoid(-1000), 0.0) # Not OverflowError
        self.assertEqual(ctrnn.sigmoid(0), 0.5)
        self.assertEqual(ctrnn.sigmoid(0, gain=4), 0.5)
        # 0.401312339887548
        assert 0.401 < ctrnn.sigmoid(0, gain=4, bias=0.1) < 0.402 
        # 2.6503965530043108e-261
        assert 0.0 < ctrnn.sigmoid(-600) < 2.66e-261 

    def testSignum(self):
        self.assertEqual(ctrnn.signum(-0.3), -1.0)       
        self.assertEqual(ctrnn.signum(-4.3), -1.0)       
        self.assertEqual(ctrnn.signum(0.3), 1.0)       
        self.assertEqual(ctrnn.signum(2.3), 1.0)       
        self.assertEqual(ctrnn.signum(0.0), 1.0)       

    def testTanhPos(self):
        assert 0.995 < ctrnn.tanh_pos(3) < 0.996
        assert 0.761 < ctrnn.tanh_pos(1) < 0.762
        assert 0.0099 < ctrnn.tanh_pos(0.01) < 0.01
        self.assertEqual(ctrnn.tanh_pos(0), 0)
        self.assertEqual(ctrnn.tanh_pos(-0.5), 0)
    

class TestCTRNN(unittest.TestCase):
    def testConstruction(self):
        neurons = 5
        net = ctrnn.CTRNN(neurons)
        self.assertEqual(net.num_neurons, neurons)
        self.assertEqual(len(net.potential), neurons)
        self.assertEqual(len(net.bias), neurons)
        self.assertEqual(len(net.timeconst), neurons)
        self.assertEqual(len(net.transfer), neurons)
        self.assertEqual(len(net.output), neurons)
        self.assertEqual(net.weight.shape, (neurons, neurons))

        self.assertEqual(list(net.potential), [0.0]*neurons)
        self.assertEqual(list(net.output), [0.0]*neurons)
        self.assertEqual(list(net.bias), [0.0]*neurons)
        for row in net.weight:
            self.assertEqual(list(row), [0.0]*neurons)

    def testConstructionDefault(self):
        neurons = 5
        net = ctrnn.CTRNN(neurons)
        # Default timeconst should be 1.0
        self.assertEqual(list(net.timeconst), [1.0]*neurons)
        # Default transfer should be sigmoid()
        self.assertEqual(list(net.transfer), [ctrnn.sigmoid]*neurons)

    def testConstructionParameters(self):
        neurons = 5
        timeconst = 1.5
        transfer = ctrnn.step
        net = ctrnn.CTRNN(neurons, timeconst, transfer=transfer)
        self.assertEqual(list(net.timeconst), [timeconst]*neurons)
        self.assertEqual(list(net.transfer), [transfer]*neurons)
    
    def testConstructionTooLowTimeconst(self):
        neurons = 5
        invalid_timeconst = 0.499
        valid_timeconst = 0.501
        self.assertRaises(AssertionError, ctrnn.CTRNN, neurons, 
                          invalid_timeconst)
        ctrnn.CTRNN(neurons, valid_timeconst)
    
    def testConnectAll(self):
        neurons = 3
        net = ctrnn.CTRNN(neurons)    
        # Should not be allowed to call with too few or too many
        # parameters
        self.assertRaises(AssertionError, net.connect_all)
        self.assertRaises(AssertionError, net.connect_all, 1.5, random.random)

        weight = 0.5
        # All rows should be equal to weight if ref_self
        net.connect_all(weight, ref_self=True)               
        for row in net.weight:
            self.assertEqual(list(row), [weight]*neurons)

        # All except the x==y cells should now be equal
        net.connect_all(weight)
        for x,row in enumerate(net.weight):
            for y,w in enumerate(row):
                if x == y:
                    self.assertEqual(w, 0.0)
                else:
                    self.assertEqual(w, weight)    
        
        # And test by random()
        net.connect_all(func=random.random, ref_self=True)
        unique = set()
        for row in net.weight:
            for w in row:
                # All should be unique
                assert w not in unique
                unique.add(w)

    def testSetTransfer(self):    
        neurons = 5
        net = ctrnn.CTRNN(neurons)
        # Default is sigmoid
        matches = net.transfer == [ctrnn.sigmoid]*neurons
        self.assert_(matches.all())

        net.set_transfer(ctrnn.identity)
        matches = net.transfer == [ctrnn.identity]*neurons
        self.assert_(matches.all())

class TestCalcTimestep(unittest.TestCase):
    def setUp(self):
        self.neurons = 3
        self.net = ctrnn.CTRNN(self.neurons)

    def testNeutral(self):
        self.assertEqual(list(self.net.output), [0.0]*self.neurons)
        self.net.calc_timestep() 
        self.assertEqual(list(self.net.output), [0.5]*self.neurons)
        self.assertEqual(list(self.net.potential), [0.0]*self.neurons)
    
    def testBias(self):    
        self.net.bias[0] = 1.0
        self.net.calc_timestep() 
        self.assertEqual(list(self.net.potential), [1.0, 0.0, 0.0])
        #0.7310585786300049 
        output0 = self.net.output[0]
        assert 0.730 < output0 < 0.732
        self.assertEqual(self.net.output[1], 0.5)
        self.assertEqual(self.net.output[2], 0.5)
        self.net.calc_timestep() 
        # Should not change (timeconst=1)
        self.assertEqual(output0, self.net.output[0]) 
    
    def testTimeconst(self):     
        self.net.bias[0] = 1.0
        self.net.calc_timestep() 
        output0 = self.net.output[0]
        self.net.timeconst[0] = 1.5 
        self.net.calc_timestep() 
        # Should not change as we have reached the bias
        self.assertEqual(output0, self.net.output[0]) 
        self.net.bias[0] = 0.0
        # should now drop gradually towards 0.5
        self.net.calc_timestep() 
        # 0.58257020646231472
        assert 0.582 < self.net.output[0] < 0.583
        self.net.calc_timestep() 
        # 0.527749235055
        assert 0.527 < self.net.output[0] < 0.528
    
    def testWeights(self):
        self.net.bias[0] = 1.0
        # from 0 to 1    
        self.net.weight[0,1] = 1.0
        self.net.calc_timestep() 
        # Does not reach it on this timestep
        self.assertEqual(self.net.potential[1], 0.0)
        self.assertEqual(self.net.output[1], 0.5)
        self.net.calc_timestep() 
        # Should be the only input, and therefore set the potential
        self.assertEqual(self.net.potential[1], self.net.output[0])
        # 0.67503752737682365 
        assert 0.675 < self.net.output[1] < 0.676
    
    def testSlice(self):
        self.net.bias[0] = 1.0 
        self.net.bias[2] = 1.0         
        self.net.calc_timestep(slice(0,1)) # ie. only 0
        # Updated
        self.assertEqual(self.net.potential[0], 1.0)
        # 0.7310585786300049 
        assert 0.730 < self.net.output[0] < 0.732
        # Unchanged
        self.assertEqual(self.net.potential[1], 0.0)
        self.assertEqual(self.net.potential[2], 0.0)
        self.assertEqual(self.net.output[1], 0.0)
        self.assertEqual(self.net.output[2], 0.0)

        # Update it all
        self.net.calc_timestep()
        self.assertEqual(self.net.output[1], 0.5)
        assert 0.730 < self.net.output[0] < 0.732
        self.net.weight[1,0] = 1.0
        self.net.calc_timestep(slice(0,1)) # ie. only 0
        # Now, even though we only updated neuron 0, he should
        # include the input from the previous calculation of 1.
        #0.817574476194
        assert 0.817 < self.net.output[0] < 0.818
    
class TestStabilize(unittest.TestCase):
    def setUp(self):
        self.neurons = 3
        self.net = ctrnn.CTRNN(self.neurons)    

    def testWeights(self):
        # Should be stable in 1 step with no connections
        self.assertEqual(self.net.stabilize(), 1)    
        self.net.weight[0,1] = 1.0
        self.net.weight[1,2] = 1.0
        # Should be stable in 2 steps to propagate
        # 0->1 and 1->2
        self.assertEqual(self.net.stabilize(), 2)
        # Let's introduce some fun
        self.net.weight[2,0] = 1.0
        steps = self.net.stabilize()
        # Should be about 24
        assert 10 < steps < 100
        # And now, let's also check that it IS stable
        output = list(self.net.output)
        self.net.calc_timestep()
        output_2 = list(self.net.output)
        self.assertEqual(output, output_2)
        # And actually, all values should be the same since our weights
        # are the same
        self.assertEqual(output[0], output[1])
        self.assertEqual(output[1], output[2])
  
    def testTimeConstant(self):
        self.net.bias[0] = 1.0
        self.net.timeconst[0] = 1.5 
        steps = self.net.stabilize()
        # Should be about 33
        assert 10 < steps < 100
        # Almost almost almost 1.0 
        assert 0.99999 < self.net.potential[0] < 1.00001
    
    def testMaxSteps(self):
        self.net.bias[0] = 1.0
        # NOTE: Illegal timeconstant < 0.5 -> unstable network    
        self.net.timeconst[0] = 0.4
        steps = self.net.stabilize()
        self.assertEqual(steps, None)

        self.net.timeconst[0] = 0.95 # stable again, in about 40 steps
        steps = self.net.stabilize(max_steps=5) # but 5 is not enough
        self.assertEqual(steps, None)
        steps = self.net.stabilize()
        # about 35
        assert 2 < steps < 100
   
    def testPrecision(self):
        precision = 0.01
        self.net.bias[0] = 1.0
        self.net.timeconst[0] = 0.6
        #steps = self.net.stabilize(precision=precision)
        steps = self.net.stabilize(precision=precision)
        # Would take about 88 steps with precision=None
        assert 7 < steps < 11

        # And check that we actually are within precision
        prev = self.net.output
        self.net.calc_timestep()
        now = self.net.output
        diff = now - prev
        assert numpy.absolute(diff).max() < precision



class TestLayers(unittest.TestCase):
    def testAdd(self):
        layers = ctrnn.Layers()
        hid_neurons = 5
        out_neurons = 2
        layers.add_layer("hidden", hid_neurons)
        layers.add_layer("output", out_neurons)
        self.assertEqual(layers.neurons, hid_neurons+out_neurons)
    
    def testAddInput(self):
        layers = ctrnn.Layers()
        in_neurons = 4
        layers.add_input_layer("input", in_neurons)
        self.assertEqual(layers.neurons, in_neurons)
        assert "input" in layers.input_layers
    
    def testBuild(self):
        hid_neurons = 5
        out_neurons = 2
        in_neurons = 4
        neurons = hid_neurons+out_neurons+in_neurons
        layers = ctrnn.Layers()
        layers.add_input_layer("input", in_neurons)
        layers.add_layer("hidden", hid_neurons)
        layers.add_layer("output", out_neurons)
        # Name already in use
        self.assertRaises(Exception, layers.add_layer, "hidden",
                          hid_neurons)
        self.assertEqual(layers.neurons, neurons)
        layers.build_net()
        self.assertEqual(layers.net.num_neurons, neurons)
        assert isinstance(layers.input, ctrnn._Layer)
        assert isinstance(layers.hidden, ctrnn._Layer)
        assert isinstance(layers.output, ctrnn._Layer)
        assert isinstance(layers.input, ctrnn._InputLayer)
        assert not isinstance(layers.hidden, ctrnn._InputLayer)
        assert not isinstance(layers.output, ctrnn._InputLayer)
   
        # Make sure they are separate 
        layers.input.bias[:] = [0,1,2,3]
        layers.hidden.bias[:] = [4,5,6,7,8]
        layers.output.bias[:] = [9,10]
        self.assertEqual(list(layers.input.bias), [0,1,2,3])
        self.assertEqual(list(layers.hidden.bias), [4,5,6,7,8])
        self.assertEqual(list(layers.output.bias), [9,10])
        # And that they are added in order, and that changes to layers
        # are reflected back in the actual net
        self.assertEqual(list(layers.net.bias), range(11)) 
        
class TestLayer(unittest.TestCase):
    def setUp(self):
        self.in_neurons = 4
        self.hid_neurons = 5
        self.out_neurons = 2
        self.layers = ctrnn.Layers()
        self.layers.add_input_layer("input", self.in_neurons)
        self.layers.add_layer("hidden", self.hid_neurons)
        self.layers.add_layer("output", self.out_neurons)
        self.layers.build_net()
    
    def testLength(self):
        hidden = self.layers.hidden
        self.assertEqual(len(hidden), self.hid_neurons)
    
    def testGetItem(self):
        hidden = self.layers.hidden
        self.assertEqual(hidden[0], self.in_neurons)
        self.assertEqual(hidden[-1], self.in_neurons+self.hid_neurons-1)
        # Check that boundaries are enforced (This would be valid
        # indexes because they would cross into "input" or "output", but
        # are not supposed to be returned from the "hidden" layer.
        self.assertRaises(IndexError, lambda: hidden[self.hid_neurons])
        self.assertRaises(IndexError, lambda: hidden[-self.hid_neurons-1])
        self.assertEqual(self.layers.input[-1]+1, hidden[0])
        self.assertEqual(hidden[-1], self.layers.output[0] - 1)
    
    def testProperties(self):
        # Set weird values before and after hidden
        self.layers.input.bias[:] = [4.0]*self.in_neurons
        self.layers.output.bias[:] = [7.0]*self.out_neurons
        hidden = self.layers.hidden
        
        self.assertEqual(list(hidden.bias), [0.0]*self.hid_neurons)
        self.assertEqual(list(hidden.potential), [0.0]*self.hid_neurons)
        self.assertEqual(list(hidden.output), [0.0]*self.hid_neurons)
        self.assertEqual(list(hidden.timeconst), [1.0]*self.hid_neurons)
        self.assertEqual(list(hidden.transfer), [ctrnn.sigmoid]*self.hid_neurons)
    def testSetTransfer(self):    
        hidden = self.layers.hidden
        neurons = len(hidden)
        # Default is sigmoid
        matches = hidden.transfer == [ctrnn.sigmoid]*neurons
        self.assert_(matches.all())

        hidden.set_transfer(ctrnn.signum)
        matches = hidden.transfer == [ctrnn.signum]*neurons
        self.assert_(matches.all())

        # And that the other are untouched
        # (Note how the input layer has identity by default)
        self.assertEqual(list(self.layers.input.transfer),
                         [ctrnn.identity]*self.in_neurons)
        self.assertEqual(list(self.layers.output.transfer),
                         [ctrnn.sigmoid]*self.out_neurons)

    def testTimestep(self):
        hidden = self.layers.hidden
        hidden.calc_timestep()
        self.assertEqual(list(hidden.output), [0.5]*self.hid_neurons)
        self.assertEqual(list(hidden.potential), [0.0]*self.hid_neurons)
        hidden.bias[:] = [1.0] * self.hid_neurons
        hidden.calc_timestep()
        self.assertEqual(list(hidden.potential), [1.0]*self.hid_neurons)

        # Make sure nothing else changed 
        self.assertEqual(list(self.layers.input.bias), [0.0]*self.in_neurons)
        self.assertEqual(list(self.layers.input.potential), [0.0]*self.in_neurons)
        self.assertEqual(list(self.layers.input.output), [0.0]*self.in_neurons)
        self.assertEqual(list(self.layers.output.bias), [0.0]*self.out_neurons)
        self.assertEqual(list(self.layers.output.potential), [0.0]*self.out_neurons)
        self.assertEqual(list(self.layers.output.output), [0.0]*self.out_neurons)
    
    def testInput(self):
        input = self.layers.input
        values = [-0.5, -0.1, 0.1, 0.5]
        input.set_inputs(values)
        self.assertEqual(list(input.bias), values)
        self.assertEqual(list(input.potential), values)
        self.assertEqual(list(input.output), values)
        self.assertEqual(list(input.transfer),
                         [ctrnn.identity]*self.in_neurons)
        

        

if __name__ == "__main__":
    unittest.main()

back to top