def setup_method(self):
     oscillator = models.Generic2dOscillator()
     white_matter = connectivity.Connectivity.from_file(
         'connectivity_%d.zip' % (self.n_regions, ))
     white_matter.speed = numpy.array([self.speed])
     white_matter_coupling = coupling.Difference(a=self.coupling_a)
     heunint = integrators.HeunStochastic(
         dt=2**-4, noise=noise.Additive(nsig=numpy.array([
             2**-10,
         ])))
     mons = (
         monitors.EEG.from_file(period=self.period),
         monitors.MEG.from_file(period=self.period),
         monitors.iEEG.from_file(period=self.period),
     )
     local_coupling_strength = numpy.array([2**-10])
     region_mapping = RegionMapping.from_file('regionMapping_16k_%d.txt' %
                                              (self.n_regions, ))
     default_cortex = Cortex(region_mapping_data=region_mapping,
                             load_default=True)
     default_cortex.coupling_strength = local_coupling_strength
     self.sim = simulator.Simulator(model=oscillator,
                                    connectivity=white_matter,
                                    coupling=white_matter_coupling,
                                    integrator=heunint,
                                    monitors=mons,
                                    surface=default_cortex)
     self.sim.configure()
 def test_g2d(self):
     """
     Default parameters:
         
               ---------------------------
              |  EXCITABLE CONFIGURATION  |
              ---------------------------
              |Parameter     |  Value     |
              -----------------------------
              | a            |     -2.0   |
              | b            |    -10.0   |
              | c            |      0.0   |
              | d            |      0.02  |
              | I            |      0.0   |
              -----------------------------
              |* limit cylce if a = 2.0   |
              -----------------------------
     
     
     """
     model = models.Generic2dOscillator()
     history_shape = (1, model._nvar, 1, model.number_of_modes)
     model_ic = model.initial(dt, history_shape)
     self.assertEqual(model._nvar, 2)
     self.assertTrue(ArrayAlmostEqual(model_ic, numpy.array([[[[ 1.70245989]], 
                                                    [[ 0.2765286 ]]]])) is None)
Exemple #3
0
 def test_g2d_voi(self):
     model = models.Generic2dOscillator(
         variables_of_interest = ['W', 'W - V']
     )
     (V, W), (voi_W, voi_WmV) = self._validate_initialization(model, 2)
     numpy.testing.assert_allclose(voi_W, W)
     numpy.testing.assert_allclose(voi_WmV, W - V)
    def __init__(self,
                 connectivity,
                 default_model=None,
                 default_integrator=None,
                 compute_phase_plane_params=True):
        self.logger = get_logger(self.__class__.__module__)
        self.default_model = default_model
        self.default_integrator = default_integrator
        self.connectivity = connectivity
        self.connectivity_models = dict()

        if self.default_model is None:
            self.default_model = models_module.Generic2dOscillator()
        if self.default_integrator is None:
            self.default_integrator = integrators_module.RungeKutta4thOrderDeterministic(
            )

        self.model_parameter_names = deepcopy(
            self.default_model.ui_configurable_parameters)
        if not len(self.model_parameter_names):
            self.logger.warning(
                "The 'ui_configurable_parameters' list of the current model is empty!"
            )
        self.prepared_model_parameter_names = self._prepare_parameter_names(
            self.model_parameter_names)

        model = self._get_model_for_region(0)
        if compute_phase_plane_params:
            self._phase_plane = PhasePlaneInteractive(
                deepcopy(model), deepcopy(self.default_integrator))
            self.phase_plane_params = self._phase_plane.draw_phase_plane()
Exemple #5
0
    def test_g2d(self):
        """
        Default parameters:

        +---------------------------+
        |  SanzLeonetAl  2013       |
        +--------------+------------+
        |Parameter     |  Value     |
        +==============+============+
        | a            |    - 0.5   |
        +--------------+------------+
        | b            |    -10.0   |
        +--------------+------------+
        | c            |      0.0   |
        +--------------+------------+
        | d            |      0.02  |
        +--------------+------------+
        | I            |      0.0   |
        +--------------+------------+
        |* excitable regime if      |
        |* intrinsic frequency is   |
        |  approx 10 Hz             |
        +---------------------------+


        """
        model = models.Generic2dOscillator()
        state, obser = self._validate_initialization(model, 2)
        numpy.testing.assert_allclose(obser[0], state[0])
 def test_expr_pre(self):
     sim, ys = self._run_sim(5, models.Generic2dOscillator(),
                             Raw(pre_expr='V;W;V**2;W-V', post_expr=''))
     self.assertTrue(hasattr(sim.monitors[0], '_transforms'))
     v, w, v2, wmv = ys.transpose((1, 0, 2, 3))
     self.assertTrue(numpy.allclose(v**2, v2))
     self.assertTrue(numpy.allclose(w - v, wmv))
Exemple #7
0
    def test_g2d(self):
        """
        Default parameters:

        +---------------------------+
        |  SanzLeonetAl  2013       |
        +--------------+------------+
        |Parameter     |  Value     |
        +==============+============+
        | a            |    - 0.5   |
        +--------------+------------+
        | b            |    -10.0   |
        +--------------+------------+
        | c            |      0.0   |
        +--------------+------------+
        | d            |      0.02  |
        +--------------+------------+
        | I            |      0.0   |
        +--------------+------------+
        |* excitable regime if      |
        |* intrinsic frequency is   |
        |  approx 10 Hz             |
        +---------------------------+


        """
        model = models.Generic2dOscillator()
        history_shape = (1, model._nvar, 1, model.number_of_modes)
        model_ic = model.initial(dt, history_shape)
        self.assertEqual(model._nvar, 2)
        assert_array_almost_equal(
            model_ic, numpy.array([[[[0.97607082]], [[-0.03384097]]]]))
Exemple #8
0
 def test_expr_post(self):
     sim, ys = self._run_sim(
         5, models.Generic2dOscillator(),
         Raw(pre_expr='V;W;V;W', post_expr=';;mon**2; exp(mon)'))
     self.assertTrue(hasattr(sim.monitors[0], '_transforms'))
     v, w, v2, ew = ys.transpose((1, 0, 2, 3))
     self.assertTrue(numpy.allclose(v**2, v2))
     self.assertTrue(numpy.allclose(numpy.exp(w), ew))
Exemple #9
0
    def test_surface_sim_with_projections(self):

        # Setup Simulator obj
        oscillator = models.Generic2dOscillator()
        white_matter = connectivity.Connectivity.from_file('connectivity_%d.zip' % (self.n_regions,))
        white_matter.speed = numpy.array([self.speed])
        white_matter_coupling = coupling.Difference(a=self.coupling_a)
        heunint = integrators.HeunStochastic(
            dt=2 ** -4,
            noise=noise.Additive(nsig=numpy.array([2 ** -10, ]))
        )
        mons = (
            monitors.EEG.from_file(period=self.period),
            monitors.MEG.from_file(period=self.period),
            # monitors.iEEG.from_file(period=self.period),
            # SEEG projection data is not part of tvb-data on Pypi, thus this can not work generic
        )
        local_coupling_strength = numpy.array([2 ** -10])
        region_mapping = RegionMapping.from_file('regionMapping_16k_%d.txt' % (self.n_regions,))
        region_mapping.surface = CorticalSurface.from_file()
        default_cortex = Cortex.from_file()
        default_cortex.region_mapping_data = region_mapping
        default_cortex.coupling_strength = local_coupling_strength

        sim = simulator.Simulator(model=oscillator, connectivity=white_matter, coupling=white_matter_coupling,
                                  integrator=heunint, monitors=mons, surface=default_cortex)
        sim.configure()

        # check configured simulation connectivity attribute
        conn = sim.connectivity
        assert conn.number_of_regions == self.n_regions
        assert conn.speed == self.speed

        # test monitor properties
        lc_n_node = sim.surface.local_connectivity.matrix.shape[0]
        for mon in sim.monitors:
            assert mon.period == self.period
            n_sens, g_n_node = mon.gain.shape
            assert g_n_node == sim.number_of_nodes
            assert n_sens == mon.sensors.number_of_sensors
            assert lc_n_node == g_n_node

        # check output shape
        ys = {}
        mons = 'eeg meg seeg'.split()
        for key in mons:
            ys[key] = []
        for data in sim(simulation_length=3.0):
            for key, dat in zip(mons, data):
                if dat:
                    _, y = dat
                    ys[key].append(y)
        for mon, key in zip(sim.monitors, mons):
            ys[key] = numpy.array(ys[key])
            assert ys[key].shape[2] == mon.gain.shape[0]
    def __init__(self, model=None, integrator=None):
        if model is None:
            model = models.Generic2dOscillator()
        if integrator is None:
            integrator = integrators.HeunDeterministic()

        model.configure()
        self.model = model
        self.integrator = integrator

        # Only one instance should exist for a browser page.
        # To achieve something close to that we store it here
        self.phase_plane = phase_space_d3(model, integrator)
    def __init__(self, connectivity, default_model = None, default_integrator = None):
        self.logger = get_logger(self.__class__.__module__)
        self.default_model = default_model
        self.default_integrator = default_integrator
        self.connectivity = connectivity
        self.connectivity_models = dict()

        if self.default_model is None:
            self.default_model = models_module.Generic2dOscillator()
        if self.default_integrator is None:
            self.default_integrator = integrators_module.RungeKutta4thOrderDeterministic()

        self.model_parameter_names = deepcopy(self.default_model.ui_configurable_parameters)
        if not len(self.model_parameter_names):
            self.logger.warning("The 'ui_configurable_parameters' list of the current model is empty!")
Exemple #12
0
    def setUp(self):
        """
        Reset the database before each test.
        """
        self.flow_service = FlowService()

        self.test_user = TestFactory.create_user()
        self.test_project = TestFactory.create_project(self.test_user)
        TestFactory.import_cff(test_user=self.test_user,
                               test_project=self.test_project)
        self.default_model = models_module.Generic2dOscillator()

        all_connectivities = self.flow_service.get_available_datatypes(
            self.test_project.id, Connectivity)
        self.connectivity = ABCAdapter.load_entity_by_gid(
            all_connectivities[0][2])
        self.connectivity.number_of_regions = 74
        self.context_model_param = ContextModelParameters(
            self.connectivity, self.default_model)
 def setup_method(self):
     oscillator = models.Generic2dOscillator()
     white_matter = connectivity.Connectivity(load_file='connectivity_' +
                                              str(self.n_regions) + '.zip')
     white_matter.speed = numpy.array([self.speed])
     white_matter_coupling = coupling.Difference(a=self.coupling_a)
     heunint = integrators.HeunStochastic(
         dt=2**-4, noise=noise.Additive(nsig=numpy.array([
             2**-10,
         ])))
     mons = (
         monitors.EEG(projection=ProjectionMatrix(
             load_file='projection_eeg_65_surface_16k.npy'),
                      sensors=SensorsEEG(load_file="eeg_brainstorm_65.txt"),
                      period=self.period),
         monitors.MEG(
             projection=ProjectionMatrix(
                 load_file='projection_meg_276_surface_16k.npy'),
             sensors=SensorsMEG(load_file='meg_brainstorm_276.txt'),
             period=self.period),
         monitors.iEEG(projection=ProjectionMatrix(
             load_file='projection_seeg_588_surface_16k.npy'),
                       sensors=SensorsInternal(load_file='seeg_588.txt'),
                       period=self.period),
     )
     local_coupling_strength = numpy.array([2**-10])
     region_mapping = RegionMapping(load_file='regionMapping_16k_' +
                                    str(self.n_regions) + '.txt')
     default_cortex = Cortex(
         region_mapping_data=region_mapping, load_file="cortex_16384.zip"
     )  #region_mapping_file="regionMapping_16k_192.txt")
     default_cortex.coupling_strength = local_coupling_strength
     self.sim = simulator.Simulator(model=oscillator,
                                    connectivity=white_matter,
                                    coupling=white_matter_coupling,
                                    integrator=heunint,
                                    monitors=mons,
                                    surface=default_cortex)
     self.sim.configure()
Exemple #14
0
 def test_period_handling(self):
     """Test that expression application working for monitors with a period."""
     sim, ys = self._run_sim(5, models.Generic2dOscillator(),
                             TemporalAverage(pre_expr='V+W'))
Exemple #15
0
class Simulator(HasTraits):
    """A Simulator assembles components required to perform simulations."""

    connectivity = Attr(
        field_type=connectivity.Connectivity,
        label="Long-range connectivity",
        default=None,
        required=True,
        doc="""A tvb.datatypes.Connectivity object which contains the
         structural long-range connectivity data (i.e., white-matter tracts). In
         combination with the ``Long-range coupling function`` it defines the inter-regional
         connections. These couplings undergo a time delay via signal propagation
         with a propagation speed of ``Conduction Speed``""")

    conduction_speed = Float(
        label="Conduction Speed",
        default=3.0,
        required=False,
        # range=basic.Range(lo=0.01, hi=100.0, step=1.0),
        doc="""Conduction speed for ``Long-range connectivity`` (mm/ms)""")

    coupling = Attr(
        field_type=coupling.Coupling,
        label="Long-range coupling function",
        default=coupling.Linear(),
        required=True,
        doc="""The coupling function is applied to the activity propagated
        between regions by the ``Long-range connectivity`` before it enters the local
        dynamic equations of the Model. Its primary purpose is to 'rescale' the
        incoming activity to a level appropriate to Model.""")

    surface = Attr(field_type=cortex.Cortex,
                   label="Cortical surface",
                   default=None,
                   required=False,
                   doc="""By default, a Cortex object which represents the
        cortical surface defined by points in the 3D physical space and their
        neighborhood relationship. In the current TVB version, when setting up a
        surface-based simulation, the option to configure the spatial spread of
        the ``Local Connectivity`` is available.""")

    stimulus = Attr(
        field_type=patterns.SpatioTemporalPattern,
        label="Spatiotemporal stimulus",
        default=None,
        required=False,
        doc=
        """A ``Spatiotemporal stimulus`` can be defined at the region or surface level.
        It's composed of spatial and temporal components. For region defined stimuli
        the spatial component is just the strength with which the temporal
        component is applied to each region. For surface defined stimuli,  a
        (spatial) function, with finite-support, is used to define the strength
        of the stimuli on the surface centred around one or more focal points.
        In the current version of TVB, stimuli are applied to the first state
        variable of the ``Local dynamic model``.""")

    model = Attr(
        field_type=models.Model,
        label="Local dynamic model",
        default=models.Generic2dOscillator(),
        required=True,
        doc="""A tvb.simulator.Model object which describe the local dynamic
        equations, their parameters, and, to some extent, where connectivity
        (local and long-range) enters and which state-variables the Monitors
        monitor. By default the 'Generic2dOscillator' model is used. Read the
        Scientific documentation to learn more about this model.""")

    integrator = Attr(field_type=integrators.Integrator,
                      label="Integration scheme",
                      default=integrators.HeunDeterministic(),
                      required=True,
                      doc="""A tvb.simulator.Integrator object which is
            an integration scheme with supporting attributes such as
            integration step size and noise specification for stochastic
            methods. It is used to compute the time courses of the model state
            variables.""")

    initial_conditions = NArray(
        label="Initial Conditions",
        required=False,
        doc="""Initial conditions from which the simulation will begin. By
        default, random initial conditions are provided. Needs to be the same shape
        as simulator 'history', ie, initial history function which defines the 
        minimal initial state of the network with time delays before time t=0. 
        If the number of time points in the provided array is insufficient the 
        array will be padded with random values based on the 'state_variables_range'
        attribute.""")

    monitors = List(
        of=monitors.Monitor,
        label="Monitor(s)",
        default=(monitors.TemporalAverage(), ),
        doc="""A tvb.simulator.Monitor or a list of tvb.simulator.Monitor
        objects that 'know' how to record relevant data from the simulation. Two
        main types exist: 1) simple, spatial and temporal, reductions (subsets
        or averages); 2) physiological measurements, such as EEG, MEG and fMRI.
        By default the Model's specified variables_of_interest are returned,
        temporally downsampled from the raw integration rate to a sample rate of
        1024Hz.""")

    simulation_length = Float(
        label="Simulation Length (ms, s, m, h)",
        default=1000.0,  # ie 1 second
        required=True,
        doc="""The length of a simulation (default in milliseconds).""")

    history = None  # type: SparseHistory

    @property
    def good_history_shape(self):
        """Returns expected history shape."""
        n_reg = self.connectivity.number_of_regions
        shape = self.horizon, len(
            self.model.state_variables), n_reg, self.model.number_of_modes
        return shape

    calls = 0
    current_step = 0
    number_of_nodes = None
    _memory_requirement_guess = None
    _memory_requirement_census = None
    _storage_requirement = None
    _runtime = None

    # methods consist of
    # 1) generic configure
    # 2) component specific configure
    # 3) loop preparation
    # 4) loop step
    # 5) estimations

    @property
    def is_surface_simulation(self):
        if self.surface:
            return True
        return False

    def _configure_integrator_boundaries(self):
        if self.model.state_variable_boundaries is not None:
            indices = []
            boundaries = []
            for sv, sv_bounds in self.model.state_variable_boundaries.items():
                indices.append(self.model.state_variables.index(sv))
                boundaries.append(sv_bounds)
            sort_inds = numpy.argsort(indices)
            self.integrator.bounded_state_variable_indices = numpy.array(
                indices)[sort_inds]
            self.integrator.state_variable_boundaries = numpy.array(
                boundaries).astype("float64")[sort_inds]
        else:
            self.integrator.bounded_state_variable_indices = None
            self.integrator.state_variable_boundaries = None

    def preconfigure(self):
        """Configure just the basic fields, so that memory can be estimated."""
        self.connectivity.configure()
        if self.surface:
            self.surface.configure()
        if self.stimulus:
            self.stimulus.configure()
        self.coupling.configure()
        self.model.configure()
        self.integrator.configure()
        self._configure_integrator_boundaries()
        # monitors needs to be a list or tuple, even if there is only one...
        if not isinstance(self.monitors, (list, tuple)):
            self.monitors = [self.monitors]
        # Configure monitors
        for monitor in self.monitors:
            monitor.configure()
        # "Nodes" refers to either regions or vertices + non-cortical regions.
        if self.surface is None:
            self.number_of_nodes = self.connectivity.number_of_regions
            self.log.info('Region simulation with %d ROI nodes',
                          self.number_of_nodes)
        else:
            rm = self.surface.region_mapping
            unmapped = self.connectivity.unmapped_indices(rm)
            self._regmap = numpy.r_[rm, unmapped]
            self.number_of_nodes = self._regmap.shape[0]
            self.log.info(
                'Surface simulation with %d vertices + %d non-cortical, %d total nodes',
                rm.size, unmapped.size, self.number_of_nodes)
        self._guesstimate_memory_requirement()

    def configure(self, full_configure=True):
        """Configure simulator and its components.

        The first step of configuration is to run the configure methods of all
        the Simulator's components, ie its traited attributes.

        Configuration of a Simulator primarily consists of calculating the
        attributes, etc, which depend on the combinations of the Simulator's
        traited attributes (keyword args).

        Converts delays from physical time units into integration steps
        and updates attributes that depend on combinations of the 6 inputs.

        Returns
        -------
        sim: Simulator
            The configured Simulator instance.

        """
        if full_configure:
            # When run from GUI, preconfigure is run separately, and we want to avoid running that part twice
            self.preconfigure()
        # Make sure spatialised model parameters have the right shape (number_of_nodes, 1)
        # todo: this exclusion list is fragile, consider excluding declarative attrs that are not arrays
        excluded_params = ("state_variable_range", "state_variable_boundaries",
                           "variables_of_interest", "noise", "psi_table",
                           "nerf_table", "gid")
        spatial_reshape = self.model.spatial_param_reshape
        for param in type(self.model).declarative_attrs:
            if param in excluded_params:
                continue
            # If it's a surface sim and model parameters were provided at the region level
            region_parameters = getattr(self.model, param)
            if self.surface is not None:
                if region_parameters.size == self.connectivity.number_of_regions:
                    new_parameters = region_parameters[
                        self.surface.region_mapping].reshape(spatial_reshape)
                    setattr(self.model, param, new_parameters)
            region_parameters = getattr(self.model, param)
            if region_parameters.size == self.number_of_nodes:
                new_parameters = region_parameters.reshape(spatial_reshape)
                setattr(self.model, param, new_parameters)
        # Configure spatial component of any stimuli
        self._configure_stimuli()
        # Set delays, provided in physical units, in integration steps.
        self.connectivity.set_idelays(self.integrator.dt)
        self.horizon = self.connectivity.idelays.max() + 1
        # Reshape integrator.noise.nsig, if necessary.
        if isinstance(self.integrator, integrators.IntegratorStochastic):
            self._configure_integrator_noise()
        # Setup history
        self._configure_history(self.initial_conditions)
        # Configure Monitors to work with selected Model, etc...
        self._configure_monitors()
        # Estimate of memory usage.
        self._census_memory_requirement()
        # Allow user to chain configure to another call or assignment.
        return self

    def _handle_random_state(self, random_state):
        if random_state is not None:
            if isinstance(self.integrator, integrators.IntegratorStochastic):
                self.integrator.noise.random_stream.set_state(random_state)
                msg = "random_state supplied with seed %s"
                self.log.info(
                    msg,
                    self.integrator.noise.random_stream.get_state()[1][0])
            else:
                self.log.warn(
                    "random_state supplied for non-stochastic integration")

    def _prepare_local_coupling(self):
        if self.surface is None:
            local_coupling = 0.0
        else:
            if self.surface.coupling_strength.size == 1:
                local_coupling = (self.surface.coupling_strength[0] *
                                  self.surface.local_connectivity.matrix)
            elif self.surface.coupling_strength.size == self.surface.number_of_vertices:
                ind = numpy.arange(self.number_of_nodes, dtype=numpy.intc)
                vec_cs = numpy.zeros((self.number_of_nodes, ))
                vec_cs[:self.surface.
                       number_of_vertices] = self.surface.coupling_strength
                sp_cs = scipy.sparse.csc_matrix(
                    (vec_cs, (ind, ind)),
                    shape=(self.number_of_nodes, self.number_of_nodes))
                local_coupling = sp_cs * self.surface.local_connectivity.matrix
            if local_coupling.shape[1] < self.number_of_nodes:
                # must match unmapped indices handling in preconfigure
                from scipy.sparse import csr_matrix, vstack, hstack
                nn = self.number_of_nodes
                npad = nn - local_coupling.shape[0]
                rpad = csr_matrix((local_coupling.shape[0], npad))
                bpad = csr_matrix((npad, nn))
                local_coupling = vstack([hstack([local_coupling, rpad]), bpad])
        return local_coupling

    def _prepare_stimulus(self):
        if self.stimulus is None:
            stimulus = 0.0
        else:
            time = numpy.r_[0.0:self.simulation_length:self.integrator.dt]
            self.stimulus.configure_time(time.reshape((1, -1)))
            stimulus = numpy.zeros((self.model.nvar, self.number_of_nodes, 1))
            self.log.debug("stimulus shape is: %s", stimulus.shape)
        return stimulus

    def _loop_compute_node_coupling(self, step):
        """Compute delayed node coupling values."""
        coupling = self.coupling(step, self.history)
        if self.surface is not None:
            coupling = coupling[:, self._regmap]
        return coupling

    def _loop_update_stimulus(self, step, stimulus):
        """Update stimulus values for current time step."""
        if self.stimulus is not None:
            # TODO stim_step != current step
            stim_step = step - (self.current_step + 1)
            stimulus[self.model.stvar, :, :] = self.stimulus(
                stim_step).reshape((1, -1, 1))

    def _loop_update_history(self, step, n_reg, state):
        """Update history."""
        if self.surface is not None and state.shape[
                1] > self.connectivity.number_of_regions:
            region_state = numpy.zeros(
                (n_reg, state.shape[0],
                 state.shape[2]))  # temp (node, cvar, mode)
            numpy_add_at(region_state, self._regmap, state.transpose(
                (1, 0, 2)))  # sum within region
            region_state /= numpy.bincount(self._regmap).reshape(
                (-1, 1, 1))  # div by n node in region
            state = region_state.transpose((1, 0, 2))  # (cvar, node, mode)
        self.history.update(step, state)

    def _loop_monitor_output(self, step, state):
        observed = self.model.observe(state)
        output = [monitor.record(step, observed) for monitor in self.monitors]
        if any(outputi is not None for outputi in output):
            return output

    def __call__(self, simulation_length=None, random_state=None):
        """
        Return an iterator which steps through simulation time, generating monitor outputs.

        See the run method for a convenient way to collect all output in one call.

        :param simulation_length: Length of the simulation to perform in ms.
        :param random_state:  State of NumPy RNG to use for stochastic integration.
        :return: Iterator over monitor outputs.
        """

        self.calls += 1
        if simulation_length is not None:
            self.simulation_length = float(simulation_length)

        # intialization
        self._guesstimate_runtime()
        self._calculate_storage_requirement()
        self._handle_random_state(random_state)
        n_reg = self.connectivity.number_of_regions
        local_coupling = self._prepare_local_coupling()
        stimulus = self._prepare_stimulus()
        state = self.current_state

        # integration loop
        n_steps = int(math.ceil(self.simulation_length / self.integrator.dt))
        for step in range(self.current_step + 1,
                          self.current_step + n_steps + 1):
            # needs implementing by hsitory + coupling?
            node_coupling = self._loop_compute_node_coupling(step)
            self._loop_update_stimulus(step, stimulus)
            state = self.integrator.scheme(state, self.model.dfun,
                                           node_coupling, local_coupling,
                                           stimulus)
            self._loop_update_history(step, n_reg, state)
            output = self._loop_monitor_output(step, state)
            if output is not None:
                yield output

        self.current_state = state
        self.current_step = self.current_step + n_steps

    def _configure_history(self, initial_conditions):
        """
        Set initial conditions for the simulation using either the provided
        initial_conditions or, if none are provided, the model's initial()
        method. This method is called durin the Simulator's __init__().

        Any initial_conditions that are provided as an argument are expected
        to have dimensions 1, 2, and 3 with shapse corresponding to the number
        of state_variables, nodes and modes, respectively. If the provided
        inital_conditions are shorter in time (dim=0) than the required history
        the model's initial() method is called to make up the difference.

        """
        rng = numpy.random
        if hasattr(self.integrator, 'noise'):
            rng = self.integrator.noise.random_stream
        # Default initial conditions
        if initial_conditions is None:
            n_time, n_svar, n_node, n_mode = self.good_history_shape
            self.log.info(
                'Preparing initial history of shape %r using model.initial()',
                self.good_history_shape)
            if self.surface is not None:
                n_node = self.number_of_nodes
            history = self.model.initial(self.integrator.dt,
                                         (n_time, n_svar, n_node, n_mode), rng)
        # ICs provided
        else:
            # history should be [timepoints, state_variables, nodes, modes]
            self.log.info('Using provided initial history of shape %r',
                          initial_conditions.shape)
            n_time, n_svar, n_node, n_mode = ic_shape = initial_conditions.shape
            nr = self.connectivity.number_of_regions
            if self.surface is not None and n_node == nr:
                initial_conditions = initial_conditions[:, :, self._regmap]
                return self._configure_history(initial_conditions)
            elif ic_shape[1:] != self.good_history_shape[1:]:
                raise ValueError(
                    "Incorrect history sample shape %s, expected %s" %
                    (ic_shape[1:], self.good_history_shape[1:]))
            else:
                if ic_shape[0] >= self.horizon:
                    self.log.debug("Using last %d time-steps for history.",
                                   self.horizon)
                    history = initial_conditions[
                        -self.horizon:, :, :, :].copy()
                else:
                    self.log.debug(
                        'Padding initial conditions with model.initial')
                    history = self.model.initial(self.integrator.dt,
                                                 self.good_history_shape, rng)
                    shift = self.current_step % self.horizon
                    history = numpy.roll(history, -shift, axis=0)
                    history[:ic_shape[0], :, :, :] = initial_conditions
                    history = numpy.roll(history, shift, axis=0)
                self.current_step += ic_shape[0] - 1

        if self.integrator.state_variable_boundaries is not None:
            self.integrator.bound_state(numpy.swapaxes(history, 0, 1))
        self.log.info('Final initial history shape is %r', history.shape)

        # create initial state from history
        self.current_state = history[self.current_step % self.horizon].copy()
        self.log.debug('initial state has shape %r' %
                       (self.current_state.shape, ))
        if self.surface is not None and history.shape[
                2] > self.connectivity.number_of_regions:
            n_reg = self.connectivity.number_of_regions
            (nt, ns, _, nm), ax = history.shape, (2, 0, 1, 3)
            region_history = numpy.zeros((nt, ns, n_reg, nm))
            numpy_add_at(region_history.transpose(ax), self._regmap,
                         history.transpose(ax))
            region_history /= numpy.bincount(self._regmap).reshape((-1, 1))
            history = region_history
        # create history query implementation
        self.history = SparseHistory(self.connectivity.weights,
                                     self.connectivity.idelays,
                                     self.model.cvar,
                                     self.model.number_of_modes)
        # initialize its buffer
        self.history.initialize(history)

    def _configure_integrator_noise(self):
        """
        This enables having noise to be state variable specific and/or to enter 
        only via specific brain structures, for example it we only want to 
        consider noise as an external input entering the brain via appropriate
        thalamic nuclei.

        Support 3 possible shapes:
            1) number_of_nodes;

            2) number_of_state_variables; and 

            3) (number_of_state_variables, number_of_nodes).

        """

        noise = self.integrator.noise

        if self.integrator.noise.ntau > 0.0:
            self.integrator.noise.configure_coloured(
                self.integrator.dt, self.good_history_shape[1:])
        else:
            self.integrator.noise.configure_white(self.integrator.dt,
                                                  self.good_history_shape[1:])

        if self.surface is not None:
            if self.integrator.noise.nsig.size == self.connectivity.number_of_regions:
                self.integrator.noise.nsig = self.integrator.noise.nsig[
                    self.surface.region_mapping]
            elif self.integrator.noise.nsig.size == self.model.nvar * self.connectivity.number_of_regions:
                self.integrator.noise.nsig = self.integrator.noise.nsig[:,
                                                                        self.
                                                                        surface
                                                                        .
                                                                        region_mapping]

        good_nsig_shape = (self.model.nvar, self.number_of_nodes,
                           self.model.number_of_modes)
        nsig = self.integrator.noise.nsig
        self.log.debug("Given noise shape is %s", nsig.shape)
        if nsig.shape in (good_nsig_shape, (1, )):
            return
        elif nsig.shape == (self.model.nvar, ):
            nsig = nsig.reshape((self.model.nvar, 1, 1))
        elif nsig.shape == (self.number_of_nodes, ):
            nsig = nsig.reshape((1, self.number_of_nodes, 1))
        elif nsig.shape == (self.model.nvar, self.number_of_nodes):
            nsig = nsig.reshape((self.model.nvar, self.number_of_nodes, 1))
        else:
            msg = "Bad Simulator.integrator.noise.nsig shape: %s"
            self.log.error(msg % str(nsig.shape))

        self.log.debug("Corrected noise shape is %s", nsig.shape)
        self.integrator.noise.nsig = nsig

    def _configure_monitors(self):
        """ Configure the requested Monitors for this Simulator """
        # Coerce to list if required
        if not isinstance(self.monitors, (list, tuple)):
            self.monitors = [self.monitors]
        # Configure monitors
        for monitor in self.monitors:
            monitor.config_for_sim(self)

    def _configure_stimuli(self):
        """ Configure the defined Stimuli for this Simulator """
        if self.stimulus is not None:
            if self.surface:
                self.stimulus.configure_space(self.surface.region_mapping)
            else:
                self.stimulus.configure_space()

    # used by simulator adaptor
    def memory_requirement(self):
        """
        Return an estimated of the memory requirements (Bytes) for this
        simulator's current configuration.
        """
        self._guesstimate_memory_requirement()
        return self._memory_requirement_guess

    # appears to be unused
    def runtime(self, simulation_length):
        """
        Return an estimated run time (seconds) for the simulator's current 
        configuration and a specified simulation length.

        """
        self.simulation_length = simulation_length
        self._guesstimate_runtime()
        return self._runtime

    # used by simulator adaptor
    def storage_requirement(self):
        """
        Return an estimated storage requirement (Bytes) for the simulator's
        current configuration and a specified simulation length.

        """
        self._calculate_storage_requirement()
        return self._storage_requirement

    def _guesstimate_memory_requirement(self):
        """
        guesstimate the memory required for this simulator.

        Guesstimate is based on the shape of the dominant arrays, and as such 
        can operate before configuration.

        NOTE: Assumes returned/yeilded data is in some sense "taken care of" in
            the world outside the simulator, and so doesn't consider it, making
            the simulator's history, and surface if present, the dominant 
            memory pigs...

        """
        if self.surface:
            number_of_nodes = self.surface.number_of_vertices
        else:
            number_of_nodes = self.connectivity.number_of_regions

        number_of_regions = self.connectivity.number_of_regions

        magic_number = 2.42  # Current guesstimate is low by about a factor of 2, seems safer to over estimate...
        bits_64 = 8.0  # Bytes
        bits_32 = 4.0  # Bytes
        # NOTE: The speed hack for getting the first element of hist shape should
        #      partially resolves calling of this method with a non-configured
        #     connectivity, there remains the less common issue if no tract_lengths...
        hist_shape = (
            self.connectivity.tract_lengths.max() /
            (self.conduction_speed or self.connectivity.speed or 3.0) /
            self.integrator.dt, self.model.nvar, number_of_nodes,
            self.model.number_of_modes)
        self.log.debug("Estimated history shape is %r", hist_shape)

        memreq = numpy.prod(hist_shape) * bits_64
        if self.surface:
            memreq += self.surface.number_of_triangles * 3 * bits_32 * 2  # normals
            memreq += self.surface.number_of_vertices * 3 * bits_64 * 2  # normals
            memreq += number_of_nodes * number_of_regions * bits_64 * 4  # region_mapping, region_average, region_sum
            # ???memreq += self.surface.local_connectivity.matrix.nnz * 8

        if not hasattr(self.monitors, '__len__'):
            self.monitors = [self.monitors]

        for monitor in self.monitors:
            if not isinstance(monitor, monitors.Bold):
                stock_shape = (monitor.period / self.integrator.dt,
                               len(self.model.variables_of_interest),
                               number_of_nodes, self.model.number_of_modes)
                memreq += numpy.prod(stock_shape) * bits_64
                if hasattr(monitor, "sensors"):
                    try:
                        memreq += number_of_nodes * monitor.sensors.number_of_sensors * bits_64  # projection_matrix
                    except AttributeError:
                        self.log.debug(
                            "No sensors specified, guessing memory based on default EEG."
                        )
                        memreq += number_of_nodes * 62.0 * bits_64

            else:
                stock_shape = (monitor.hrf_length * monitor._stock_sample_rate,
                               len(self.model.variables_of_interest),
                               number_of_nodes, self.model.number_of_modes)
                interim_stock_shape = (1.0 / (2.0**-2 * self.integrator.dt),
                                       len(self.model.variables_of_interest),
                                       number_of_nodes,
                                       self.model.number_of_modes)
                memreq += numpy.prod(stock_shape) * bits_64
                memreq += numpy.prod(interim_stock_shape) * bits_64

        if psutil and memreq > psutil.virtual_memory().total:
            self.log.warning(
                "There may be insufficient memory for this simulation.")

        self._memory_requirement_guess = magic_number * memreq
        msg = "Memory requirement estimate: simulation will need about %.1f MB"
        self.log.info(msg, self._memory_requirement_guess / 2**20)

    def _census_memory_requirement(self):
        """
        Guesstimate the memory required for this simulator. 

        Guesstimate is based on a census of the dominant arrays after the
        simulator has been configured.

        NOTE: Assumes returned/yeilded data is in some sense "taken care of" in
            the world outside the simulator, and so doesn't consider it, making
            the simulator's history, and surface if present, the dominant 
            memory pigs...

        """
        magic_number = 2.42  # Current guesstimate is low by about a factor of 2, seems safer to over estimate...
        memreq = self.history.nbytes
        try:
            memreq += self.surface.triangles.nbytes * 2
            memreq += self.surface.vertices.nbytes * 2
            memreq += self.surface.region_mapping.nbytes * self.number_of_nodes * 8. * 4  # region_average, region_sum
            memreq += self.surface.local_connectivity.matrix.nnz * 8
        except AttributeError:
            pass

        for monitor in self.monitors:
            memreq += monitor._stock.nbytes
            if isinstance(monitor, monitors.Bold):
                memreq += monitor._interim_stock.nbytes

        if psutil and memreq > psutil.virtual_memory().total:
            self.log.warning("Memory estimate exceeds total available RAM.")

        self._memory_requirement_census = magic_number * memreq
        # import pdb; pdb.set_trace()
        msg = "Memory requirement census: simulation will need about %.1f MB"
        self.log.info(msg % (self._memory_requirement_census / 1048576.0))

    def _guesstimate_runtime(self):
        """
        Estimate the runtime for this simulator.

        Spread in parallel executions of larger arrays means this will be an over-estimation,
        or rather a single threaded estimation...
        Different choice of integrators and monitors has an additional effect,
        on the magic number though relatively minor

        """
        magic_number = 6.57e-06  # seconds
        self._runtime = (magic_number * self.number_of_nodes *
                         self.model.nvar * self.model.number_of_modes *
                         self.simulation_length / self.integrator.dt)
        msg = "Simulation runtime should be about %0.3f seconds"
        self.log.info(msg, self._runtime)

    def _calculate_storage_requirement(self):
        """
        Calculate the storage requirement for the simulator, configured with
        models, monitors, etc being run for a particular simulation length. 
        While this is only approximate, it is far more reliable/accurate than
        the memory and runtime guesstimates.
        """
        self.log.info("Calculating storage requirement for ...")
        strgreq = 0
        for monitor in self.monitors:
            # Avoid division by zero for monitor not yet configured
            # (in framework this is executed, when only preconfigure has been called):
            current_period = monitor.period or self.integrator.dt
            strgreq += (TvbProfile.current.MAGIC_NUMBER *
                        self.simulation_length * self.number_of_nodes *
                        self.model.nvar * self.model.number_of_modes /
                        current_period)
        self.log.info("Calculated storage requirement for simulation: %d " %
                      int(strgreq))
        self._storage_requirement = int(strgreq)

    def run(self, **kwds):
        """Convenience method to call the simulator with **kwds and collect output data."""
        ts, xs = [], []
        for _ in self.monitors:
            ts.append([])
            xs.append([])
        wall_time_start = time.time()
        for data in self(**kwds):
            for tl, xl, t_x in zip(ts, xs, data):
                if t_x is not None:
                    t, x = t_x
                    tl.append(t)
                    xl.append(x)
        elapsed_wall_time = time.time() - wall_time_start
        self.log.info("%.3f s elapsed, %.3fx real time", elapsed_wall_time,
                      elapsed_wall_time * 1e3 / self.simulation_length)
        for i in range(len(ts)):
            ts[i] = numpy.array(ts[i])
            xs[i] = numpy.array(xs[i])
        return list(zip(ts, xs))
Exemple #16
0
import tvb.simulator.coupling as coupling
import tvb.simulator.integrators as integrators
import tvb.simulator.monitors as monitors

import tvb.datatypes.connectivity as connectivity

import tvb.datatypes.equations as equations
import tvb.datatypes.patterns as patterns

##----------------------------------------------------------------------------##
##-                      Perform the simulation                              -##
##----------------------------------------------------------------------------##

LOG.info("Configuring...")
#Initialise a Model, Coupling, and Connectivity.
oscilator = models.Generic2dOscillator()  #ReducedSetHindmarshRose() #
white_matter = connectivity.Connectivity()
white_matter.speed = numpy.array([4.0])

white_matter_coupling = coupling.Linear(a=0.0126)

#Initialise an Integrator
heunint = integrators.HeunDeterministic(dt=2**-4)

#Initialise some Monitors with period in physical time
momo = monitors.TemporalAverage(period=1.0)  #1000Hz
mama = monitors.Bold(period=500)  #defaults to one data point every 2s

#Bundle them
what_to_watch = (momo, mama)
Exemple #17
0
        self.pp_ax.scatter(x, y, s=42, c='g', marker='o', edgecolor=None)
        self.pp_ax.plot(traj[:, svx_ind, 0, self.mode],
                        traj[:, svy_ind, 0, self.mode])

        #Plot the selected state variable trajectories as a function of time
        self.pp_splt.plot(numpy.arange(TRAJ_STEPS+1) * self.integrator.dt,
                          traj[:, :, 0, self.mode])

        pylab.draw()


    def click_trajectory(self, event):
        """
        This method captures mouse clicks on the phase-plane and then uses the 
        plot_trajectory() method to generate a sample trajectory.
        """
        if event.inaxes is self.pp_ax:
            x, y = event.xdata, event.ydata
            LOG.info('trajectory starting at (%f, %f)', x, y)
            self.plot_trajectory(x, y)



if __name__ == "__main__":
    # Do some stuff that tests or makes use of this module...
    LOG.info("Testing %s module..." % __file__)
    MODEL = models_module.Generic2dOscillator()
    ppi_fig = PhasePlaneInteractive(model = MODEL)
    ppi_fig.show()

class PhasePlaneInteractive(HasTraits):
    """
    The GUI for the interactive phase-plane viewer provides sliders for setting:
        - The value of all parameters of the Model.
        - The extent of the axes.
        - A fixed value for the state-variables which aren't currently selected.
        - The noise strength, if a stocahstic integrator is specified.

    and radio buttons for selecting:
        - Which state-variables to show on each axis.
        - Which mode to show, if the Model has them.

    Clicking on the phase-plane will generate a sample trajectory, originating
    from where you clicked.

    """

    model = Attr(
        field_type=models_module.Model,
        label="Model",
        default=models_module.Generic2dOscillator(),
        doc="""An instance of the local dynamic model to be investigated with
        PhasePlaneInteractive.""")

    integrator = Attr(
        field_type=integrators_module.Integrator,
        label="Integrator",
        default=integrators_module.RungeKutta4thOrderDeterministic(),
        doc="""The integration scheme used to for generating sample
        trajectories on the phase-plane. NOTE: This is not used for generating
        the phase-plane itself, ie the vector field and nulclines.""")

    exclude_sliders = List(of=str)

    def __init__(self, **kwargs):
        """
        Initialise based on provided keywords or their traited defaults. Also,
        initialise the place-holder attributes that aren't filled until the
        show() method is called.
        """
        super(PhasePlaneInteractive, self).__init__(**kwargs)
        LOG.debug(str(kwargs))

        #figure
        self.ipp_fig = None

        #phase-plane
        self.pp_ax = None
        self.X = None
        self.Y = None
        self.U = None
        self.V = None
        self.UVmag = None
        self.nullcline_x = None
        self.nullcline_y = None
        self.pp_quivers = None

        #Current state
        self.svx = None
        self.svy = None
        self.default_sv = None
        self.no_coupling = None
        self.mode = None

        #Selectors
        self.state_variable_x = None
        self.state_variable_y = None
        self.mode_selector = None

        #Sliders
        self.param_sliders = None
        self.axes_range_sliders = None
        self.sv_sliders = None
        self.noise_slider = None

        #Reset buttons
        self.reset_param_button = None
        self.reset_sv_button = None
        self.reset_axes_button = None
        self.reset_noise_button = None
        self.reset_seed_button = None

    def show(self):
        """ Generate the interactive phase-plane figure. """
        model_name = self.model.__class__.__name__
        msg = "Generating an interactive phase-plane plot for %s"
        LOG.info(msg % model_name)

        #Make sure the model is fully configured...
        self.model.configure()

        #Setup the inital(current) state
        try:
            self.svx = self.model.state_variables[
                0]  #x-axis: 1st state variable
            self.svy = self.model.state_variables[
                1]  #y-axis: 2nd state variable
        except:
            import pdb
            pdb.set_trace()
            self.svx = self.model.state_variables[
                'S1']  #x-axis: 1st state variable
            self.svy = self.model.state_variables[
                'S2']  #y-axis: 2nd state variable

        self.mode = 0
        self.set_state_vector()

        #Make the figure:
        self.create_figure()

        #Selectors
        self.add_state_variable_selector()
        self.add_mode_selector()

        #Sliders
        self.add_axes_range_sliders()
        self.add_state_variable_sliders()
        self.add_param_sliders()
        if isinstance(self.integrator,
                      integrators_module.IntegratorStochastic):
            if self.integrator.noise.ntau > 0.0:
                self.integrator.noise.configure_coloured(
                    self.integrator.dt,
                    (1, self.model.nvar, 1, self.model.number_of_modes))
            else:
                self.integrator.noise.configure_white(
                    self.integrator.dt,
                    (1, self.model.nvar, 1, self.model.number_of_modes))

            self.add_noise_slider()
            self.add_reset_noise_button()
            self.add_reset_seed_button()

        #Reset buttons
        self.add_reset_param_button()
        self.add_reset_sv_button()
        self.add_reset_axes_button()

        #Calculate the phase plane
        self.set_mesh_grid()
        self.calc_phase_plane()

        #Plot phase plane
        self.plot_phase_plane()

        # add mouse handler for trajectory clicking
        self.ipp_fig.canvas.mpl_connect('button_press_event',
                                        self.click_trajectory)
        #import pdb; pdb.set_trace()

        pylab.show()

    ##------------------------------------------------------------------------##
    ##----------------- Functions for building the figure --------------------##
    ##------------------------------------------------------------------------##
    def create_figure(self):
        """ Create the figure and phase-plane axes. """
        #Figure and main phase-plane axes
        model_name = self.model.__class__.__name__
        integrator_name = self.integrator.__class__.__name__
        figsize = 10, 5
        try:
            figure_window_title = "Interactive phase-plane: " + model_name
            figure_window_title += "   --   %s" % integrator_name
            self.ipp_fig = pylab.figure(num=figure_window_title,
                                        figsize=figsize,
                                        facecolor=BACKGROUNDCOLOUR,
                                        edgecolor=EDGECOLOUR)
        except ValueError:
            LOG.info("My life would be easier if you'd update your PyLab...")
            self.ipp_fig = pylab.figure(num=42,
                                        figsize=figsize,
                                        facecolor=BACKGROUNDCOLOUR,
                                        edgecolor=EDGECOLOUR)

        self.pp_ax = self.ipp_fig.add_axes([0.265, 0.2, 0.5, 0.75])

        self.pp_splt = self.ipp_fig.add_subplot(212)
        self.ipp_fig.subplots_adjust(left=0.265,
                                     bottom=0.02,
                                     right=0.765,
                                     top=0.3,
                                     wspace=0.1,
                                     hspace=None)
        self.pp_splt.set_prop_cycle(color=get_color(self.model.nvar))
        self.pp_splt.plot(
            numpy.arange(TRAJ_STEPS + 1) * self.integrator.dt,
            numpy.zeros((TRAJ_STEPS + 1, self.model.nvar)))
        if hasattr(self.pp_splt, 'autoscale'):
            self.pp_splt.autoscale(enable=True, axis='y', tight=True)
        self.pp_splt.legend(self.model.state_variables)

    def add_state_variable_selector(self):
        """
        Generate radio selector buttons to set which state variable is displayed
        on the x and y axis of the phase-plane plot.
        """
        svx_ind = self.model.state_variables.index(self.svx)
        svy_ind = self.model.state_variables.index(self.svy)

        #State variable for the x axis
        pos_shp = [0.07, 0.05, 0.065, 0.12 + 0.006 * self.model.nvar]
        rax = self.ipp_fig.add_axes(pos_shp,
                                    facecolor=AXCOLOUR,
                                    title="x-axis")
        self.state_variable_x = widgets.RadioButtons(
            rax, tuple(self.model.state_variables), active=svx_ind)
        self.state_variable_x.on_clicked(self.update_svx)

        #State variable for the y axis
        pos_shp = [0.14, 0.05, 0.065, 0.12 + 0.006 * self.model.nvar]
        rax = self.ipp_fig.add_axes(pos_shp,
                                    facecolor=AXCOLOUR,
                                    title="y-axis")
        self.state_variable_y = widgets.RadioButtons(
            rax, tuple(self.model.state_variables), active=svy_ind)
        self.state_variable_y.on_clicked(self.update_svy)

    def add_mode_selector(self):
        """
        Add a radio button to the figure for selecting which mode of the model
        should be displayed.
        """
        pos_shp = [0.02, 0.07, 0.04, 0.1 + 0.002 * self.model.number_of_modes]
        rax = self.ipp_fig.add_axes(pos_shp, facecolor=AXCOLOUR, title="Mode")
        mode_tuple = tuple(range(self.model.number_of_modes))
        self.mode_selector = widgets.RadioButtons(rax, mode_tuple, active=0)
        self.mode_selector.on_clicked(self.update_mode)

    def add_axes_range_sliders(self):
        """
        Add sliders to the figure to allow the phase-planes axes to be set.
        """
        self.axes_range_sliders = dict()

        default_range_x = (self.model.state_variable_range[self.svx][1] -
                           self.model.state_variable_range[self.svx][0])
        default_range_y = (self.model.state_variable_range[self.svy][1] -
                           self.model.state_variable_range[self.svy][0])
        min_val_x = self.model.state_variable_range[
            self.svx][0] - 4.0 * default_range_x
        max_val_x = self.model.state_variable_range[
            self.svx][1] + 4.0 * default_range_x
        min_val_y = self.model.state_variable_range[
            self.svy][0] - 4.0 * default_range_y
        max_val_y = self.model.state_variable_range[
            self.svy][1] + 4.0 * default_range_y

        sax = self.ipp_fig.add_axes([0.04, 0.835, 0.125, 0.025],
                                    facecolor=AXCOLOUR)
        sl_x_min = widgets.Slider(
            sax,
            "xlo",
            min_val_x,
            max_val_x,
            valinit=self.model.state_variable_range[self.svx][0])
        sl_x_min.on_changed(self.update_range)

        sax = self.ipp_fig.add_axes([0.04, 0.8, 0.125, 0.025],
                                    facecolor=AXCOLOUR)
        sl_x_max = widgets.Slider(
            sax,
            "xhi",
            min_val_x,
            max_val_x,
            valinit=self.model.state_variable_range[self.svx][1])
        sl_x_max.on_changed(self.update_range)

        sax = self.ipp_fig.add_axes([0.04, 0.765, 0.125, 0.025],
                                    facecolor=AXCOLOUR)
        sl_y_min = widgets.Slider(
            sax,
            "ylo",
            min_val_y,
            max_val_y,
            valinit=self.model.state_variable_range[self.svy][0])
        sl_y_min.on_changed(self.update_range)

        sax = self.ipp_fig.add_axes([0.04, 0.73, 0.125, 0.025],
                                    facecolor=AXCOLOUR)
        sl_y_max = widgets.Slider(
            sax,
            "yhi",
            min_val_y,
            max_val_y,
            valinit=self.model.state_variable_range[self.svy][1])
        sl_y_max.on_changed(self.update_range)

        self.axes_range_sliders["sl_x_min"] = sl_x_min
        self.axes_range_sliders["sl_x_max"] = sl_x_max
        self.axes_range_sliders["sl_y_min"] = sl_y_min
        self.axes_range_sliders["sl_y_max"] = sl_y_max

    def add_state_variable_sliders(self):
        """
        Add sliders to the figure to allow default values for the models state
        variable to be set.
        """
        msv_range = self.model.state_variable_range
        offset = 0.0
        self.sv_sliders = dict()
        for sv in range(self.model.nvar):
            offset += 0.035
            pos_shp = [0.04, 0.6 - offset, 0.125, 0.025]
            sax = self.ipp_fig.add_axes(pos_shp, facecolor=AXCOLOUR)
            sv_str = self.model.state_variables[sv]
            self.sv_sliders[sv_str] = widgets.Slider(
                sax,
                sv_str,
                msv_range[sv_str][0],
                msv_range[sv_str][1],
                valinit=self.default_sv[sv, 0, 0])
            self.sv_sliders[sv_str].on_changed(self.update_state_variables)

    # Traited paramaters as sliders
    def add_param_sliders(self):
        """
        Add sliders to the figure to allow the models parameters to be set.
        """
        offset = 0.0
        self.param_sliders = dict()
        # import pdb; pdb.set_trace()
        for param_name in type(self.model).declarative_attrs:
            if self.exclude_sliders is not None and param_name in self.exclude_sliders:
                continue
            param_def = getattr(type(self.model), param_name)
            if not isinstance(param_def,
                              NArray) or not param_def.dtype == numpy.float:
                continue
            param_range = param_def.domain
            if param_range is None:
                continue
            offset += 0.035
            sax = self.ipp_fig.add_axes([0.825, 0.865 - offset, 0.125, 0.025],
                                        facecolor=AXCOLOUR)
            param_value = getattr(self.model, param_name)[0]
            self.param_sliders[param_name] = widgets.Slider(
                sax,
                param_name,
                param_range.lo,
                param_range.hi,
                valinit=param_value)
            self.param_sliders[param_name].on_changed(self.update_parameters)

    def add_noise_slider(self):
        """
        Add a slider to the figure to allow the integrators noise strength to
        be set.
        """
        pos_shp = [0.825, 0.1, 0.125, 0.025]
        sax = self.ipp_fig.add_axes(pos_shp, facecolor=AXCOLOUR)

        self.noise_slider = widgets.Slider(sax,
                                           "Log Noise",
                                           -9.0,
                                           1.0,
                                           valinit=self.integrator.noise.nsig)
        self.noise_slider.on_changed(self.update_noise)

    def add_reset_param_button(self):
        """
        Add a button to the figure for reseting the model parameter values to
        their original values.
        """
        bax = self.ipp_fig.add_axes([0.825, 0.865, 0.125, 0.04])
        self.reset_param_button = widgets.Button(bax,
                                                 'Reset parameters',
                                                 color=BUTTONCOLOUR,
                                                 hovercolor=HOVERCOLOUR)

        def reset_parameters(event):
            for param_slider in self.param_sliders:
                self.param_sliders[param_slider].reset()

        self.reset_param_button.on_clicked(reset_parameters)

    def add_reset_sv_button(self):
        """
        Add a button to the figure for reseting the model state variables to
        their default values.
        """
        bax = self.ipp_fig.add_axes([0.04, 0.60, 0.125, 0.04])
        self.reset_sv_button = widgets.Button(bax,
                                              'Reset state-variables',
                                              color=BUTTONCOLOUR,
                                              hovercolor=HOVERCOLOUR)

        def reset_state_variables(event):
            for svsl in self.sv_sliders.values():
                svsl.reset()

        self.reset_sv_button.on_clicked(reset_state_variables)

    def add_reset_noise_button(self):
        """
        Add a button to the figure for reseting the noise to its default value.
        """
        bax = self.ipp_fig.add_axes([0.825, 0.135, 0.125, 0.04])
        self.reset_noise_button = widgets.Button(bax,
                                                 'Reset noise strength',
                                                 color=BUTTONCOLOUR,
                                                 hovercolor=HOVERCOLOUR)

        def reset_noise(event):
            self.noise_slider.reset()

        self.reset_noise_button.on_clicked(reset_noise)

    def add_reset_seed_button(self):
        """
        Add a button to the figure for reseting the random number generator to
        its intial state. For reproducible noise...
        """
        bax = self.ipp_fig.add_axes([0.825, 0.05, 0.125, 0.04])
        self.reset_seed_button = widgets.Button(bax,
                                                'Reset random stream',
                                                color=BUTTONCOLOUR,
                                                hovercolor=HOVERCOLOUR)

        def reset_seed(event):
            self.integrator.noise.trait["random_stream"].reset()

        self.reset_seed_button.on_clicked(reset_seed)

    def add_reset_axes_button(self):
        """
        Add a button to the figure for reseting the phase-plane axes to their
        default ranges.
        """
        bax = self.ipp_fig.add_axes([0.04, 0.87, 0.125, 0.04])
        self.reset_axes_button = widgets.Button(bax,
                                                'Reset axes',
                                                color=BUTTONCOLOUR,
                                                hovercolor=HOVERCOLOUR)

        def reset_ranges(event):
            self.axes_range_sliders["sl_x_min"].reset()
            self.axes_range_sliders["sl_x_max"].reset()
            self.axes_range_sliders["sl_y_min"].reset()
            self.axes_range_sliders["sl_y_max"].reset()

        self.reset_axes_button.on_clicked(reset_ranges)

    ##------------------------------------------------------------------------##
    ##------------------- Functions for updating the figure ------------------##
    ##------------------------------------------------------------------------##

    #NOTE: All the ax.set_xlim, poly.xy, etc, garbage below is fragile. It works
    #      at the moment, but there are currently bugs in Slider and the hackery
    #      below takes these into account... If the bugs are fixed/changed then
    #      this could break. As an example, the Slider doc says poly is a
    #      Rectangle, but it's actually a Polygon. The Slider set_val method
    #      assumes a Rectangle even though this is not the case, so the array
    #      Slider.poly.xy is corrupted by that method. The corruption isn't
    #      visible in the plot, which is probably why it hasn't been fixed...

    def update_xrange_sliders(self):
        """
        A hacky update of the x-axis range sliders that is called when the
        state-variable selected for the x-axis is changed.
        """
        default_range_x = (self.model.state_variable_range[self.svx][1] -
                           self.model.state_variable_range[self.svx][0])
        min_val_x = self.model.state_variable_range[
            self.svx][0] - 4.0 * default_range_x
        max_val_x = self.model.state_variable_range[
            self.svx][1] + 4.0 * default_range_x
        self.axes_range_sliders[
            "sl_x_min"].valinit = self.model.state_variable_range[self.svx][0]
        self.axes_range_sliders["sl_x_min"].valmin = min_val_x
        self.axes_range_sliders["sl_x_min"].valmax = max_val_x
        self.axes_range_sliders["sl_x_min"].ax.set_xlim(min_val_x, max_val_x)
        self.axes_range_sliders["sl_x_min"].poly.axes.set_xlim(
            min_val_x, max_val_x)
        self.axes_range_sliders["sl_x_min"].poly.xy[[0, 1], 0] = min_val_x
        self.axes_range_sliders["sl_x_min"].vline.set_data(([
            self.axes_range_sliders["sl_x_min"].valinit,
            self.axes_range_sliders["sl_x_min"].valinit
        ], [0, 1]))
        self.axes_range_sliders[
            "sl_x_max"].valinit = self.model.state_variable_range[self.svx][1]
        self.axes_range_sliders["sl_x_max"].valmin = min_val_x
        self.axes_range_sliders["sl_x_max"].valmax = max_val_x
        self.axes_range_sliders["sl_x_max"].ax.set_xlim(min_val_x, max_val_x)
        self.axes_range_sliders["sl_x_max"].poly.axes.set_xlim(
            min_val_x, max_val_x)
        self.axes_range_sliders["sl_x_max"].poly.xy[[0, 1], 0] = min_val_x
        self.axes_range_sliders["sl_x_max"].vline.set_data(([
            self.axes_range_sliders["sl_x_max"].valinit,
            self.axes_range_sliders["sl_x_max"].valinit
        ], [0, 1]))
        self.axes_range_sliders["sl_x_min"].reset()
        self.axes_range_sliders["sl_x_max"].reset()

    def update_yrange_sliders(self):
        """
        A hacky update of the y-axis range sliders that is called when the
        state-variable selected for the y-axis is changed.
        """
        #svy_ind = self.model.state_variables.index(self.svy)
        default_range_y = (self.model.state_variable_range[self.svy][1] -
                           self.model.state_variable_range[self.svy][0])
        min_val_y = self.model.state_variable_range[
            self.svy][0] - 4.0 * default_range_y
        max_val_y = self.model.state_variable_range[
            self.svy][1] + 4.0 * default_range_y
        self.axes_range_sliders[
            "sl_y_min"].valinit = self.model.state_variable_range[self.svy][0]
        self.axes_range_sliders["sl_y_min"].valmin = min_val_y
        self.axes_range_sliders["sl_y_min"].valmax = max_val_y
        self.axes_range_sliders["sl_y_min"].ax.set_xlim(min_val_y, max_val_y)
        self.axes_range_sliders["sl_y_min"].poly.axes.set_xlim(
            min_val_y, max_val_y)
        self.axes_range_sliders["sl_y_min"].poly.xy[[0, 1], 0] = min_val_y
        self.axes_range_sliders["sl_y_min"].vline.set_data(([
            self.axes_range_sliders["sl_y_min"].valinit,
            self.axes_range_sliders["sl_y_min"].valinit
        ], [0, 1]))
        self.axes_range_sliders[
            "sl_y_max"].valinit = self.model.state_variable_range[self.svy][1]
        self.axes_range_sliders["sl_y_max"].valmin = min_val_y
        self.axes_range_sliders["sl_y_max"].valmax = max_val_y
        self.axes_range_sliders["sl_y_max"].ax.set_xlim(min_val_y, max_val_y)
        self.axes_range_sliders["sl_y_max"].poly.axes.set_xlim(
            min_val_y, max_val_y)
        self.axes_range_sliders["sl_y_max"].poly.xy[[0, 1], 0] = min_val_y
        self.axes_range_sliders["sl_y_max"].vline.set_data(([
            self.axes_range_sliders["sl_y_max"].valinit,
            self.axes_range_sliders["sl_y_max"].valinit
        ], [0, 1]))
        self.axes_range_sliders["sl_y_min"].reset()
        self.axes_range_sliders["sl_y_max"].reset()

    def update_svx(self, label):
        """ 
        Update state variable used for x-axis based on radio buttton selection.
        """
        self.svx = label
        self.update_xrange_sliders()
        self.set_mesh_grid()
        self.calc_phase_plane()
        self.update_phase_plane()

    def update_svy(self, label):
        """ 
        Update state variable used for y-axis based on radio buttton selection.
        """
        self.svy = label
        self.update_yrange_sliders()
        self.set_mesh_grid()
        self.calc_phase_plane()
        self.update_phase_plane()

    def update_mode(self, label):
        """ Update the visualised mode based on radio button selection. """
        self.mode = label
        self.update_phase_plane()

    def update_parameters(self, val):
        """
        Update model parameters based on the current parameter slider values.

        NOTE: Haven't figured out how to update independantly, so just update
            everything.
        """
        #TODO: Grab caller and use val directly, ie independent parameter update.
        #import pdb; pdb.set_trace()
        for param in self.param_sliders:
            setattr(self.model, param,
                    numpy.array([self.param_sliders[param].val]))

        self.model.update_derived_parameters()
        self.calc_phase_plane()
        self.update_phase_plane()

    def update_noise(self, nsig):
        """ Update integrator noise based on the noise slider value. """
        self.integrator.noise.nsig = numpy.array([
            10**nsig,
        ])

    def update_range(self, val):
        """
        Update the axes ranges based on the current axes slider values.

        NOTE: Haven't figured out how to update independantly, so just update
            everything.

        """
        #TODO: Grab caller and use val directly, ie independent range update.
        self.axes_range_sliders["sl_x_min"].ax.set_facecolor(AXCOLOUR)
        self.axes_range_sliders["sl_x_max"].ax.set_facecolor(AXCOLOUR)
        self.axes_range_sliders["sl_y_min"].ax.set_facecolor(AXCOLOUR)
        self.axes_range_sliders["sl_y_max"].ax.set_facecolor(AXCOLOUR)

        if (self.axes_range_sliders["sl_x_min"].val >=
                self.axes_range_sliders["sl_x_max"].val):
            LOG.error("X-axis min must be less than max...")
            self.axes_range_sliders["sl_x_min"].ax.set_facecolor("Red")
            self.axes_range_sliders["sl_x_max"].ax.set_facecolor("Red")
            return
        if (self.axes_range_sliders["sl_y_min"].val >=
                self.axes_range_sliders["sl_y_max"].val):
            LOG.error("Y-axis min must be less than max...")
            self.axes_range_sliders["sl_y_min"].ax.set_facecolor("Red")
            self.axes_range_sliders["sl_y_max"].ax.set_facecolor("Red")
            return

        msv_range = self.model.state_variable_range
        msv_range[self.svx][0] = self.axes_range_sliders["sl_x_min"].val
        msv_range[self.svx][1] = self.axes_range_sliders["sl_x_max"].val
        msv_range[self.svy][0] = self.axes_range_sliders["sl_y_min"].val
        msv_range[self.svy][1] = self.axes_range_sliders["sl_y_max"].val
        self.set_mesh_grid()
        self.calc_phase_plane()
        self.update_phase_plane()

    def update_phase_plane(self):
        """ Clear the axes and redraw the phase-plane. """
        self.pp_ax.clear()
        self.pp_splt.clear()
        self.pp_splt.set_prop_cycle('color', get_color(self.model.nvar))
        self.pp_splt.plot(
            numpy.arange(TRAJ_STEPS + 1) * self.integrator.dt,
            numpy.zeros((TRAJ_STEPS + 1, self.model.nvar)))
        if hasattr(self.pp_splt, 'autoscale'):
            self.pp_splt.autoscale(enable=True, axis='y', tight=True)
        self.pp_splt.legend(self.model.state_variables)
        self.plot_phase_plane()

    def update_state_variables(self, val):
        """
        Update the default state-variable values, used for non-visualised state 
        variables, based of the current slider values.
        """
        for sv in self.sv_sliders:
            k = self.model.state_variables.index(sv)
            self.default_sv[k] = self.sv_sliders[sv].val

        self.calc_phase_plane()
        self.update_phase_plane()

    def set_mesh_grid(self):
        """
        Generate the phase-plane gridding based on currently selected 
        state-variables and their range values.
        """
        xlo = self.model.state_variable_range[self.svx][0]
        xhi = self.model.state_variable_range[self.svx][1]
        ylo = self.model.state_variable_range[self.svy][0]
        yhi = self.model.state_variable_range[self.svy][1]

        self.X = numpy.mgrid[xlo:xhi:(NUMBEROFGRIDPOINTS * 1j)]
        self.Y = numpy.mgrid[ylo:yhi:(NUMBEROFGRIDPOINTS * 1j)]

    def set_state_vector(self):
        """
        Set up a vector containing the default state-variable values and create
        a filler(all zeros) for the coupling arg of the Model's dfun method.
        This method is called once at initialisation (show()).
        """
        #import pdb; pdb.set_trace()
        sv_mean = numpy.array([
            self.model.state_variable_range[key].mean()
            for key in self.model.state_variables
        ])
        sv_mean = sv_mean.reshape((self.model.nvar, 1, 1))
        self.default_sv = sv_mean.repeat(self.model.number_of_modes, axis=2)
        self.no_coupling = numpy.zeros(
            (self.model.nvar, 1, self.model.number_of_modes))

    def calc_phase_plane(self):
        """ Calculate the vector field. """
        svx_ind = self.model.state_variables.index(self.svx)
        svy_ind = self.model.state_variables.index(self.svy)

        #Calculate the vector field discretely sampled at a grid of points
        grid_point = self.default_sv.copy()
        self.U = numpy.zeros((NUMBEROFGRIDPOINTS, NUMBEROFGRIDPOINTS,
                              self.model.number_of_modes))
        self.V = numpy.zeros((NUMBEROFGRIDPOINTS, NUMBEROFGRIDPOINTS,
                              self.model.number_of_modes))
        for ii in range(NUMBEROFGRIDPOINTS):
            grid_point[svy_ind] = self.Y[ii]
            for jj in range(NUMBEROFGRIDPOINTS):
                #import pdb; pdb.set_trace()
                grid_point[svx_ind] = self.X[jj]

                d = self.model.dfun(grid_point, self.no_coupling)

                for kk in range(self.model.number_of_modes):
                    self.U[ii, jj, kk] = d[svx_ind, 0, kk]
                    self.V[ii, jj, kk] = d[svy_ind, 0, kk]

        #Colours for the vector field quivers
        #self.UVmag = numpy.sqrt(self.U**2 + self.V**2)

        #import pdb; pdb.set_trace()
        if numpy.isnan(self.U).any() or numpy.isnan(self.V).any():
            LOG.error("NaN")

    def plot_phase_plane(self):
        """ Plot the vector field and its nullclines. """
        # Set title and axis labels
        model_name = self.model.__class__.__name__
        self.pp_ax.set(title=model_name + " mode " + str(self.mode))
        self.pp_ax.set(xlabel="State Variable " + self.svx)
        self.pp_ax.set(ylabel="State Variable " + self.svy)

        #import pdb; pdb.set_trace()
        #Plot a discrete representation of the vector field
        if numpy.all(self.U[:, :, self.mode] + self.V[:, :, self.mode] == 0):
            self.pp_ax.set(title=model_name + " mode " + str(self.mode) +
                           ": NO MOTION IN THIS PLANE")
            X, Y = numpy.meshgrid(self.X, self.Y)
            self.pp_quivers = self.pp_ax.scatter(X, Y, s=8, marker=".", c="k")
        else:
            self.pp_quivers = self.pp_ax.quiver(
                self.X,
                self.Y,
                self.U[:, :, self.mode],
                self.V[:, :, self.mode],
                #self.UVmag[:, :, self.mode],
                width=0.001,
                headwidth=8)

        #Plot the nullclines
        self.nullcline_x = self.pp_ax.contour(self.X,
                                              self.Y,
                                              self.U[:, :, self.mode], [0],
                                              colors="r")
        self.nullcline_y = self.pp_ax.contour(self.X,
                                              self.Y,
                                              self.V[:, :, self.mode], [0],
                                              colors="g")
        pylab.draw()

    def plot_trajectory(self, x, y):
        """
        Plot a sample trajectory, starting at the position x,y in the
        phase-plane. This method is called as a result of a mouse click on the 
        phase-plane.
        """
        svx_ind = self.model.state_variables.index(self.svx)
        svy_ind = self.model.state_variables.index(self.svy)
        print(svx_ind)
        #Calculate an example trajectory
        state = self.default_sv.copy()
        self.integrator.clamped_state_variable_indices = numpy.setdiff1d(
            numpy.r_[:len(self.model.state_variables)], numpy.r_[svx_ind,
                                                                 svy_ind])
        self.integrator.clamped_state_variable_values = self.default_sv[
            self.integrator.clamped_state_variable_indices]
        state[svx_ind] = x
        state[svy_ind] = y
        scheme = self.integrator.scheme
        traj = numpy.zeros(
            (TRAJ_STEPS + 1, self.model.nvar, 1, self.model.number_of_modes))
        traj[0, :] = state
        for step in range(TRAJ_STEPS):
            #import pdb; pdb.set_trace()
            state = scheme(state, self.model.dfun, self.no_coupling, 0.0, 0.0)
            traj[step + 1, :] = state

        self.pp_ax.scatter(x, y, s=42, c='g', marker='o', edgecolor=None)
        self.pp_ax.plot(traj[:, svx_ind, 0, self.mode], traj[:, svy_ind, 0,
                                                             self.mode])

        #Plot the selected state variable trajectories as a function of time
        self.pp_splt.plot(
            numpy.arange(TRAJ_STEPS + 1) * self.integrator.dt, traj[:, :, 0,
                                                                    self.mode])

        pylab.draw()

    def click_trajectory(self, event):
        """
        This method captures mouse clicks on the phase-plane and then uses the 
        plot_trajectory() method to generate a sample trajectory.
        """
        if event.inaxes is self.pp_ax:
            x, y = event.xdata, event.ydata
            LOG.info('trajectory starting at (%f, %f)', x, y)
            self.plot_trajectory(x, y)
Exemple #19
0
import tvb.simulator.coupling as coupling
import tvb.simulator.integrators as integrators
import tvb.simulator.monitors as monitors

import tvb.datatypes.connectivity as connectivity
import tvb.datatypes.surfaces as surfaces
import tvb.datatypes.equations as equations
import tvb.datatypes.patterns as patterns

##----------------------------------------------------------------------------##
##-                      Perform the simulation                              -##
##----------------------------------------------------------------------------##

LOG.info("Configuring...")
#Initialise a Model, Coupling, and Connectivity.
oscilator = models.Generic2dOscillator()
white_matter = connectivity.Connectivity()
white_matter.speed = 4.0

white_matter_coupling = coupling.Linear(a=-2**-9)

#Initialise an Integrator
heunint = integrators.HeunDeterministic(dt=2**-4)

#Initialise some Monitors with period in physical time
mon_tavg = monitors.TemporalAverage(period=2**-2)
mon_savg = monitors.SpatialAverage(period=2**-2)
mon_eeg = monitors.EEG(period=2**-2)

#Bundle them
what_to_watch = (mon_tavg, mon_savg, mon_eeg)
Exemple #20
0
class Simulator(HasTraits):
    """A Simulator assembles components required to perform simulations."""

    connectivity = Attr(
        field_type=connectivity.Connectivity,
        label="Long-range connectivity",
        default=None,
        required=True,
        doc="""A tvb.datatypes.Connectivity object which contains the
         structural long-range connectivity data (i.e., white-matter tracts). In
         combination with the ``Long-range coupling function`` it defines the inter-regional
         connections. These couplings undergo a time delay via signal propagation
         with a propagation speed of ``Conduction Speed``""")

    conduction_speed = Float(
        label="Conduction Speed",
        default=3.0,
        required=False,
        # range=basic.Range(lo=0.01, hi=100.0, step=1.0),
        doc="""Conduction speed for ``Long-range connectivity`` (mm/ms)""")

    coupling = Attr(
        field_type=coupling.Coupling,
        label="Long-range coupling function",
        default=coupling.Linear(),
        required=True,
        doc="""The coupling function is applied to the activity propagated
        between regions by the ``Long-range connectivity`` before it enters the local
        dynamic equations of the Model. Its primary purpose is to 'rescale' the
        incoming activity to a level appropriate to Model.""")

    surface: cortex.Cortex = Attr(
        field_type=cortex.Cortex,
        label="Cortical surface",
        default=None,
        required=False,
        doc="""By default, a Cortex object which represents the
        cortical surface defined by points in the 3D physical space and their
        neighborhood relationship. In the current TVB version, when setting up a
        surface-based simulation, the option to configure the spatial spread of
        the ``Local Connectivity`` is available.""")

    stimulus = Attr(
        field_type=patterns.SpatioTemporalPattern,
        label="Spatiotemporal stimulus",
        default=None,
        required=False,
        doc=
        """A ``Spatiotemporal stimulus`` can be defined at the region or surface level.
        It's composed of spatial and temporal components. For region defined stimuli
        the spatial component is just the strength with which the temporal
        component is applied to each region. For surface defined stimuli,  a
        (spatial) function, with finite-support, is used to define the strength
        of the stimuli on the surface centred around one or more focal points.
        In the current version of TVB, stimuli are applied to the first state
        variable of the ``Local dynamic model``.""")

    model: Model = Attr(
        field_type=models.Model,
        label="Local dynamic model",
        default=models.Generic2dOscillator(),
        required=True,
        doc="""A tvb.simulator.Model object which describe the local dynamic
        equations, their parameters, and, to some extent, where connectivity
        (local and long-range) enters and which state-variables the Monitors
        monitor. By default the 'Generic2dOscillator' model is used. Read the
        Scientific documentation to learn more about this model.""")

    integrator = Attr(field_type=integrators.Integrator,
                      label="Integration scheme",
                      default=integrators.HeunDeterministic(),
                      required=True,
                      doc="""A tvb.simulator.Integrator object which is
            an integration scheme with supporting attributes such as
            integration step size and noise specification for stochastic
            methods. It is used to compute the time courses of the model state
            variables.""")

    initial_conditions = NArray(
        label="Initial Conditions",
        required=False,
        doc="""Initial conditions from which the simulation will begin. By
        default, random initial conditions are provided. Needs to be the same shape
        as simulator 'history', ie, initial history function which defines the 
        minimal initial state of the network with time delays before time t=0. 
        If the number of time points in the provided array is insufficient the 
        array will be padded with random values based on the 'state_variables_range'
        attribute.""")

    monitors = List(
        of=monitors.Monitor,
        label="Monitor(s)",
        default=(monitors.TemporalAverage(), ),
        doc="""A tvb.simulator.Monitor or a list of tvb.simulator.Monitor
        objects that 'know' how to record relevant data from the simulation. Two
        main types exist: 1) simple, spatial and temporal, reductions (subsets
        or averages); 2) physiological measurements, such as EEG, MEG and fMRI.
        By default the Model's specified variables_of_interest are returned,
        temporally downsampled from the raw integration rate to a sample rate of
        1024Hz.""")

    simulation_length = Float(
        label="Simulation Length (ms, s, m, h)",
        default=1000.0,  # ie 1 second
        required=True,
        doc="""The length of a simulation (default in milliseconds).""")

    backend = ReferenceBackend()

    history = None  # type: SparseHistory

    @property
    def good_history_shape(self):
        """Returns expected history shape."""
        n_reg = self.connectivity.number_of_regions
        shape = self.connectivity.horizon, len(
            self.model.state_variables), n_reg, self.model.number_of_modes
        return shape

    calls = 0
    current_step = 0
    number_of_nodes = None
    _memory_requirement_guess = None
    _memory_requirement_census = None
    _storage_requirement = None
    _runtime = None

    integrate_next_step = None

    # methods consist of
    # 1) generic configure
    # 2) component specific configure
    # 3) loop preparation
    # 4) loop step
    # 5) estimations

    @property
    def is_surface_simulation(self):
        if self.surface:
            return True
        return False

    def configure_integration_for_model(self):
        self.integrator.configure_boundaries(self.model)
        if self.model.has_nonint_vars:
            self.integrate_next_step = self.integrator.integrate_with_update
            self.integrator. \
                reconfigure_boundaries_and_clamping_for_integration_state_variables(self.model)
        else:
            self.integrate_next_step = self.integrator.integrate

    def preconfigure(self):
        """Configure just the basic fields, so that memory can be estimated."""
        self.connectivity.configure()
        if self.surface:
            self.surface.configure()
        if self.stimulus:
            self.stimulus.configure()
        self.coupling.configure()
        # ------- Keep this order of configurations ----
        self.model.configure()  # 1
        self.integrator.configure()  # 2
        # Configure integrators' next step computation
        # and state variables' boundaries and clamping,
        # based on model attributes  # 3
        self.configure_integration_for_model()
        # ----------------------------------------------
        # monitors needs to be a list or tuple, even if there is only one...
        if not isinstance(self.monitors, (list, tuple)):
            self.monitors = [self.monitors]
        # Configure monitors
        for monitor in self.monitors:
            monitor.configure()
        self._set_number_of_nodes()
        self._guesstimate_memory_requirement()

    def _set_number_of_nodes(self):
        # "Nodes" refers to either regions or vertices + non-cortical regions.
        if self.surface is None:
            self.number_of_nodes = self.connectivity.number_of_regions
            self.log.info('Region simulation with %d ROI nodes',
                          self.number_of_nodes)
        else:
            self._regmap, nc, nsc = self.backend.full_region_map(
                self.surface, self.connectivity)
            self.number_of_nodes = nc + nsc
            self.log.info(
                'Surface simulation with %d vertices + %d non-cortical, %d total nodes',
                nc, nsc, self.number_of_nodes)

    def configure(self, full_configure=True):
        """Configure simulator and its components.

        The first step of configuration is to run the configure methods of all
        the Simulator's components, ie its traited attributes.

        Configuration of a Simulator primarily consists of calculating the
        attributes, etc, which depend on the combinations of the Simulator's
        traited attributes (keyword args).

        Converts delays from physical time units into integration steps
        and updates attributes that depend on combinations of the 6 inputs.

        Returns
        -------
        sim: Simulator
            The configured Simulator instance.

        """
        if full_configure:
            # When run from GUI, preconfigure is run separately, and we want to avoid running that part twice
            self.preconfigure()
        self.model._spatialize_model_parameters(sim=self)
        # Configure spatial component of any stimuli
        self._configure_stimuli()
        # Set delays, provided in physical units, in integration steps.
        self.connectivity.set_idelays(self.integrator.dt)
        # Reshape integrator.noise.nsig, if necessary.
        if isinstance(self.integrator, integrators.IntegratorStochastic):
            self._configure_integrator_noise()
        # create history
        # TODO refactor history impl to backend
        self._configure_history()
        # Configure Monitors to work with selected Model, etc...
        self._configure_monitors()
        # Estimate of memory usage.
        self._census_memory_requirement()
        # Allow user to chain configure to another call or assignment.
        return self

    def _prepare_local_coupling(self):
        if self.surface is None:
            return 0.0
        return self.surface.prepare_local_coupling(self.number_of_nodes)

    def _loop_compute_node_coupling(self, step):
        """Compute delayed node coupling values."""
        coupling = self.coupling(step, self.history)
        if self.surface is not None:
            coupling = coupling[:, self._regmap]
        return coupling

    def _prepare_stimulus(self):
        if self.stimulus is None:
            stimulus = 0.0
        else:
            # TODO time grid wrong for continuations
            time = numpy.r_[0.0:self.simulation_length:self.integrator.dt]
            self.stimulus.configure_time(time.reshape((1, -1)))
            stimulus = numpy.zeros((self.model.nvar, self.number_of_nodes, 1))
            self.log.debug("stimulus shape is: %s", stimulus.shape)
        return stimulus

    def _loop_update_stimulus(self, step, stimulus):
        """Update stimulus values for current time step."""
        if self.stimulus is not None:
            # TODO stim_step != current step
            stim_step = step - (self.current_step + 1)
            stimulus[self.model.stvar, :, :] = self.stimulus(
                stim_step).reshape((1, -1, 1))

    def _loop_update_history(self, step, state):
        """Update history."""
        if self.surface is not None and state.shape[
                1] > self.connectivity.number_of_regions:
            state = self.backend.surface_state_to_rois(
                self._regmap, self.connectivity.number_of_regions, state)
        self.history.update(step, state)

    def _loop_monitor_output(self, step, state, node_coupling):
        observed = self.model.observe(state)
        output = [
            monitor.record(
                step, node_coupling if isinstance(
                    monitor, monitors.AfferentCoupling) else observed)
            for monitor in self.monitors
        ]
        if any(outputi is not None for outputi in output):
            return output

    def __call__(self,
                 simulation_length=None,
                 random_state=None,
                 n_steps=None):
        """
        Return an iterator which steps through simulation time, generating monitor outputs.

        See the run method for a convenient way to collect all output in one call.

        :param simulation_length: Length of the simulation to perform in ms.
        :param random_state:  State of NumPy RNG to use for stochastic integration.
        :param n_steps: Length of the simulation to perform in integration steps. Overrides simulation_length.
        :return: Iterator over monitor outputs.
        """

        self.calls += 1
        if simulation_length is not None:
            self.simulation_length = float(simulation_length)

        # initialization
        self._guesstimate_runtime()
        self._calculate_storage_requirement()
        # TODO a provided random_state should be used for history init
        self.integrator.set_random_state(random_state)
        local_coupling = self._prepare_local_coupling()
        stimulus = self._prepare_stimulus()
        state = self.current_state
        start_step = self.current_step + 1
        node_coupling = self._loop_compute_node_coupling(start_step)

        # integration loop
        if n_steps is None:
            n_steps = int(
                math.ceil(self.simulation_length / self.integrator.dt))
        else:
            if not numpy.issubdtype(type(n_steps), numpy.integer):
                raise TypeError(
                    "Incorrect type for n_steps: %s, expected integer" %
                    type(n_steps))

        for step in range(start_step, start_step + n_steps):
            self._loop_update_stimulus(step, stimulus)
            state = self.integrate_next_step(state, self.model, node_coupling,
                                             local_coupling, stimulus)
            self._loop_update_history(step, state)
            node_coupling = self._loop_compute_node_coupling(step + 1)
            output = self._loop_monitor_output(step, state, node_coupling)
            if output is not None:
                yield output

        self.current_state = state
        self.current_step = self.current_step + n_steps

    def _configure_history(self, initial_conditions=None):
        self.history = SparseHistory.from_simulator(self, initial_conditions)

    def _configure_integrator_noise(self):
        """
        This enables having noise to be state variable specific and/or to enter
        only via specific brain structures, for example it we only want to
        consider noise as an external input entering the brain via appropriate
        thalamic nuclei.

        Support 3 possible shapes:
            1) number_of_nodes;

            2) number_of_state_variables or number_of_integrated_state_variables; and

            3) (number_of_state_variables or number_of_integrated_state_variables, number_of_nodes).

        """
        # Noise has to have a shape corresponding to only the integrated state variables!
        good_history_shape = list(self.good_history_shape[1:])
        good_history_shape[0] = self.model.nintvar
        if self.integrator.noise.ntau > 0.0:
            self.integrator.noise.configure_coloured(self.integrator.dt,
                                                     tuple(good_history_shape))
        else:
            self.integrator.noise.configure_white(self.integrator.dt,
                                                  tuple(good_history_shape))

        if self.surface is not None:
            if self.integrator.noise.nsig.size == self.connectivity.number_of_regions:
                self.integrator.noise.nsig = self.integrator.noise.nsig[
                    self.surface.region_mapping]
            elif self.integrator.noise.nsig.size == self.model.nvar * self.connectivity.number_of_regions:
                self.integrator.noise.nsig = \
                    self.integrator.noise.nsig[self.model.state_variable_mask][:, self.surface.region_mapping]
            elif self.integrator.noise.nsig.size == self.model.nintvar * self.connectivity.number_of_regions:
                self.integrator.noise.nsig = self.integrator.noise.nsig[:,
                                                                        self.
                                                                        surface
                                                                        .
                                                                        region_mapping]

        good_nsig_shape = (self.model.nintvar, self.number_of_nodes,
                           self.model.number_of_modes)
        nsig = self.integrator.noise.nsig
        self.log.debug("Given noise shape is %s", nsig.shape)
        if nsig.shape in (good_nsig_shape, (1, )):
            return
        elif nsig.shape == (self.model.nvar, ):
            nsig = nsig[self.model.state_variable_mask].reshape(
                (self.model.nintvar, 1, 1))
        elif nsig.shape == (self.model.nintvar, ):
            nsig = nsig.reshape((self.model.nintvar, 1, 1))
        elif nsig.shape == (self.number_of_nodes, ):
            nsig = nsig.reshape((1, self.number_of_nodes, 1))
        elif nsig.shape == (self.model.nvar, self.number_of_nodes):
            nsig = nsig[self.model.state_variable_mask].reshape(
                (self.n_intvar, self.number_of_nodes, 1))
        elif nsig.shape == (self.model.nintvar, self.number_of_nodes):
            nsig = nsig.reshape((self.model.nintvar, self.number_of_nodes, 1))
        else:
            msg = "Bad Simulator.integrator.noise.nsig shape: %s"
            self.log.error(msg % str(nsig.shape))

        self.log.debug("Corrected noise shape is %s", nsig.shape)
        self.integrator.noise.nsig = nsig

    def _configure_monitors(self):
        """ Configure the requested Monitors for this Simulator """
        # Coerce to list if required
        if not isinstance(self.monitors, (list, tuple)):
            self.monitors = [self.monitors]
        # Configure monitors
        for monitor in self.monitors:
            monitor.config_for_sim(self)

    def _configure_stimuli(self):
        """ Configure the defined Stimuli for this Simulator """
        if self.stimulus is not None:
            if self.surface:
                # NOTE the region mapping of the stimuli should also include the subcortical areas
                self.stimulus.configure_space(region_mapping=numpy.r_[
                    self.surface.region_mapping,
                    self.connectivity.unmapped_indices(self.surface.
                                                       region_mapping)])
            else:
                self.stimulus.configure_space()

    # used by simulator adaptor
    def memory_requirement(self):
        """
        Return an estimated of the memory requirements (Bytes) for this
        simulator's current configuration.
        """
        self._guesstimate_memory_requirement()
        return self._memory_requirement_guess

    # appears to be unused
    def runtime(self, simulation_length):
        """
        Return an estimated run time (seconds) for the simulator's current
        configuration and a specified simulation length.

        """
        self.simulation_length = simulation_length
        self._guesstimate_runtime()
        return self._runtime

    # used by simulator adaptor
    def storage_requirement(self):
        """
        Return an estimated storage requirement (Bytes) for the simulator's
        current configuration and a specified simulation length.

        """
        self._calculate_storage_requirement()
        return self._storage_requirement

    def _guesstimate_memory_requirement(self):
        """
        guesstimate the memory required for this simulator.

        Guesstimate is based on the shape of the dominant arrays, and as such
        can operate before configuration.

        NOTE: Assumes returned/yeilded data is in some sense "taken care of" in
            the world outside the simulator, and so doesn't consider it, making
            the simulator's history, and surface if present, the dominant
            memory pigs...

        """
        if self.surface:
            number_of_nodes = self.surface.number_of_vertices
        else:
            number_of_nodes = self.connectivity.number_of_regions

        number_of_regions = self.connectivity.number_of_regions

        magic_number = 2.42  # Current guesstimate is low by about a factor of 2, seems safer to over estimate...
        bits_64 = 8.0  # Bytes
        bits_32 = 4.0  # Bytes
        # NOTE: The speed hack for getting the first element of hist shape should
        #      partially resolves calling of this method with a non-configured
        #     connectivity, there remains the less common issue if no tract_lengths...
        hist_shape = (
            self.connectivity.tract_lengths.max() /
            (self.conduction_speed or self.connectivity.speed or 3.0) /
            self.integrator.dt, self.model.nvar, number_of_nodes,
            self.model.number_of_modes)
        self.log.debug("Estimated history shape is %r", hist_shape)

        memreq = numpy.prod(hist_shape) * bits_64
        if self.surface:
            memreq += self.surface.number_of_triangles * 3 * bits_32 * 2  # normals
            memreq += self.surface.number_of_vertices * 3 * bits_64 * 2  # normals
            memreq += number_of_nodes * number_of_regions * bits_64 * 4  # region_mapping, region_average, region_sum
            # ???memreq += self.surface.local_connectivity.matrix.nnz * 8

        if not hasattr(self.monitors, '__len__'):
            self.monitors = [self.monitors]

        for monitor in self.monitors:
            if not isinstance(monitor, monitors.Bold):
                stock_shape = (monitor.period / self.integrator.dt,
                               len(self.model.variables_of_interest),
                               number_of_nodes, self.model.number_of_modes)
                memreq += numpy.prod(stock_shape) * bits_64
                if hasattr(monitor, "sensors"):
                    try:
                        memreq += number_of_nodes * monitor.sensors.number_of_sensors * bits_64  # projection_matrix
                    except AttributeError:
                        self.log.debug(
                            "No sensors specified, guessing memory based on default EEG."
                        )
                        memreq += number_of_nodes * 62.0 * bits_64

            else:
                stock_shape = (monitor.hrf_length * monitor._stock_sample_rate,
                               len(self.model.variables_of_interest),
                               number_of_nodes, self.model.number_of_modes)
                interim_stock_shape = (1.0 / (2.0**-2 * self.integrator.dt),
                                       len(self.model.variables_of_interest),
                                       number_of_nodes,
                                       self.model.number_of_modes)
                memreq += numpy.prod(stock_shape) * bits_64
                memreq += numpy.prod(interim_stock_shape) * bits_64

        if psutil and memreq > psutil.virtual_memory().total:
            self.log.warning(
                "There may be insufficient memory for this simulation.")

        self._memory_requirement_guess = magic_number * memreq
        msg = "Memory requirement estimate: simulation will need about %.1f MB"
        self.log.info(msg, self._memory_requirement_guess / 2**20)

    def _census_memory_requirement(self):
        """
        Guesstimate the memory required for this simulator.

        Guesstimate is based on a census of the dominant arrays after the
        simulator has been configured.

        NOTE: Assumes returned/yeilded data is in some sense "taken care of" in
            the world outside the simulator, and so doesn't consider it, making
            the simulator's history, and surface if present, the dominant
            memory pigs...

        """
        magic_number = 2.42  # Current guesstimate is low by about a factor of 2, seems safer to over estimate...
        memreq = self.history.nbytes
        try:
            memreq += self.surface.triangles.nbytes * 2
            memreq += self.surface.vertices.nbytes * 2
            memreq += self.surface.region_mapping.nbytes * self.number_of_nodes * 8. * 4  # region_average, region_sum
            memreq += self.surface.local_connectivity.matrix.nnz * 8
        except AttributeError:
            pass

        for monitor in self.monitors:
            memreq += monitor._stock.nbytes
            if isinstance(monitor, monitors.Bold):
                memreq += monitor._interim_stock.nbytes

        if psutil and memreq > psutil.virtual_memory().total:
            self.log.warning("Memory estimate exceeds total available RAM.")

        self._memory_requirement_census = magic_number * memreq
        # import pdb; pdb.set_trace()
        msg = "Memory requirement census: simulation will need about %.1f MB"
        self.log.info(msg % (self._memory_requirement_census / 1048576.0))

    def _guesstimate_runtime(self):
        """
        Estimate the runtime for this simulator.

        Spread in parallel executions of larger arrays means this will be an over-estimation,
        or rather a single threaded estimation...
        Different choice of integrators and monitors has an additional effect,
        on the magic number though relatively minor

        """
        magic_number = 6.57e-06  # seconds
        self._runtime = (magic_number * self.number_of_nodes *
                         self.model.nvar * self.model.number_of_modes *
                         self.simulation_length / self.integrator.dt)
        msg = "Simulation runtime should be about %0.3f seconds"
        self.log.info(msg, self._runtime)

    def _calculate_storage_requirement(self):
        """
        Calculate the storage requirement for the simulator, configured with
        models, monitors, etc being run for a particular simulation length.
        While this is only approximate, it is far more reliable/accurate than
        the memory and runtime guesstimates.
        """
        self.log.info("Calculating storage requirement for ...")
        strgreq = 0
        for monitor in self.monitors:
            # Avoid division by zero for monitor not yet configured
            # (in framework this is executed, when only preconfigure has been called):
            current_period = monitor.period or self.integrator.dt
            strgreq += (TvbProfile.current.MAGIC_NUMBER *
                        self.simulation_length * self.number_of_nodes *
                        self.model.nvar * self.model.number_of_modes /
                        current_period)
        self.log.info("Calculated storage requirement for simulation: %d " %
                      int(strgreq))
        self._storage_requirement = int(strgreq)

    def run(self, **kwds):
        """Convenience method to call the simulator with **kwds and collect output data."""
        ts, xs = [], []
        for _ in self.monitors:
            ts.append([])
            xs.append([])
        wall_time_start = time.time()
        for data in self(**kwds):
            for tl, xl, t_x in zip(ts, xs, data):
                if t_x is not None:
                    t, x = t_x
                    tl.append(t)
                    xl.append(x)
        elapsed_wall_time = time.time() - wall_time_start
        self.log.info("%.3f s elapsed, %.3fx real time", elapsed_wall_time,
                      elapsed_wall_time * 1e3 / self.simulation_length)
        for i in range(len(ts)):
            ts[i] = numpy.array(ts[i])
            xs[i] = numpy.array(xs[i])
        return list(zip(ts, xs))