From 402f91f07e53581a85bad7eb35f02dc9e6dc64df Mon Sep 17 00:00:00 2001 From: DeLaVlag Date: Wed, 17 Jun 2020 16:54:57 +0200 Subject: [PATCH 01/10] latest dsl files; folder restructure --- GPUmemindex.png | Bin 0 -> 564033 bytes README.md | 188 ++ TVB_testsuit/__init__.py | 0 TVB_testsuit/benchAll.sh | 17 + TVB_testsuit/cuda_run.py | 157 ++ TVB_testsuit/runthings | 19 + TVB_testsuit/tvbRegCudaNumba.py | 408 +++++ __init__.py | 0 dsl_cuda/CUDAmodels/epileptor.c | 269 +++ dsl_cuda/CUDAmodels/kuramoto.c | 146 ++ dsl_cuda/CUDAmodels/kuramotoref.c | 128 ++ dsl_cuda/CUDAmodels/network.no_defines.c | 285 +++ dsl_cuda/CUDAmodels/oscillator.c | 180 ++ dsl_cuda/CUDAmodels/oscillatorref.c | 323 ++++ dsl_cuda/CUDAmodels/refs/balloon.c | 67 + dsl_cuda/CUDAmodels/refs/covar.c | 109 ++ dsl_cuda/CUDAmodels/refs/kuramoto_network.c | 128 ++ dsl_cuda/CUDAmodels/refs/network.c | 121 ++ dsl_cuda/CUDAmodels/refs/network_2d.c | 142 ++ dsl_cuda/CUDAmodels/refs/network_rww.c | 157 ++ dsl_cuda/CUDAmodels/rwongwang.c | 205 +++ dsl_cuda/CUDAmodels/rwongwangref.c | 158 ++ dsl_cuda/LEMS2CUDA.py | 118 ++ dsl_cuda/XMLmodels/__init__.py | 0 dsl_cuda/XMLmodels/epileptor_CUDA.xml | 102 ++ dsl_cuda/XMLmodels/kuramoto_CUDA.xml | 52 + dsl_cuda/XMLmodels/oscillator_CUDA.xml | 67 + dsl_cuda/XMLmodels/rwongwang_CUDA.xml | 89 + dsl_cuda/__init__.py | 0 dsl_cuda/tmpl8_CUDA.py | 260 +++ lems/__init__.py | 0 lems/base/__init__.py | 5 + lems/base/base.py | 20 + lems/base/errors.py | 86 + lems/base/map.py | 31 + lems/base/stack.py | 96 + lems/base/util.py | 60 + lems/model/__init__.py | 5 + lems/model/component.py | 1294 ++++++++++++++ lems/model/dynamics.py | 909 ++++++++++ lems/model/fundamental.py | 162 ++ lems/model/model.py | 850 +++++++++ lems/model/simulation.py | 397 +++++ lems/model/structure.py | 500 ++++++ lems/parser/LEMS.py | 1764 +++++++++++++++++++ lems/parser/__init__.py | 5 + lems/parser/expr.py | 520 ++++++ 47 files changed, 10599 insertions(+) create mode 100755 GPUmemindex.png create mode 100755 README.md create mode 100755 TVB_testsuit/__init__.py create mode 100755 TVB_testsuit/benchAll.sh create mode 100755 TVB_testsuit/cuda_run.py create mode 100755 TVB_testsuit/runthings create mode 100755 TVB_testsuit/tvbRegCudaNumba.py create mode 100755 __init__.py create mode 100755 dsl_cuda/CUDAmodels/epileptor.c create mode 100755 dsl_cuda/CUDAmodels/kuramoto.c create mode 100755 dsl_cuda/CUDAmodels/kuramotoref.c create mode 100755 dsl_cuda/CUDAmodels/network.no_defines.c create mode 100755 dsl_cuda/CUDAmodels/oscillator.c create mode 100755 dsl_cuda/CUDAmodels/oscillatorref.c create mode 100755 dsl_cuda/CUDAmodels/refs/balloon.c create mode 100755 dsl_cuda/CUDAmodels/refs/covar.c create mode 100755 dsl_cuda/CUDAmodels/refs/kuramoto_network.c create mode 100755 dsl_cuda/CUDAmodels/refs/network.c create mode 100755 dsl_cuda/CUDAmodels/refs/network_2d.c create mode 100755 dsl_cuda/CUDAmodels/refs/network_rww.c create mode 100755 dsl_cuda/CUDAmodels/rwongwang.c create mode 100755 dsl_cuda/CUDAmodels/rwongwangref.c create mode 100755 dsl_cuda/LEMS2CUDA.py create mode 100755 dsl_cuda/XMLmodels/__init__.py create mode 100755 dsl_cuda/XMLmodels/epileptor_CUDA.xml create mode 100755 dsl_cuda/XMLmodels/kuramoto_CUDA.xml create mode 100755 dsl_cuda/XMLmodels/oscillator_CUDA.xml create mode 100755 dsl_cuda/XMLmodels/rwongwang_CUDA.xml create mode 100755 dsl_cuda/__init__.py create mode 100755 dsl_cuda/tmpl8_CUDA.py create mode 100755 lems/__init__.py create mode 100755 lems/base/__init__.py create mode 100755 lems/base/base.py create mode 100755 lems/base/errors.py create mode 100755 lems/base/map.py create mode 100755 lems/base/stack.py create mode 100755 lems/base/util.py create mode 100755 lems/model/__init__.py create mode 100755 lems/model/component.py create mode 100755 lems/model/dynamics.py create mode 100755 lems/model/fundamental.py create mode 100755 lems/model/model.py create mode 100755 lems/model/simulation.py create mode 100755 lems/model/structure.py create mode 100755 lems/parser/LEMS.py create mode 100755 lems/parser/__init__.py create mode 100755 lems/parser/expr.py diff --git a/GPUmemindex.png b/GPUmemindex.png new file mode 100755 index 0000000000000000000000000000000000000000..b954e55beec4637e52a02e0646ae510a73b4abdb GIT binary patch literal 564033 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

J!4b!@PEroCYiYUp~yB5Uu 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%0Difd#y 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 f04)fXHcw#M`{mRF1z3<)cGm8+=^|$ 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%0D&i#N; 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

8?InY8SFyI(Cmb&^>jhJ3`iA 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 z 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�TUPcmaWy>Kx%WGO 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100755 index 0000000000..084ed4387d --- /dev/null +++ b/README.md @@ -0,0 +1,188 @@ +# TVB CUDA model generation using LEMS format +This readme describes the usage of the code generation for models defined in LEMS based XML to Cuda (C) format. +The LEMS format PR has been adopted and altered to match TVB model names. +In LEMSCUDA.py the function "cuda_templating(Model+'_CUDA')" will start the code generation. +It expects a [model+'_CUDA'].xml file to be present in tvb/dsl_cuda/NeuroML/XMLmodels. +The generated file will be placed in tvb/simulator/models. +The produced filename is a lower cased [model].py which contains a class named [model]. +In the directory TVB_testsuite the files to run the models on the GPU can be found. +Execute './runthings cuda Modelname' to start the parameter sweep based simulation. + + .. moduleauthor:: Michiel. A. van der Vlag + +# The CUDA memory model specification +![](GPUmemindex.png) + +# Files +* dsl_cuda/LEMS2CUDA.py : python script for initiating model code generation +* dsl_cuda/NeuroML/XMLmodels : directory containing LEMS based XML model files +* dsl_cuda/tmpl8_CUDA.py : Mako template converting XML to CUDA +* dsl_cuda/NeuroML/lems : modified pyLEMS library tuned for TVB CUDA generation +* dsl_cuda/NeuroML/lems/component.py : maintains constants and exposures +* dsl_cuda/NeuroML/lems/dynamics.py : maintains all dynamic attributes +* dsl_cuda/NeuroML/lems/LEMS.py : LEMS XML file parser +* dsl_cuda/NeuroML/lems/expr.py : expression parser +* dsl_cuda/TVB_testsuite/ : directory for run parameter based GPU with generated model + +# Prerequisites +Mako templating + +# XML LEMS Definitions +Based on http://lems.github.io/LEMS/elements.html but attributes are tuned for TVB CUDA models. +As an example an XML line and its translation to CUDA are given. + +* Constants\ +If domain = 'none' no domain range will be added.\ +Label is fixed to ':math:constant.name' + +```xml + +``` +translates to: +```c +const float x0 = -1.6; +``` + +* State variables +State variable ranges [lo, hi]" values are entered with keyword "boundaries" with a comma separator.\ +The default option can be used to initialize if necessary (future) +For each state variable a set of boundaries can be added to encompass the dynamic range.\ +A wrapping function according to the values entered will be generated and in numerical solving the wrapper +will be applied. \ + +```xml + +``` +translates to: +```c + __device__ float wrap_it_x1(float x1) +{ + int x1dim[] = {-2.0, 1.0}; + if (x1 < x1dim[0]) x1 = x1dim[0]; + else if (x1 > x1dim[1]) x1 = x1dim[1]; + + return x1; +} + + double x1 = 0.0; + + for(nsteps) + for(inodes) + x1 = state((t) % nh, i_node + 0 * n_node); +``` + +* Exposures\ +Exposures are used for observables and translate to variables_of_interest.\ +For the name enter variable to be observed (usually states).\ +The field 'choices' are treated as lists with a (,) separator.\ +The define is hardcoded for the the term tavg.\ +Tavg is the datastruct containing the results of the parameter sweep simulation. + +```xml + +``` +translates to: +```c +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) +tavg(i_node + 0 * n_node) = x1; +``` + +* Derived variables\ +DerivedVariables can be used to 'easify' the time derivatives, enter the local coupling formulas or any formula.\ +sytax: [name]=[expression].\ +Define for example global and local coupling: c_0 = coupling.\ + +```xml + +``` +translates to: +```c +c_pop1 = coupling[0] +``` + +* Conditional Derived Variables\ +ConditionalDerivedVariables are used to created if, else constructs.\ +Use <(=); or >(=) for less- or greater then (equal to).\ +Syntax: if {condition} -> {cases[0]} else {cases[1]}. Cases are separated by (,).\ +It will not produce an else if {cases[1]} is not present. +```xml + +``` +translates to: +```c +if (x1 < 0.0): + ydot0 = -a * x1**2 + b * x1 +else: + ydot0 = slope - x2 + 0.6 * (z - 4)**2 +``` + +* Time Derivatives\ +Used to define the models derivates functions solved numerically.\ +Syntax: dx[n] = {expression}. Name field is not used. +```xml + + +``` +translates to: +```c +V = tt * (y1 - z + Iext + Kvf * c_pop1 + ydot0 * x1) +W = ... +``` + +* Coupling function\ +For the coupling function a new component type can be defined.\ +The dynamics can be defined with attributes similar to the solving of the numerical analysis.\ + +```xml + + + + + + + + + + + + + + + + + + + +``` +translates to: +```c +for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + //***// Get the weight of the coupling between node i and node j + float wij = weights[i_n + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + + //***// Get the delay between node i and node j + unsigned int dij = lengths[i_n + j_node] * rec_speed_dt; + + //***// Get the state of node j which is delayed by dij + float x1_j = state(((t - dij + nh) % nh), j_node + 0 * n_node); + + // Sum it all together using the coupling function. Kuramoto coupling: (postsyn * presyn) == ((a) * (sin(xj - xi))) + coupling += c_a * sin(x1_j - x1); + + } // j_node */ + + // rec_n is used for the scaling over nodes + c_pop1 = global_coupling * coupling; + c_pop2 = g; +``` + + +# Running +Place model file in directory and execute cuda_templating('modelname') function. Resulting model will be +placed in the CUDA model directory + +# TODO +Add CUDA model validation tests. \ No newline at end of file diff --git a/TVB_testsuit/__init__.py b/TVB_testsuit/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/TVB_testsuit/benchAll.sh b/TVB_testsuit/benchAll.sh new file mode 100755 index 0000000000..692305dacf --- /dev/null +++ b/TVB_testsuit/benchAll.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +#BSUB -q normal +#BSUB -W 00:30 +#BSUB -n 1 +#BSUB -R "span[ptile=1]" +#BSUB -gpu "num=1:j_exclusive=yes" +##BSUB -e "./error.%J.er" +##BSUB -o "./output_%J.out" +#BSUB -e "./error.er" +#BSUB -o "./output.out" +#BSUB -J testbench + +# Run the program + +mpirun python ./tvbRegCudaNumba.py -b bencharg + diff --git a/TVB_testsuit/cuda_run.py b/TVB_testsuit/cuda_run.py new file mode 100755 index 0000000000..295074e93c --- /dev/null +++ b/TVB_testsuit/cuda_run.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 + +from __future__ import print_function +import sys +import numpy as np +import os.path +import numpy as np +import itertools +import pycuda.autoinit +import pycuda.driver as drv +from pycuda.compiler import SourceModule +import pycuda.gpuarray as gpuarray +import pytools +import time +import argparse +import logging +import scipy.io as io + +here = os.path.dirname(os.path.abspath(__file__)) + +class CudaRun: + + def make_kernel(self, source_file, warp_size, block_dim_x, args, lineinfo=False, nh='nh'): + with open(source_file, 'r') as fd: + source = fd.read() + source = source.replace('M_PI_F', '%ff' % (np.pi, )) + opts = ["--ptxas-options=-v"]#, '-maxrregcount=32', '-lineinfo'] + if lineinfo: + opts.append('-lineinfo') + opts.append('-DWARP_SIZE=%d' % (warp_size, )) + opts.append('-DBLOCK_DIM_X=%d' % (block_dim_x, )) + opts.append('-DNH=%s' % (nh, )) + + idirs = [here] + # logger.info('nvcc options %r', opts) + network_module = SourceModule( + source, options=opts, include_dirs=idirs, + no_extern_c=True, + keep=False, + ) + # no API to know the mangled function name ahead of time + # if the func sig changes, just copy-paste the new name here.. + # TODO parse verbose output of nvcc to get function name and make dynamic + + # mod_func = '_Z9EpileptorjjjjjffPfS_S_S_S_' + # mod_func = '_Z8KuramotojjjjjffPfS_S_S_S_' + # mod_func = '_Z9RwongwangjjjjjffPfS_S_S_S_' + # mod_func = '_Z12KuratmotorefjjjjjffPfS_S_S_S_' + mod_func = "{}{}{}{}".format('_Z', len(args.model), args.model.capitalize(), 'jjjjjffPfS_S_S_S_') + + step_fn = network_module.get_function(mod_func) + + return step_fn #}}} + + def cf(self, array):#{{{ + # coerce possibly mixed-stride, double precision array to C-order single precision + return array.astype(dtype='f', order='C', copy=True)#}}} + + def nbytes(self, data):#{{{ + # count total bytes used in all data arrays + nbytes = 0 + for name, array in data.items(): + nbytes += array.nbytes + return nbytes#}}} + + def make_gpu_data(self, data):#{{{ + # put data onto gpu + gpu_data = {} + for name, array in data.items(): + gpu_data[name] = gpuarray.to_gpu(self.cf(array)) + return gpu_data#}}} + + + def run_simulation(self, weights, lengths, params_matrix, couplings, speeds, logger, args, n_nodes, n_work_items, n_params, nstep, n_inner_steps, + buf_len, states, dt, min_speed, pop): + + # setup data#{{{ + data = { 'weights': weights, 'lengths': lengths, 'params': params_matrix.T } + base_shape = n_work_items, + for name, shape in dict( + tavg=(n_nodes,), + state=(buf_len, states * n_nodes), + ).items(): + data[name] = np.zeros(shape + base_shape, 'f') + + gpu_data = self.make_gpu_data(data)#{{{ + logger.info('history shape %r', data['state'].shape) + logger.info('on device mem: %.3f MiB' % (self.nbytes(data) / 1024 / 1024, ))#}}} + + # setup CUDA stuff#{{{ + step_fn = self.make_kernel( + source_file=args.filename, + # warp_size=32, + # block_dim_x=args.n_coupling, + warp_size=8, + block_dim_x=8, + # ext_options=preproccesor_defines, + # caching=args.caching, + args=args, + lineinfo=args.lineinfo, + nh=buf_len, + # model=args.model, + )#}}} + + # setup simulation#{{{ + tic = time.time() + logger.info('nstep %i', nstep) + streams = [drv.Stream() for i in range(32)] + events = [drv.Event() for i in range(32)] + tavg_unpinned = [] + tavg = drv.pagelocked_zeros(data['tavg'].shape, dtype=np.float32) + logger.info('data[tavg].shape %s', data['tavg'].shape) + #}}} + + # adjust gridDim to keep block size <= 1024 {{{ + # block_size_lim = 1024 + block_size_lim = 64 + n_coupling_per_block = block_size_lim // args.node_threads + n_coupling_blocks = args.n_coupling // n_coupling_per_block + if n_coupling_blocks == 0: + n_coupling_per_block = args.n_coupling + n_coupling_blocks = 1 + final_block_dim = n_coupling_per_block, args.node_threads, 1 + final_grid_dim = speeds.size, n_coupling_blocks + logger.info('final block dim %r', final_block_dim) + logger.info('final grid dim %r', final_grid_dim) + assert n_coupling_per_block * n_coupling_blocks == args.n_coupling #}}} + logger.info('gpu_data[lengts] %s', gpu_data['lengths'].shape) + logger.info('nnodes %r', n_nodes) + # logger.info('gpu_data[lengths] %r', gpu_data['lengths']) + + # run simulation#{{{ + logger.info('submitting work') + for i in range(nstep): + + # event = events[i % 32] + # stream = streams[i % 32] + + # stream.wait_for_event(events[(i - 1) % 32]) + + step_fn(np.uintc(i * n_inner_steps), np.uintc(n_nodes), np.uintc(buf_len), np.uintc(n_inner_steps), + np.uintc(n_params), np.float32(dt), np.float32(min_speed), + gpu_data['weights'], gpu_data['lengths'], gpu_data['params'], gpu_data['state'], + gpu_data['tavg'], + block=final_block_dim, + grid=final_grid_dim) + + # event.record(streams[i % 32]) + tavg_unpinned.append(tavg.copy()) + drv.memcpy_dtoh( + tavg, + gpu_data['tavg'].ptr) + + logger.info('kernel finish..') + # release pinned memory + tavg = np.array(tavg_unpinned) + return tavg diff --git a/TVB_testsuit/runthings b/TVB_testsuit/runthings new file mode 100755 index 0000000000..cb4515fd2a --- /dev/null +++ b/TVB_testsuit/runthings @@ -0,0 +1,19 @@ +#!/bin/bash + +rm error.er +rm output.out +sed "s/bencharg/$1/" benchAll.sh | bsub +#sleep 2 + +while [ ! -f ./error.er ] ; +do + sleep 0.2 +done + +if [ $1 == "regular" ]; +then + tail -f ./output.out | grep -i -E "INFO|WARNING|ERROR" + tail -f ./error.er +else + tail -f ./error.er +fi diff --git a/TVB_testsuit/tvbRegCudaNumba.py b/TVB_testsuit/tvbRegCudaNumba.py new file mode 100755 index 0000000000..26d3fc8126 --- /dev/null +++ b/TVB_testsuit/tvbRegCudaNumba.py @@ -0,0 +1,408 @@ +from tvb.simulator.lab import * +# from tvb.datatypes import connectivity +# from tvb.simulator import integrators +# from tvb.simulator import coupling +import numpy as np +import numpy.random as rgn +import matplotlib.pyplot as plt +import math +import sys +import os + +from numpy import corrcoef +import seaborn as sns + +import time +import logging +import itertools +import argparse + +import os, sys, inspect + +current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +parent_dir = os.path.dirname(current_dir) +sys.path.insert(0, parent_dir) + +sys.path.append("{}{}".format(parent_dir, '/NeuroML/')) + +print(sys.path) + +np.set_printoptions(threshold=sys.maxsize) + +# for numexpr package missing and no permissions to install: +# clone package, copy to hpc, build with $ python setup.py build, copy numexpr folder from build/lib.linux-ppc64le-3.6 to project root +# and do: export PATH=$PATH:/\$PROJECT_cpcp0/vandervlag/all-benchmarking/numexpr/build/lib.linux-ppc64le-3.6/numexpr // preferrable pythonpath +# same for tvb-data dependancy: move build directory (python setyp.py build) to root + +# set global logger level in tvb.logger.library_logger.conf + + +rgn.seed(79) + + +class TVB_test: + + def __init__(self): + self.args = self.parse_args() + self.sim_length = self.args.n_time # 400 + self.g = np.array([1.0]) + self.s = np.array([1.0]) + self.dt = 0.1 + self.period = 10.0 + self.omega = 60.0 * 2.0 * math.pi / 1e3 + (self.connectivity, self.coupling) = self.tvb_connectivity(self.s, self.g, self.dt) + self.integrator = integrators.EulerDeterministic(dt=self.dt) + self.weights = self.SC = self.connectivity.weights + self.lengths = self.connectivity.tract_lengths + self.n_nodes = self.weights.shape[0] + self.tavg_period = 10.0 + self.nstep = self.args.n_time # 4s + self.n_inner_steps = int(self.tavg_period / self.dt) + self.nc = self.args.n_coupling + self.ns = self.args.n_speed + self.couplings, self.speeds = self.setup_params(self.nc, self.ns) + self.params = self.expand_params(self.couplings, self.speeds) + self.n_work_items, self.n_params = self.params.shape + self.min_speed = self.speeds.min() + self.buf_len_ = ((self.lengths / self.min_speed / self.dt).astype('i').max() + 1) + self.buf_len = 2 ** np.argwhere(2 ** np.r_[:30] > self.buf_len_)[0][0] # use next power of 2 + self.states = 1 + + def tvb_connectivity(self, speed, global_coupling, dt=0.1): + white_matter = connectivity.Connectivity.from_file(source_file="paupau.zip") + white_matter.configure() + white_matter.speed = np.array([speed]) + white_matter_coupling = coupling.Linear(a=global_coupling) + return white_matter, white_matter_coupling + + def tvb_python_model(self): + whatmodel=self.args.model.lower() + print(whatmodel) + + switcher = { + 'kuramoto': models.Kuramoto, + 'oscillator': models.Generic2dOscillator, + 'wongwang': models.ReducedWongWang, + # 'montbrio': models.Montbrio, + 'epileptor': models.Epileptor + } + modelexe = switcher.get(whatmodel, 'invalid model') + # print(modelexe) + populations = modelexe() + + # populations = models.Kuramoto() + populations.configure() + populations.omega = np.array([self.omega]) + return populations + + def parse_args(self): # {{{ + parser = argparse.ArgumentParser(description='Run parameter sweep.') + parser.add_argument('-c', '--n_coupling', help='num grid points for coupling parameter', default=32, type=int) + parser.add_argument('-s', '--n_speed', help='num grid points for speed parameter', default=32, type=int) + parser.add_argument('-t', '--test', help='check results', action='store_true') + parser.add_argument('-n', '--n_time', help='number of time steps to do (default 400)', type=int, default=400) + parser.add_argument('-v', '--verbose', help='increase logging verbosity', action='store_true', default='-v') + # parser.add_argument('-p', '--no_progress_bar', help='suppress progress bar', action='store_false') + # parser.add_argument('--caching', + # choices=['none', 'shared', 'shared_sync', 'shuffle'], + # help="caching strategy for j_node loop (default shuffle)", + # default='none' + # ) + # parser.add_argument('--dataset', + # choices=['hcp', 'sep'], + # help="dataset to use (hcp: 100 nodes, sep: 645 nodes", + # default='hcp' + # ) + parser.add_argument('--node_threads', default=1, type=int) + parser.add_argument('--model', + choices=['Rwongwang', 'Kuramoto', 'Epileptor', 'Oscillator', \ + 'Oscillatorref', 'Kuramotoref', 'Rwongwangref'], + help="neural mass model to be used during the simulation", + default='Oscillator' + ) + parser.add_argument('--lineinfo', default=True, action='store_true') + + parser.add_argument('--filename', default="kuramoto_network.c", type=str, + help="Filename to use as GPU kernel definition") + # parser.add_argument("bench", default="all", nargs='*', choices=["noop", "scatter", "gather", "all"], help="Which sub-set of kernel to run") + + parser.add_argument('-b', '--bench', default="regular", type=str, help="What to bench: regular, numba, cuda") + + args = parser.parse_args() + return args + + # numba load + def make_data(self): + c = network.Connectivity.hcp0() + return c.nnode, c.lengths, c.nnz, c.row, c.col, c.wnz, c.nz, c.weights + + # cuda load + def load_connectome(self, dataset): + # load connectome & normalize + if dataset == 'hcp': + npz = np.load('data/hcp0.npz') + weights = npz['weights'].astype(np.float32) + lengths = npz['lengths'].astype(np.float32) + elif dataset == 'sep': + npz = np.load('sep.npz') + weights = npz['weights'].astype(np.float32) + lengths = npz['lengths'].astype(np.float32) + else: + raise ValueError('unknown dataset name %r' % (dataset,)) + # weights /= {'N':2e3, 'Nfa': 1e3, 'FA': 1.0}[mattype] + weights /= weights.max() + assert (weights <= 1.0).all() + return weights, lengths + + def expand_params(self, couplings, speeds): # {{{ + # the params array is transformed into a 2d array + # by first creating tuples of (speed, coup) and arrayfying then + # pycuda (check) threats them as flattenened arrays but numba needs 2d indexing + params = itertools.product(speeds, couplings) + params = np.array([vals for vals in params], np.float32) + return params # }}} + + def setup_params(self, nc, ns): # {{{ + # the correctness checks at the end of the simulation + # are matched to these parameter values, for the moment + couplings = np.logspace(1.6, 3.0, nc) + speeds = np.logspace(0.0, 2.0, ns) + return couplings, speeds # }}} + + def calculate_FC(self, timeseries): + return corrcoef(timeseries.T) + + def correlation_SC_FC(self, SC, FC): + return corrcoef(FC.ravel(), SC.ravel())[0, 1] + + def plot_SC_FC(self, SC, FC, tag): + # print(FC) + fig, ax = plt.subplots(ncols=2, figsize=(12, 3)) + sns.heatmap((FC), xticklabels='', + yticklabels='', ax=ax[0], + cmap='coolwarm') + sns.heatmap(SC / SC.max(), xticklabels='', yticklabels='', + ax=ax[1], cmap='coolwarm', vmin=0, vmax=1) # + r = self.correlation_SC_FC(SC, FC) + ax[0].set_title('simulated FC. \n(SC-FC r = %1.4s )' % r) + ax[1].set_title('SC') + # plt.savefig("FC_SC_"+tag+".png") + return r + + # Todo: check if this function work. derr_speed > 500 and derr_coupl < -1500 evaluate to false for pyCuda runs + def check_results(self, n_nodes, n_work_items, tavg, weights, speeds, couplings, logger, args): + r, c = np.triu_indices(n_nodes, 1) + win_size = args.n_time # 4s? orig 200 # 2s? + win_tavg = tavg.reshape((-1, win_size) + tavg.shape[1:]) + err = np.zeros((len(win_tavg), n_work_items)) + logger.info('err.shape %s', err.shape) + # TODO do cov/corr in kernel + for i, tavg_ in enumerate(win_tavg): + for j in range(n_work_items): + fc = np.corrcoef(tavg_[:, :, j].T) + # err[i, j] = ((fc[r, c] - weights[r, c])**2).sum() weights is 1 dim array + # logger.info('fc[r, c].shape %s, weights[r].shape %s', fc[r, c].shape, weights[r].shape) + err[i, j] = ((fc[r, c] - weights[r, c]) ** 2).sum() + # look at 2nd 2s window (converges quickly) + err_ = err[-1].reshape((speeds.size, couplings.size)) + # change on fc-sc metric wrt. speed & coupling strength + derr_speed = np.diff(err_.mean(axis=1)).sum() + derr_coupl = np.diff(err_.mean(axis=0)).sum() + logger.info('derr_speed=%f, derr_coupl=%f', derr_speed, derr_coupl) + # if args.dataset == 'hcp': + assert derr_speed > 350.0 + assert derr_coupl < -500.0 + # if args.dataset == 'sep': + # assert derr_speed > 5e4 + # assert derr_coupl > 1e4 + + logger.info('result OK') + + def regular(self, logger, pop): + logger.info('start regular TVB run') + logger.info('model to run %s', pop) + # Initialize Model + model = self.tvb_python_model() + # Initialize Monitors + monitorsen = (monitors.TemporalAverage(period=self.period)) + # Initialize Simulator + sim = simulator.Simulator(model=model, connectivity=self.connectivity, coupling=self.coupling, + integrator=self.integrator, + monitors=[monitorsen]) + sim.configure() + (_, tavg_data) = sim.run(simulation_length=self.sim_length)[0] + # print(np.squeeze(np.array(tavg_data)).shape) + # + # FC = self.calculate_FC(np.squeeze(np.array(tavg_data))) + # r = self.plot_SC_FC(self.SC, FC,"regular") + # print(r) + return np.squeeze(tavg_data) + + def numba(self, logger, pop): + logger.info('start Numba run') + from numbacuda_run import NumbaCudaRun + numbacuda = NumbaCudaRun() + trace = numbacuda.run_simulation(dt) + + # + # (numbacuda_FC, python_r) = tvbhpc.simulate_numbacuda() + # print(numbacuda_FC) + # tavg_data = np.transpose(trace, (1, 2, 0)) + # tvbhpc.check_results(n_nodes, n_work_items, tavg_data, weights, speeds, couplings, logger, args) + + # numba kernel based on the c index used for cuda + def numbac(self, logger, pop): + logger.info('start Numba run') + from cindex_numbacuda_run import NumbaCudaRun + numbacuda = NumbaCudaRun() + + threadsperblock = len(self.couplings) + blockspergrid = len(self.speeds) + logger.info('threadsperblock %d', threadsperblock) + logger.info('blockspergrid %d', blockspergrid) + + tavg_data = numbacuda.run_simulation(blockspergrid, threadsperblock, self.n_inner_steps, self.n_nodes, + self.buf_len, self.dt, self.weights, self.lengths, self.params.T, + logger) + logger.info('tavg_data.shape %s', tavg_data.shape) + # logger.info('tavg_data %f', tavg_data) + + # + # (numbacuda_FC, python_r) = tvbhpc.simulate_numbacuda() + # print(numbacuda_FC) + # tavg_data = np.transpose(trace, (1, 2, 0)) + # tvbhpc.check_results(n_nodes, n_work_items, tavg_data, weights, speeds, couplings, logger, args) + + def cuda(self, logger, pop): + logger.info('start Cuda run') + from cuda_run import CudaRun + cudarun = CudaRun() + tavg_data = cudarun.run_simulation(self.weights, self.lengths, self.params, self.couplings, self.speeds, logger, + self.args, self.n_nodes, self.n_work_items, self.n_params, self.nstep, + self.n_inner_steps, self.buf_len, self.states, self.dt, self.min_speed, pop) + # logger.info('tavg_data %f', tavg_data) + + # Todo: fix this for cuda + self.check_results(self.n_nodes, self.n_work_items, tavg_data, self.weights, self.speeds, self.couplings, logger, self.args) + + return tavg_data + + def startsim(self, pop, tmpld): + + tic = time.time() + tvbhpc = TVB_test() + + # args = tvbhpc.parse_args() + logging.basicConfig(level=logging.DEBUG if self.args.verbose else logging.INFO) + logger = logging.getLogger('[tvbBench.py]') + + # dimensions#{{{ + # dt, tavg_period = 1.0, 10.0 + # nstep = args.n_time # 4s + # n_inner_steps = int(tavg_period / dt) + # states = 1 + # if args.model == 'rww': + # states = 2 + # TODO buf_len per speed/block + logger.info('dt %f', self.dt) + logger.info('nstep %d', self.nstep) + # logger.info('caching strategy %r', self.args.caching) + logger.info('n_inner_steps %f', self.n_inner_steps) + if self.args.test and self.args.n_time % 200: + logger.warning('rerun w/ a multiple of 200 time steps (-n 200, -n 400, etc) for testing') # }}} + + # setup data + # weights = tvbhpc.connectivity.weights + logger.info('weights.shape %s', self.weights.shape) + # lengths = tvbhpc.connectivity.tract_lengths + logger.info('lengths.shape %s', self.lengths.shape) + # n_nodes = weights.shape[0] + logger.info('n_nodes %d', self.n_nodes) + + # couplings and speeds are not derived from the regular TVB connection setup routine. these parameters are swooped every GPU spawn + # nc = args.n_coupling + # ns = args.n_speed + logger.info('single connectome, %d x %d parameter space', self.ns, self.nc) + logger.info('%d total num threads', self.ns * self.nc) + # couplings, speeds = tvbhpc.setup_params(nc=nc, ns=ns) + # params = tvbhpc.expand_params(couplings, speeds) + # # params = tvbhpc.expand_params(tvbhpc.coupling, tvbhpc.connectivity.speed) + # logger.info('coupling %s', couplings) + # logger.info('connectivity.speed %s', speeds) + # logger.info('coupling %s', (tvbhpc.coupling.pre)) + # logger.info('connectivity.speed %s', dir(tvbhpc.connectivity.speed)) + # logger.info('%f', params.T) + + # n_work_items, n_params = params.shape + # min_speed = speeds.min() + # buf_len_ = ((lengths / min_speed / dt).astype('i').max() + 1) + logger.info('min_speed %f', self.min_speed) + # buf_len = 2**np.argwhere(2**np.r_[:30] > buf_len_)[0][0] # use next power of 2 + logger.info('real buf_len %d, using power of 2 %d', self.buf_len_, self.buf_len) + + tac = time.time() + logger.info("Setup in: {}".format(tac - tic)) + + benchwhat = self.args.bench + + self.args.filename = "{}{}{}{}".format(parent_dir, '/dsl_cuda/CUDAmodels/', self.args.model.lower(), '.c') + logger.info('modellow %s', self.args.model.lower()) + logger.info('modellow %s', 'wongwang' in self.args.model.lower()) + + if ('kuramoto' in self.args.model.lower()): + self.states = 1 + elif 'oscillator' in self.args.model.lower(): + self.states = 2 + elif 'wongwang' in self.args.model.lower(): + self.states = 2 + elif 'montbrio' in self.args.model.lower(): + self.states = 2 + elif 'epileptor' in self.args.model.lower(): + self.states = 6 + logger.info('number of states %d', self.states) + + # locals()[benchwhat]() + logger.info('benchwhat: %s', benchwhat) + # def bencher(benchwhat): + switcher = { + 'regular': self.regular, + 'numba': self.numba, + 'numbac': self.numbac, + 'cuda': self.cuda + } + func = switcher.get(benchwhat, 'invalid bench choice') + logger.info('func %s', func) + # quick and dirty comparison between old and templated version + if tmpld: + pop = pop + 'T' + # for k in range(2): + # if k == 0: + # pop = pop + 'T' + # print(pop) + # tavg0 = func(logger, pop) + # if k == 1: + # pop = pop + 'T' + # print(pop) + # tavg1 = func(logger, pop) + + logger.info('filename %s', self.args.filename) + logger.info('model %s', self.args.model) + tavg = func(logger, pop) + # print('tavg0', tavg0.shape, '\ntavg1', tavg1.shape) + # print('coercoef=', corrcoef(tavg0.ravel(), tavg1.ravel())[0, 1]) + + toc = time.time() + print("Finished python simulation successfully in: {}".format(toc - tac)) + elapsed = toc - tic + # inform about time + logger.info('elapsed time %0.3f', elapsed) + logger.info('%0.3f M step/s', 1e-6 * self.nstep * self.n_inner_steps * self.n_work_items / elapsed) + logger.info('finished') + + return tavg + + +if __name__ == '__main__': + zelf = TVB_test() + # zelf.args.filename = "../NeuroML/CUDAmodels/network.no_defines.c" + tavg = zelf.startsim('Kuramoto', tmpld=0) diff --git a/__init__.py b/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/dsl_cuda/CUDAmodels/epileptor.c b/dsl_cuda/CUDAmodels/epileptor.c new file mode 100755 index 0000000000..933d1bc698 --- /dev/null +++ b/dsl_cuda/CUDAmodels/epileptor.c @@ -0,0 +1,269 @@ +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + +#include +#include +#include + +__device__ float wrap_it_PI(float x) +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +} +__device__ float wrap_it_x1(float x1) +{ + float x1dim[] = {-2.0, 1.0}; + if (x1 < x1dim[0]) x1 = x1dim[0]; + else if (x1 > x1dim[1]) x1 = x1dim[1]; + + return x1; +} +__device__ float wrap_it_y1(float y1) +{ + float y1dim[] = {-20.0, 2.0}; + if (y1 < y1dim[0]) y1 = y1dim[0]; + else if (y1 > y1dim[1]) y1 = y1dim[1]; + + return y1; +} +__device__ float wrap_it_z(float z) +{ + float zdim[] = {-2.0, 5.0}; + if (z < zdim[0]) z = zdim[0]; + else if (z > zdim[1]) z = zdim[1]; + + return z; +} +__device__ float wrap_it_x2(float x2) +{ + float x2dim[] = {-2.0, 0.0}; + if (x2 < x2dim[0]) x2 = x2dim[0]; + else if (x2 > x2dim[1]) x2 = x2dim[1]; + + return x2; +} +__device__ float wrap_it_y2(float y2) +{ + float y2dim[] = {0.0, 2.0}; + if (y2 < y2dim[0]) y2 = y2dim[0]; + else if (y2 > y2dim[1]) y2 = y2dim[1]; + + return y2; +} +__device__ float wrap_it_g(float g) +{ + float gdim[] = {-1.0, 1.0}; + if (g < gdim[0]) g = gdim[0]; + else if (g > gdim[1]) g = gdim[1]; + + return g; +} + +__global__ void Epileptor( + + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, float * __restrict__ weights, float * __restrict__ lengths, + float * __restrict__ params_pwi, // pwi: per work item + // state + float * __restrict__ state_pwi, + // outputs + float * __restrict__ tavg_pwi + ) +{ + // work id & size + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y; + +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) * 6 * n_node + (i_node))*size + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + + // unpack params + // These are the two parameters which are usually explore in fitting in this model + const float global_speed = params(0); + const float global_coupling = params(1); + + // regular constants + const float a = 1.0; + const float b = 3.0; + const float c = 1.0; + const float d = 5.0; + const float r = 0.00035; + const float s = 4.0; + const float x0 = -1.6; + const float Iext = 3.1; + const float slope = 0.; + const float Iext2 = 3.1; + const float tau = 10.0; + const float aa = 6.0; + const float bb = 2.0; + const float Kvf = 0.0; + const float Kf = 0.0; + const float Ks = 0.0; + const float tt = 1.0; + const float modification = 1.0; + + // coupling constants, coupling itself is hardcoded in kernel + const float c_a = 1; + + // coupling parameters + float c_pop1 = 0.0; + float c_pop2 = 0.0; + + // derived parameters + const float rec_n = 1 / n_node; + const float rec_speed_dt = 1.0f / global_speed / (dt); + const float nsig = sqrt(dt) * sqrt(2.0 * 1e-5); + + + // conditional_derived variable declaration + float ydot0 = 0.0; + float ydot2 = 0.0; + float h = 0.0; + float ydot4 = 0.0; + + curandState crndst; + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &crndst); + + float x1 = 0.0; + float y1 = 0.0; + float z = 0.0; + float x2 = 0.0; + float y2 = 0.0; + float g = 0.0; + + //***// This is only initialization of the observable + for (unsigned int i_node = 0; i_node < n_node; i_node++) + { + tavg(i_node) = 0.0f; + if (i_step == 0){ + state(i_step, i_node) = 0.001; + } + } + + //***// This is the loop over time, should stay always the same + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + //***// This is the loop over nodes, which also should stay the same + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + { + c_pop1 = 0.0f; + c_pop2 = 0.0f; + + x1 = state((t) % nh, i_node + 0 * n_node); + y1 = state((t) % nh, i_node + 1 * n_node); + z = state((t) % nh, i_node + 2 * n_node); + x2 = state((t) % nh, i_node + 3 * n_node); + y2 = state((t) % nh, i_node + 4 * n_node); + g = state((t) % nh, i_node + 5 * n_node); + + // This variable is used to traverse the weights and lengths matrix, which is really just a vector. It is just a displacement. / + unsigned int i_n = i_node * n_node; + + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + //***// Get the weight of the coupling between node i and node j + float wij = weights[i_n + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + + //***// Get the delay between node i and node j + unsigned int dij = lengths[i_n + j_node] * rec_speed_dt; + + //***// Get the state of node j which is delayed by dij + float x1_j = state(((t - dij + nh) % nh), j_node + 0 * n_node); + + // Sum it all together using the coupling function. Kuramoto coupling: (postsyn * presyn) == ((a) * (sin(xj - xi))) + c_pop1 += wij * c_a * sin(x1_j - x1); + + } // j_node */ + + // rec_n is used for the scaling over nodes + c_pop1 *= global_coupling * coupling; + c_pop2 *= g; + + if (x1 < 0.0) + // The conditional variables + ydot0 = -a * powf(x1, 2) + b * x1; + else + ydot0 = slope - x2 + 0.6 * powf((z - 4),2); + if (z < 0.0) + // The conditional variables + ydot2 = - 0.1 * (powf(z, 7)); + else + ydot2 = 0; + if (modification) + // The conditional variables + h = x0 + 3. / (1. + exp(-(x1 + 0.5) / 0.1)); + else + h = 4 * (x1 - x0) + ydot2; + if (x2 < -0.25) + // The conditional variables + ydot4 = 0.0; + else + ydot4 = aa * (x2 + 0.25); + // This is dynamics step and the update in the state of the node + x1 += dt * (tt * (y1 - z + Iext + Kvf * c_pop1 + ydot0 )); + y1 += dt * (tt * (c - d * powf(x1, 2) - y1)); + z += dt * (tt * (r * (h - z + Ks * c_pop1))); + x2 += dt * (tt * (-y2 + x2 - powf(x2, 3) + Iext2 + bb * g - 0.3 * (z - 3.5) + Kf * c_pop2)); + y2 += dt * (tt * (-y2 + ydot4) / tau); + g += dt * (tt * (-0.01 * (g - 0.1 * x1) )); + + // Add noise (if noise components are present in model), integrate with stochastic forward euler and wrap it up + x1 += nsig * curand_normal2(&crndst).x; + y1 += nsig * curand_normal2(&crndst).x; + z += nsig * curand_normal2(&crndst).x; + x2 += nsig * curand_normal2(&crndst).x; + y2 += nsig * curand_normal2(&crndst).x; + g += nsig * curand_normal2(&crndst).x; + + // Wrap it within the limits of the model + x1 = wrap_it_x1(x1); + y1 = wrap_it_y1(y1); + z = wrap_it_z(z); + x2 = wrap_it_x2(x2); + y2 = wrap_it_y2(y2); + g = wrap_it_g(g); + + // Update the state + state((t + 1) % nh, i_node + 0 * n_node) = x1; + state((t + 1) % nh, i_node + 1 * n_node) = y1; + state((t + 1) % nh, i_node + 2 * n_node) = z; + state((t + 1) % nh, i_node + 3 * n_node) = x2; + state((t + 1) % nh, i_node + 4 * n_node) = y2; + state((t + 1) % nh, i_node + 5 * n_node) = g; + + // Update the observable only for the last timestep + if (t == (i_step + n_step - 1)){ + tavg(i_node + 0 * n_node) = x1; + } + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + + } // for i_node + } // for t + +// cleanup macros/*{{{*/ +#undef params +#undef state +#undef tavg/*}}}*/ + +} // kernel integrate \ No newline at end of file diff --git a/dsl_cuda/CUDAmodels/kuramoto.c b/dsl_cuda/CUDAmodels/kuramoto.c new file mode 100755 index 0000000000..c6bff4ed6f --- /dev/null +++ b/dsl_cuda/CUDAmodels/kuramoto.c @@ -0,0 +1,146 @@ +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + +#include +#include +#include + +__device__ float wrap_it_PI(float x) +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +} + +__global__ void Kuramoto( + + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, float * __restrict__ weights, float * __restrict__ lengths, + float * __restrict__ params_pwi, // pwi: per work item + // state + float * __restrict__ state_pwi, + // outputs + float * __restrict__ tavg_pwi + ) +{ + // work id & size + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y; + +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) * 1 * n_node + (i_node))*size + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + + // unpack params + // These are the two parameters which are usually explore in fitting in this model + const float global_speed = params(0); + const float global_coupling = params(1); + + // regular constants + const float omega = 60.0 * 2.0 * M_PI_F / 1e3; + + // coupling constants, coupling itself is hardcoded in kernel + const float a = 1; + + // coupling parameters + float c_0 = 0.0; + + // derived parameters + const float rec_n = 1.0f / n_node; + const float rec_speed_dt = 1.0f / global_speed / (dt); + const float nsig = sqrt(dt) * sqrt(2.0 * 1e-5); + + + + curandState crndst; + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &crndst); + + float V = 0.0; + + //***// This is only initialization of the observable + for (unsigned int i_node = 0; i_node < n_node; i_node++) + { + tavg(i_node) = 0.0f; + if (i_step == 0){ + state(i_step, i_node) = 0.001; + } + } + + //***// This is the loop over time, should stay always the same + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + //***// This is the loop over nodes, which also should stay the same + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + { + c_0 = 0.0f; + + V = state((t) % nh, i_node + 0 * n_node); + + // This variable is used to traverse the weights and lengths matrix, which is really just a vector. It is just a displacement. / + unsigned int i_n = i_node * n_node; + + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + //***// Get the weight of the coupling between node i and node j + float wij = weights[i_n + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + + //***// Get the delay between node i and node j + unsigned int dij = lengths[i_n + j_node] * rec_speed_dt; + + //***// Get the state of node j which is delayed by dij + float V_j = state(((t - dij + nh) % nh), j_node + 0 * n_node); + + // Sum it all together using the coupling function. Kuramoto coupling: (postsyn * presyn) == ((a) * (sin(xj - xi))) + c_0 += wij * a * sin(V_j - V); + + } // j_node */ + + // rec_n is used for the scaling over nodes + c_0 *= global_coupling * rec_n; + + // This is dynamics step and the update in the state of the node + V += dt * (omega + c_0); + + // Add noise (if noise components are present in model), integrate with stochastic forward euler and wrap it up + V += nsig * curand_normal2(&crndst).x; + + // Wrap it within the limits of the model + V = wrap_it_PI(V); + + // Update the state + state((t + 1) % nh, i_node + 0 * n_node) = V; + + // Update the observable only for the last timestep + if (t == (i_step + n_step - 1)){ + tavg(i_node + 0 * n_node) = sin(V); + } + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + + } // for i_node + } // for t + +// cleanup macros/*{{{*/ +#undef params +#undef state +#undef tavg/*}}}*/ + +} // kernel integrate \ No newline at end of file diff --git a/dsl_cuda/CUDAmodels/kuramotoref.c b/dsl_cuda/CUDAmodels/kuramotoref.c new file mode 100755 index 0000000000..7c562c9d13 --- /dev/null +++ b/dsl_cuda/CUDAmodels/kuramotoref.c @@ -0,0 +1,128 @@ +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + +#include // for printf +#include +#include + +__device__ float wrap_2_pi_(float x)/*{{{*/ +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +}/*}}}*/ + +__device__ float wrap_2_pi(float x) // not divergent/*{{{*/ +{ + bool lt_0 = x < 0.0f; + bool gt_2pi = x > PI_2; + return (x + PI_2)*lt_0 + x*(!lt_0)*(!gt_2pi) + (x - PI_2)*gt_2pi; +}/*}}}*/ + +__global__ void Kuramotoref(/*{{{*/ + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, + float * __restrict__ weights, + float * __restrict__ lengths, + float * __restrict__ params_pwi, // pwi: per work item + // state + float * __restrict__ state_pwi, + // outputs + float * __restrict__ tavg_pwi + ) +{/*}}}*/ + + // work id & size/*{{{*/ + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y;/*}}}*/ + + // ND array accessors (TODO autogen from py shape info)/*{{{*/ +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) * n_node + (i_node))*size + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id])/*}}}*/ + + // unpack params/*{{{*/ + //***// These are the two parameters which are usually explore in fitting in this model + const float global_coupling = params(1); + const float global_speed = params(0);/*}}}*/ + + // derived/*{{{*/ + const float rec_n = 1.0f / n_node; + //***// The speed affects the delay and speed_value is a parameter which is usually explored in fitting *** + const float rec_speed_dt = 1.0f / global_speed / (dt); + //***// This is a parameter specific to the Kuramoto model + const float omega = 60.0 * 2.0 * M_PI_F / 1e3; + //***// This is a parameter for the stochastic integration step, you can leave this constant for the moment + const float sig = sqrt(dt) * sqrt(2.0 * 1e-5);/*}}}*/ //-->noise sigma value + + curandState s; + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &s); + + //***// This is only initialization of the observable + for (unsigned int i_node = 0; i_node < n_node; i_node++) + tavg(i_node) = 0.0f; + + //***// This is the loop over time, should stay always the same + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + //***// This is the loop over nodes, which also should stay the same + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + { + //***// We here gather the current state of the node + float theta_i = state(t % NH, i_node); + //***// This variable is used to traverse the weights and lengths matrix, which is really just a vector. It is just a displacement. + unsigned int i_n = i_node * n_node; + float coupling_value = 0.0f; + + //***// For all nodes that are not the current node (i_node) sum the coupling + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + //***// Get the weight of the coupling between node i and node j + float wij = weights[i_n + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + //***// Get the delay between node i and node j + unsigned int dij = lengths[i_n + j_node] * rec_speed_dt; + //***// Get the state of node j which is delayed by dij + float theta_j = state((t - dij + NH) % NH, j_node); + //***// Sum it all together using the coupling function. This is a kuramoto coupling so: a * sin(pre_syn - post_syn) + coupling_value += wij * sin(theta_j - theta_i); + } // j_node + + //***// This is actually the integration step and the update in the state of the node + theta_i += dt * (omega + global_coupling * rec_n * coupling_value); + //***// We add some noise if noise is selected + theta_i += sig * curand_normal2(&s).x; + //***// Wrap it within the limits of the model (0-2pi) + theta_i = wrap_2_pi_(theta_i); + //***// Update the state + state((t + 1) % NH, i_node) = theta_i; + //***// Update the observable + tavg(i_node) = sin(theta_i); + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + + } // for i_node + } // for t + +// cleanup macros/*{{{*/ +#undef params +#undef state +#undef tavg/*}}}*/ + +} // kernel integrate \ No newline at end of file diff --git a/dsl_cuda/CUDAmodels/network.no_defines.c b/dsl_cuda/CUDAmodels/network.no_defines.c new file mode 100755 index 0000000000..dfc106cd08 --- /dev/null +++ b/dsl_cuda/CUDAmodels/network.no_defines.c @@ -0,0 +1,285 @@ +#include // for printf +#include +#include + +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + +#ifdef RAND123/*{{{*/ +#include "Random123/threefry.h" +#include "Random123/boxmuller.hpp" + +struct rng_state +{ + threefry4x32_ctr_t ctr; + threefry4x32_key_t key; + long long int seed; + float out[4]; +}; + +__device__ void rng_gen_normal(struct rng_state *r) +{ + threefry4x32_ctr_t result; + r123::float2 normal; + + ++r->ctr.v[0]; + + result = threefry4x32(r->ctr, r->key); + + normal = r123::boxmuller(result.v[0], result.v[1]); + + r->out[0] = normal.x; + r->out[1] = normal.y; + + normal = r123::boxmuller(result.v[2], result.v[3]); + r->out[2] = normal.x; + r->out[3] = normal.y; +} + +__device__ void rng_init(struct rng_state *r, int seed1, int seed2) +{ + r->ctr[0] = 0; + r->ctr[1] = 0; + r->ctr[2] = seed1; + r->ctr[3] = seed2; +} + +__device__ float rng_next_normal(struct rng_state *r) +{ + const int count = r->ctr.v[0] % 4; + if (count == 0) + rng_gen_normal(r); + return r->out[count]; +} +#endif //RANDOM123/*}}}*/ + +#ifdef CURAND/*{{{*/ +#include +#include +#endif //CURAND/*}}}*/ + +__device__ float wrap_2_pi_(float x)/*{{{*/ +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +}/*}}}*/ + +__device__ float wrap_2_pi(float x) // not divergent/*{{{*/ +{ + bool lt_0 = x < 0.0f; + bool gt_2pi = x > PI_2; + return (x + PI_2)*lt_0 + x*(!lt_0)*(!gt_2pi) + (x - PI_2)*gt_2pi; +}/*}}}*/ + +__global__ void integrate(/*{{{*/ + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, + float * __restrict__ weights, + float * __restrict__ lengths, + float * __restrict__ params_pwi, // pwi: per work item + // state + float * __restrict__ state_pwi, + // outputs + float * __restrict__ tavg_pwi + ) +{/*}}}*/ + + // work id & size/*{{{*/ + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y;/*}}}*/ + + // ND array accessors (TODO autogen from py shape info)/*{{{*/ +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) * n_node + (i_node))*size + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id])/*}}}*/ + + // unpack params/*{{{*/ + const float coupling_value = params(1); + const float speed_value = params(0);/*}}}*/ + + // derived/*{{{*/ + const float rec_n = 1.0f / n_node; + const float rec_speed_dt = 1.0f / speed_value / dt; + const float omega = 10.0 * 2.0 * M_PI_F / 1e3; + const float sig = sqrt(dt) * sqrt(2.0 * 1e-5);/*}}}*/ + + curandState s; + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &s); + + for (unsigned int i_node = 0; i_node < n_node; i_node++) + tavg(i_node) = 0.0f; + + for (unsigned int t = i_step; t < (i_step + n_step); t++) { + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) { + if(i_node >= n_node) continue; + + float theta_i = state(t % NH, i_node); + unsigned int i_n = i_node * n_node; + float sum = 0.0f; + + for (unsigned int j_node = 0; j_node < n_node; j_node++) { + float wij = weights[i_n + j_node]; // nb. not coalesced + if(lengths[i_n + j_node]>0 && i_node>0 && threadIdx.x == 0) + // printf("%d %d %d %f\n", t, i_n + j_node, i_node, lengths[i_n + j_node]); + + if (wij == 0.0) continue; + unsigned int dij = lengths[i_n + j_node] * rec_speed_dt; + unsigned time = (t - dij + NH) % NH; + float theta_j = state_pwi[(time * n_node + j_node)*size + id]; + sum += wij * sin(theta_j - theta_i); + } // j_node + theta_i += dt * (omega + coupling_value * rec_n * sum); + + theta_i += sig * curand_normal2(&s).x; + + theta_i = wrap_2_pi(theta_i); + state((t + 1) % NH, i_node) = theta_i; + tavg(i_node) += sin(theta_i); + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + + } // for i_node + } // for t + +// cleanup macros/*{{{*/ +#undef params +#undef state +#undef tavg/*}}}*/ + +} // kernel integrate + +// const float w_plus=1.4f; +// const float a_E=310.0f; +// const float b_E=125.0f; +// const float d_E=0.16f; +// const float a_I=615.0f; +// const float b_I=177.0f; +// const float d_I=0.087f; +// // const float gamma_E=0.641f / 1000.0f; +// // const float tau_E=100.0f; +// // const float tau_I=10.0f; +// const float I_0=0.382f; +// const float w_E=1.0f; +// const float w_I=0.7f; +// // const float gamma_I= 1.0f / 1000.0f; +// const float min_d_E = (-1.0f * d_E); +// const float min_d_I = (-1.0f * d_I); +// // const float imintau_E = (-1.0f / tau_E); +// // const float imintau_I = (-1.0f / tau_I); +// const float w_E__I_0 = (w_E * I_0); +// const float w_I__I_0 = (w_I * I_0); + +// __global__ void integrate_wongwang( +// // config +// unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, +// float dt, float speed, +// float * weights, +// float * lengths, +// float * params_pwi, // pwi: per work item +// // state +// float * state_pwi, +// // outputs +// float * tavg_pwi +// ) +// { +// // const int i_step_dev = i_step; +// // const int n_node_dev = n_node; +// // work id & size +// const unsigned int id = (blockIdx.x * blockDim.x) + threadIdx.x; +// const unsigned int size = blockDim.x * gridDim.x; + +// // ND array accessors (TODO autogen from py shape info) +// #define params(i_par) (params_pwi[(size * (i_par)) + id]) +// #define state(time, i_node) (state_pwi[((time) *2 * n_node + (i_node))*(size) + id]) +// #define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + +// // unpack params +// const float G = params(1); +// const float J_NMDA = params(0); +// const float G_J_NMDA = G*J_NMDA; +// // derived + +// const float w_plus__J_NMDA = (w_plus * J_NMDA); +// const float sig = sqrt(dt) * sqrt(2.0 * 1e-5); +// // We have three variables which could be changed here. Actually 4 +// // G (the global coupling), sigma (the noise), J_NMDA(the excitatory synaptic coupling) and J_i(the inner inhibition for each region) +// // For now we are making things simple and only change two parameters, G and J_NMDA. +// #ifdef RAND123 +// // rng +// struct rng_state rng; +// rng_init(&rng, id, i_step); +// #endif +// #ifdef CURAND +// curandState s; +// curand_init(id * (blockDim.x * gridDim.x), 0, 0, &s); +// #endif +// float tmp_I_E; +// float tmp_H_E; +// float tmp_I_I; +// float tmp_H_I; +// float sum; + + +// for (unsigned int i_node = 0; i_node < n_node; i_node++) +// tavg(i_node) = 0.0f; + +// for (unsigned int t = i_step; t < (i_step + n_step); t++) +// { +// for (unsigned int i_node = 0; i_node < n_node; i_node++) +// { +// sum = 0.0f; +// float S_E = state((t) % nh, i_node); +// float S_I = state((t) % nh, i_node + n_node); +// for (unsigned int j_node = 0; j_node < n_node; j_node++) +// { +// //we are not considering delays in this model +// float wij = G_J_NMDA*weights[(i_node*n_node) + j_node]; // nb. not coalesced +// if (wij == 0.0) +// continue; +// sum += wij * state((t) % nh, j_node); //of J +// } +// // external Input set to 0, no task evoked activity +// tmp_I_E = S_I; // Inner inhibition set to 1 +// tmp_I_E = sum - tmp_I_E ; +// tmp_I_E = ((w_E__I_0)+(w_plus__J_NMDA * S_E)) + tmp_I_E ; +// tmp_I_E = a_E * tmp_I_E ; +// tmp_I_E = tmp_I_E - b_E; +// tmp_I_E = (a_E * (((w_E__I_0)+(w_plus__J_NMDA * S_E))+( sum-(S_I))))-b_E; +// tmp_H_E = tmp_I_E/(1-expf(min_d_E * tmp_I_E)); +// //meanFR[i] += tmp_H_E; Not storing mean firing rate +// // r_Edd_i = tmp_H_E; not observing the firing rate for now +// tmp_I_I = (a_I*(((w_I__I_0)+(J_NMDA * S_E))-( S_I)))-b_I; +// tmp_H_I = tmp_I_I/(1-expf(min_d_I*tmp_I_I)); +// // r_I[i] = tmp_H_I; not observing the firing rate for now + +// #ifdef RAND123 +// S_E = ((sig * rng_next_normal(&rng))+S_E)+(dt*((imintau_E* S_E)+(tmp_H_E*((1-S_E)*gamma_E)))); +// S_I = ((sig * rng_next_normal(&rng))+S_I)+(dt*((imintau_I* S_I)+(tmp_H_I*gamma_I))); +// #endif +// #ifdef CURAND +// S_E = ((sig * curand_normal2(&s).x)+S_E)+(dt*((imintau_E* S_E)+(tmp_H_E*((1-S_E)*gamma_E)))); +// S_I = ((sig * curand_normal2(&s).x)+S_I)+(dt*((imintau_I* S_I)+(tmp_H_I*gamma_I))); +// #endif +// state((t+1) % nh, i_node) = S_E; +// state((t+1) % nh, i_node+(n_node)) = S_I; +// tavg(i_node) += S_E + S_I; +// } // for i_node +// } // for t +// } // kernel integrate +// vim: sw=4 sts=4 ts=8 et ai diff --git a/dsl_cuda/CUDAmodels/oscillator.c b/dsl_cuda/CUDAmodels/oscillator.c new file mode 100755 index 0000000000..ffc1c66f33 --- /dev/null +++ b/dsl_cuda/CUDAmodels/oscillator.c @@ -0,0 +1,180 @@ +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + +#include +#include +#include + +__device__ float wrap_it_PI(float x) +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +} +__device__ float wrap_it_V(float V) +{ + float Vdim[] = {-2.0, 4.0}; + if (V < Vdim[0]) V = Vdim[0]; + else if (V > Vdim[1]) V = Vdim[1]; + + return V; +} +__device__ float wrap_it_W(float W) +{ + float Wdim[] = {-6.0, 6.0}; + if (W < Wdim[0]) W = Wdim[0]; + else if (W > Wdim[1]) W = Wdim[1]; + + return W; +} + +__global__ void Oscillator( + + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, float * __restrict__ weights, float * __restrict__ lengths, + float * __restrict__ params_pwi, // pwi: per work item + // state + float * __restrict__ state_pwi, + // outputs + float * __restrict__ tavg_pwi + ) +{ + // work id & size + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y; + +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) * 2 * n_node + (i_node))*size + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + + // unpack params + // These are the two parameters which are usually explore in fitting in this model + const float global_speed = params(0); + const float global_coupling = params(1); + + // regular constants + const float tau = 1.0; + const float I = 0.0; + const float a = -2.0; + const float b = -10.0; + const float c = 0; + const float d = 0.02; + const float e = 3.0; + const float f = 1.0; + const float g = 0.0; + const float alpha = 1.0; + const float beta = 1.0; + const float gamma = 1.0; + + // coupling constants, coupling itself is hardcoded in kernel + const float c_a = 1; + + // coupling parameters + float c_0 = 0.0; + + // derived parameters + const float rec_n = 1 / n_node; + const float rec_speed_dt = 1.0f / global_speed / (dt); + const float nsig = sqrt(dt) * sqrt(2.0 * 1e-3); + const float lc = 0.0; + + + + curandState crndst; + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &crndst); + + float V = 0.0; + float W = 0.0; + + //***// This is only initialization of the observable + for (unsigned int i_node = 0; i_node < n_node; i_node++) + { + tavg(i_node) = 0.0f; + if (i_step == 0){ + state(i_step, i_node) = 0.001; + } + } + + //***// This is the loop over time, should stay always the same + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + //***// This is the loop over nodes, which also should stay the same + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + { + c_0 = 0.0f; + + V = state((t) % nh, i_node + 0 * n_node); + W = state((t) % nh, i_node + 1 * n_node); + + // This variable is used to traverse the weights and lengths matrix, which is really just a vector. It is just a displacement. / + unsigned int i_n = i_node * n_node; + + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + //***// Get the weight of the coupling between node i and node j + float wij = weights[i_n + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + + //***// Get the delay between node i and node j + unsigned int dij = lengths[i_n + j_node] * rec_speed_dt; + + //***// Get the state of node j which is delayed by dij + float V_j = state(((t - dij + nh) % nh), j_node + 0 * n_node); + + // Sum it all together using the coupling function. Kuramoto coupling: (postsyn * presyn) == ((a) * (sin(xj - xi))) + c_0 += wij * c_a * sin(V_j - V); + + } // j_node */ + + // rec_n is used for the scaling over nodes + c_0 *= global_coupling; + + // This is dynamics step and the update in the state of the node + V += dt * (d * tau * (alpha * W - f * powf(V, 3) + e * powf(V, 2) + g * V + gamma * I + gamma * c_0 + lc * V)); + W += dt * (d * (a + b * V + c * powf(V, 2) - beta * W) / tau); + + // Add noise (if noise components are present in model), integrate with stochastic forward euler and wrap it up + V += nsig * curand_normal2(&crndst).x; + W += nsig * curand_normal2(&crndst).x; + + // Wrap it within the limits of the model + V = wrap_it_V(V); + W = wrap_it_W(W); + + // Update the state + state((t + 1) % nh, i_node + 0 * n_node) = V; + state((t + 1) % nh, i_node + 1 * n_node) = W; + + // Update the observable only for the last timestep + if (t == (i_step + n_step - 1)){ + tavg(i_node + 0 * n_node) = V; + } + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + + } // for i_node + } // for t + +// cleanup macros/*{{{*/ +#undef params +#undef state +#undef tavg/*}}}*/ + +} // kernel integrate \ No newline at end of file diff --git a/dsl_cuda/CUDAmodels/oscillatorref.c b/dsl_cuda/CUDAmodels/oscillatorref.c new file mode 100755 index 0000000000..6f6e29a288 --- /dev/null +++ b/dsl_cuda/CUDAmodels/oscillatorref.c @@ -0,0 +1,323 @@ +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + + +#include +#include + +__device__ float wrap_2_pi_(float x)/*{{{*/ +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +}/*}}}*/ + +__device__ float wrap_2_pi(float x) // not divergent/*{{{*/ +{ + bool lt_0 = x < 0.0f; + bool gt_2pi = x > PI_2; + return (x + PI_2)*lt_0 + x*(!lt_0)*(!gt_2pi) + (x - PI_2)*gt_2pi; +}/*}}}*/ + + +const float tau = 1.0; +const float I = 0.0; +const float a = -2.0; +const float b = -10.0; +const float c = 0.0; +const float d = 0.02; +const float e = 3.0; +const float f = 1.0; +const float g = 0.0; +const float beta = 1.0; +const float alpha = 1.0; +const float gam = 1.0; + + +__global__ void Oscillatorref( + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, + float * weights, + float * lengths, + float * params_pwi, // pwi: per work item + // state + float * state_pwi, + // outputs + float * tavg_pwi + ) +{ + // work id & size + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y; + + // ND array accessors (TODO autogen from py shape info) +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) *2 * n_node + (i_node))*(size) + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + + // unpack params + + + // derived + const float sig = 0.0001; //params(0);//0.001;//sqrt(dt) * sqrt(2.0 * 1e-3); + const float sig = sqrt(dt) * sqrt(2.0 * 1e-3); + const float rec_speed_dt = params(0); + const float G = params(1); + const float lc = 0.0; + + curandState s; +// curand_init(id + (unsigned int) clock64(), 0, 0, &s); + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &s); + + double derivV = 0.0; + double derivW = 0.0; + double V = 0.0; + double W = 0.0; + + double sum = 0.0; + float wij = 0.0f; + float V_j = 0.0; + unsigned int dij = 0; + + + for (unsigned int i_node = 0; i_node < n_node; i_node++){ + tavg(i_node) = 0.0f; + if (i_step == 0){ + state(i_step, i_node) = 0.001; + } + } + + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + for (unsigned int i_node = 0; i_node < n_node; i_node++) + { + sum = 0.0f; + V = state((t) % nh, i_node); + W = state((t) % nh, i_node + n_node); + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + float wij = weights[(i_node*n_node) + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + dij = lengths[(i_node*n_node) + j_node] * rec_speed_dt; + V_j = state((t - dij + NH) % NH, j_node); + sum += wij * sin(V_j - V); + } + sum = G*sum; // Global coupling + + derivV = d * tau * (alpha * W - f * powf(V,3) + e * powf(V,2) + g * V + gam * I + gam * sum + lc * V); + derivW = d * (a + b * V + c * powf(V,2) - beta * W) / tau; + V = (V)+(dt*(sig * curand_normal(&s)))+((derivV)); + W = (W)+(dt*(sig * curand_normal(&s)))+((derivW)); + if(V>4) V = 4; + if(W>6) W = 6; + if(V<-2) V = -2; + if(W<-6) W = -6; + state((t+1) % nh, i_node) = V; + state((t+1) % nh, i_node+(n_node)) = W; + tavg(i_node) = V; + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + } // for i_node + } // for t + // cleanup macros/*{{{*/ + #undef params + #undef state + #undef tavg/*}}}*/ +} // kernel integrate +// vim: sw=4 sts=4 ts=8 et ai + + + +/*/////Such to be in DSL. + +There is a slight difference between output of ref and dsl model. This has to do with using a temp variable (derivV..) +and directly adding the result into the state variable (which acts basically as a temp var). Due to intermediate +rounding error and float to double conversions, the outputs of the state vars can vary. In the generated model, the +temp var (derivV) has been removed. Also there is an error in the solving. The dt is not multplied with the derivative, +only to the noise. + +//////*/ + +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + +#include +#include +#include + +__device__ float wrap_it_PI(float x) +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +} +__device__ float wrap_it_V(float V) +{ + int Vdim[] = {-2.0, 4.0}; + if (V < Vdim[0]) V = Vdim[0]; + else if (V > Vdim[1]) V = Vdim[1]; + + return V; +} +__device__ float wrap_it_W(float W) +{ + int Wdim[] = {-6.0, 6.0}; + if (W < Wdim[0]) W = Wdim[0]; + else if (W > Wdim[1]) W = Wdim[1]; + + return W; +} + +__global__ void Oscillator( + + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, float * __restrict__ weights, float * __restrict__ lengths, + float * __restrict__ params_pwi, // pwi: per work item + // state + float * __restrict__ state_pwi, + // outputs + float * __restrict__ tavg_pwi + ) +{ + // work id & size + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y; + +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) * 2 * n_node + (i_node))*size + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + + // unpack params + // These are the two parameters which are usually explore in fitting in this model + const float global_speed = params(0); + const float global_coupling = params(1); + + // regular constants + const float tau = 1.0; + const float I = 0.0; + const float a = -2.0; + const float b = -10.0; + const float c = 0; + const float d = 0.02; + const float e = 3.0; + const float f = 1.0; + const float g = 0.0; + const float alpha = 1.0; + const float beta = 1.0; + const float gamma = 1.0; + + // coupling constants, coupling itself is hardcoded in kernel + const float c_a = 1; + + // coupling parameters + float c_0 = 0.0; + + // derived parameters + const float rec_n = 1 / n_node; +// const float rec_speed_dt = 1.0f / global_speed / (dt); + const float rec_speed_dt = global_speed; + const float nsig = sqrt(dt) * sqrt(2.0 * 1e-3); + const float lc = 0.0; + + + + curandState crndst; + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &crndst); + + float V = 0.0; + float W = 0.0; + float coupling = 0.0f; + + float wij = 0.0f; + float V_j = 0.0; + unsigned int dij = 0; + + //***// This is only initialization of the observable + for (unsigned int i_node = 0; i_node < n_node; i_node++) + { + tavg(i_node) = 0.0f; + if (i_step == 0){ + state(i_step, i_node) = 0.001; + } + } + + //***// This is the loop over time, should stay always the same + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + for (unsigned int i_node = 0; i_node < n_node; i_node++) + { + unsigned int i_n = i_node * n_node; + + c_0 = 0.0f; + V = state((t) % nh, i_node); + W = state((t) % nh, i_node + n_node); + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + float wij = weights[(i_n) + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + dij = lengths[(i_n) + j_node] * rec_speed_dt; + V_j = state((t - dij + NH) % NH, j_node); + c_0 += wij * c_a * sin(V_j - V); + } + c_0 *= global_coupling; // Global coupling + + // +// // This is dynamics step and the update in the state of the node + V += dt * (d * tau * (alpha * W - f * powf(V, 3) + e * powf(V, 2) + g * V + gamma * I + gamma * c_0 + lc * V)); + W += dt * (d * (a + b * V + c * powf(V, 2) - beta * W) / tau); + + V += nsig * curand_normal(&crndst); + W += nsig * curand_normal(&crndst); + + V = wrap_it_V(V); + W = wrap_it_W(W); + + state((t+1) % nh, i_node) = V; + state((t+1) % nh, i_node+(n_node)) = W; + tavg(i_node) = V; + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + + } // for i_node + } // for t + +// cleanup macros/*{{{*/ +#undef params +#undef state +#undef tavg/*}}}*/ + +} // kernel integrate diff --git a/dsl_cuda/CUDAmodels/refs/balloon.c b/dsl_cuda/CUDAmodels/refs/balloon.c new file mode 100755 index 0000000000..c3a6483d44 --- /dev/null +++ b/dsl_cuda/CUDAmodels/refs/balloon.c @@ -0,0 +1,67 @@ +// defaults from Stefan 2007, cf tvb/analyzers/fmri_balloon.py +#define TAU_S 0.65f +#define TAU_F 0.41f +#define TAU_O 0.98f +#define ALPHA 0.32f +#define TE 0.04f +#define V0 4.0f +#define E0 0.4f +#define EPSILON 0.5f +#define NU_0 40.3f +#define R_0 25.0f + +#define RECIP_TAU_S (1.0f / TAU_S) +#define RECIP_TAU_F (1.0f / TAU_F) +#define RECIP_TAU_O (1.0f / TAU_O) +#define RECIP_ALPHA (1.0f / ALPHA) +#define RECIP_E0 (1.0f / E0) + +// "derived parameters" +#define k1 (4.3f * NU_0 * E0 * TE) +#define k2 (EPSILON * R_0 * E0 * TE) +#define k3 (1.0f - EPSILON) + +__global__ void bold_update(int n_node, float dt, + // bold.shape = (4, n_nodes, n_threads) + float * __restrict__ bold_state, + // nrl.shape = (n_nodes, n_threads) + float * __restrict__ neural_state, + // out.shape = (n_nodes, n_threads) + float * __restrict__ out) +{ + const unsigned int it = (blockIdx.x * blockDim.x) + threadIdx.x; + const unsigned int nt = blockDim.x * gridDim.x; + + int var_stride = n_node * nt; + for (int i_node=0; i_node < n_node; i_node++) + { + float *node_bold = bold_state + i_node * nt + it; + + float s = node_bold[0 * var_stride]; + float f = node_bold[1 * var_stride]; + float v = node_bold[2 * var_stride]; + float q = node_bold[3 * var_stride]; + + float x = neural_state[i_node * nt + it]; + + float ds = x - RECIP_TAU_S * s - RECIP_TAU_F * (f - 1.0f); + float df = s; + float dv = RECIP_TAU_O * (f - pow(v, RECIP_ALPHA)); + float dq = RECIP_TAU_O * (f * (1.0f - pow(1.0f - E0, 1.0f / f)) + * RECIP_E0 - pow(v, RECIP_ALPHA) * (q / v)); + + s += dt * ds; + f += dt * df; + v += dt * dv; + q += dt * dq; + + node_bold[0 * var_stride] = s; + node_bold[1 * var_stride] = f; + node_bold[2 * var_stride] = v; + node_bold[3 * var_stride] = q; + + out[i_node * nt + it] = V0 * ( k1 * (1.0f - q ) + + k2 * (1.0f - q / v) + + k3 * (1.0f - v) ); + } // i_node +} // kernel diff --git a/dsl_cuda/CUDAmodels/refs/covar.c b/dsl_cuda/CUDAmodels/refs/covar.c new file mode 100755 index 0000000000..c158a8b274 --- /dev/null +++ b/dsl_cuda/CUDAmodels/refs/covar.c @@ -0,0 +1,109 @@ +#include +#ifdef TEST_COVAR +#include +#endif + +// stable one-pass co-moment algo, cf wikipedia + +#ifndef TEST_COVAR +__global__ +#endif +void update_cov( + unsigned int i_sample, + unsigned int n_node, + float * __restrict__ cov, + float * __restrict__ means, + const float * __restrict__ data +) +{ +#ifdef TEST_COVAR + const unsigned int it = 0; + const unsigned int nt = 1; +#else + const unsigned int it = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int nt = blockDim.x * gridDim.x * gridDim.y; +#endif + + if (i_sample == 0) + { + for (int i_node = 0; i_node < n_node; i_node++) + means[i_node * nt + it] = data[i_node * nt + it]; + return; + } + + const float recip_n = 1.0f / i_sample; + + // double buffer to avoid copying memory + float *next_mean = means, *prev_mean = means; + if ((i_sample % 2) == 0) { + prev_mean += n_node * nt; + } else { + next_mean += n_node * nt; + } + + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node += blockDim.y) + { + if (i_node >= n_node) continue; + + int i_idx = i_node * nt + it; + next_mean[i_idx] = prev_mean[i_idx] + (data[i_idx] - prev_mean[i_idx]) * recip_n; + } + + // TODO shared mem useful here? + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node += blockDim.y) + { + if (i_node >= n_node) continue; + + int i_idx = i_node * nt + it; + float data_mean_i = data[i_idx] - prev_mean[i_idx]; + + for (int j_node = 0; j_node < n_node; ++j_node) + { + int j_idx = j_node * nt + it; + float data_mean_j = data[j_idx] - next_mean[j_idx]; + int cij_idx = (j_node * n_node + i_node) * nt + it; + cov[cij_idx] += data_mean_j * data_mean_i; + } + } +} + +#ifndef TEST_COVAR +__global__ +#endif +void cov_to_corr( + unsigned int n_sample, + unsigned int n_node, + float * __restrict__ cov, + float * __restrict__ corr +) +{ +#ifdef TEST_COVAR + const unsigned int it = 0; + const unsigned int nt = 1; +#else + const unsigned int it = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int nt = blockDim.x * gridDim.x * gridDim.y; +#endif + + float recip_n_samp = 1.0f / n_sample; + + // normalize comoment to covariance + for (unsigned int ij = 0; ij < (n_node * n_node); ++ij) + cov[ij*nt + it] *= recip_n_samp; + + // compute correlation coefficient +#define COV(i, j) cov[((i)*n_node + (j))*nt + it] +#define CORR(i, j) corr[((i)*n_node + (j))*nt + it] + + for (unsigned int i = threadIdx.y; i < n_node; i += blockDim.y) + { + if (i >= n_node) continue; + + float var_i = COV(i, i); + for (unsigned int j = 0; j < n_node; ++j) + { + float var_j = COV(j, j); + CORR(i, j) = COV(i, j) * rsqrtf(var_i * var_j); + } + } +} diff --git a/dsl_cuda/CUDAmodels/refs/kuramoto_network.c b/dsl_cuda/CUDAmodels/refs/kuramoto_network.c new file mode 100755 index 0000000000..da2cdb88db --- /dev/null +++ b/dsl_cuda/CUDAmodels/refs/kuramoto_network.c @@ -0,0 +1,128 @@ +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + +#include // for printf +#include +#include + +__device__ float wrap_2_pi_(float x)/*{{{*/ +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +}/*}}}*/ + +__device__ float wrap_2_pi(float x) // not divergent/*{{{*/ +{ + bool lt_0 = x < 0.0f; + bool gt_2pi = x > PI_2; + return (x + PI_2)*lt_0 + x*(!lt_0)*(!gt_2pi) + (x - PI_2)*gt_2pi; +}/*}}}*/ + +__global__ void integrate(/*{{{*/ + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, + float * __restrict__ weights, + float * __restrict__ lengths, + float * __restrict__ params_pwi, // pwi: per work item + // state + float * __restrict__ state_pwi, + // outputs + float * __restrict__ tavg_pwi + ) +{/*}}}*/ + + // work id & size/*{{{*/ + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y;/*}}}*/ + + // ND array accessors (TODO autogen from py shape info)/*{{{*/ +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) * n_node + (i_node))*size + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id])/*}}}*/ + + // unpack params/*{{{*/ + //***// These are the two parameters which are usually explore in fitting in this model + const float global_coupling = params(1); + const float global_speed = params(0);/*}}}*/ + + // derived/*{{{*/ + const float rec_n = 1.0f / n_node; + //***// The speed affects the delay and speed_value is a parameter which is usually explored in fitting *** + const float rec_speed_dt = 1.0f / global_speed / (dt); + //***// This is a parameter specific to the Kuramoto model + const float omega = 60.0 * 2.0 * M_PI_F / 1e3; + //***// This is a parameter for the stochastic integration step, you can leave this constant for the moment + const float sig = sqrt(dt) * sqrt(2.0 * 1e-5);/*}}}*/ //-->noise sigma value + + curandState s; + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &s); + + //***// This is only initialization of the observable + for (unsigned int i_node = 0; i_node < n_node; i_node++) + tavg(i_node) = 0.0f; + + //***// This is the loop over time, should stay always the same + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + //***// This is the loop over nodes, which also should stay the same + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + { + //***// We here gather the current state of the node + float theta_i = state(t % NH, i_node); + //***// This variable is used to traverse the weights and lengths matrix, which is really just a vector. It is just a displacement. + unsigned int i_n = i_node * n_node; + float sum = 0.0f; + + //***// For all nodes that are not the current node (i_node) sum the coupling + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + //***// Get the weight of the coupling between node i and node j + float wij = weights[i_n + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + //***// Get the delay between node i and node j + unsigned int dij = lengths[i_n + j_node] * rec_speed_dt; + //***// Get the state of node j which is delayed by dij + float theta_j = state((t - dij + NH) % NH, j_node); + //***// Sum it all together using the coupling function. This is a kuramoto coupling so: a * sin(pre_syn - post_syn) + coupling_value += wij * sin(theta_j - theta_i); + } // j_node + + //***// This is actually the integration step and the update in the state of the node + theta_i += dt * (omega + global_coupling * rec_n * coupling_value); + //***// We add some noise if noise is selected + theta_i += sig * curand_normal2(&s).x; + //***// Wrap it within the limits of the model (0-2pi) + theta_i = wrap_2_pi_(theta_i); + //***// Update the state + state((t + 1) % NH, i_node) = theta_i; + //***// Update the observable + tavg(i_node) = sin(theta_i); + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + + } // for i_node + } // for t + +// cleanup macros/*{{{*/ +#undef params +#undef state +#undef tavg/*}}}*/ + +} // kernel integrate \ No newline at end of file diff --git a/dsl_cuda/CUDAmodels/refs/network.c b/dsl_cuda/CUDAmodels/refs/network.c new file mode 100755 index 0000000000..9b1e1b4c4c --- /dev/null +++ b/dsl_cuda/CUDAmodels/refs/network.c @@ -0,0 +1,121 @@ +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + + +#include +#include + +__device__ float wrap_2_pi_(float x)/*{{{*/ +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +}/*}}}*/ + +__device__ float wrap_2_pi(float x) // not divergent/*{{{*/ +{ + bool lt_0 = x < 0.0f; + bool gt_2pi = x > PI_2; + return (x + PI_2)*lt_0 + x*(!lt_0)*(!gt_2pi) + (x - PI_2)*gt_2pi; +}/*}}}*/ + +__global__ void integrate(/*{{{*/ + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, + float * __restrict__ rand_omega, + float * __restrict__ weights, + float * __restrict__ lengths, + float * __restrict__ params_pwi, // pwi: per work item + // state + float * __restrict__ state_pwi, + // outputs + float * __restrict__ tavg_pwi + ) +{/*}}}*/ + + // work id & size/*{{{*/ + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y;/*}}}*/ + + // ND array accessors (TODO autogen from py shape info)/*{{{*/ +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) * n_node + (i_node))*size + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id])/*}}}*/ + + // unpack params/*{{{*/ + const float coupling_value = params(1); + const float speed_value = params(0);/*}}}*/ + + // derived/*{{{*/ + const float rec_n = 1.0f / n_node; + const float rec_speed_dt = 1.0f / speed_value / (dt); + //const float omega = 60.0 * 2.0 * M_PI_F / 1e3; + //const float omega = 1.0 * 2.0 * M_PI_F / 1e3; + float omega = 0.0; + const float sig = sqrt(dt) * sqrt(2.0 * 1e-5);/*}}}*/ //-->noise sigma value + + curandState s; + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &s); + + + for (unsigned int i_node = 0; i_node < n_node; i_node++) + tavg(i_node) = 0.0f; + + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + { + if (i_node >= n_node) continue; + + float theta_i = state(t % NH, i_node); + unsigned int i_n = i_node * n_node; + float sum = 0.0f; + + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + float wij = weights[i_n + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + unsigned int dij = lengths[i_n + j_node] * rec_speed_dt; + // int dij = 5; + float theta_j = state((t - dij + NH) % NH, j_node); + sum += wij * sin(theta_j - theta_i); + } // j_node + + omega = rand_omega[i_node]; + + theta_i += dt * (omega + coupling_value * rec_n * sum); + theta_i += sig * curand_normal2(&s).x; + theta_i = wrap_2_pi_(theta_i); + state((t + 1) % NH, i_node) = theta_i; + tavg(i_node) = sin(theta_i); //removed += + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + + } // for i_node + } // for t + +// cleanup macros/*{{{*/ +#undef params +#undef state +#undef tavg/*}}}*/ + +} // kernel integrate + diff --git a/dsl_cuda/CUDAmodels/refs/network_2d.c b/dsl_cuda/CUDAmodels/refs/network_2d.c new file mode 100755 index 0000000000..e84e4910d4 --- /dev/null +++ b/dsl_cuda/CUDAmodels/refs/network_2d.c @@ -0,0 +1,142 @@ +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + + +#include +#include + +__device__ float wrap_2_pi_(float x)/*{{{*/ +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +}/*}}}*/ + +__device__ float wrap_2_pi(float x) // not divergent/*{{{*/ +{ + bool lt_0 = x < 0.0f; + bool gt_2pi = x > PI_2; + return (x + PI_2)*lt_0 + x*(!lt_0)*(!gt_2pi) + (x - PI_2)*gt_2pi; +}/*}}}*/ + + +const float tau = 1.0; +const float I = 0.0; +const float a = -2.0; +const float b = -10.0; +const float c = 0.0; +const float d = 0.02; +const float e = 3.0; +const float f = 1.0; +const float g = 0.0; +const float beta = 1.0; +const float alpha = 1.0; +const float gam = 1.0; + + +__global__ void integrate_2d( + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, + float * weights, + float * lengths, + float * params_pwi, // pwi: per work item + // state + float * state_pwi, + // outputs + float * tavg_pwi + ) +{ + // work id & size + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y; + + // ND array accessors (TODO autogen from py shape info) +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) *2 * n_node + (i_node))*(size) + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + + // unpack params + + + // derived + const float sig = 0.0001; //params(0);//0.001;//sqrt(dt) * sqrt(2.0 * 1e-3); + const float rec_speed_dt = params(0); + const float G = params(1); + const float lc = 0.0; + + curandState s; + curand_init(id + (unsigned int) clock64(), 0, 0, &s); + + double derivV = 0.0; + double derivW = 0.0; + double V = 0.0; + double W = 0.0; + + double sum = 0.0; + float wij = 0.0f; + float V_j = 0.0; + unsigned int dij = 0; + + + for (unsigned int i_node = 0; i_node < n_node; i_node++){ + tavg(i_node) = 0.0f; + if (i_step == 0){ + state(i_step, i_node) = 0.001; + } + } + + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + for (unsigned int i_node = 0; i_node < n_node; i_node++) + { + sum = 0.0f; + V = state((t) % nh, i_node); + W = state((t) % nh, i_node + n_node); + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + float wij = weights[(i_node*n_node) + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + dij = lengths[(i_node*n_node) + j_node] * rec_speed_dt; + V_j = state((t - dij + NH) % NH, j_node); + sum += wij * sin(V_j - V); + } + sum = G*sum; // Global coupling + + derivV = d * tau * (alpha * W - f * powf(V,3) + e * powf(V,2) + g * V + gam * I + gam * sum + lc * V); + derivW = d * (a + b * V + c * powf(V,2) - beta * W) / tau; + V = (V)+(dt*(sig * curand_normal(&s)))+((derivV)); + W = (W)+(dt*(sig * curand_normal(&s)))+((derivW)); + if(V>4) V = 4; + if(W>6) W = 6; + if(V<-2) V = -2; + if(W<-6) W = -6; + state((t+1) % nh, i_node) = V; + state((t+1) % nh, i_node+(n_node)) = W; + tavg(i_node) = V; + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + } // for i_node + } // for t + // cleanup macros/*{{{*/ + #undef params + #undef state + #undef tavg/*}}}*/ +} // kernel integrate +// vim: sw=4 sts=4 ts=8 et ai diff --git a/dsl_cuda/CUDAmodels/refs/network_rww.c b/dsl_cuda/CUDAmodels/refs/network_rww.c new file mode 100755 index 0000000000..9dc0a83227 --- /dev/null +++ b/dsl_cuda/CUDAmodels/refs/network_rww.c @@ -0,0 +1,157 @@ +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + + +#include +#include + +__device__ float wrap_2_pi_(float x)/*{{{*/ +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +}/*}}}*/ + +__device__ float wrap_2_pi(float x) // not divergent/*{{{*/ +{ + bool lt_0 = x < 0.0f; + bool gt_2pi = x > PI_2; + return (x + PI_2)*lt_0 + x*(!lt_0)*(!gt_2pi) + (x - PI_2)*gt_2pi; +}/*}}}*/ + + +const float w_plus=1.4f; +const float a_E=310.0f; +const float b_E=125.0f; +const float d_E=0.154f; +const float a_I=615.0f; +const float b_I=177.0f; +const float d_I=0.087f; +const float gamma_E=0.641f / 1000.0f; +const float tau_E=100.0f; +const float tau_I=10.0f; +const float I_0=0.382f; +const float w_E=1.0f; +const float w_I=0.7f; +const float gamma_I= 1.0f / 1000.0f; +const float min_d_E = (-1.0f * d_E); +const float min_d_I = (-1.0f * d_I); +const float imintau_E = (-1.0f / tau_E); +const float imintau_I = (-1.0f / tau_I); +const float w_E__I_0 = (w_E * I_0); +const float w_I__I_0 = (w_I * I_0); + +__global__ void integrate_wongwang( + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, + float * weights, + float * lengths, + float * params_pwi, // pwi: per work item + // state + float * state_pwi, + // outputs + float * tavg_pwi + ) +{ + // const int i_step_dev = i_step; + // const int n_node_dev = n_node; + // work id & size + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y; + + // ND array accessors (TODO autogen from py shape info) +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) *2 * n_node + (i_node))*(size) + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + + // unpack params + const float G = params(0); + const float J_NMDA = 0.15;//params(0); + const float JI= 1.0; + const float G_J_NMDA = G*J_NMDA; + // derived + + const float w_plus__J_NMDA = (w_plus * J_NMDA); + const float sig = params(1);//0.001;//sqrt(dt) * sqrt(2.0 * 1e-3); + // We have three variables which could be changed here. Actually 4 + // G (the global coupling), sigma (the noise), J_NMDA(the excitatory synaptic coupling) and J_i(the inner inhibition for each region) + // For now we are making things simple and only change two parameters, G and J_NMDA. + + curandState s; + curand_init(id + (unsigned int) clock64(), 0, 0, &s); + + double tmp_I_E; + double tmp_H_E; + double tmp_I_I; + double tmp_H_I; + double sum; + double S_E = 0.0; + double S_I = 0.0; + + + for (unsigned int i_node = 0; i_node < n_node; i_node++){ + tavg(i_node) = 0.0f; + if (i_step == 0){ + state(i_step, i_node) = 0.001; + } + } + + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + for (unsigned int i_node = 0; i_node < n_node; i_node++) + { + sum = 0.0f; + S_E = state((t) % nh, i_node); + S_I = state((t) % nh, i_node + n_node); + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + //we are not considering delays in this model + float wij = G_J_NMDA*weights[(i_node*n_node) + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + sum += wij * state((t) % nh, j_node); //of J + } + // external Input set to 0, no task evoked activity + tmp_I_E = JI*S_I; // Inner inhibition set to 1 + tmp_I_E = sum - tmp_I_E ; + tmp_I_E = ((w_E__I_0)+(w_plus__J_NMDA * S_E)) + tmp_I_E ; + tmp_I_E = a_E * tmp_I_E - b_E; + tmp_H_E = tmp_I_E/(1.0-exp(min_d_E * tmp_I_E)); + tmp_I_I = (a_I*(((w_I__I_0)+(J_NMDA * S_E))-(S_I)))-b_I; + tmp_H_I = tmp_I_I/(1.0-exp(min_d_I*tmp_I_I)); + + S_E = (S_E)+(dt*(sig * curand_normal(&s)))+(dt*((imintau_E* S_E)+(tmp_H_E*((1-S_E)*gamma_E)))); + S_I = (S_I)+(dt*(sig * curand_normal(&s)))+(dt*((imintau_I* S_I)+(tmp_H_I*gamma_I))); + if(S_E>1) S_E = 1; + if(S_I>1) S_I = 1; + if(S_E<0) S_E = 0; + if(S_I<0) S_I = 0; + state((t+1) % nh, i_node) = S_E; + state((t+1) % nh, i_node+(n_node)) = S_I; + tavg(i_node) = S_E; + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + } // for i_node + } // for t + // cleanup macros/*{{{*/ + #undef params + #undef state + #undef tavg/*}}}*/ +} // kernel integrate +// vim: sw=4 sts=4 ts=8 et ai diff --git a/dsl_cuda/CUDAmodels/rwongwang.c b/dsl_cuda/CUDAmodels/rwongwang.c new file mode 100755 index 0000000000..701f0bad03 --- /dev/null +++ b/dsl_cuda/CUDAmodels/rwongwang.c @@ -0,0 +1,205 @@ +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + +#include +#include +#include + +__device__ float wrap_it_PI(float x) +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +} +__device__ float wrap_it_V(float V) +{ + float Vdim[] = {0.0000001, 1}; + if (V < Vdim[0]) V = Vdim[0]; + else if (V > Vdim[1]) V = Vdim[1]; + + return V; +} +__device__ float wrap_it_W(float W) +{ + float Wdim[] = {0.0000001, 1}; + if (W < Wdim[0]) W = Wdim[0]; + else if (W > Wdim[1]) W = Wdim[1]; + + return W; +} + +__global__ void Rwongwang( + + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, float * __restrict__ weights, float * __restrict__ lengths, + float * __restrict__ params_pwi, // pwi: per work item + // state + float * __restrict__ state_pwi, + // outputs + float * __restrict__ tavg_pwi + ) +{ + // work id & size + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y; + +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) * 2 * n_node + (i_node))*size + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + + // unpack params + // These are the two parameters which are usually explore in fitting in this model + const float global_speed = params(0); + const float global_coupling = params(1); + + // regular constants + const float w_plus = 1.4f; + const float a_E = 310.0f; + const float b_E = 125.0f; + const float d_E = 0.154f; + const float a_I = 615.0f; + const float b_I = 177.0f; + const float d_I = 0.087f; + const float gamma_E = 0.641f / 1000.0f; + const float tau_E = 100.0f; + const float tau_I = 10.0f; + const float I_0 = 0.382f; + const float w_E = 1.0f; + const float w_I = 0.7f; + const float gamma_I = 1.0f / 1000.0f; + const float min_d_E = -1.0f * d_E; + const float min_d_I = -1.0f * d_I; + const float imintau_E = -1.0f / tau_E; + const float imintau_I = -1.0f / tau_I; + const float w_E__I_0 = w_E * I_0; + const float w_I__I_0 = w_I * I_0; + const float J_N = 0.15; + const float J_I = 1.0; + const float G = 2.0; + const float lamda = 0.0; + const float J_NMDA = 0.15; + const float JI = 1.0; + const float G_J_NMDA = G*J_NMDA; + const float w_plus__J_NMDA = w_plus * J_NMDA; + + // coupling constants, coupling itself is hardcoded in kernel + const float a = 1; + + // coupling parameters + float c_0 = 0.0; + + // derived parameters + const float rec_n = 1 / n_node; + const float rec_speed_dt = 0; + const float nsig = sqrt(dt) * sqrt(2.0 * 1e-5); + + // the dynamic derived variables declarations + float tmp_I_E = 0.0; + float tmp_H_E = 0.0; + float tmp_I_I = 0.0; + float tmp_H_I = 0.0; + + + curandState crndst; + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &crndst); + + float V = 0.0; + float W = 0.0; + + //***// This is only initialization of the observable + for (unsigned int i_node = 0; i_node < n_node; i_node++) + { + tavg(i_node) = 0.0f; + if (i_step == 0){ + state(i_step, i_node) = 0.001; + } + } + + //***// This is the loop over time, should stay always the same + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + //***// This is the loop over nodes, which also should stay the same + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + { + c_0 = 0.0f; + + V = state((t) % nh, i_node + 0 * n_node); + W = state((t) % nh, i_node + 1 * n_node); + + // This variable is used to traverse the weights and lengths matrix, which is really just a vector. It is just a displacement. / + unsigned int i_n = i_node * n_node; + + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + //***// Get the weight of the coupling between node i and node j + float wij = weights[i_n + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + + // no delay specified + unsigned int dij = 0; + + //***// Get the state of node j which is delayed by dij + float V_j = state(((t - dij + nh) % nh), j_node + 0 * n_node); + + // Sum it all together using the coupling function. Kuramoto coupling: (postsyn * presyn) == ((a) * (sin(xj - xi))) + c_0 += wij * a * V_j * G_J_NMDA; + + } // j_node */ + + // rec_n is used for the scaling over nodes + c_0 *= None; + // the dynamic derived variables + tmp_I_E = a_E * (w_E__I_0 + w_plus__J_NMDA * V + c_0 - JI*W) - b_E; + tmp_H_E = tmp_I_E/(1.0-exp(min_d_E * tmp_I_E)); + tmp_I_I = (a_I*((w_I__I_0+(J_NMDA * V))-W))-b_I; + tmp_H_I = tmp_I_I/(1.0-exp(min_d_I*tmp_I_I)); + + // This is dynamics step and the update in the state of the node + V += dt * ((imintau_E* V)+(tmp_H_E*(1-V)*gamma_E)); + W += dt * ((imintau_I* W)+(tmp_H_I*gamma_I)); + + // Add noise (if noise components are present in model), integrate with stochastic forward euler and wrap it up + V += nsig * curand_normal2(&crndst).x; + W += nsig * curand_normal2(&crndst).x; + + // Wrap it within the limits of the model + V = wrap_it_V(V); + W = wrap_it_W(W); + + // Update the state + state((t + 1) % nh, i_node + 0 * n_node) = V; + state((t + 1) % nh, i_node + 1 * n_node) = W; + + // Update the observable only for the last timestep + if (t == (i_step + n_step - 1)){ + tavg(i_node + 0 * n_node) = V; + } + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + + } // for i_node + } // for t + +// cleanup macros/*{{{*/ +#undef params +#undef state +#undef tavg/*}}}*/ + +} // kernel integrate \ No newline at end of file diff --git a/dsl_cuda/CUDAmodels/rwongwangref.c b/dsl_cuda/CUDAmodels/rwongwangref.c new file mode 100755 index 0000000000..27916345ba --- /dev/null +++ b/dsl_cuda/CUDAmodels/rwongwangref.c @@ -0,0 +1,158 @@ +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + + +#include +#include + +__device__ float wrap_2_pi_(float x)/*{{{*/ +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +}/*}}}*/ + +__device__ float wrap_2_pi(float x) // not divergent/*{{{*/ +{ + bool lt_0 = x < 0.0f; + bool gt_2pi = x > PI_2; + return (x + PI_2)*lt_0 + x*(!lt_0)*(!gt_2pi) + (x - PI_2)*gt_2pi; +}/*}}}*/ + + +const float w_plus=1.4f; +const float a_E=310.0f; +const float b_E=125.0f; +const float d_E=0.154f; +const float a_I=615.0f; +const float b_I=177.0f; +const float d_I=0.087f; +const float gamma_E=0.641f / 1000.0f; +const float tau_E=100.0f; +const float tau_I=10.0f; +const float I_0=0.382f; +const float w_E=1.0f; +const float w_I=0.7f; +const float gamma_I= 1.0f / 1000.0f; +const float min_d_E = (-1.0f * d_E); +const float min_d_I = (-1.0f * d_I); +const float imintau_E = (-1.0f / tau_E); +const float imintau_I = (-1.0f / tau_I); +const float w_E__I_0 = (w_E * I_0); +const float w_I__I_0 = (w_I * I_0); + +__global__ void Rwongwangref( + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, + float * weights, + float * lengths, + float * params_pwi, // pwi: per work item + // state + float * state_pwi, + // outputs + float * tavg_pwi + ) +{ + // const int i_step_dev = i_step; + // const int n_node_dev = n_node; + // work id & size + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y; + + // ND array accessors (TODO autogen from py shape info) +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) *2 * n_node + (i_node))*(size) + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + + // unpack params + const float G = 2.0; //params(0); + const float J_NMDA = 0.15;//params(0); + const float JI= 1.0; + const float G_J_NMDA = G*J_NMDA; + // derived + + const float w_plus__J_NMDA = (w_plus * J_NMDA); + const float sig = sqrt(dt) * sqrt(2.0 * 1e-5); //params(1);//0.001;// + // We have three variables which could be changed here. Actually 4 + // G (the global coupling), sigma (the noise), J_NMDA(the excitatory synaptic coupling) and J_i(the inner inhibition for each region) + // For now we are making things simple and only change two parameters, G and J_NMDA. + + curandState s; +// curand_init(id + (unsigned int) clock64(), 0, 0, &s); + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &s); + + double tmp_I_E; + double tmp_H_E; + double tmp_I_I; + double tmp_H_I; + double sum; + double S_E = 0.0; + double S_I = 0.0; + + + for (unsigned int i_node = 0; i_node < n_node; i_node++){ + tavg(i_node) = 0.0f; + if (i_step == 0){ + state(i_step, i_node) = 0.001; + } + } + + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + { + sum = 0.0f; + S_E = state((t) % nh, i_node); + S_I = state((t) % nh, i_node + n_node); + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + //we are not considering delays in this model + float wij = G_J_NMDA*weights[(i_node*n_node) + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + sum += wij * state((t) % nh, j_node); //of Jx + } + // external Input set to 0, no task evoked activity + tmp_I_E = JI*S_I; // Inner inhibition set to 1 + tmp_I_E = sum - tmp_I_E ; + tmp_I_E = ((w_E__I_0)+(w_plus__J_NMDA * S_E)) + tmp_I_E ; + tmp_I_E = a_E * tmp_I_E - b_E; + tmp_H_E = tmp_I_E/(1.0-exp(min_d_E * tmp_I_E)); + tmp_I_I = (a_I*(((w_I__I_0)+(J_NMDA * S_E))-(S_I)))-b_I; + tmp_H_I = tmp_I_I/(1.0-exp(min_d_I*tmp_I_I)); + + S_E = (S_E)+((sig * curand_normal(&s)))+(dt*((imintau_E* S_E)+(tmp_H_E*((1-S_E)*gamma_E)))); + S_I = (S_I)+((sig * curand_normal(&s)))+(dt*((imintau_I* S_I)+(tmp_H_I*gamma_I))); + if(S_E>1) S_E = 1; + if(S_I>1) S_I = 1; + if(S_E<0) S_E = 0.0000001; + if(S_I<0) S_I = 0.0000001; + state((t+1) % nh, i_node) = S_E; + state((t+1) % nh, i_node+(n_node)) = S_I; + tavg(i_node) = S_E; + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + } // for i_node + } // for t + // cleanup macros/*{{{*/ + #undef params + #undef state + #undef tavg/*}}}*/ +} // kernel integrate +// vim: sw=4 sts=4 ts=8 et ai diff --git a/dsl_cuda/LEMS2CUDA.py b/dsl_cuda/LEMS2CUDA.py new file mode 100755 index 0000000000..d2bb7019c8 --- /dev/null +++ b/dsl_cuda/LEMS2CUDA.py @@ -0,0 +1,118 @@ +# from models import G2DO +from mako.template import Template + +import os +import sys + +for p in sys.path: + print(p) + +import dsl +sys.path.append("{}".format(os.path.dirname(dsl.__file__))) + +from lems.model.model import Model + +# model file location +# model_filename = 'Oscillator' +# model_filename = 'Kuramoto' +# model_filename = 'Rwongwang' +model_filename = 'Epileptor' + + +def default_lems_folder(): + here = os.path.dirname(os.path.abspath(__file__)) + xmlpath = os.path.join(here, 'XMLmodels') + return xmlpath + +def lems_file(model_name, folder=None): + folder = folder or default_lems_folder() + return os.path.join(folder, model_name.lower() + '_CUDA.xml') + +def default_template(): + here = os.path.dirname(os.path.abspath(__file__)) + tmp_filename = os.path.join(here, 'tmpl8_CUDA.py') + template = Template(filename=tmp_filename) + return template + +def load_model(model_filename): + "Load model from filename" + + fp_xml = lems_file(model_filename) + + model = Model() + model.import_from_file(fp_xml) + # modelextended = model.resolve() + + return model + +def render_model(model_name, template=None): + # drift dynamics + # modelist = list() + # modelist.append(model.component_types[modelname]) + + model = load_model(model_name) + template = template or default_template() + + modellist = model.component_types[model_name] + + # coupling functionality + couplinglist = list() + # couplinglist.append(model.component_types['coupling_function_pop1']) + + for i, cplists in enumerate(model.component_types): + if 'coupling' in cplists.name: + couplinglist.append(cplists) + + # collect all signal amplification factors per state variable. + # signalampl = list() + # for i, sig in enumerate(modellist.dynamics.derived_variables): + # if 'sig' in sig.name: + # signalampl.append(sig) + + # collect total number of exposures combinations. + expolist = list() + for i, expo in enumerate(modellist.exposures): + for chc in expo.choices: + expolist.append(chc) + + # print((couplinglist[0].dynamics.derived_variables['pre'].expression)) + # + # for m in range(len(couplinglist)): + # # print((m)) + # for k in (couplinglist[m].functions): + # print(k) + + # only check whether noise is there, if so then activate it + noisepresent=False + for ct in (model.component_types): + if ct.name == 'noise' and ct.description == 'on': + noisepresent=True + + # start templating + # template = Template(filename='tmpl8_CUDA.py') + model_str = template.render( + modelname=model_name, + const=modellist.constants, + dynamics=modellist.dynamics, + params=modellist.parameters, + derparams=modellist.derived_parameters, + coupling=couplinglist, + noisepresent=noisepresent, + expolist=expolist + ) + + return model_str + +def cuda_templating(model_filename): + + modelfile = "{}{}{}{}".format(os.path.dirname(dsl.__file__), '/dsl_cuda/CUDAmodels/', model_filename.lower(), '.c') + + # start templating + model_str = render_model(model_filename, template=default_template()) + + # write template to file + with open(modelfile, "w") as f: + f.writelines(model_str) + + +cuda_templating(model_filename) \ No newline at end of file diff --git a/dsl_cuda/XMLmodels/__init__.py b/dsl_cuda/XMLmodels/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/dsl_cuda/XMLmodels/epileptor_CUDA.xml b/dsl_cuda/XMLmodels/epileptor_CUDA.xml new file mode 100755 index 0000000000..2bb043ffd0 --- /dev/null +++ b/dsl_cuda/XMLmodels/epileptor_CUDA.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dsl_cuda/XMLmodels/kuramoto_CUDA.xml b/dsl_cuda/XMLmodels/kuramoto_CUDA.xml new file mode 100755 index 0000000000..14fe242fb3 --- /dev/null +++ b/dsl_cuda/XMLmodels/kuramoto_CUDA.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dsl_cuda/XMLmodels/oscillator_CUDA.xml b/dsl_cuda/XMLmodels/oscillator_CUDA.xml new file mode 100755 index 0000000000..cf631fb95c --- /dev/null +++ b/dsl_cuda/XMLmodels/oscillator_CUDA.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dsl_cuda/XMLmodels/rwongwang_CUDA.xml b/dsl_cuda/XMLmodels/rwongwang_CUDA.xml new file mode 100755 index 0000000000..d83dc9199b --- /dev/null +++ b/dsl_cuda/XMLmodels/rwongwang_CUDA.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dsl_cuda/__init__.py b/dsl_cuda/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/dsl_cuda/tmpl8_CUDA.py b/dsl_cuda/tmpl8_CUDA.py new file mode 100755 index 0000000000..2ae16095fa --- /dev/null +++ b/dsl_cuda/tmpl8_CUDA.py @@ -0,0 +1,260 @@ +#include // for printf +#define PI_2 (2 * M_PI_F) + +// buffer length defaults to the argument to the integrate kernel +// but if it's known at compile time, it can be provided which allows +// compiler to change i%n to i&(n-1) if n is a power of two. +#ifndef NH +#define NH nh +#endif + +#ifndef WARP_SIZE +#define WARP_SIZE 32 +#endif + +#include +#include +#include + +__device__ float wrap_it_PI(float x) +{ + bool neg_mask = x < 0.0f; + bool pos_mask = !neg_mask; + // fmodf diverges 51% of time + float pos_val = fmodf(x, PI_2); + float neg_val = PI_2 - fmodf(-x, PI_2); + return neg_mask * neg_val + pos_mask * pos_val; +} +\ +% for state_var in (dynamics.state_variables): +% if (state_var.boundaries != "PI"): +__device__ float wrap_it_${state_var.name}(float ${state_var.name}) +{ + float ${state_var.name}dim[] = {${state_var.boundaries}}; + if (${state_var.name} < ${state_var.name}dim[0]) ${state_var.name} = ${state_var.name}dim[0]; + else if (${state_var.name} > ${state_var.name}dim[1]) ${state_var.name} = ${state_var.name}dim[1]; + + return ${state_var.name}; +} +% endif / +% endfor + +__global__ void ${modelname}( + + // config + unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, + float dt, float speed, float * __restrict__ weights, float * __restrict__ lengths, + float * __restrict__ params_pwi, // pwi: per work item + // state + float * __restrict__ state_pwi, + // outputs + float * __restrict__ tavg_pwi + ) +{ + // work id & size + const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; + const unsigned int size = blockDim.x * gridDim.x * gridDim.y; + +#define params(i_par) (params_pwi[(size * (i_par)) + id]) +#define state(time, i_node) (state_pwi[((time) * ${dynamics.state_variables.__len__()} * n_node + (i_node))*size + id]) +#define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) + + // unpack params + // These are the two parameters which are usually explore in fitting in this model + ## printing the to be sweeped parameters + % for paramcounter, par_var in enumerate(params): + const ${par_var.dimension} ${par_var.name} = params(${paramcounter}); + % endfor + + // regular constants +% for item in const: + const float ${item.name} = ${item.default}; +% endfor / + + // coupling constants, coupling itself is hardcoded in kernel +% for m in range(len(coupling)): + % for cc in (coupling[m].constants): + const float ${cc.name} = ${cc.default}; + %endfor / +% endfor + + // coupling parameters +% for m in range(len(coupling)): + % for cc in (coupling[m].derived_parameters): + float ${cc.name} = 0.0; + %endfor / +% endfor + + % if derparams: + // derived parameters + % for par_var in derparams: + const float ${par_var.name} = ${par_var.expression}; + % endfor / + %endif / + + % if dynamics.derived_variables: + // the dynamic derived variables declarations + % for i, dv in enumerate(dynamics.derived_variables): + float ${dv.name} = 0.0; + % endfor / + % endif / + + % if dynamics.conditional_derived_variables: + // conditional_derived variable declaration + % for cd in dynamics.conditional_derived_variables: + float ${cd.name} = 0.0; + % endfor / + % endif / + + curandState crndst; + curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &crndst); + + % for state_var in (dynamics.state_variables): + float ${state_var.name} = 0.0; + % endfor + + //***// This is only initialization of the observable + for (unsigned int i_node = 0; i_node < n_node; i_node++) + { + tavg(i_node) = 0.0f; + if (i_step == 0){ + state(i_step, i_node) = 0.001; + } + } + + //***// This is the loop over time, should stay always the same + for (unsigned int t = i_step; t < (i_step + n_step); t++) + { + //***// This is the loop over nodes, which also should stay the same + for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + { + % for m in range(len(coupling)): + % for cdp in (coupling[m].derived_parameters): + ${cdp.name} = 0.0f; + %endfor + % endfor / + ## float coupling = 0.0f; + + % for i, item in enumerate(dynamics.state_variables): + ${item.name} = state((t) % nh, i_node + ${i} * n_node); + % endfor / + + // This variable is used to traverse the weights and lengths matrix, which is really just a vector. It is just a displacement. / + unsigned int i_n = i_node * n_node; + + for (unsigned int j_node = 0; j_node < n_node; j_node++) + { + //***// Get the weight of the coupling between node i and node j + float wij = weights[i_n + j_node]; // nb. not coalesced + if (wij == 0.0) + continue; + + % if (derparams['rec_speed_dt'] and derparams['rec_speed_dt'].expression != '0'): + //***// Get the delay between node i and node j + unsigned int dij = lengths[i_n + j_node] * rec_speed_dt; + % else: + // no delay specified + unsigned int dij = 0; + % endif + + //***// Get the state of node j which is delayed by dij + % for m in range(len(coupling)): + % for cp in (coupling[m].parameters): + float ${cp.name} = state(((t - dij + nh) % nh), j_node + ${cp.dimension} * n_node); + % endfor / + %endfor + + // Sum it all together using the coupling function. Kuramoto coupling: (postsyn * presyn) == ((a) * (sin(xj - xi))) \ + % for ml in range(len(coupling)): + ## only do this if pre or post is specified + % if coupling[ml].dynamics.derived_variables and \ + (coupling[ml].dynamics.derived_variables['pre'].expression != 'None' or \ + coupling[ml].dynamics.derived_variables['post'].expression != 'None'): + % for cdp in (coupling[ml].derived_parameters): + + ${cdp.name} += wij * ${coupling[ml].dynamics.derived_variables['post'].expression} * ${coupling[ml].dynamics.derived_variables['pre'].expression}; + ## coupling += wij * ${coupling[ml].dynamics.derived_variables['post'].expression} * ${coupling[ml].dynamics.derived_variables['pre'].expression}; + + %endfor + % endif / + % endfor / + } // j_node */ + + // rec_n is used for the scaling over nodes + % for m in range(len(coupling)): + % for cdp in (coupling[m].derived_parameters): + % if cdp.expression: + ${cdp.name} *= ${cdp.expression}; + % endif / + % endfor + % endfor \ + + % if dynamics.derived_variables: + // the dynamic derived variables + % for i, dv in enumerate(dynamics.derived_variables): + ${dv.name} = ${dv.expression}; + % endfor / + % endif + + % for con_der in dynamics.conditional_derived_variables: + if (${con_der.condition}) + // The conditional variables + % for case in (con_der.cases): + % if (loop.first): + ${con_der.name} = ${case}; + % elif (loop.last and not loop.first): + else + ${con_der.name} = ${case}; + %endif / + % endfor + % endfor \ + + // This is dynamics step and the update in the state of the node + % for i, tim_der in enumerate(dynamics.time_derivatives): + ${tim_der.name} += dt * (${tim_der.expression}); + % endfor + + % if noisepresent: + // Add noise (if noise components are present in model), integrate with stochastic forward euler and wrap it up + % for ds, td in zip(dynamics.state_variables, dynamics.time_derivatives): + ${ds.name} += nsig * curand_normal2(&crndst).x; + % endfor / + ##% else: + ##% for ds, td in zip(dynamics.state_variables, dynamics.time_derivatives): + ##${ds.name} += dt a * ${td.name}); + ##% endfor / + % endif + + // Wrap it within the limits of the model + % for state_var in (dynamics.state_variables): + % if state_var.boundaries == 'PI': + ${state_var.name} = wrap_it_${state_var.boundaries}(${state_var.name}); + % else: + ${state_var.name} = wrap_it_${state_var.name}(${state_var.name}); + % endif + % endfor / + + // Update the state + % for i, state_var in enumerate(dynamics.state_variables): + state((t + 1) % nh, i_node + ${i} * n_node) = ${state_var.name}; + % endfor / + + // Update the observable only for the last timestep + if (t == (i_step + n_step - 1)){ + % for i, expo in enumerate(expolist): + tavg(i_node + ${i} * n_node) = ${expo}; + % endfor / + } + + // sync across warps executing nodes for single sim, before going on to next time step + __syncthreads(); + + } // for i_node + } // for t + +// cleanup macros/*{{{*/ +#undef params +#undef state +#undef tavg/*}}}*/ + +} // kernel integrate \ No newline at end of file diff --git a/lems/__init__.py b/lems/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/lems/base/__init__.py b/lems/base/__init__.py new file mode 100755 index 0000000000..aaeb1c49f0 --- /dev/null +++ b/lems/base/__init__.py @@ -0,0 +1,5 @@ +""" +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" diff --git a/lems/base/base.py b/lems/base/base.py new file mode 100755 index 0000000000..3fa58239a3 --- /dev/null +++ b/lems/base/base.py @@ -0,0 +1,20 @@ +""" +PyLEMS base class. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" + +import copy + +class LEMSBase(object): + """ + Base object for PyLEMS. + """ + + def copy(self): + return copy.deepcopy(self) + + def toxml(self): + return '' diff --git a/lems/base/errors.py b/lems/base/errors.py new file mode 100755 index 0000000000..a0b21dbb37 --- /dev/null +++ b/lems/base/errors.py @@ -0,0 +1,86 @@ +""" +Error classes. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" + +class LEMSError(Exception): + """ + Base exception class. + """ + + def __init__(self, message, *params, **key_params): + """ + Constructor + + @param message: Error message. + @type message: string + + @param params: Optional arguments for formatting. + @type params: list + + @param key_params: Named arguments for formatting. + @type key_params: dict + """ + + self.message = None + """ Error message + @type: string """ + + if params: + if key_params: + self.message = message.format(*params, **key_params) + else: + self.message = message.format(*params) + else: + if key_params: + self.message = message(**key_params) + else: + self.message = message + + def __str__(self): + """ + Returns the error message string. + + @return: The error message + @rtype: string + """ + + return self.message + +class StackError(LEMSError): + """ + Exception class to signal errors in the Stack class. + """ + + pass + +class ParseError(LEMSError): + """ + Exception class to signal errors found during parsing. + """ + + pass + +class ModelError(LEMSError): + """ + Exception class to signal errors in creating the model. + """ + + pass + +class SimBuildError(LEMSError): + """ + Exception class to signal errors in building the simulation. + """ + + pass + +class SimError(LEMSError): + """ + Exception class to signal errors in simulation. + """ + + pass diff --git a/lems/base/map.py b/lems/base/map.py new file mode 100755 index 0000000000..e69bde09cf --- /dev/null +++ b/lems/base/map.py @@ -0,0 +1,31 @@ +""" +Map class. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" + +from lems.base.base import LEMSBase + +from collections import OrderedDict + +class Map(OrderedDict, LEMSBase): + """ + Map class. + Same as dict, but iterates over values. + """ + + def __init__(self, *params, **key_params): + """ + Constructor. + """ + + OrderedDict.__init__(self, *params, **key_params) + + def __iter__(self): + """ + Returns an iterator. + """ + + return iter(self.values()) \ No newline at end of file diff --git a/lems/base/stack.py b/lems/base/stack.py new file mode 100755 index 0000000000..a9c6ddc261 --- /dev/null +++ b/lems/base/stack.py @@ -0,0 +1,96 @@ +""" +Stack class. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" + +from lems.base.base import LEMSBase +from lems.base.errors import StackError + +class Stack(LEMSBase): + """ + Basic stack implementation. + """ + + def __init__(self): + """ + Constructor. + """ + + self.stack = [] + """ List used to store the stack contents. + @type: list """ + + def push(self, val): + """ + Pushed a value onto the stack. + + @param val: Value to be pushed. + @type val: * + """ + + self.stack = [val] + self.stack + + def pop(self): + """ + Pops a value off the top of the stack. + + @return: Value popped off the stack. + @rtype: * + + @raise StackError: Raised when there is a stack underflow. + """ + + if self.stack: + val = self.stack[0] + self.stack = self.stack[1:] + return val + else: + raise StackError('Stack empty') + + def top(self): + """ + Returns the value off the top of the stack without popping. + + @return: Value on the top of the stack. + @rtype: * + + @raise StackError: Raised when there is a stack underflow. + """ + + if self.stack: + return self.stack[0] + else: + raise StackError('Stack empty') + + def is_empty(self): + """ + Checks if the stack is empty. + + @return: True if the stack is empty, otherwise False. + @rtype: Boolean + """ + + return self.stack == [] + + def __str__(self): + """ + Returns a string representation of the stack. + + @note: This assumes that the stack contents are capable of generating + string representations. + """ + + if len(self.stack) == 0: + s = '[]' + else: + s = '[' + str(self.stack[0]) + for i in range(1, len(self.stack)): + s += ', ' + str(self.stack[i]) + s += ']' + return s + + def __repr__(self): + return self.__str__() \ No newline at end of file diff --git a/lems/base/util.py b/lems/base/util.py new file mode 100755 index 0000000000..1c61950b01 --- /dev/null +++ b/lems/base/util.py @@ -0,0 +1,60 @@ +""" +PyLEMS utility classes / functions + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" + +id_counter = 0 + +def make_id(): + global id_counter + id_counter = id_counter + 1 + return '__id_{0}__'.format(id_counter) + +def merge_maps(m, base): + """ + Merge in undefined map entries from given map. + + @param m: Map to be merged into. + @type m: lems.util.Map + + @param base: Map to be merged into. + @type base: lems.util.Map + """ + + for k in base.keys(): + if k not in m: + m[k] = base[k] + +def merge_lists(l, base): + """ + Merge in undefined list entries from given list. + + @param l: List to be merged into. + @type l: list + + @param base: List to be merged into. + @type base: list + """ + + for i in base: + if i not in l: + l.append(i) + + +def validate_lems(file_name): + + from lxml import etree + try: + from urllib2 import urlopen # Python 2 + except: + from urllib.request import urlopen # Python 3 + + schema_file = urlopen("https://raw.githubusercontent.com/LEMS/LEMS/development/Schemas/LEMS/LEMS_v0.7.3.xsd") + xmlschema = etree.XMLSchema(etree.parse(schema_file)) + print("Validating {0} against {1}".format(file_name, schema_file.geturl())) + xmlschema.assertValid(etree.parse(file_name)) + print("It's valid!") + diff --git a/lems/model/__init__.py b/lems/model/__init__.py new file mode 100755 index 0000000000..aaeb1c49f0 --- /dev/null +++ b/lems/model/__init__.py @@ -0,0 +1,5 @@ +""" +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" diff --git a/lems/model/component.py b/lems/model/component.py new file mode 100755 index 0000000000..a5cda9e5a0 --- /dev/null +++ b/lems/model/component.py @@ -0,0 +1,1294 @@ +""" +Parameter, ComponentType and Component class definitions. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" + +from lems.base.base import LEMSBase +from lems.base.map import Map +from lems.base.errors import ModelError,ParseError + +from lems.model.dynamics import Dynamics +from lems.model.structure import Structure +from lems.model.simulation import Simulation + +from lems.parser.expr import ExprParser + + +class Parameter(LEMSBase): + """ + Stores a parameter declaration. + """ + + def __init__(self, name, dimension, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the parameter. + @type: str """ + + self.dimension = dimension + """ Physical dimension of the parameter. + @type: str """ + + self.fixed = False + """ Whether the parameter has been fixed or not. + @type: bool """ + + self.fixed_value = None + """ Value if fixed. + @type: str """ + + self.value = None + """ Value of the parameter. + @type: str """ + + self.numeric_value = None + """ Resolved numeric value. + @type: float """ + + self.description = description + """ Description of this parameter. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '<{0} name="{1}" dimension="{2}"'.format('Fixed' if self.fixed else 'Parameter', self.name, + self.dimension) + \ + (' description = "{0}"'.format(self.description) if self.description else '') + \ + '/>' + + def __str__(self): + return '{0}: name="{1}" dimension="{2}"'.format('Fixed' if self.fixed else 'Parameter', self.name, + self.dimension) + \ + (' description = "{0}"'.format(self.description) if self.description else '') + + def __repr__(self): + return self.__str__() + + +class Fixed(Parameter): + """ + Stores a fixed parameter specification. + """ + + def __init__(self, parameter, value, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + Parameter.__init__(self, parameter, '__dimension_inherited__', description) + + self.fixed = True + self.fixed_value = value + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class Property(LEMSBase): + """ + Store the specification of a property. + """ + + def __init__(self, name, dimension=None, default_value=None, description=''): + """ + Constructor. + """ + + self.name = name + """ Name of the property. + @type: str """ + + self.dimension = dimension + """ Physical dimensions of the property. + @type: str """ + + self.description = description + """ Description of the property. + @type: str """ + + self.default_value = default_value + """ Default value of the property. + @type: float """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class IndexParameter(LEMSBase): + """ + Stores a parameter which is an index (integer > 0). + """ + + def __init__(self, name, description=''): + """ + Constructor. + """ + + self.name = name + """ Name of the parameter. + @type: str """ + + self.description = description + """ Description of this parameter. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class DerivedParameter(LEMSBase): + """ + Store the specification of a derived parameter. + """ + + def __init__(self, name, value, expression=None, description=''): + """ + Constructor. + + See instance variable documentation for more info on derived parameters. + """ + + self.name = name + """ Name of the derived parameter. + @type: str """ + + self.expression = expression + """ Physical dimensions of the derived parameter. + @type: str """ + + self.value = value + """ Value of the derived parameter. + @type: str """ + + self.description = description + """ Description of the derived parameter. + @type: str """ + + try: + ep = ExprParser(self.expression) + self.expression_tree = ep.parse() + except: + raise ParseError("Parse error when parsing value expression " + "'{0}' for derived parameter {1}", + self.expression, self.name) + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class Constant(LEMSBase): + """ + Stores a constant specification. + """ + + def __init__(self, name, default, domain=None, symbol=None, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the constant. + @type: str """ + + self.symbol = symbol + """ Symbol of the constant. + @type: str """ + + self.default = default + """ Value of the constant. + @type: str """ + + self.domain = domain + """ Physical dimensions of the constant. + @type: str """ + + self.description = description + """ Description of the constant. + @type: str """ + + self.numeric_value = None + """ Numeric value of the constant. + @type: float """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class Function(LEMSBase): + """ + Stores a constant specification. + """ + + def __init__(self, name, value, dimension=None, symbol=None, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the constant. + @type: str """ + + self.symbol = symbol + """ Symbol of the constant. + @type: str """ + + self.value = value + """ Value of the constant. + @type: str """ + + self.dimension = dimension + """ Physical dimensions of the constant. + @type: str """ + + self.description = description + """ Description of the constant. + @type: str """ + + self.numeric_value = None + """ Numeric value of the constant. + @type: float """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class Exposure(LEMSBase): + """ + Stores a exposure specification. + """ + + def __init__(self, name, choices, default, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the exposure. + @type: str """ + + self.choices = list(choices.split(", ")) + """ Choices of the exposure. + @type: str """ + + self.default = list(default.split(", ")) + """ Default option of the exposure. + @type: str """ + + self.description = description + """ Description of this exposure. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class Requirement(LEMSBase): + """ + Stores a requirement specification. + """ + + def __init__(self, name, dimension, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the requirement. + @type: str """ + + self.dimension = dimension + """ Physical dimension of the requirement. + @type: str """ + + self.description = description + """ Description of this requirement. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class ComponentRequirement(LEMSBase): + """ + Specifies a component that is required + """ + + def __init__(self, name, description=''): + """ + Constructor. + """ + + self.name = name + """ Name of the Component required. + @type: str """ + + self.description = description + """ Description of this ComponentRequirement. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class InstanceRequirement(LEMSBase): + """ + Stores an instance requirement specification. + """ + + def __init__(self, name, type, description=''): + """ + Constructor. + """ + + self.name = name + """ Name of the instance requirement. + @type: str """ + + self.type = type + """ Type of the instance required. + @type: str """ + + self.description = description + """ Description of this InstanceRequirement. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class Children(LEMSBase): + """ + Stores children specification. + """ + + def __init__(self, name, type_, multiple=False): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the children. + @type: str """ + + self.type = type_ + """ Component type of the children. + @type: str """ + + self.multiple = multiple + """ Single child / multiple children. + @type: bool """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '<{2} name="{0}" type="{1}"/>'.format(self.name, self.type, 'Children' if self.multiple else 'Child') + + +class Text(LEMSBase): + """ + Stores a text entry specification. + """ + + def __init__(self, name, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the text entry. + @type: str """ + + self.description = description + """ Description of the text entry. + @type: str """ + + self.value = None + """ Value of the text entry. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + def __str__(self): + return 'Text, name: {0}'.format(self.name) + \ + (', description = "{0}"'.format(self.description) if self.description else '') + \ + (', value = "{0}"'.format(self.value) if self.value else '') + + def __repr__(self): + return self.__str__() + + +class Link(LEMSBase): + """ + Stores a link specification. + """ + + def __init__(self, name, type_, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the link entry. + @type: str """ + + self.type = type_ + """ Type of the link. + @type: str """ + + self.description = description + """ Description of the link. + @type: str """ + + self.value = None + """ Value of the link. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class Path(LEMSBase): + """ + Stores a path entry specification. + """ + + def __init__(self, name, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the path entry. + @type: str """ + + self.description = description + """ Description of the path entry. + @type: str """ + + self.value = None + """ Value of the path entry. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class EventPort(LEMSBase): + """ + Stores an event port specification. + """ + + def __init__(self, name, direction, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the event port. + @type: str """ + + d = direction.lower() + if d != 'in' and d != 'out': + raise ModelError("Invalid direction '{0}' in event port '{1}'".format(direction, name)) + + self.direction = direction + """ Direction - IN/OUT . + @type: str """ + + self.description = description + """ Description of the event port. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class ComponentReference(LEMSBase): + """ + Stores a component reference. + """ + + def __init__(self, name, type_, local=None): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the component reference. + @type: str """ + + self.type = type_ + """ Type of the component reference. + @type: str """ + + self.local = local + """ ??? + @type: str """ + + self.referenced_component = None + """ Component being referenced. + @type: FatComponent """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class Attachments(LEMSBase): + """ + Stores an attachment type specification. + """ + + def __init__(self, name, type_, description=''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the attachment collection. + @type: str """ + + self.type = type_ + """ Type of attachment. + @type: str """ + + self.description = description + """ Description about the attachment. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class Fat(LEMSBase): + """ + Stores common elements for a component type / fat component. + """ + + def __init__(self): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.parameters = Map() + """ Map of parameters in this component type. + @type: Map(str -> lems.model.component.Parameter) """ + + self.properties = Map() + """ Map of properties in this component type. + @type: Map(str -> lems.model.component.Property) """ + + self.derived_parameters = Map() + """ Map of derived_parameters in this component type. + @type: Map(str -> lems.model.component.Parameter) """ + + self.index_parameters = Map() + """ Map of index_parameters in this component type. + @type: Map(str -> lems.model.component.IndexParameter) """ + + self.constants = Map() + """ Map of constants in this component type. + @type: Map(str -> lems.model.component.Constant) """ + + self.functions = Map() + + self.exposures = Map() + """ Map of exposures in this component type. + @type: Map(str -> lems.model.component.Exposure) """ + + self.requirements = Map() + """ Map of requirements. + @type: Map(str -> lems.model.component.Requirement) """ + + self.component_requirements = Map() + """ Map of component requirements. + @type: Map(str -> lems.model.component.ComponentRequirement) """ + + self.instance_requirements = Map() + """ Map of instance requirements. + @type: Map(str -> lems.model.component.InstanceRequirement) """ + + self.children = Map() + """ Map of children. + @type: Map(str -> lems.model.component.Children) """ + + self.texts = Map() + """ Map of text entries. + @type: Map(str -> lems.model.component.Text) """ + + self.links = Map() + """ Map of links. + @type: Map(str -> lems.model.component.Link) """ + + self.paths = Map() + """ Map of path entries. + @type: Map(str -> lems.model.component.Path) """ + + self.event_ports = Map() + """ Map of event ports. + @type: Map(str -> lems.model.component.EventPort """ + + self.component_references = Map() + """ Map of component references. + @type: Map(str -> lems.model.component.ComponentReference) """ + + self.attachments = Map() + """ Map of attachment type specifications. + @type: Map(str -> lems.model.component.Attachments) """ + + self.dynamics = Dynamics() + """ Behavioural dynamics object. + @type: lems.model.dynamics.Dynamics """ + + self.structure = Structure() + """ Structural properties object. + @type: lems.model.structure.Structure """ + + self.simulation = Simulation() + """ Simulation attributes. + @type: lems.model.simulation.Simulation """ + + self.types = set() + """ Set of compatible component types. + @type: set(str) """ + + def add_parameter(self, parameter): + """ + Adds a paramter to this component type. + + @param parameter: Parameter to be added. + @type parameter: lems.model.component.Parameter + """ + + self.parameters[parameter.name] = parameter + + def add_property(self, property): + """ + Adds a property to this component type. + + @param property: Property to be added. + @type property: lems.model.component.Property + """ + + self.properties[property.name] = property + + def add_derived_parameter(self, derived_parameter): + """ + Adds a derived_parameter to this component type. + + @param derived_parameter: Derived Parameter to be added. + @type derived_parameter: lems.model.component.DerivedParameter + """ + + self.derived_parameters[derived_parameter.name] = derived_parameter + + def add_index_parameter(self, index_parameter): + """ + Adds an index_parameter to this component type. + + @param index_parameter: Index Parameter to be added. + @type index_parameter: lems.model.component.IndexParameter + """ + + self.index_parameters[index_parameter.name] = index_parameter + + def add_constant(self, constant): + """ + Adds a paramter to this component type. + + @param constant: Constant to be added. + @type constant: lems.model.component.Constant + """ + + self.constants[constant.name] = constant + + def add_function(self, function): + """ + Adds a paramter to this component type. + + @param constant: Constant to be added. + @type constant: lems.model.component.Constant + """ + + self.functions[function.name] = function + + def add_exposure(self, exposure): + """ + Adds a exposure to this component type. + + @param exposure: Exposure to be added. + @type exposure: lems.model.component.Exposure + """ + + self.exposures[exposure.name] = exposure + + def add_requirement(self, requirement): + """ + Adds a requirement to this component type. + + @param requirement: Requirement to be added. + @type requirement: lems.model.component.Requirement + """ + + self.requirements[requirement.name] = requirement + + def add_component_requirement(self, component_requirement): + """ + Adds a component requirement to this component type. + + @param component_requirement: ComponentRequirement to be added. + @type component_requirement: lems.model.component.ComponentRequirement + """ + + self.component_requirements[component_requirement.name] = component_requirement + + def add_instance_requirement(self, instance_requirement): + """ + Adds an instance requirement to this component type. + + @param instance_requirement: InstanceRequirement to be added. + @type instance_requirement: lems.model.component.InstanceRequirement + """ + + self.instance_requirements[instance_requirement.name] = instance_requirement + + def add_children(self, children): + """ + Adds children to this component type. + + @param children: Children to be added. + @type children: lems.model.component.Children + """ + + self.children[children.name] = children + + def add_text(self, text): + """ + Adds a text to this component type. + + @param text: Text to be added. + @type text: lems.model.component.Text + """ + + self.texts[text.name] = text + + def add_link(self, link): + """ + Adds a link to this component type. + + @param link: Link to be added. + @type link: lems.model.component.Link + """ + + self.links[link.name] = link + + def add_path(self, path): + """ + Adds a path to this component type. + + @param path: Path to be added. + @type path: lems.model.component.Path + """ + + self.paths[path.name] = path + + def add_event_port(self, event_port): + """ + Adds a event port to this component type. + + @param event_port: Event port to be added. + @type event_port: lems.model.component.EventPort + """ + + self.event_ports[event_port.name] = event_port + + def add_component_reference(self, component_reference): + """ + Adds a component reference to this component type. + + @param component_reference: Component reference to be added. + @type component_reference: lems.model.component.ComponentReference + """ + + self.component_references[component_reference.name] = component_reference + + def add_attachments(self, attachments): + """ + Adds an attachments type specification to this component type. + + @param attachments: Attachments specification to be added. + @type attachments: lems.model.component.Attachments + """ + + self.attachments[attachments.name] = attachments + + def add(self, child): + """ + Adds a typed child object to the component type. + + @param child: Child object to be added. + """ + + if isinstance(child, Parameter): + self.add_parameter(child) + elif isinstance(child, Property): + self.add_property(child) + elif isinstance(child, DerivedParameter): + self.add_derived_parameter(child) + elif isinstance(child, IndexParameter): + self.add_index_parameter(child) + elif isinstance(child, Constant): + self.add_constant(child) + elif isinstance(child, Exposure): + self.add_exposure(child) + elif isinstance(child, Requirement): + self.add_requirement(child) + elif isinstance(child, ComponentRequirement): + self.add_component_requirement(child) + elif isinstance(child, InstanceRequirement): + self.add_instance_requirement(child) + elif isinstance(child, Children): + self.add_children(child) + elif isinstance(child, Text): + self.add_text(child) + elif isinstance(child, Link): + self.add_link(child) + elif isinstance(child, Path): + self.add_path(child) + elif isinstance(child, EventPort): + self.add_event_port(child) + elif isinstance(child, ComponentReference): + self.add_component_reference(child) + elif isinstance(child, Attachments): + self.add_attachments(child) + else: + raise ModelError('Unsupported child element') + + +class ComponentType(Fat): + """ + Stores a component type declaration. + """ + + def __init__(self, name, description='', extends=None): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + Fat.__init__(self) + + self.name = name + """ Name of the component type. + @type: str """ + + self.extends = extends + """ Base component type. + @type: str """ + + self.description = description + """ Description of this component type. + @type: str """ + + self.types.add(name) + + def __str__(self): + return 'ComponentType, name: {0}'.format(self.name) + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + xmlstr = '' + else: + xmlstr += '/>' + + return xmlstr + + +class Component(LEMSBase): + """ + Stores a component instantiation. + """ + + def __init__(self, id_, type_, **params): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.id = id_ + """ ID of the component. + @type: str """ + + self.type = type_ + """ Type of the component. + @type: str """ + + self.parameters = dict() + """ Dictionary of parameter values. + @type: str """ + for key in params.keys(): + self.parameters[key] = params[key] + + self.children = list() + """ List of child components. + @type: list(lems.model.component.Component) """ + + self.parent_id = None + """ Optional id of parent + @type: str """ + + def __str__(self): + return 'Component, id: {0}, type: {1},\n parameters: {2}\n parent: {3}\n'.format(self.id, self.type, + self.parameters, + self.parent_id) + + def __repr__(self): + return self.__str__() + + def set_parameter(self, parameter, value): + """ + Set a parameter. + + @param parameter: Parameter to be set. + @type parameter: str + + @param value: Value to be set to. + @type value: str + """ + + self.parameters[parameter] = value + + def add_child(self, child): + """ + Adds a child component. + + @param child: Child component to be added. + @type child: lems.model.component.Component + """ + + self.children.append(child) + + def add(self, child): + """ + Adds a typed child object to the component. + + @param child: Child object to be added. + """ + + if isinstance(child, Component): + self.add_child(child) + else: + raise ModelError('Unsupported child element') + + def set_parent_id(self, parent_id): + """ + Sets the id of the parent Component + + @param parent_id: id of the parent Component + @type parent_id: str + """ + + self.parent_id = parent_id + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + xmlstr = ' 0 else '') + + def add_child_component(self, child_component): + """ + Adds a child component to this fat component. + + @param child_component: Child component to be added. + @type child_component: lems.model.component.FatComponent + """ + + self.child_components.append(child_component) + + def add(self, child): + """ + Adds a typed child object to the component type. + + @param child: Child object to be added. + """ + + if isinstance(child, FatComponent): + self.add_child_component(child) + else: + Fat.add(self, child) + + def set_parent_id(self, parent_id): + """ + Sets the id of the parent Component + + @param parent_id: id of the parent Component + @type parent_id: str + """ + + self.parent_id = parent_id diff --git a/lems/model/dynamics.py b/lems/model/dynamics.py new file mode 100755 index 0000000000..0ae9a972ab --- /dev/null +++ b/lems/model/dynamics.py @@ -0,0 +1,909 @@ +""" +Behavioral dynamics of component types. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org + +MAvdVlag: altered attributes for state_variables, derived_variables, time_derivatives and +conditional_derived_variable. +""" + +from lems.base.base import LEMSBase +from lems.base.map import Map +from lems.base.errors import ModelError,ParseError + +from lems.parser.expr import ExprParser + + +class StateVariable(LEMSBase): + """ + Store the specification of a state variable. + """ + + def __init__(self, name, default, boundaries=None): + """ + Constructor. + + See instance variable documentation for more info on parameters. + """ + + self.name = name + """ Name of the state variable. + @type: str """ + + self.default = default + """ Default of the state variable. + @type: str """ + + self.boundaries = boundaries + """ Boundaries name for the state variable. + @type: str """ + + def __str__(self): + return 'StateVariable name="{0}" default="{1}"'.format(self.name, self.default) + \ + (' boundaries="{0}"'.format(self.boundaries) if self.boundaries else '') + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class DerivedVariable(LEMSBase): + """ + Store the specification of a derived variable. + """ + + def __init__(self, name, **params): + """ + Constructor. + + See instance variable documentation for more info on parameters. + """ + + self.name = name + """ Name of the derived variable. + @type: str """ + + self.dimension = params['dimension'] if 'dimension' in params else None + """ Dimension of the derived variable or None if computed. + @type: str """ + + self.exposure = params['exposure'] if 'exposure' in params else None + """ Exposure name for the derived variable. + @type: str """ + + self.select = params['select'] if 'select' in params else None + """ Selection path/expression for the derived variable. + @type: str """ + + self.expression = params['expression'] if 'expression' in params else None + """ Value of the derived variable. + @type: str """ + + self.reduce = params['reduce'] if 'reduce' in params else None + """ Reduce method for the derived variable. + @type: str """ + + self.required = params['required'] if 'required' in params else None + """ Requried or not. + @type: str """ + + self.expression_tree = None + """ Parse tree for the time derivative expression. + @type: lems.parser.expr.ExprNode """ + + if self.expression != None: + try: + self.expression_tree = ExprParser(self.expression).parse() + except: + raise ParseError("Parse error when parsing value expression " + "'{0}' for derived variable {1}", + self.expression, self.name) + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class Case(LEMSBase): + """ + Store the specification of a case for a Conditional Derived Variable. + """ + + def __init__(self, condition, value): + """ + Constructor. + """ + + self.condition = condition + """ Condition for this case. + @type: str """ + + self.value = value + """ Value if the condition is true. + @type: str """ + + self.condition_expression_tree = None + """ Parse tree for the case condition expression. + @type: lems.parser.expr.ExprNode """ + + self.value_expression_tree = None + """ Parse tree for the case condition expression. + @type: lems.parser.expr.ExprNode """ + + try: + self.value_expression_tree = ExprParser(self.value).parse() + + if not self.condition: + self.condition_expression_tree = None + else: + self.condition_expression_tree = ExprParser(self.condition).parse() + except: + raise ParseError("Parse error when parsing case with condition " + "'{0}' and value {1}", + self.condition, self.value) + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + + +class ConditionalDerivedVariable(LEMSBase): + """ + Store the specification of a conditional derived variable. + """ + + def __init__(self, name, condition, exposure=None, cases=None): + """ + Constructor. + + See instance variable documentation for more info on parameters. + """ + + self.name = name + """ Name of the derived variable. + @type: str """ + + self.condition = condition + """ Dimension of the state variable. + @type: str """ + + self.exposure = exposure + """ Exposure name for the state variable. + @type: str """ + + self.cases = list(cases.split("; ")) + """ List of cases related to this conditional derived variable. + @type: list(lems.model.dynamics.Case) """ + + def add_case(self, case): + """ + Adds a case to this conditional derived variable. + + @param case: Case to be added. + @type case: lems.model.dynamics.Case + """ + + self.cases.append(case) + + def add(self, child): + """ + Adds a typed child object to the conditional derived variable. + + @param child: Child object to be added. + """ + + if isinstance(child, Case): + self.add_case(child) + else: + raise ModelError('Unsupported child element') + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + xmlstr = '' + else: + xmlstr += '/>' + + return xmlstr + + +class TimeDerivative(LEMSBase): + """ + Store the specification of a time derivative specifcation. + """ + + def __init__(self, name, expression): + """ + Constructor. + + See instance variable documentation for more info on parameters. + """ + + self.name = name + """ Name of the variable for which the time derivative is being specified. + @type: str """ + + self.expression = expression + """ Derivative expression. + @type: str """ + + self.expression_tree = None + """ Parse tree for the time derivative expression. + @type: lems.parser.expr.ExprNode """ + + try: + self.expression_tree = ExprParser(expression).parse() + except: + raise ParseError("Parse error when parsing value expression " + "'{0}' for state variable {1}", + self.expression, self.name) + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.variable, self.value) + + +class Action(LEMSBase): + """ + Base class for event handler actions. + """ + + pass + + +class StateAssignment(Action): + """ + State assignment specification. + """ + + def __init__(self, variable, value): + """ + Constructor. + + See instance variable documentation for more info on parameters. + """ + + Action.__init__(self) + + self.variable = variable + """ Name of the variable for which the time derivative is being specified. + @type: str """ + + self.value = value + """ Derivative expression. + @type: str """ + + self.expression_tree = None + """ Parse tree for the time derivative expression. + @type: lems.parser.expr.ExprNode """ + + try: + self.expression_tree = ExprParser(value).parse() + except: + raise ParseError("Parse error when parsing state assignment " + "value expression " + "'{0}' for state variable {1}", + self.value, self.variable) + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.variable, self.value) + + +class EventOut(Action): + """ + Event transmission specification. + """ + + def __init__(self, port): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + Action.__init__(self) + + self.port = port + """ Port on which the event comes in. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.port) + + +class Transition(Action): + """ + Regime transition specification. + """ + + def __init__(self, regime): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + Action.__init__(self) + + self.regime = regime + """ Regime to transition to. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.regime) + + +class EventHandler(LEMSBase): + """ + Base class for event handlers. + """ + + def __init__(self): + """ + Constructor. + """ + + self.actions = list() + """ List of actions to be performed in response to this event. + @type: list(lems.model.dynamics.Action) """ + + def __str__(self): + istr = 'EventHandler...' + return istr + + def add_action(self, action): + """ + Adds an action to this event handler. + + @param action: Action to be added. + @type action: lems.model.dynamics.Action + """ + + self.actions.append(action) + + def add(self, child): + """ + Adds a typed child object to the event handler. + + @param child: Child object to be added. + """ + + if isinstance(child, Action): + self.add_action(child) + else: + raise ModelError('Unsupported child element') + + +class OnStart(EventHandler): + """ + Specification for event handler called upon initialization of the component. + """ + + def __init__(self): + """ + Constructor. + """ + + EventHandler.__init__(self) + + def __str__(self): + istr = 'OnStart: [' + for action in self.actions: + istr += str(action) + istr += ']' + return str(istr) + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + xmlstr = '' + else: + xmlstr += '/>' + + return xmlstr + + +class OnCondition(EventHandler): + """ + Specification for event handler called upon satisfying a given condition. + """ + + def __init__(self, test): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + EventHandler.__init__(self) + + self.test = test + """ Condition to be tested for. + @type: str """ + + try: + self.expression_tree = ExprParser(test).parse() + except: + raise ParseError("Parse error when parsing OnCondition test '{0}'", test) + + def __str__(self): + istr = 'OnCondition...' + return istr + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + xmlstr = '' + else: + xmlstr += '/>' + + return xmlstr + + +class OnEvent(EventHandler): + """ + Specification for event handler called upon receiving en event sent by another component. + """ + + def __init__(self, port): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + EventHandler.__init__(self) + + self.port = port + """ Port on which the event comes in. + @type: str """ + + def __str__(self): + istr = 'OnEvent, port: %s' % self.port + return istr + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + xmlstr = '' + else: + xmlstr += '/>' + + return xmlstr + + +class OnEntry(EventHandler): + """ + Specification for event handler called upon entry into a new behavior regime. + """ + + def __init__(self): + """ + Constructor. + """ + + EventHandler.__init__(self) + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + xmlstr = '' + else: + xmlstr += '/>' + + return xmlstr + + +class KineticScheme(LEMSBase): + """ + Kinetic scheme specifications. + """ + + def __init__(self, name, nodes, state_variable, + edges, edge_source, edge_target, + forward_rate, reverse_rate): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the kinetic scheme. + @type: str """ + + self.nodes = nodes + """ Nodes to be used for the kinetic scheme. + @type: str """ + + self.state_variable = state_variable + """ State variable updated by the kinetic scheme. + @type: str """ + + self.edges = edges + """ Edges to be used for the kinetic scheme. + @type: str """ + + self.edge_source = edge_source + """ Attribute that defines the source of the transition. + @type: str """ + + self.edge_target = edge_target + """ Attribute that defines the target of the transition. + @type: str """ + + self.forward_rate = forward_rate + """ Name of the forward rate exposure. + @type: str """ + + self.reverse_rate = reverse_rate + """ Name of the reverse rate exposure. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ('').format(self.name, + self.nodes, + self.edges, + self.state_variable, + self.edge_source, + self.edge_target, + self.forward_rate, + self.reverse_rate) + + +class Behavioral(LEMSBase): + """ + Store dynamic behavioral attributes. + """ + + def __init__(self): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.parent_behavioral = None + """ Parent behavioral object. + @type: lems.model.dynamics.Behavioral """ + + self.state_variables = Map() + """ Map of state variables in this behavior regime. + @type: dict(str -> lems.model.dynamics.StateVariable """ + + self.derived_variables = Map() + """ Map of derived variables in this behavior regime. + @type: dict(str -> lems.model.dynamics.DerivedVariable """ + + self.conditional_derived_variables = Map() + """ Map of conditional derived variables in this behavior regime. + @type: dict(str -> lems.model.dynamics.ConditionalDerivedVariable """ + + self.time_derivatives = Map() + """ Map of time derivatives in this behavior regime. + @type: dict(str -> lems.model.dynamics.TimeDerivative) """ + + self.event_handlers = list() + """ List of event handlers in this behavior regime. + @type: list(lems.model.dynamics.EventHandler) """ + + self.kinetic_schemes = Map() + """ Map of kinetic schemes in this behavior regime. + @type: dict(str -> lems.model.dynamics.KineticScheme) """ + + def has_content(self): + if len(self.state_variables) == 0 and \ + len(self.derived_variables) == 0 and \ + len(self.conditional_derived_variables) == 0 and \ + len(self.time_derivatives) == 0 and \ + len(self.event_handlers) == 0 and \ + len(self.kinetic_schemes) == 0: + return False + else: + return True + + def clear(self): + """ + Clear behavioral entities. + """ + + self.time_derivatives = Map() + + def add_state_variable(self, sv): + """ + Adds a state variable to this behavior regime. + + @param sv: State variable. + @type sv: lems.model.dynamics.StateVariable + """ + + self.state_variables[sv.name] = sv + + def add_derived_variable(self, dv): + """ + Adds a derived variable to this behavior regime. + + @param dv: Derived variable. + @type dv: lems.model.dynamics.DerivedVariable + """ + + self.derived_variables[dv.name] = dv + + def add_conditional_derived_variable(self, cdv): + """ + Adds a conditional derived variable to this behavior regime. + + @param cdv: Conditional Derived variable. + @type cdv: lems.model.dynamics.ConditionalDerivedVariable + """ + + self.conditional_derived_variables[cdv.name] = cdv + + def add_time_derivative(self, td): + """ + Adds a time derivative to this behavior regime. + + @param td: Time derivative. + @type td: lems.model.dynamics.TimeDerivative + """ + + self.time_derivatives[td.name] = td + + def add_event_handler(self, eh): + """ + Adds an event handler to this behavior regime. + + @param eh: Event handler. + @type eh: lems.model.dynamics.EventHandler + """ + + self.event_handlers.append(eh) + + def add_kinetic_scheme(self, ks): + """ + Adds a kinetic scheme to this behavior regime. + + @param ks: Kinetic scheme. + @type ks: lems.model.dynamics.KineticScheme + """ + + self.kinetic_schemes[ks.name] = ks + + def add(self, child): + """ + Adds a typed child object to the behavioral object. + + @param child: Child object to be added. + """ + + if isinstance(child, StateVariable): + self.add_state_variable(child) + elif isinstance(child, DerivedVariable): + self.add_derived_variable(child) + elif isinstance(child, ConditionalDerivedVariable): + self.add_conditional_derived_variable(child) + elif isinstance(child, TimeDerivative): + self.add_time_derivative(child) + elif isinstance(child, EventHandler): + self.add_event_handler(child) + elif isinstance(child, KineticScheme): + self.add_kinetic_scheme(child) + else: + raise ModelError('Unsupported child element') + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + chxmlstr = '' + + for state_variable in self.state_variables: + chxmlstr += state_variable.toxml() + + for derived_variable in self.derived_variables: + chxmlstr += derived_variable.toxml() + + for conditional_derived_variable in self.conditional_derived_variables: + chxmlstr += conditional_derived_variable.toxml() + + for time_derivative in self.time_derivatives: + chxmlstr += time_derivative.toxml() + + for event_handler in self.event_handlers: + chxmlstr += event_handler.toxml() + + for kinetic_scheme in self.kinetic_schemes: + chxmlstr += kinetic_scheme.toxml() + + if isinstance(self, Dynamics): + for regime in self.regimes: + chxmlstr += regime.toxml() + + if isinstance(self, Dynamics): + xmlprefix = 'Dynamics' + xmlsuffix = 'Dynamics' + xmlempty = '' + else: + xmlprefix = 'Regime name="{0}"'.format(self.name) + \ + (' initial="true"' if self.initial else '') + xmlsuffix = 'Regime' + xmlempty = '<{0}/>', format(xmlprefix) + + if chxmlstr: + xmlstr = '<{0}>'.format(xmlprefix) + chxmlstr + ''.format(xmlsuffix) + else: + xmlstr = xmlempty + + return xmlstr + + +class Regime(Behavioral): + """ + Stores a single behavioral regime for a component type. + """ + + def __init__(self, name, parent_behavioral, initial=False): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + Behavioral.__init__(self) + + self.name = name + """ Name of this behavior regime. + @type: str """ + + self.parent_behavioral = parent_behavioral + """ Parent behavioral object. + @type: lems.model.dynamics.Behavioral """ + + self.initial = initial + """ Initial behavior regime. + @type: bool """ + + +class Dynamics(Behavioral): + """ + Stores behavioral dynamics specification for a component type. + """ + + def __init__(self): + """ + Constructor. + """ + + Behavioral.__init__(self) + + self.regimes = Map() + """ Map of behavior regimes. + @type: Map(str -> lems.model.dynamics.Regime) """ + + def add_regime(self, regime): + """ + Adds a behavior regime to this dynamics object. + + @param regime: Behavior regime to be added. + @type regime: lems.model.dynamics.Regime """ + + self.regimes[regime.name] = regime + + def add(self, child): + """ + Adds a typed child object to the dynamics object. + + @param child: Child object to be added. + """ + + if isinstance(child, Regime): + self.add_regime(child) + else: + Behavioral.add(self, child) + + def has_content(self): + if len(self.regimes) > 0: + return True + else: + return Behavioral.has_content(self) + diff --git a/lems/model/fundamental.py b/lems/model/fundamental.py new file mode 100755 index 0000000000..02600f961e --- /dev/null +++ b/lems/model/fundamental.py @@ -0,0 +1,162 @@ +""" +Dimension and Unit definitions in terms of the fundamental SI units. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" + +from lems.base.base import LEMSBase + +class Include(LEMSBase): + """ + Include another LEMS file. + """ + + def __init__(self, filename): + """ + Constructor. + + @param filename: Name of the file. + @type name: str + + """ + + self.file = filename + """ Name of the file. + @type: str """ + + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''%self.file + +class Dimension(LEMSBase): + """ + Stores a dimension in terms of the seven fundamental SI units. + """ + + def __init__(self, name, description = '', **params): + """ + Constructor. + + @param name: Name of the dimension. + @type name: str + + @param params: Key arguments specifying powers for each of the + seven fundamental SI dimensions. + @type params: dict() + """ + + self.name = name + """ Name of the dimension. + @type: str """ + + self.m = params['m'] if 'm' in params else 0 + """ Power for the mass dimension. + @type: int """ + + self.l = params['l'] if 'l' in params else 0 + """ Power for the length dimension. + @type: int """ + + self.t = params['t'] if 't' in params else 0 + """ Power for the time dimension. + @type: int """ + + self.i = params['i'] if 'i' in params else 0 + """ Power for the electic current dimension. + @type: int """ + + self.k = params['k'] if 'k' in params else 0 + """ Power for the temperature dimension. + @type: int """ + + self.n = params['n'] if 'n' in params else 0 + """ Power for the quantity dimension. + @type: int """ + + self.j = params['j'] if 'j' in params else 0 + """ Power for the luminous intensity dimension. + @type: int """ + + self.description = description + """ Description of this dimension. + @type: str """ + + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + +class Unit(LEMSBase): + """ + Stores a unit definition. + """ + + def __init__(self, name, symbol, dimension, power = 0, scale = 1.0, offset = 0.0, description = ''): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.name = name + """ Name of the unit. + @type: str """ + + self.symbol = symbol + """ Symbol for the unit. + @type: str """ + + self.dimension = dimension + """ Dimension for the unit. + @type: str """ + + self.power = power + """ Scaling by power of 10. + @type: int """ + + self.scale = scale + """ Scaling. + @type: float """ + + self.offset = offset + """ Offset for non-zero units. + @type: float """ + + self.description = description + """ Description of this unit. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + # Probably name should be removed altogether until its usage is decided, see + # https://github.com/LEMS/LEMS/issues/4 + # '''(' name = "{0}"'.format(self.name) if self.name else '') +\''' + + return '' diff --git a/lems/model/model.py b/lems/model/model.py new file mode 100755 index 0000000000..665ffe2539 --- /dev/null +++ b/lems/model/model.py @@ -0,0 +1,850 @@ +""" +Model storage. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" + +import os +from os.path import dirname +import sys + +from lems.base.base import LEMSBase +from lems.base.map import Map +from lems.parser.LEMS import LEMSFileParser +from lems.base.util import merge_maps, merge_lists +from lems.model.component import Constant,ComponentType,Component,FatComponent + +from lems.base.errors import ModelError +from lems.base.errors import SimBuildError + +from lems.model.fundamental import Dimension,Unit,Include +# from lems.model.component import Constant,ComponentType,Component,FatComponent +from lems.model.simulation import Run,Record,EventRecord,DataDisplay,DataWriter,EventWriter +from lems.model.structure import With,EventConnection,ChildInstance,MultiInstantiate + +import xml.dom.minidom as minidom + +import logging + +class Model(LEMSBase): + """ + Stores a model. + """ + + logging.basicConfig(level=logging.INFO) + + target_lems_version = '0.7.3' + branch = 'development' + schema_location = 'https://raw.githubusercontent.com/LEMS/LEMS/{0}/Schemas/LEMS/LEMS_v{1}.xsd'.format(branch, target_lems_version) + #schema_location = '/home/padraig/LEMS/Schemas/LEMS/LEMS_v%s.xsd'%target_lems_version + + debug = False + + def __init__(self, include_includes=True, fail_on_missing_includes=True): + """ + Constructor. + """ + + self.targets = list() + """ List of targets to be run on startup. + @type: list(str) """ + + self.includes = Map() + """ Dictionary of includes defined in the model. + @type: dict(str -> lems.model.fundamental.Include """ + + self.dimensions = Map() + """ Dictionary of dimensions defined in the model. + @type: dict(str -> lems.model.fundamental.Dimension """ + + self.units = Map() + """ Map of units defined in the model. + @type: dict(str -> lems.model.fundamental.Unit """ + + self.component_types = Map() + """ Map of component types defined in the model. + @type: dict(str -> lems.model.component.ComponentType) """ + + self.components = Map() + """ Map of root components defined in the model. + @type: dict(str -> lems.model.component.Component) """ + + self.fat_components = Map() + """ Map of root fattened components defined in the model. + @type: dict(str -> lems.model.component.FatComponent) """ + + self.constants = Map() + """ Map of constants in this component type. + @type: dict(str -> lems.model.component.Constant) """ + + self.include_directories = [] + """ List of include directories to search for included LEMS files. + @type: list(str) """ + + self.included_files = [] + """ List of files already included. + @type: list(str) """ + + self.description = None + """ Short description of contents of LEMS file + @type: str """ + + self.include_includes = include_includes + """ Whether to include LEMS definitions in elements + @type: boolean """ + + self.fail_on_missing_includes = fail_on_missing_includes + """ Whether to raise an Exception when a file in an element is not found + @type: boolean """ + + def add_target(self, target): + """ + Adds a simulation target to the model. + + @param target: Name of the component to be added as a + simulation target. + @type target: str + """ + + self.targets.append(target) + + def add_include(self, include): + """ + Adds an include to the model. + + @param include: Include to be added. + @type include: lems.model.fundamental.Include + """ + + self.includes[include.file] = include + + def add_dimension(self, dimension): + """ + Adds a dimension to the model. + + @param dimension: Dimension to be added. + @type dimension: lems.model.fundamental.Dimension + """ + + self.dimensions[dimension.name] = dimension + + def add_unit(self, unit): + """ + Adds a unit to the model. + + @param unit: Unit to be added. + @type unit: lems.model.fundamental.Unit + """ + + self.units[unit.symbol] = unit + + def add_component_type(self, component_type): + """ + Adds a component type to the model. + + @param component_type: Component type to be added. + @type component_type: lems.model.fundamental.ComponentType + """ + name = component_type.name + + # To handle colons in names in LEMS + if ':' in name: + name = name.replace(':', '_') + component_type.name = name + + self.component_types[name] = component_type + + def add_component(self, component): + """ + Adds a component to the model. + + @param component: Component to be added. + @type component: lems.model.fundamental.Component + """ + + self.components[component.id] = component + + def add_fat_component(self, fat_component): + """ + Adds a fattened component to the model. + + @param fat_component: Fattened component to be added. + @type fat_component: lems.model.fundamental.Fat_component + """ + + self.fat_components[fat_component.id] = fat_component + + def add_constant(self, constant): + """ + Adds a paramter to the model. + + @param constant: Constant to be added. + @type constant: lems.model.component.Constant + """ + + self.constants[constant.name] = constant + + def add(self, child): + """ + Adds a typed child object to the model. + + @param child: Child object to be added. + """ + + if isinstance(child, Include): + self.add_include(child) + elif isinstance(child, Dimension): + self.add_dimension(child) + elif isinstance(child, Unit): + self.add_unit(child) + elif isinstance(child, ComponentType): + self.add_component_type(child) + elif isinstance(child, Component): + self.add_component(child) + elif isinstance(child, FatComponent): + self.add_fat_component(child) + elif isinstance(child, Constant): + self.add_constant(child) + else: + raise ModelError('Unsupported child element') + + # def add_include_directory(self, path): + # """ + # Adds a directory to the include file search path. + # + # @param path: Directory to be added. + # @type path: str + # """ + # + # self.include_directories.append(path) + + # def include_file(self, path, include_dirs = []): + # """ + # Includes a file into the current model. + # + # @param path: Path to the file to be included. + # @type path: str + # + # @param include_dirs: Optional alternate include search path. + # @type include_dirs: list(str) + # """ + # if self.include_includes: + # if self.debug: print("------------------ Including a file: %s"%path) + # inc_dirs = include_dirs if include_dirs else self.include_dirs + # + # parser = LEMSFileParser(self, inc_dirs, self.include_includes) + # if os.access(path, os.F_OK): + # if not path in self.included_files: + # parser.parse(open(path).read()) + # self.included_files.append(path) + # return + # else: + # if self.debug: print("Already included: %s"%path) + # return + # else: + # for inc_dir in inc_dirs: + # new_path = (inc_dir + '/' + path) + # if os.access(new_path, os.F_OK): + # if not new_path in self.included_files: + # parser.parse(open(new_path).read()) + # self.included_files.append(new_path) + # return + # else: + # if self.debug: print("Already included: %s"%path) + # return + # msg = 'Unable to open ' + path + # if self.fail_on_missing_includes: + # raise Exception(msg) + # elif self.debug: + # print(msg) + + def import_from_file(self, filepath): + """ + Import a model from a file. + + @param filepath: File to be imported. + @type filepath: str + """ + + inc_dirs = self.include_directories[:] + inc_dirs.append(dirname(filepath)) + + parser = LEMSFileParser(self, inc_dirs, self.include_includes) + with open(filepath) as f: + parser.parse(f.read()) + + # def export_to_dom(self): + # """ + # Exports this model to a DOM. + # """ + # namespaces = 'xmlns="http://www.neuroml.org/lems/%s" ' + \ + # 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + \ + # 'xsi:schemaLocation="http://www.neuroml.org/lems/%s %s"' + # + # namespaces = namespaces%(self.target_lems_version,self.target_lems_version,self.schema_location) + # + # xmlstr = ''%namespaces + # + # for include in self.includes: + # xmlstr += include.toxml() + # + # for target in self.targets: + # xmlstr += ''.format(target) + # + # for dimension in self.dimensions: + # xmlstr += dimension.toxml() + # + # for unit in self.units: + # xmlstr += unit.toxml() + # + # for constant in self.constants: + # xmlstr += constant.toxml() + # + # for component_type in self.component_types: + # xmlstr += component_type.toxml() + # + # for component in self.components: + # xmlstr += component.toxml() + # + # xmlstr += '' + # + # xmldom = minidom.parseString(xmlstr) + # return xmldom + # + # def export_to_file(self, filepath, level_prefix = ' '): + # """ + # Exports this model to a file. + # + # @param filepath: File to be exported to. + # @type filepath: str + # """ + # xmldom = self.export_to_dom() + # xmlstr = xmldom.toprettyxml(level_prefix, '\n',) + # + # + # f = open(filepath, 'w') + # f.write(xmlstr) + # f.close() + # + def resolve(self): + """ + Resolves references in this model. + """ + model = self.copy() + + for ct in model.component_types: + model.resolve_component_type(ct) + + for c in model.components: + if c.id not in model.fat_components: + model.add(model.fatten_component(c)) + + for c in ct.constants: + c2 = c.copy() + c2.numeric_value = model.get_numeric_value(c2.value, c2.dimension) + model.add(c2) + + return model + + def resolve_component_type(self, component_type): + """ + Resolves references in the specified component type. + + @param component_type: Component type to be resolved. + @type component_type: lems.model.component.ComponentType + """ + + # Resolve component type from base types if present. + if component_type.extends: + try: + base_ct = self.component_types[component_type.extends] + except: + raise ModelError("Component type '{0}' trying to extend unknown component type '{1}'", + component_type.name, component_type.extends) + + self.resolve_component_type(base_ct) + self.merge_component_types(component_type, base_ct) + component_type.types = set.union(component_type.types, base_ct.types) + component_type.extends = None + + def merge_component_types(self, ct, base_ct): + """ + Merge various maps in the given component type from a base + component type. + + @param ct: Component type to be resolved. + @type ct: lems.model.component.ComponentType + + @param base_ct: Component type to be resolved. + @type base_ct: lems.model.component.ComponentType + """ + + #merge_maps(ct.parameters, base_ct.parameters) + for parameter in base_ct.parameters: + if parameter.name in ct.parameters: + p = ct.parameters[parameter.name] + basep = base_ct.parameters[parameter.name] + if p.fixed: + p.value = p.fixed_value + p.dimension = basep.dimension + else: + ct.parameters[parameter.name] = base_ct.parameters[parameter.name] + + merge_maps(ct.properties, base_ct.properties) + + merge_maps(ct.derived_parameters, base_ct.derived_parameters) + merge_maps(ct.index_parameters, base_ct.index_parameters) + merge_maps(ct.constants, base_ct.constants) + merge_maps(ct.exposures, base_ct.exposures) + merge_maps(ct.requirements, base_ct.requirements) + merge_maps(ct.component_requirements, base_ct.component_requirements) + merge_maps(ct.instance_requirements, base_ct.instance_requirements) + merge_maps(ct.children, base_ct.children) + merge_maps(ct.texts, base_ct.texts) + merge_maps(ct.links, base_ct.links) + merge_maps(ct.paths, base_ct.paths) + merge_maps(ct.event_ports, base_ct.event_ports) + merge_maps(ct.component_references, base_ct.component_references) + merge_maps(ct.attachments, base_ct.attachments) + + merge_maps(ct.dynamics.state_variables, base_ct.dynamics.state_variables) + merge_maps(ct.dynamics.derived_variables, base_ct.dynamics.derived_variables) + merge_maps(ct.dynamics.conditional_derived_variables, base_ct.dynamics.conditional_derived_variables) + merge_maps(ct.dynamics.time_derivatives, base_ct.dynamics.time_derivatives) + + #merge_lists(ct.dynamics.event_handlers, base_ct.dynamics.event_handlers) + + merge_maps(ct.dynamics.kinetic_schemes, base_ct.dynamics.kinetic_schemes) + + merge_lists(ct.structure.event_connections, base_ct.structure.event_connections) + merge_lists(ct.structure.child_instances, base_ct.structure.child_instances) + merge_lists(ct.structure.multi_instantiates, base_ct.structure.multi_instantiates) + + merge_maps(ct.simulation.runs, base_ct.simulation.runs) + merge_maps(ct.simulation.records, base_ct.simulation.records) + merge_maps(ct.simulation.event_records, base_ct.simulation.event_records) + merge_maps(ct.simulation.data_displays, base_ct.simulation.data_displays) + merge_maps(ct.simulation.data_writers, base_ct.simulation.data_writers) + merge_maps(ct.simulation.event_writers, base_ct.simulation.event_writers) + + def fatten_component(self, c): + """ + Fatten a component but resolving all references into the corresponding component type. + + @param c: Lean component to be fattened. + @type c: lems.model.component.Component + + @return: Fattened component. + @rtype: lems.model.component.FatComponent + """ + if self.debug: print("Fattening %s"%c.id) + try: + ct = self.component_types[c.type] + except: + raise ModelError("Unable to resolve type '{0}' for component '{1}'; existing: {2}", + c.type, c.id, self.component_types.keys()) + + fc = FatComponent(c.id, c.type) + if c.parent_id: fc.set_parent_id(c.parent_id) + + ### Resolve parameters + for parameter in ct.parameters: + if self.debug: print("Checking: %s"%parameter) + if parameter.name in c.parameters: + p = parameter.copy() + p.value = c.parameters[parameter.name] + p.numeric_value = self.get_numeric_value(p.value, p.dimension) + fc.add_parameter(p) + elif parameter.fixed: + p = parameter.copy() + p.numeric_value = self.get_numeric_value(p.value, p.dimension) + fc.add_parameter(p) + else: + raise ModelError("Parameter '{0}' not initialized for component '{1}'", + parameter.name, c.id) + + ### Resolve properties + for property in ct.properties: + property2 = property.copy() + fc.add(property2) + + ### Resolve derived_parameters + for derived_parameter in ct.derived_parameters: + derived_parameter2 = derived_parameter.copy() + fc.add(derived_parameter2) + + ### Resolve derived_parameters + for index_parameter in ct.index_parameters: + raise ModelError("IndexParameter not yet implemented in PyLEMS!") + index_parameter2 = index_parameter.copy() + fc.add(index_parameter2) + + ### Resolve constants + for constant in ct.constants: + constant2 = constant.copy() + constant2.numeric_value = self.get_numeric_value(constant2.value, constant2.dimension) + fc.add(constant2) + + ### Resolve texts + for text in ct.texts: + t = text.copy() + t.value = c.parameters[text.name] if text.name in c.parameters else '' + fc.add(t) + + ### Resolve texts + for link in ct.links: + if link.name in c.parameters: + l = link.copy() + l.value = c.parameters[link.name] + fc.add(l) + else: + raise ModelError("Link parameter '{0}' not initialized for component '{1}'", + link.name, c.id) + + ### Resolve paths + for path in ct.paths: + if path.name in c.parameters: + p = path.copy() + p.value = c.parameters[path.name] + fc.add(p) + else: + raise ModelError("Path parameter '{0}' not initialized for component '{1}'", + path.name, c.id) + + if len(ct.component_requirements)>0: + raise ModelError("ComponentRequirement not yet implemented in PyLEMS!") + if len(ct.instance_requirements)>0: + raise ModelError("InstanceRequirement not yet implemented in PyLEMS!") + + ### Resolve component references. + for cref in ct.component_references: + if cref.local: + raise ModelError("Attribute local on ComponentReference not yet implemented in PyLEMS!") + if cref.name in c.parameters: + cref2 = cref.copy() + cid = c.parameters[cref.name] + + if cid not in self.fat_components: + self.add(self.fatten_component(self.components[cid])) + + cref2.referenced_component = self.fat_components[cid] + fc.add(cref2) + else: + raise ModelError("Component reference '{0}' not initialized for component '{1}'", + cref.name, c.id) + + merge_maps(fc.exposures, ct.exposures) + merge_maps(fc.requirements, ct.requirements) + merge_maps(fc.component_requirements, ct.component_requirements) + merge_maps(fc.instance_requirements, ct.instance_requirements) + merge_maps(fc.children, ct.children) + merge_maps(fc.texts, ct.texts) + merge_maps(fc.links, ct.links) + merge_maps(fc.paths, ct.paths) + merge_maps(fc.event_ports, ct.event_ports) + merge_maps(fc.attachments, ct.attachments) + + fc.dynamics = ct.dynamics.copy() + if len(fc.dynamics.regimes) != 0: + fc.dynamics.clear() + + self.resolve_structure(fc, ct) + self.resolve_simulation(fc, ct) + + fc.types = ct.types + + ### Resolve children + for child in c.children: + fc.add(self.fatten_component(child)) + + return fc + + + def get_parent_component(self, fc): + """ + TODO: Replace with more efficient way to do this... + """ + if self.debug: print("Looking for parent of %s (%s)"%(fc.id, fc.parent_id)) + parent_comp = None + for comp in self.components.values(): + if self.debug: print(" - Checking "+comp.id) + for child in comp.children: + if parent_comp == None: + if child.id == fc.id and comp.id == fc.parent_id: + if self.debug: print("1) It is "+comp.id) + parent_comp = comp + else: + for child2 in child.children: + if self.debug: print(" - Checking child: %s, %s"%(child.id,child2.id)) + if parent_comp == None and child2.id == fc.id and child.id == fc.parent_id: + if self.debug: print("2) It is "+child.id) + parent_comp = child + break + else: + if self.debug: print("No..." ) + return parent_comp + + + + def resolve_structure(self, fc, ct): + """ + Resolve structure specifications. + """ + if self.debug: print("++++++++ Resolving structure of (%s) with %s"%(fc, ct)) + for w in ct.structure.withs: + try: + if w.instance == 'parent' or w.instance == 'this': + w2 = With(w.instance, w.as_) + else: + w2 = With(fc.paths[w.instance].value, + w.as_) + except: + raise ModelError("Unable to resolve With parameters for " + "'{0}' in component '{1}'", + w.as_, fc.id) + fc.structure.add(w2) + + if len(ct.structure.tunnels) > 0: + raise ModelError("Tunnel is not yet supported in PyLEMS!"); + + for fe in ct.structure.for_eachs: + fc.structure.add_for_each(fe) + + for ev in ct.structure.event_connections: + try: + + from_inst = fc.structure.withs[ev.from_].instance + to_inst = fc.structure.withs[ev.to].instance + + if self.debug: print("EC..: "+from_inst+" to "+to_inst+ " in "+str(fc.paths)) + + if len(fc.texts) > 0 or len(fc.paths) > 0: + + source_port = fc.texts[ev.source_port].value if ev.source_port and len(ev.source_port)>0 and ev.source_port in fc.texts else None + target_port = fc.texts[ev.target_port].value if ev.target_port and len(ev.target_port)>0 and ev.target_port in fc.texts else None + + if self.debug: print("sp: %s"%source_port) + if self.debug: print("tp: %s"%target_port) + + receiver = None + + # TODO: Get more efficient way to find parent comp + if '../' in ev.receiver: + receiver_id = None + parent_attr = ev.receiver[3:] + if self.debug: print("Finding %s in the parent of: %s (%i)"%(parent_attr, fc, id(fc))) + + for comp in self.components.values(): + if self.debug: print(" - Checking %s (%i)" %(comp.id,id(comp))) + for child in comp.children: + if self.debug: print(" - Checking %s (%i)" %(child.id,id(child))) + for child2 in child.children: + if child2.id == fc.id and child2.type == fc.type and child.id == fc.parent_id: + if self.debug: print(" - Got it?: %s (%i), child: %s"%(child.id, id(child), child2)) + receiver_id = child.parameters[parent_attr] + if self.debug: print("Got it: "+receiver_id) + break + + if receiver_id is not None: + for comp in self.fat_components: + if comp.id == receiver_id: + receiver = comp + if self.debug: print("receiver is: %s"%receiver) + + if self.debug: print("rec1: %s"%receiver) + if not receiver: + receiver = fc.component_references[ev.receiver].referenced_component if ev.receiver else None + receiver_container = fc.texts[ev.receiver_container].value if (fc.texts and ev.receiver_container) else '' + + if self.debug: print("rec2: %s"%receiver) + if len(receiver_container)==0: + # TODO: remove this hard coded check! + receiver_container = 'synapses' + + else: + #if from_inst == 'parent': + #par = fc.component_references[ev.receiver] + + if self.debug: + print("+++++++++++++++++++") + print(ev.toxml()) + print(ev.source_port) + print(fc) + source_port = ev.source_port + target_port = ev.target_port + receiver = None + receiver_container = None + + ev2 = EventConnection(from_inst, + to_inst, + source_port, + target_port, + receiver, + receiver_container) + if self.debug: + print("Created EC: "+ev2.toxml()) + print(receiver) + print(receiver_container) + except: + logging.exception("Something awful happened!") + raise ModelError("Unable to resolve event connection parameters in component '{0}'",fc) + fc.structure.add(ev2) + + for ch in ct.structure.child_instances: + try: + if self.debug: print(ch.toxml()) + if '../' in ch.component: + parent = self.get_parent_component(fc) + if self.debug: print("Parent: %s"%parent) + comp_ref = ch.component[3:] + if self.debug: print("comp_ref: %s"%comp_ref) + comp_id = parent.parameters[comp_ref] + comp = self.fat_components[comp_id] + ch2 = ChildInstance(ch.component, comp) + else: + ref_comp = fc.component_references[ch.component].referenced_component + ch2 = ChildInstance(ch.component, ref_comp) + except Exception as e: + if self.debug: print(e) + raise ModelError("Unable to resolve child instance parameters for " + "'{0}' in component '{1}'", + ch.component, fc.id) + fc.structure.add(ch2) + + for mi in ct.structure.multi_instantiates: + try: + if mi.component: + mi2 = MultiInstantiate(component=fc.component_references[mi.component].referenced_component, + number=int(fc.parameters[mi.number].numeric_value)) + else: + mi2 = MultiInstantiate(component_type=fc.component_references[mi.component_type].referenced_component, + number=int(fc.parameters[mi.number].numeric_value)) + except: + raise ModelError("Unable to resolve multi-instantiate parameters for " + "'{0}' in component '{1}'", + mi.component, fc) + fc.structure.add(mi2) + + def resolve_simulation(self, fc, ct): + """ + Resolve simulation specifications. + """ + + for run in ct.simulation.runs: + try: + run2 = Run(fc.component_references[run.component].referenced_component, + run.variable, + fc.parameters[run.increment].numeric_value, + fc.parameters[run.total].numeric_value) + except: + raise ModelError("Unable to resolve simulation run parameters in component '{0}'", + fc.id) + fc.simulation.add(run2) + + for record in ct.simulation.records: + try: + record2 = Record(fc.paths[record.quantity].value, + fc.parameters[record.scale].numeric_value if record.scale else 1, + fc.texts[record.color].value if record.color else '#000000') + except: + raise ModelError("Unable to resolve simulation record parameters in component '{0}'", + fc.id) + fc.simulation.add(record2) + + for event_record in ct.simulation.event_records: + try: + event_record2 = EventRecord(fc.paths[event_record.quantity].value, + fc.texts[event_record.eventPort].value) + except: + raise ModelError("Unable to resolve simulation event_record parameters in component '{0}'", + fc.id) + fc.simulation.add(event_record2) + + for dd in ct.simulation.data_displays: + try: + dd2 = DataDisplay(fc.texts[dd.title].value, + '') + if 'timeScale' in fc.parameters: + dd2.timeScale = fc.parameters['timeScale'].numeric_value + except: + raise ModelError("Unable to resolve simulation display parameters in component '{0}'", + fc.id) + fc.simulation.add(dd2) + + for dw in ct.simulation.data_writers: + try: + path = '' + if fc.texts[dw.path] and fc.texts[dw.path].value: + path = fc.texts[dw.path].value + + dw2 = DataWriter(path, + fc.texts[dw.file_name].value) + except: + raise ModelError("Unable to resolve simulation writer parameters in component '{0}'", + fc.id) + fc.simulation.add(dw2) + + for ew in ct.simulation.event_writers: + try: + path = '' + if fc.texts[ew.path] and fc.texts[ew.path].value: + path = fc.texts[ew.path].value + + ew2 = EventWriter(path, + fc.texts[ew.file_name].value, + fc.texts[ew.format].value) + except: + raise ModelError("Unable to resolve simulation writer parameters in component '{0}'", + fc.id) + fc.simulation.add(ew2) + + + def get_numeric_value(self, value_str, dimension = None): + """ + Get the numeric value for a parameter value specification. + + @param value_str: Value string + @type value_str: str + + @param dimension: Dimension of the value + @type dimension: str + """ + + n = None + i = len(value_str) + while n is None: + try: + part = value_str[0:i] + nn = float(part) + n = nn + s = value_str[i:] + except ValueError: + i = i-1 + + + number = n + sym = s + numeric_value = None + + if sym == '': + numeric_value = number + else: + if sym in self.units: + unit = self.units[sym] + if dimension: + if dimension != unit.dimension and dimension != '*': + raise SimBuildError("Unit symbol '{0}' cannot " + "be used for dimension '{1}'", + sym, dimension) + else: + dimension = unit.dimension + + numeric_value = (number * (10 ** unit.power) * unit.scale) + unit.offset + else: + raise SimBuildError("Unknown unit symbol '{0}'. Known: {1}", + sym, self.units) + + #print("Have converted %s to value: %s, dimension %s"%(value_str, numeric_value, dimension)) + return numeric_value diff --git a/lems/model/simulation.py b/lems/model/simulation.py new file mode 100755 index 0000000000..2175958d46 --- /dev/null +++ b/lems/model/simulation.py @@ -0,0 +1,397 @@ +""" +Simulation specification classes. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" + +from lems.base.base import LEMSBase +from lems.base.errors import ModelError +from lems.base.map import Map + +class Run(LEMSBase): + """ + Stores the description of an object to be run according to an independent + variable (usually time). + """ + + def __init__(self, component, variable, increment, total): + """ + Constructor. + + See instance variable documentation for information on parameters. + """ + + self.component = component + """ Name of the target component to be run according to the + specification given for an independent state variable. + @type: str """ + + self.variable = variable + """ The name of an independent state variable according to which the + target component will be run. + @type: str """ + + self.increment = increment + """ Increment of the state variable on each step. + @type: str """ + + self.total = total + """ Final value of the state variable. + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.component, + self.variable, + self.increment, + self.total) + +class Record(LEMSBase): + """ + Stores the parameters of a statement. + """ + + def __init__(self, quantity, scale = None, color = None, id = None): + """ + Constructor. + + See instance variable documentation for information on parameters. + """ + + self.id = '' + """ Id of the quantity + @type: str """ + + self.quantity = quantity + """ Path to the quantity to be recorded. + @type: str """ + + self.scale = scale + """ Text parameter to be used for scaling the quantity before display. + @type: str """ + + self.color = color + """ Text parameter to be used to specify the color for display. + @type: str """ + + self.id = id + """ Text parameter to be used to specify an id for the record + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.quantity, + self.scale, + self.color, + self.id) +class EventRecord(LEMSBase): + """ + Stores the parameters of an statement. + """ + + def __init__(self, quantity, eventPort): + """ + Constructor. + + See instance variable documentation for information on parameters. + """ + + self.id = '' + """ Id of the quantity + @type: str """ + + self.quantity = quantity + """ Path to the quantity to be recorded. + @type: str """ + + self.eventPort = eventPort + """ eventPort to be used for the event record + @type: str """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.quantity, + self.eventPort) + +class DataOutput(LEMSBase): + """ + Generic data output specification class. + """ + + def __init__(self): + """ + Constuctor. + """ + + pass + +class DataDisplay(DataOutput): + """ + Stores specification for a data display. + """ + + def __init__(self, title, data_region): + """ + Constuctor. + + See instance variable documentation for information on parameters. + """ + + DataOutput.__init__(self) + + self.title = title + """ Title for the display. + @type: string """ + + self.data_region = data_region + """ Display position + @type: string """ + + self.time_scale = 1 + """ Time scale + @type: Number """ + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.title, + self.data_region) + +class DataWriter(DataOutput): + """ + Stores specification for a data writer. + """ + + def __init__(self, path, file_name): + """ + Constuctor. + + See instance variable documentation for information on parameters. + """ + + DataOutput.__init__(self) + + self.path = path + """ Path to the quantity to be saved to file. + @type: string """ + + self.file_name = file_name + """ Text parameter to be used for the file name + @type: string """ + + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.path, + self.file_name) + + def __str__(self): + return 'DataWriter, path: {0}, fileName: {1}'.format(self.path, self.file_name) + + +class EventWriter(DataOutput): + """ + Stores specification for an event writer. + """ + + def __init__(self, path, file_name, format): + """ + Constuctor. + + See instance variable documentation for information on parameters. + """ + + DataOutput.__init__(self) + + self.path = path + """ Path to the quantity to be saved to file. + @type: string """ + + self.file_name = file_name + """ Text parameter to be used for the file name + @type: string """ + + self.format = format + """ Text parameter to be used for the format + @type: string """ + + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.path, + self.file_name, self.format) + + def __str__(self): + return 'EventWriter, path: {0}, fileName: {1}, format: {2}'.format(self.path, self.file_name, self.format) + + + +class Simulation(LEMSBase): + """ + Stores the simulation-related attributes of a component-type. + """ + + def __init__(self): + """ + Constructor. + """ + + self.runs = Map() + """ Map of runs in this dynamics regime. + @type: Map(string -> lems.model.simulation.Run) """ + + self.records = Map() + """ Map of recorded variables in this dynamics regime. + @type: Map(string -> lems.model.simulation.Record """ + + self.event_records = Map() + """ Map of recorded events in this dynamics regime. + @type: Map(string -> lems.model.simulation.EventRecord """ + + self.data_displays = Map() + """ Map of data displays mapping titles to regions. + @type: Map(string -> string) """ + + self.data_writers = Map() + """ Map of recorded variables to data writers. + @type: Map(string -> lems.model.simulation.DataWriter """ + + self.event_writers = Map() + """ Map of recorded variables to event writers. + @type: Map(string -> lems.model.simulation.EventWriter """ + + def add_run(self, run): + """ + Adds a runnable target component definition to the list of runnable + components stored in this context. + + @param run: Run specification + @type run: lems.model.simulation.Run + """ + + self.runs[run.component] = run + + def add_record(self, record): + """ + Adds a record object to the list of record objects in this dynamics + regime. + + @param record: Record object to be added. + @type record: lems.model.simulation.Record + """ + + self.records[record.quantity] = record + + def add_event_record(self, event_record): + """ + Adds an eventrecord object to the list of event_record objects in this dynamics + regime. + + @param event_record: EventRecord object to be added. + @type event_record: lems.model.simulation.EventRecord + """ + + self.event_records[event_record.quantity] = event_record + + def add_data_display(self, data_display): + """ + Adds a data display to this simulation section. + + @param data_display: Data display to be added. + @type data_display: lems.model.simulation.DataDisplay + """ + + self.data_displays[data_display.title] = data_display + + def add_data_writer(self, data_writer): + """ + Adds a data writer to this simulation section. + + @param data_writer: Data writer to be added. + @type data_writer: lems.model.simulation.DataWriter + """ + + self.data_writers[data_writer.path] = data_writer + + def add_event_writer(self, event_writer): + """ + Adds an event writer to this simulation section. + + @param event_writer: event writer to be added. + @type event_writer: lems.model.simulation.EventWriter + """ + + self.event_writers[event_writer.path] = event_writer + + def add(self, child): + """ + Adds a typed child object to the simulation spec. + + @param child: Child object to be added. + """ + + if isinstance(child, Run): + self.add_run(child) + elif isinstance(child, Record): + self.add_record(child) + elif isinstance(child, EventRecord): + self.add_event_record(child) + elif isinstance(child, DataDisplay): + self.add_data_display(child) + elif isinstance(child, DataWriter): + self.add_data_writer(child) + elif isinstance(child, EventWriter): + self.add_event_writer(child) + else: + raise ModelError('Unsupported child element') + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + chxmlstr = '' + + for run in self.runs: + chxmlstr += run.toxml() + + for record in self.records: + chxmlstr += record.toxml() + + for event_record in self.event_records: + chxmlstr += event_record.toxml() + + for data_display in self.data_displays: + chxmlstr += data_display.toxml() + + for data_writer in self.data_writers: + chxmlstr += data_writer.toxml() + + for event_writer in self.event_writers: + chxmlstr += event_writer.toxml() + + if chxmlstr: + xmlstr = '' + chxmlstr + '' + else: + xmlstr = '' + + return xmlstr diff --git a/lems/model/structure.py b/lems/model/structure.py new file mode 100755 index 0000000000..cb89bd6ae5 --- /dev/null +++ b/lems/model/structure.py @@ -0,0 +1,500 @@ +""" +Structural properties of component types. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" + +from lems.base.base import LEMSBase +from lems.base.map import Map +from lems.base.errors import ModelError + +class With(LEMSBase): + """ + Stores a with-as statement. + """ + + def __init__(self, instance, as_, list=None, index=None): + """ + Constructor referencing single identified instance, or list/index. + + """ + + self.instance = instance + """ Instance to be referenced. + @type: str """ + + self.as_ = as_ + """ Alternative name. + @type: str """ + + self.list = list + """ list of components, e.g. population + @type: str """ + + self.index = index + """ index in list to be referenced. + @type: str """ + + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.instance, self.as_) + + +class Tunnel(LEMSBase): + """ + Stores a Tunnel. + """ + + def __init__(self, name, end_a, end_b, component_a, component_b): + """ + Constructor. + + """ + + self.name = name + """ name of Tunnel. + @type: str """ + + self.end_a = end_a + """ 'A' end of Tunnel. + @type: str """ + + self.end_b = end_b + """ 'B' end of Tunnel. + @type: str """ + + self.component_a = component_a + """ Component to create at A. + @type: str """ + + self.component_b = component_b + """ Component to create at B. + @type: str """ + + + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + +class EventConnection(LEMSBase): + """ + Stores an event connection specification. + """ + + def __init__(self, from_, to, + source_port, target_port, + receiver, receiver_container): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.from_ = from_ + """ Name of the source component for event. + @type: str """ + + self.to = to + """ Name of the target component for the event. + @type: str """ + + self.source_port = source_port + """ Source port name. + @type: str """ + + self.target_port = target_port + """ Target port name. + @type: str """ + + self.receiver = receiver + """ Name of the proxy receiver component attached to the target component that actually receiving the event. + @type: str """ + + self.receiver_container = receiver_container + """ Name of the child component grouping to add the receiver to. + @type: str """ + + def __eq__(self, o): + return (self.from_ == o.from_ and self.to == o.to and + self.source_port == o.source_port and self.target_port == o.target_port) + + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return '' + +class ChildInstance(LEMSBase): + """ + Stores a child instantiation specification. + """ + + def __init__(self, component, referenced_component = None): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.component = component + """ Name of the component reference to be used for instantiation. + @type: str """ + + self.referenced_component = referenced_component + """ Target component being referenced after resolution. + @type: lems.model.component.FatComponent """ + + def __eq__(self, o): + return self.component == o.component + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + + return ''.format(self.component) + +class Assign(LEMSBase): + """ + Stores a child assign specification. + """ + + def __init__(self, property, value): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.property_ = property + """ Name of the property reference to be used for instantiation. + @type: str """ + + self.value = value + """ Value of the property. + @type: str""" + + def __eq__(self, o): + return self.property_ == o.property_ and self.value == o.value + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + return ''.format(self.property_, self.value) + + +class MultiInstantiate(LEMSBase): + """ + Stores a child multi-instantiation specification. + """ + + def __init__(self, component=None, number=None, component_type=None): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + if component and component_type: + raise AttributeError("MultiInstantiate should contain either" + " an attribute component or an attribute" + " component_type, not both.") + + self.component = component + """ Name of the component reference to be used for instantiation. + @type: str """ + + self.component_type = component_type + """ Name of the component type reference to be used for instantiation. + @type: str """ + + self.number = number + """ Name of the paramter specifying the number of times the component + reference is to be instantiated. + @type: str""" + + self.assignments = [] + """ List of assignments included in MultiInstantiate. + @type: list(Assign) """ + + def __eq__(self, o): + if self.component: + flag = self.component == o.component and self.number == o.number + else: + flag = self.component_type == o.component_type and self.number == o.number + return flag + + def add_assign(self, assign): + """ + Adds an Assign to the structure. + + @param assign: Assign structure. + @type assign: lems.model.structure.Assign + """ + self.assignments.append(assign) + + def add(self, child): + """ + Adds a typed child object to the structure object. + + @param child: Child object to be added. + """ + + if isinstance(child, Assign): + self.add_assign(child) + else: + raise ModelError('Unsupported child element') + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + argstr = '' + if self.component: + argstr += 'component="{0}" '.format(self.component) + if self.component_type: + argstr += 'componentType="{0}" '.format(self.component_type) + if self.number: + argstr += 'number="{0}" '.format(self.number) + if self.assignments: + chxmlstr = '' + for assign in self.assignments: + chxmlstr += assign.toxml() + return '{1}'.format(argstr, chxmlstr) + else: + return ''.format(argstr) + +class ForEach(LEMSBase): + """ + ForEach specification. + """ + def __init__(self, instances, as_): + + self.instances = instances + + self.as_ = as_ + + self.event_connections = list() + """ List of event connections. + @type: list(lems.model.structure.EventConnection) """ + + self.for_eachs = list() + """ List of for each specs. + @type: list(lems.model.structure.ForEach) """ + + + def add_for_each(self, fe): + """ + Adds a for-each specification. + + @param fe: For-each specification. + @type fe: lems.model.structure.ForEach + """ + + self.for_eachs.append(fe) + + + def add_event_connection(self, ec): + """ + Adds an event conenction to the structure. + + @param ec: Event connection. + @type ec: lems.model.structure.EventConnection + """ + + self.event_connections.append(ec) + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + chxmlstr = '' + + for event_connection in self.event_connections: + chxmlstr += event_connection.toxml() + + for for_each in self.for_eachs: + chxmlstr += for_each.toxml() + + + return '{2}'.format(self.instances, self.as_, chxmlstr) + + +class Structure(LEMSBase): + """ + Stores structural properties of a component type. + """ + + def __init__(self): + """ + Constructor. + """ + + self.withs = Map() + """ Map of With statements. + @type: Map(str -> lems.model.structure.With) """ + + self.tunnels = Map() + """ Map of tunnel statements. + @type: Map(str -> lems.model.structure.Tunnel) """ + + self.event_connections = list() + """ List of event connections. + @type: list(lems.model.structure.EventConnection) """ + + self.child_instances = list() + """ List of child instantations. + @type: list(lems.model.structure.ChildInstance) """ + + self.multi_instantiates = list() + """ List of child multi-instantiations. + @type: list(lems.model.structure.MultiInstantiate) """ + + self.for_eachs = list() + """ List of for each specs. + @type: list(lems.model.structure.ForEach) """ + + def has_content(self): + if len(self.withs)==0 and \ + len(self.event_connections)==0 and \ + len(self.child_instances)==0 and \ + len(self.multi_instantiates)==0 and \ + len(self.for_eachs)==0: + return False + else: + return True + + def add_with(self, with_): + """ + Adds a with-as specification to the structure. + + @param with_: With-as specification. + @type with_: lems.model.structure.With + """ + + self.withs[with_.as_] = with_ + + def add_tunnel(self, tunnel): + """ + Adds a tunnel specification to the structure. + + @param tunnel: tunnel specification. + @type tunnel: lems.model.structure.Tunnel + """ + + self.tunnels[tunnel.name] = tunnel + + def add_event_connection(self, ec): + """ + Adds an event conenction to the structure. + + @param ec: Event connection. + @type ec: lems.model.structure.EventConnection + """ + + self.event_connections.append(ec) + + def add_child_instance(self, ci): + """ + Adds a child instantiation specification. + + @param ci: Child instantiation specification. + @type ci: lems.model.structure.ChildInstance + """ + + self.child_instances.append(ci) + + def add_multi_instantiate(self, mi): + """ + Adds a child multi-instantiation specification. + + @param mi: Child multi-instantiation specification. + @type mi: lems.model.structure.MultiInstantiate + """ + + self.multi_instantiates.append(mi) + + def add_for_each(self, fe): + """ + Adds a for-each specification. + + @param fe: For-each specification. + @type fe: lems.model.structure.ForEach + """ + + self.for_eachs.append(fe) + + def add(self, child): + """ + Adds a typed child object to the structure object. + + @param child: Child object to be added. + """ + + if isinstance(child, With): + self.add_with(child) + elif isinstance(child, EventConnection): + self.add_event_connection(child) + elif isinstance(child, ChildInstance): + self.add_child_instance(child) + elif isinstance(child, MultiInstantiate): + self.add_multi_instantiate(child) + elif isinstance(child, ForEach): + self.add_for_each(child) + else: + raise ModelError('Unsupported child element') + + def toxml(self): + """ + Exports this object into a LEMS XML object + """ + chxmlstr = '' + + for with_ in self.withs: + chxmlstr += with_.toxml() + + for event_connection in self.event_connections: + chxmlstr += event_connection.toxml() + + for child_instance in self.child_instances: + chxmlstr += child_instance.toxml() + + for multi_instantiate in self.multi_instantiates: + chxmlstr += multi_instantiate.toxml() + + for for_each in self.for_eachs: + chxmlstr += for_each.toxml() + + if chxmlstr: + xmlstr = '' + chxmlstr + '' + else: + xmlstr = '' + + return xmlstr diff --git a/lems/parser/LEMS.py b/lems/parser/LEMS.py new file mode 100755 index 0000000000..61dbe06e0e --- /dev/null +++ b/lems/parser/LEMS.py @@ -0,0 +1,1764 @@ +""" +LEMS XML file format parser. + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org + +MAvdVlag: altered attributes for constants, state_variables, derived_variables, time_derivatives, +conditional_derived_variable and exposures +""" + +import xml.etree.ElementTree as xe + +from lems.base.base import LEMSBase +# from lems.model.fundamental import * +from lems.model.component import * +# from lems.model.dynamics import * +# from lems.model.structure import * +# from lems.model.simulation import * + +# from lems.base.util import make_id + +from lems.model.dynamics import * + + +def get_nons_tag_from_node(node): + tag = node.tag + bits = tag.split('}') + if len(bits) == 1: + return tag + elif '/lems/' in bits[0]: + return bits[1] + elif 'neuroml2' in bits[0]: + return bits[1] + elif 'rdf' in bits[0]: + return "rdf_"+bits[1] + elif 'model-qualifiers' in bits[0]: + return "bqmodel_"+bits[1] + elif 'biology-qualifiers' in bits[0]: + return "bqbiol_"+bits[1] + else: + return "%s:%s"%(bits[0],bits[1]) + +class LEMSXMLNode: + def __init__(self, pyxmlnode): + self.tag = get_nons_tag_from_node(pyxmlnode) + self.ltag = self.tag.lower() + + self.attrib = dict() + self.lattrib = dict() + + for k in pyxmlnode.attrib: + self.attrib[k] = pyxmlnode.attrib[k] + self.lattrib[k.lower()] = pyxmlnode.attrib[k] + + self.children = list() + for pyxmlchild in pyxmlnode: + self.children.append(LEMSXMLNode(pyxmlchild)) + + def __str__(self): + return 'LEMSXMLNode <{0} {1}>'.format(self.tag, self.attrib) + +class LEMSFileParser(LEMSBase): + """ + LEMS XML file format parser class. + """ + + def __init__(self, model, include_dirs = [], include_includes=True): + """ + Constructor. + + See instance variable documentation for more details on parameters. + """ + + self.model = model + """ Model instance to be populated from the parsed file. + @type: lems.model.model.Model """ + + self.include_dirs = include_dirs + """ List of directories to search for included files. + @type: list(str) """ + + self.tag_parse_table = None + """ Dictionary of xml tags to parse methods + @type: dict(string -> function) """ + + self.valid_children = None + """ Dictionary mapping each tag to it's list of valid child tags. + @type: dict(string -> string) """ + + self.id_counter = None + """ Counter generator for generating unique ids. + @type: generator(int) """ + + self.include_includes = include_includes + """ Whether to include LEMS definitions in elements + @type: boolean """ + + self.init_parser() + + def init_parser(self): + """ + Initializes the parser + """ + + #self.token_list = None + #self.prev_token_lists = None + + self.valid_children = dict() + self.valid_children['lems'] = ['component', 'componenttype', + 'target', 'include', + 'dimension', 'unit', 'assertion'] + + #TODO: make this generic for any domain specific language based on LEMS + self.valid_children['neuroml'] = ['include', 'componenttype'] + + self.valid_children['componenttype'] = ['dynamics', + 'child', 'children', + 'componentreference', + 'exposure', 'eventport', + 'fixed', 'link', 'parameter', + 'property', + 'indexparameter', + 'path', 'requirement', + 'componentrequirement', + 'instancerequirement', + 'simulation', 'structure', + 'text', 'attachments', + 'constant', 'derivedparameter', + 'function'] + + self.valid_children['dynamics'] = ['derivedvariable', + 'conditionalderivedvariable', + 'oncondition', + 'onevent', 'onstart', + 'statevariable', 'timederivative', + 'kineticscheme', 'regime'] + + self.valid_children['component'] = ['component'] + + self.valid_children['conditionalderivedvariable'] = ['case'] + + self.valid_children['regime'] = ['oncondition', 'onentry', 'timederivative'] + self.valid_children['oncondition'] = ['eventout', 'stateassignment', 'transition'] + self.valid_children['onentry'] = ['eventout', 'stateassignment', 'transition'] + self.valid_children['onevent'] = ['eventout', 'stateassignment', 'transition'] + self.valid_children['onstart'] = ['eventout', 'stateassignment', 'transition'] + self.valid_children['structure'] = ['childinstance', + 'eventconnection', + 'foreach', + 'multiinstantiate', + 'with', + 'tunnel'] + + self.valid_children['foreach'] = ['foreach', 'eventconnection'] + + self.valid_children['simulation'] = ['record', 'eventrecord', 'run', + 'datadisplay', 'datawriter', 'eventwriter'] + + self.tag_parse_table = dict() + #self.tag_parse_table['assertion'] = self.parse_assertion + self.tag_parse_table['attachments'] = self.parse_attachments + self.tag_parse_table['child'] = self.parse_child + self.tag_parse_table['childinstance'] = self.parse_child_instance + self.tag_parse_table['children'] = self.parse_children + self.tag_parse_table['component'] = self.parse_component + self.tag_parse_table['componentreference'] = self.parse_component_reference + self.tag_parse_table['componentrequirement'] = self.parse_component_requirement + self.tag_parse_table['componenttype'] = self.parse_component_type + self.tag_parse_table['constant'] = self.parse_constant + self.tag_parse_table['function'] = self.parse_function + self.tag_parse_table['datadisplay'] = self.parse_data_display + self.tag_parse_table['datawriter'] = self.parse_data_writer + self.tag_parse_table['eventwriter'] = self.parse_event_writer + self.tag_parse_table['derivedparameter'] = self.parse_derived_parameter + self.tag_parse_table['derivedvariable'] = self.parse_derived_variable + self.tag_parse_table['conditionalderivedvariable'] = self.parse_conditional_derived_variable + self.tag_parse_table['case'] = self.parse_case + self.tag_parse_table['dimension'] = self.parse_dimension + self.tag_parse_table['dynamics'] = self.parse_dynamics + self.tag_parse_table['eventconnection'] = self.parse_event_connection + self.tag_parse_table['eventout'] = self.parse_event_out + self.tag_parse_table['eventport'] = self.parse_event_port + self.tag_parse_table['exposure'] = self.parse_exposure + self.tag_parse_table['fixed'] = self.parse_fixed + self.tag_parse_table['foreach'] = self.parse_for_each + self.tag_parse_table['include'] = self.parse_include + self.tag_parse_table['indexparameter'] = self.parse_index_parameter + self.tag_parse_table['kineticscheme'] = self.parse_kinetic_scheme + self.tag_parse_table['link'] = self.parse_link + self.tag_parse_table['multiinstantiate'] = self.parse_multi_instantiate + self.tag_parse_table['oncondition'] = self.parse_on_condition + self.tag_parse_table['onentry'] = self.parse_on_entry + self.tag_parse_table['onevent'] = self.parse_on_event + self.tag_parse_table['onstart'] = self.parse_on_start + self.tag_parse_table['parameter'] = self.parse_parameter + self.tag_parse_table['property'] = self.parse_property + self.tag_parse_table['path'] = self.parse_path + self.tag_parse_table['record'] = self.parse_record + self.tag_parse_table['eventrecord'] = self.parse_event_record + self.tag_parse_table['regime'] = self.parse_regime + self.tag_parse_table['requirement'] = self.parse_requirement + self.tag_parse_table['instancerequirement'] = self.parse_instance_requirement + self.tag_parse_table['run'] = self.parse_run + #self.tag_parse_table['show'] = self.parse_show + self.tag_parse_table['simulation'] = self.parse_simulation + self.tag_parse_table['stateassignment'] = self.parse_state_assignment + self.tag_parse_table['statevariable'] = self.parse_state_variable + self.tag_parse_table['structure'] = self.parse_structure + self.tag_parse_table['target'] = self.parse_target + self.tag_parse_table['text'] = self.parse_text + self.tag_parse_table['timederivative'] = self.parse_time_derivative + self.tag_parse_table['transition'] = self.parse_transition + self.tag_parse_table['tunnel'] = self.parse_tunnel + self.tag_parse_table['unit'] = self.parse_unit + self.tag_parse_table['with'] = self.parse_with + + self.xml_node_stack = [] + + self.current_component_type = None + self.current_dynamics = None + self.current_regime = None + self.current_event_handler = None + self.current_structure = None + self.current_simulation = None + self.current_component = None + + def counter(): + count = 1 + while True: + yield count + count = count + 1 + + self.id_counter = counter() + + + def process_nested_tags(self, node, tag = ''): + """ + Process child tags. + + @param node: Current node being parsed. + @type node: xml.etree.Element + + @raise ParseError: Raised when an unexpected nested tag is found. + """ + ##print("---------Processing: %s, %s"%(node.tag,tag)) + + if tag == '': + t = node.ltag + else: + t = tag.lower() + + for child in node.children: + self.xml_node_stack = [child] + self.xml_node_stack + + ctagl = child.ltag + + if ctagl in self.tag_parse_table and ctagl in self.valid_children[t]: + #print("Processing known type: %s"%ctagl) + self.tag_parse_table[ctagl](child) + else: + #print("Processing unknown type: %s"%ctagl) + self.parse_component_by_typename(child, child.tag) + + self.xml_node_stack = self.xml_node_stack[1:] + + def parse(self, xmltext): + """ + Parse a string containing LEMS XML text. + + @param xmltext: String containing LEMS XML formatted text. + @type xmltext: str + """ + + xml = LEMSXMLNode(xe.XML(xmltext)) + + if xml.ltag != 'lems' and xml.ltag != 'neuroml': + raise ParseError(' expected as root element (or even ), found: {0}'.format(xml.ltag)) + ''' + if xml.ltag == 'lems': + if 'description' in xml.lattrib: + self.description = xml.lattrib['description'] + ''' + + self.process_nested_tags(xml) + + + def raise_error(self, message, *params, **key_params): + """ + Raise a parse error. + """ + + s = 'Parser error in ' + + self.xml_node_stack.reverse() + if len(self.xml_node_stack) > 1: + node = self.xml_node_stack[0] + s += '<{0}'.format(node.tag) + if 'name' in node.lattrib: + s += ' name=\"{0}\"'.format(node.lattrib['name']) + if 'id' in node.lattrib: + s += ' id=\"{0}\"'.format(node.lattrib['id']) + s += '>' + + for node in self.xml_node_stack[1:]: + s += '.<{0}'.format(node.tag) + if 'name' in node.lattrib: + s += ' name=\"{0}\"'.format(node.lattrib['name']) + if 'id' in node.lattrib: + s += ' id=\"{0}\"'.format(node.lattrib['id']) + s += '>' + + s += ':\n ' + message + + raise ParseError(s, *params, **key_params) + + self.xml_node_stack.reverse() + + +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### +##################################################################################### + + + + + + def parse_assertion(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + print('TODO - ') + + + def parse_attachments(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name.') + + if 'type' in node.lattrib: + type_ = node.lattrib['type'] + else: + self.raise_error("Attachment '{0}' must specify a type.", + name) + + description = node.lattrib.get('description', '') + self.current_component_type.add_attachments(Attachments(name, type_, description)) + + def parse_child(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name.') + + if 'type' in node.lattrib: + type_ = node.lattrib['type'] + else: + self.raise_error("Child '{0}' must specify a type.", name) + + self.current_component_type.add_children(Children(name, type_, False)) + + def parse_child_instance(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'component' in node.lattrib: + component = node.lattrib['component'] + else: + self.raise_error(' must specify a component reference') + + self.current_structure.add_child_instance(ChildInstance(component)) + + def parse_children(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name.') + + if 'type' in node.lattrib: + type_ = node.lattrib['type'] + else: + self.raise_error("Children '{0}' must specify a type.", name) + + self.current_component_type.add_children(Children(name, type_, True)) + + def parse_component_by_typename(self, node, type_): + """ + Parses components defined directly by component name. + + @param node: Node containing the element + @type node: xml.etree.Element + + @param type_: Type of this component. + @type type_: string + + @raise ParseError: Raised when the component does not have an id. + """ + #print('Parsing component {0} by typename {1}'.format(node, type_)) + if 'id' in node.lattrib: + id_ = node.lattrib['id'] + else: + #self.raise_error('Component must have an id') + id_ = node.tag #make_id() + + if 'type' in node.lattrib: + type_ = node.lattrib['type'] + else: + type_ = node.tag + + component = Component(id_, type_) + + if self.current_component: + component.set_parent_id(self.current_component.id) + self.current_component.add_child(component) + + else: + self.model.add_component(component) + + for key in node.attrib: + if key.lower() not in ['id', 'type']: + component.set_parameter(key, node.attrib[key]) + + old_component = self.current_component + self.current_component = component + self.process_nested_tags(node, 'component') + self.current_component = old_component + + def parse_component(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'id' in node.lattrib: + id_ = node.lattrib['id'] + else: + #self.raise_error('Component must have an id') + id_ = make_id() + + if 'type' in node.lattrib: + type_ = node.lattrib['type'] + else: + self.raise_error("Component {0} must have a type.", + id_) + + component = Component(id_, type_) + + if self.current_component: + component.set_parent_id(self.current_component.id) + self.current_component.add_child(component) + else: + self.model.add_component(component) + + for key in node.attrib: + if key.lower() not in ['id', 'type']: + component.set_parameter(key, node.attrib[key]) + + old_component = self.current_component + self.current_component = component + self.process_nested_tags(node) + self.current_component = old_component + + def parse_component_reference(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name for the ' + + 'reference.') + + if 'type' in node.lattrib: + type_ = node.lattrib['type'] + else: + self.raise_error(' must specify a type for the ' + + 'reference.') + + if 'local' in node.lattrib: + local = node.lattrib['local'] + else: + local = None + + self.current_component_type.add_component_reference(ComponentReference(name, type_, local)) + + def parse_component_type(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when the component type does not have a + name. + """ + + try: + name = node.lattrib['name'] + except: + self.raise_error(' must specify a name') + + if 'extends' in node.lattrib: + extends = node.lattrib['extends'] + else: + extends = None + + if 'description' in node.lattrib: + description = node.lattrib['description'] + else: + description = '' + + component_type = ComponentType(name, description, extends) + self.model.add_component_type(component_type) + + self.current_component_type = component_type + self.process_nested_tags(node) + self.current_component_type = None + + def parse_constant(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + MV: fixed the symbol part. It was not there for constant parsing + """ + + try: + name = node.lattrib['name'] + except: + self.raise_error(' must specify a name.') + + domain = node.lattrib.get('domain', None) + symbol = node.lattrib.get('symbol', None) + + try: + default = node.lattrib['default'] + except: + self.raise_error("Constant '{0}' must have a value.", name) + + description = node.lattrib.get('description', '') + + constant = Constant(name, default, domain, symbol, description) + + if self.current_component_type: + self.current_component_type.add_constant(constant) + else: + self.model.add_constant(constant) + + def parse_function(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + MV: added function for pre and post coupling behavior + """ + try: + name = node.lattrib['name'] + except: + self.raise_error(' must specify a name.') + + try: + value = node.lattrib['value'] + except: + self.raise_error("Function '{0}' must have a value.", name) + + dimension = None + symbol = None + description = None + + function = Function(name, value, dimension, symbol, description) + + if self.current_component_type: + self.current_component_type.add_function(function) + else: + self.model.add_function(function) + + + def parse_data_display(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'title' in node.lattrib: + title = node.lattrib['title'] + else: + self.raise_error(' must have a title.') + + if 'dataregion' in node.lattrib: + data_region = node.lattrib['dataregion'] + else: + data_region = None + + self.current_simulation.add_data_display(DataDisplay(title, data_region)) + + def parse_data_writer(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'path' in node.lattrib: + path = node.lattrib['path'] + else: + self.raise_error(' must specify a path.') + + if 'filename' in node.lattrib: + file_path = node.lattrib['filename'] + else: + self.raise_error("Data writer for '{0}' must specify a filename.", + path) + + self.current_simulation.add_data_writer(DataWriter(path, file_path)) + + def parse_event_writer(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'path' in node.lattrib: + path = node.lattrib['path'] + else: + self.raise_error(' must specify a path.') + + if 'filename' in node.lattrib: + file_path = node.lattrib['filename'] + else: + self.raise_error("Event writer for '{0}' must specify a filename.", + path) + + if 'format' in node.lattrib: + format = node.lattrib['format'] + else: + self.raise_error("Event writer for '{0}' must specify a format.", + path) + + self.current_simulation.add_event_writer(EventWriter(path, file_path, format)) + + def parse_derived_parameter(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + #if self.current_context.context_type != Context.COMPONENT_TYPE: + # self.raise_error('Dynamics must be defined inside a ' + + # 'component type') + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error('A derived parameter must have a name') + + if 'expression' in node.lattrib: + expression = node.lattrib['expression'] + else: + expression = None + + if 'value' in node.lattrib: + value = node.lattrib['value'] + else: + value = None + + if 'select' in node.lattrib: + select = node.lattrib['select'] + else: + select = None + + self.current_component_type.add_derived_parameter(DerivedParameter(name, value, + expression, select)) + + def parse_derived_variable(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when no name of specified for the derived variable. + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + elif 'exposure' in node.lattrib: + name = node.lattrib['exposure'] + else: + self.raise_error(' must specify a name') + + params = dict() + for attr_name in ['dimension', 'exposure', 'select', 'expression', 'reduce', 'required']: + if attr_name in node.lattrib: + params[attr_name] = node.lattrib[attr_name] + + self.current_regime.add_derived_variable(DerivedVariable(name, **params)) + + + def parse_conditional_derived_variable(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when no name or value is specified for the conditional derived variable. + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + elif 'exposure' in node.lattrib: + name = node.lattrib['exposure'] + else: + self.raise_error(' must specify a name') + + if 'exposure' in node.lattrib: + exposure = node.lattrib['exposure'] + else: + exposure = None + + if 'condition' in node.lattrib: + condition = node.lattrib['condition'] + else: + condition = None + + if 'cases' in node.lattrib: + cases = node.lattrib['cases'] + else: + cases = None + + conditional_derived_variable = ConditionalDerivedVariable(name, condition, exposure, cases) + + self.current_regime.add_conditional_derived_variable(conditional_derived_variable) + + self.current_conditional_derived_variable = conditional_derived_variable + + self.process_nested_tags(node) + + + def parse_case(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: When no condition or value is specified + """ + + try: + condition = node.lattrib['condition'] + except: + condition = None + + try: + value = node.lattrib['value'] + except: + self.raise_error(' must specify a value') + + self.current_conditional_derived_variable.add_case(Case(condition, value)) + + def parse_dimension(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: When the name is not a string or if the + dimension is not a signed integer. + """ + + try: + name = node.lattrib['name'] + except: + self.raise_error(' must specify a name') + + description = node.lattrib.get('description', '') + + dim = dict() + for d in ['l', 'm', 't', 'i', 'k', 'c', 'n']: + dim[d] = int(node.lattrib.get(d, 0)) + + self.model.add_dimension(Dimension(name, description, **dim)) + + def parse_dynamics(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + self.current_dynamics = self.current_component_type.dynamics + self.current_regime = self.current_dynamics + self.process_nested_tags(node) + self.current_regime = None + self.current_dynamics = None + + def parse_event_connection(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'from' in node.lattrib: + from_ = node.lattrib['from'] + else: + self.raise_error(' must provide a source (from) component reference.') + + if 'to' in node.lattrib: + to = node.lattrib['to'] + else: + self.raise_error(' must provide a target (to) component reference.') + + source_port = node.lattrib.get('sourceport', '') + target_port = node.lattrib.get('targetport', '') + receiver = node.lattrib.get('receiver', '') + receiver_container = node.lattrib.get('receivercontainer', '') + + ec = EventConnection(from_, to, source_port, target_port, receiver, receiver_container) + self.current_structure.add_event_connection(ec) + + def parse_event_out(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + try: + port = node.lattrib['port'] + except: + self.raise_error(' must be specify a port.') + + action = EventOut(port) + + self.current_event_handler.add_action(action) + + def parse_event_port(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error((' must specify a name.')) + + if 'direction' in node.lattrib: + direction = node.lattrib['direction'] + else: + self.raise_error("Event port '{0}' must specify a direction.") + + direction = direction.lower() + if direction != 'in' and direction != 'out': + self.raise_error(('Event port direction must be \'in\' ' + 'or \'out\'')) + + description = node.lattrib.get('description', '') + + self.current_component_type.add_event_port(EventPort(name, direction, description)) + + def parse_exposure(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when the exposure name is not + being defined in the context of a component type. + """ + + if self.current_component_type == None: + self.raise_error('Exposures must be defined in a component type') + + try: + name = node.lattrib['name'] + except: + self.raise_error(' must specify a name') + + try: + choices = node.lattrib['choices'] + except: + self.raise_error("Exposure '{0}' must specify choices", + name) + + try: + default = node.lattrib['default'] + except: + self.raise_error("Exposure '{0}' must specify default", + name) + + description = node.lattrib.get('description', '') + + self.current_component_type.add_exposure(Exposure(name, choices, default, description)) + + def parse_fixed(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + try: + parameter = node.lattrib['parameter'] + except: + self.raise_error(' must specify a parameter to be fixed.') + + try: + value = node.lattrib['value'] + except: + self.raise_error("Fixed parameter '{0}'must specify a value.", parameter) + + description = node.lattrib.get('description', '') + + self.current_component_type.add_parameter(Fixed(parameter, value, description)) + + def parse_for_each(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if self.current_structure == None: + self.raise_error(' can only be made within ' + + 'a structure definition') + + if 'instances' in node.lattrib: + instances = node.lattrib['instances'] + else: + self.raise_error(' must specify a reference to target' + 'instances') + + if 'as' in node.lattrib: + as_ = node.lattrib['as'] + else: + self.raise_error(' must specify a name for the ' + 'enumerated target instances') + + old_structure = self.current_structure + fe = ForEach(instances, as_) + self.current_structure.add_for_each(fe) + self.current_structure = fe + + self.process_nested_tags(node) + + self.current_structure = old_structure + + def parse_include(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when the file to be included is not specified. + """ + if not self.include_includes: + if self.model.debug: print("Ignoring included LEMS file: %s"%node.lattrib['file']) + else: + + #TODO: remove this hard coding for reading NeuroML includes... + if 'file' not in node.lattrib: + if 'href' in node.lattrib: + self.model.include_file(node.lattrib['href'], self.include_dirs) + return + else: + self.raise_error(' must specify the file to be included.') + + self.model.include_file(node.lattrib['file'], self.include_dirs) + + def parse_kinetic_scheme(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name.') + + if 'nodes' in node.lattrib: + nodes = node.lattrib['nodes'] + else: + self.raise_error("Kinetic scheme '{0}' must specify nodes.", name) + + if 'statevariable' in node.lattrib: + state_variable = node.lattrib['statevariable'] + else: + self.raise_error("Kinetic scheme '{0}' must specify a state variable.", name) + + if 'edges' in node.lattrib: + edges = node.lattrib['edges'] + else: + self.raise_error("Kinetic scheme '{0}' must specify edges.", name) + + if 'edgesource' in node.lattrib: + edge_source = node.lattrib['edgesource'] + else: + self.raise_error("Kinetic scheme '{0}' must specify the edge source attribute.", name) + + if 'edgetarget' in node.lattrib: + edge_target = node.lattrib['edgetarget'] + else: + self.raise_error("Kinetic scheme '{0}' must specify the edge target attribute.", name) + + if 'forwardrate' in node.lattrib: + forward_rate = node.lattrib['forwardrate'] + else: + self.raise_error("Kinetic scheme '{0}' must specify the forward rate attribute.", name) + + if 'reverserate' in node.lattrib: + reverse_rate = node.lattrib['reverserate'] + else: + self.raise_error("Kinetic scheme '{0}' must specify the reverse rate attribute", name) + + self.current_regime.add_kinetic_scheme(KineticScheme(name, nodes, state_variable, + edges, edge_source, edge_target, + forward_rate, reverse_rate)) + + def parse_link(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name') + + if 'type' in node.lattrib: + type_ = node.lattrib['type'] + else: + self.raise_error("Link '{0}' must specify a type", name) + + description = node.lattrib.get('description', '') + + self.current_component_type.add_link(Link(name, type_, description)) + + def parse_multi_instantiate(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'component' in node.lattrib: + component = node.lattrib['component'] + else: + self.raise_error(' must specify a component reference.') + + if 'number' in node.lattrib: + number = node.lattrib['number'] + else: + self.raise_error("Multi instantiation of '{0}' must specify a parameter specifying the number.", + component) + + self.current_structure.add_multi_instantiate(MultiInstantiate(component, number)) + + def parse_on_condition(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + try: + test = node.lattrib['test'] + except: + self.raise_error(' must specify a test.') + + event_handler = OnCondition(test) + + self.current_regime.add_event_handler(event_handler) + + self.current_event_handler = event_handler + self.process_nested_tags(node) + self.current_event_handler = None + + def parse_on_entry(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + event_handler = OnEntry() + + self.current_event_handler = event_handler + self.current_regime.add_event_handler(event_handler) + + self.process_nested_tags(node) + + self.current_event_handler = None + + def parse_on_event(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + try: + port = node.lattrib['port'] + except: + self.raise_error(' must specify a port.') + + event_handler = OnEvent(port) + + self.current_regime.add_event_handler(event_handler) + + self.current_event_handler = event_handler + self.process_nested_tags(node) + self.current_event_handler = None + + def parse_on_start(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + event_handler = OnStart() + + self.current_regime.add_event_handler(event_handler) + + self.current_event_handler = event_handler + self.process_nested_tags(node) + self.current_event_handler = None + + def parse_parameter(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when the parameter does not have a name. + @raise ParseError: Raised when the parameter does not have a + dimension. + """ + + if self.current_component_type == None: + self.raise_error('Parameters can only be defined in ' + + 'a component type') + + try: + name = node.lattrib['name'] + except: + self.raise_error(' must specify a name') + + try: + dimension = node.lattrib['dimension'] + except: + self.raise_error("Parameter '{0}' has no dimension", + name) + + parameter = Parameter(name, dimension) + + self.current_component_type.add_parameter(parameter) + + def parse_property(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when the property does not have a name. + @raise ParseError: Raised when the property does not have a + dimension. + """ + + if self.current_component_type == None: + self.raise_error('Property can only be defined in ' + + 'a component type') + + try: + name = node.lattrib['name'] + except: + self.raise_error(' must specify a name') + + try: + dimension = node.lattrib['dimension'] + except: + self.raise_error("Property '{0}' has no dimension", + name) + + default_value = node.lattrib.get('defaultvalue', None) + + property = Property(name, dimension, default_value=default_value) + + self.current_component_type.add_property(property) + + + def parse_index_parameter(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when the IndexParameter does not have a name. + """ + + if self.current_component_type == None: + self.raise_error('IndexParameters can only be defined in ' + + 'a component type') + + try: + name = node.lattrib['name'] + except: + self.raise_error(' must specify a name') + + + index_parameter = IndexParameter(name) + + self.current_component_type.add_index_parameter(index_parameter) + + + def parse_tunnel(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when the Tunnel does not have a name. + """ + + try: + name = node.lattrib['name'] + except: + self.raise_error(' must specify a name') + try: + end_a = node.lattrib['enda'] + except: + self.raise_error(' must specify: endA') + try: + end_b = node.lattrib['enda'] + except: + self.raise_error(' must specify: endB') + try: + component_a = node.lattrib['componenta'] + except: + self.raise_error(' must specify: componentA') + try: + component_b = node.lattrib['componentb'] + except: + self.raise_error(' must specify: componentB') + + + tunnel = Tunnel(name, end_a, end_b, component_a, component_b) + + self.current_structure.add_tunnel(tunnel) + + + def parse_path(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name.') + + description = node.lattrib.get('description', '') + + self.current_component_type.add_path(Path(name, description)) + + def parse_record(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if self.current_simulation == None: + self.raise_error(' must be only be used inside a ' + + 'simulation specification') + + if 'quantity' in node.lattrib: + quantity = node.lattrib['quantity'] + else: + self.raise_error(' must specify a quantity.') + + scale = node.lattrib.get('scale', None) + color = node.lattrib.get('color', None) + id = node.lattrib.get('id', None) + + self.current_simulation.add_record(Record(quantity, scale, color, id)) + + def parse_event_record(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if self.current_simulation == None: + self.raise_error(' must be only be used inside a ' + + 'simulation specification') + + if 'quantity' in node.lattrib: + quantity = node.lattrib['quantity'] + else: + self.raise_error(' must specify a quantity.') + + if 'eventport' in node.lattrib: + eventPort = node.lattrib['eventport'] + else: + self.raise_error(' must specify an eventPort.') + + + self.current_simulation.add_event_record(EventRecord(quantity, eventPort)) + + def parse_regime(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + name = '' + + if 'initial' in node.lattrib: + initial = (node.lattrib['initial'].strip().lower() == 'true') + else: + initial = False + + regime = Regime(name, self.current_dynamics, initial) + old_regime = self.current_regime + self.current_dynamics.add_regime(regime) + self.current_regime = regime + + self.process_nested_tags(node) + + self.current_regime = old_regime + + def parse_requirement(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name') + + if 'dimension' in node.lattrib: + dimension = node.lattrib['dimension'] + else: + self.raise_error("Requirement \{0}' must specify a dimension.", name) + + self.current_component_type.add_requirement(Requirement(name, dimension)) + + def parse_component_requirement(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name') + + self.current_component_type.add_component_requirement(ComponentRequirement(name)) + + def parse_instance_requirement(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name') + + if 'type' in node.lattrib: + type = node.lattrib['type'] + else: + self.raise_error("InstanceRequirement \{0}' must specify a type.", name) + + self.current_component_type.add_instance_requirement(InstanceRequirement(name, type)) + + def parse_run(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'component' in node.lattrib: + component = node.lattrib['component'] + else: + self.raise_error(' must specify a target component') + + if 'variable' in node.lattrib: + variable = node.lattrib['variable'] + else: + self.raise_error(' must specify a state variable') + + if 'increment' in node.lattrib: + increment = node.lattrib['increment'] + else: + self.raise_error(' must specify an increment for the ' + + 'state variable') + + if 'total' in node.lattrib: + total = node.lattrib['total'] + else: + self.raise_error(' must specify a final value for the ' + + 'state variable') + + self.current_simulation.add_run(Run(component, variable, increment, total)) + + def parse_show(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + pass + + def parse_simulation(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + self.current_simulation = self.current_component_type.simulation + + self.process_nested_tags(node) + + self.current_simulation = None + + def parse_state_assignment(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'variable' in node.lattrib: + variable = node.lattrib['variable'] + else: + self.raise_error(' must specify a variable name') + + if 'value' in node.lattrib: + value = node.lattrib['value'] + else: + self.raise_error("State assignment for '{0}' must specify a value.", + variable) + + action = StateAssignment(variable, value) + + self.current_event_handler.add_action(action) + + + def parse_state_variable(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when the state variable is not + being defined in the context of a component type. + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name') + + if 'default' in node.lattrib: + default = node.lattrib['default'] + else: + self.raise_error("State variable '{0}' must specify a default", name) + + if 'boundaries' in node.lattrib: + boundaries = node.lattrib['boundaries'] + else: + boundaries = None + + self.current_regime.add_state_variable(StateVariable(name, default, boundaries)) + + def parse_structure(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + self.current_structure = self.current_component_type.structure + self.process_nested_tags(node) + self.current_structure = None + + def parse_target(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + self.model.add_target(node.lattrib['component']) + + def parse_text(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name.') + + description = node.lattrib.get('description', '') + + self.current_component_type.add_text(Text(name, description)) + + def parse_time_derivative(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: Raised when the time derivative does not hava a variable + name of a value. + """ + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + self.raise_error(' must specify a name.') + + if 'expression' in node.lattrib: + expression = node.lattrib['expression'] + else: + self.raise_error("Time derivative for '{0}' must specify an expression.", + variable) + + self.current_regime.add_time_derivative(TimeDerivative(name, expression)) + + def parse_transition(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'regime' in node.lattrib: + regime = node.lattrib['regime'] + else: + self.raise_error(' mut specify a regime.') + + action = Transition(regime) + + self.current_event_handler.add_action(action) + + def parse_unit(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + + @raise ParseError: When the name is not a string or the unit + specfications are incorrect. + + @raise ModelError: When the unit refers to an undefined dimension. + """ + + try: + symbol = node.lattrib['symbol'] + dimension = node.lattrib['dimension'] + except: + self.raise_error('Unit must have a symbol and dimension.') + + if 'power' in node.lattrib: + power = int(node.lattrib['power']) + else: + power = 0 + + if 'name' in node.lattrib: + name = node.lattrib['name'] + else: + name = '' + + if 'scale' in node.lattrib: + scale = float(node.lattrib['scale']) + else: + scale = 1.0 + + if 'offset' in node.lattrib: + offset = float(node.lattrib['offset']) + else: + offset = 0.0 + + self.model.add_unit(Unit(name, symbol, dimension, power, scale, offset)) + + def parse_with(self, node): + """ + Parses + + @param node: Node containing the element + @type node: xml.etree.Element + """ + + if 'instance' in node.lattrib: + instance = node.lattrib['instance'] + list = None + index = None + elif 'list' in node.lattrib and 'index' in node.lattrib: + instance = None + list = node.lattrib['list'] + index = node.lattrib['index'] + else: + self.raise_error(' must specify EITHER instance OR list & index') + + if 'as' in node.lattrib: + as_ = node.lattrib['as'] + else: + self.raise_error(' must specify a name for the ' + 'target instance') + + self.current_structure.add_with(With(instance, as_, list, index)) + diff --git a/lems/parser/__init__.py b/lems/parser/__init__.py new file mode 100755 index 0000000000..aaeb1c49f0 --- /dev/null +++ b/lems/parser/__init__.py @@ -0,0 +1,5 @@ +""" +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org +""" diff --git a/lems/parser/expr.py b/lems/parser/expr.py new file mode 100755 index 0000000000..2f1e493cc6 --- /dev/null +++ b/lems/parser/expr.py @@ -0,0 +1,520 @@ +""" +Expression parser + +@author: Gautham Ganapathy +@organization: LEMS (http://neuroml.org/lems/, https://github.com/organizations/LEMS) +@contact: gautham@lisphacker.org + +MV: added pow(f,l) function for c translations +""" + +from lems.base.base import LEMSBase +from lems.base.stack import Stack + +known_functions = ['exp', 'log', 'sqrt', 'sin', 'cos', 'tan', 'sinh', 'cosh', 'tanh', 'abs', 'ceil', 'factorial', 'random', 'H', 'pow', 'powf', 'powl'] + +class ExprNode(LEMSBase): + """ + Base class for a node in the expression parse tree. + """ + + OP = 1 + VALUE = 2 + FUNC1 = 3 + + def __init__(self, type): + """ + Constructor. + + @param type: Node type + @type type: enum(ExprNode.OP, ExprNode.VALUE) + """ + + self.type = type + """ Node type. + @type: enum(ExprNode.OP, ExprNode.VALUE) """ + +class ValueNode(ExprNode): + """ + Value node in an expression parse tree. This will always be a leaf node. + """ + + def __init__(self, value): + """ + Constructor. + + @param value: Value to be stored in this node. + @type value: string + """ + + ExprNode.__init__(self, ExprNode.VALUE) + self.value = value + """ Value to be stored in this node. + @type: string """ + + def clean_up(self): + """ + To make sure an integer is returned as a float. No division by integers!! + """ + try: + return str(float(self.value)) + except ValueError: + return self.value + + + def __str__(self): + """ + Generates a string representation of this node. + """ + return "{" + self.clean_up() + "}" + + def __repr__(self): + return self.__str__() + + def to_python_expr(self): + return self.clean_up() + +class OpNode(ExprNode): + """ + Operation node in an expression parse tree. This will always be a + non-leaf node. + """ + + def __init__(self, op, left, right): + """ + Constructor. + + @param op: Operation to be stored in this node. + @type op: string + + @param left: Left operand. + @type left: lems.parser.expr.ExprNode + + @param right: Right operand. + @type right: lems.parser.expr.ExprNode + """ + + ExprNode.__init__(self, ExprNode.OP) + + self.op = op + """ Operation stored in this node. + @type: string """ + + self.left = left + """ Left operand. + @type: lems.parser.expr.ExprNode """ + + self.right = right + """ Right operand. + @type: lems.parser.expr.ExprNode """ + + def __str__(self): + """ + Generates a string representation of this node. + """ + + return '({0} {1} {2})'.format(self.op, + str(self.left), + str(self.right)) + + def __repr__(self): + return self.__str__() + + def to_python_expr(self): + return '({0} {1} {2})'.format(self.left.to_python_expr(), + self.op, + self.right.to_python_expr()) + +class Func1Node(ExprNode): + """ + Unary function node in an expression parse tree. This will always be a + non-leaf node. + """ + + def __init__(self, func, param): + """ + Constructor. + + @param func: Function to be stored in this node. + @type func: string + + @param param: Parameter. + @type param: lems.parser.expr.ExprNode + """ + + ExprNode.__init__(self, ExprNode.FUNC1) + + self.func = func + """ Funcion stored in this node. + @type: string """ + + self.param = param + """ Parameter. + @type: lems.parser.expr.ExprNode """ + + def __str__(self): + """ + Generates a string representation of this node. + """ + + return '({0} {1})'.format(self.func, str(self.param)) + + def __repr__(self): + return self.__str__() + + def to_python_expr(self): + return '({0}({1}))'.format(self.func, self.param.to_python_expr()) + + +class ExprParser(LEMSBase): + """ + Parser class for parsing an expression and generating a parse tree. + """ + + debug = False + + op_priority = { + '$':-5, + 'func':8, + '**':6, + '+':5, + '-':5, + '*':6, + '/':6, + '^':7, + '~':8, + 'exp':8, + '.and.':1, + '.or.':1, + '.gt.':2, + '.ge.':2, + '.geq.':2, + '.lt.':2, + '.le.':2, + '.eq.':2, + '.neq.':2, + '.ne.':2} # .neq. is preferred! + + depth = 0 + + """ Dictionary mapping operators to their priorities. + @type: dict(string -> Integer) """ + + def __init__(self, parse_string): + """ + Constructor. + + @param parse_string: Expression to be parsed. + @type parse_string: string + """ + + self.parse_string = parse_string + """ Expression to be parsed. + @type: string """ + + self.token_list = None + """ List of tokens from the expression to be parsed. + @type: list(string) """ + + def is_op(self, str): + """ + Checks if a token string contains an operator. + + @param str: Token string to be checked. + @type str: string + + @return: True if the token string contains an operator. + @rtype: Boolean + """ + + return str in self.op_priority + + def is_func(self, str): + """ + Checks if a token string contains a function. + + @param str: Token string to be checked. + @type str: string + + @return: True if the token string contains a function. + @rtype: Boolean + """ + + return str in known_functions + + def is_sym(self, str): + """ + Checks if a token string contains a symbol. + + @param str: Token string to be checked. + @type str: string + + @return: True if the token string contains a symbol. + @rtype: Boolean + + MV: Added the part to recognize ** for power + """ + + return str in ['+', '-', '~', '*', '/', '^', '(', ')'] + + def priority(self, op): + if self.is_op(op): + return self.op_priority[op] + elif self.is_func(op): + return self.op_priority['func'] + else: + return self.op_priority['$'] + + def tokenize(self): + """ + Tokenizes the string stored in the parser object into a list + of tokens. + """ + + powerflag = 0 + self.token_list = [] + ps = self.parse_string.strip() + + i = 0 + last_token = None + + while i < len(ps) and ps[i].isspace(): + i += 1 + + while i < len(ps): + token = '' + + if ps[i].isalpha(): + while i < len(ps) and (ps[i].isalnum() or ps[i] == '_'): + token += ps[i] + i += 1 + elif ps[i].isdigit(): + while i < len(ps) and (ps[i].isdigit() or + ps[i] == '.' or + ps[i] == 'e' or + ps[i] == 'E' or + (ps[i] == '+' and (ps[i-1] == 'e' or ps[i-1] == 'E')) or + (ps[i] == '-' and (ps[i-1] == 'e' or ps[i-1] == 'E'))): + token += ps[i] + i += 1 + elif ps[i] == '.': + if ps[i+1].isdigit(): + while i < len(ps) and (ps[i].isdigit() or ps[i] == '.'): + token += ps[i] + i += 1 + else: + while i < len(ps) and (ps[i].isalpha() or ps[i] == '.'): + token += ps[i] + i += 1 + elif ps[i] == '*': + while i < len(ps) and ps[i] == '*':# and j < 2: + token += ps[i] + i += 1 + else: + token += ps[i] + i += 1 + + if token == '-' and \ + (last_token == None or last_token == '(' or self.is_op(last_token)): + token = '~' + + self.token_list += [token] + last_token = token + + while i < len(ps) and ps[i].isspace(): + i += 1 + + def make_op_node(self, op, right): + if self.is_func(op): + return Func1Node(op, right) + elif op == '~': + return OpNode('-', ValueNode('0'), right) + else: + left = self.val_stack.pop() + if left == '$': + left = self.node_stack.pop() + else: + left = ValueNode(left) + + return OpNode(op, left, right) + + def cleanup_stacks(self): + right = self.val_stack.pop() + if right == '$': + right = self.node_stack.pop() + else: + right = ValueNode(right) + + if self.debug: print('- Cleanup > right: %s'% right) + + while self.op_stack.top() != '$': + if self.debug: print('5> op stack: %s, val stack: %s, node stack: %s'% ( self.op_stack, self.val_stack, self.node_stack)) + op = self.op_stack.pop() + + right = self.make_op_node(op, right) + + if self.debug: print('6> op stack: %s, val stack: %s, node stack: %s'% ( self.op_stack, self.val_stack, self.node_stack)) + if self.debug: print('7> %s'% right) + #if self.debug: print('' + + return right + + def parse_token_list_rec(self, min_precedence): + """ + Parses a tokenized arithmetic expression into a parse tree. It calls + itself recursively to handle bracketed subexpressions. + + @return: Returns a token string. + @rtype: lems.parser.expr.ExprNode + + @attention: Does not handle unary minuses at the moment. Needs to be + fixed. + """ + + exit_loop = False + + ExprParser.depth = ExprParser.depth + 1 + if self.debug: print('>>>>> Depth: %i'% ExprParser.depth) + + precedence = min_precedence + + while self.token_list: + token = self.token_list[0] + la = self.token_list[1] if len(self.token_list) > 1 else None + + if self.debug: print('0> %s'% self.token_list) + if self.debug: print('1> Token: %s, next: %s, op stack: %s, val stack: %s, node stack: %s'% (token, la, self.op_stack, self.val_stack, self.node_stack)) + + self.token_list = self.token_list[1:] + + close_bracket = False + + if token == '(': + np = ExprParser('') + np.token_list = self.token_list + + nexp = np.parse2() + + self.node_stack.push(nexp) + self.val_stack.push('$') + + self.token_list = np.token_list + if self.debug: print('>>> Tokens left: %s'%self.token_list) + close_bracket = True + elif token == ')': + break + elif self.is_func(token): + self.op_stack.push(token) + elif self.is_op(token): + stack_top = self.op_stack.top() + if self.debug: print('OP Token: %s (prior: %i), top: %s (prior: %i)'% (token, self.priority(token), stack_top, self.priority(stack_top))) + if self.priority(token) < self.priority(stack_top): + if self.debug: print(' Priority of %s is less than %s'%(token, stack_top)) + self.node_stack.push(self.cleanup_stacks()) + self.val_stack.push('$') + else: + if self.debug: print(' Priority of %s is greater than %s'%(token, stack_top)) + + + self.op_stack.push(token) + else: + if self.debug: print('Not a bracket func or op...') + if la == '(': + raise Exception("Error parsing expression: %s\nToken: %s is placed like a function but is not recognised!\nKnown functions: %s"%(self.parse_string, token, known_functions)) + stack_top = self.op_stack.top() + if stack_top == '$': + if self.debug: print("option a") + self.node_stack.push(ValueNode(token)) + self.val_stack.push('$') + else: + if (self.is_op(la) and + self.priority(stack_top) < self.priority(la)): + if self.debug: print("option b") + + self.node_stack.push(ValueNode(token)) + self.val_stack.push('$') + else: + if self.debug: print("option c, nodes: %s"% self.node_stack) + op = self.op_stack.pop() + + right = ValueNode(token) + op_node = self.make_op_node(op,right) + + self.node_stack.push(op_node) + self.val_stack.push('$') + + if close_bracket: + stack_top = self.op_stack.top() + if self.debug: print("+ Closing bracket, op stack: %s, node stack: %s la: %s"%(self.op_stack, self.node_stack, la)) + if self.debug: print('>>> Tokens left: %s'%self.token_list) + + if stack_top == '$': + if self.debug: print("+ option a") + ''' + self.node_stack.push(ValueNode(token)) + self.val_stack.push('$')''' + else: + la = self.token_list[0] if len(self.token_list) > 1 else None + if (self.is_op(la) and self.priority(stack_top) < self.priority(la)): + if self.debug: print("+ option b") + #self.node_stack.push(ValueNode(token)) + #self.val_stack.push('$') + else: + if self.debug: print("+ option c, nodes: %s"% self.node_stack) + if self.debug: print('35> op stack: %s, val stack: %s, node stack: %s'% ( self.op_stack, self.val_stack, self.node_stack)) + right = self.node_stack.pop() + op = self.op_stack.pop() + op_node = self.make_op_node(stack_top,right) + if self.debug: print("Made op node: %s, right: %s"%(op_node, right)) + + self.node_stack.push(op_node) + self.val_stack.push('$') + if self.debug: print('36> op stack: %s, val stack: %s, node stack: %s'% ( self.op_stack, self.val_stack, self.node_stack)) + + + + if self.debug: print('2> Token: %s, next: %s, op stack: %s, val stack: %s, node stack: %s'% (token, la, self.op_stack, self.val_stack, self.node_stack)) + if self.debug: print('') + + if self.debug: print('3> op stack: %s, val stack: %s, node stack: %s'% ( self.op_stack, self.val_stack, self.node_stack)) + ret = self.cleanup_stacks() + + if self.debug: print('4> op stack: %s, val stack: %s, node stack: %s'% ( self.op_stack, self.val_stack, self.node_stack)) + if self.debug: print('<<<<< Depth: %s, returning: %s'% (ExprParser.depth, ret)) + ExprParser.depth = ExprParser.depth - 1 + if self.debug: print('') + return ret + + def parse(self): + """ + Tokenizes and parses an arithmetic expression into a parse tree. + + @return: Returns a token string. + @rtype: lems.parser.expr.ExprNode + """ + #print("Parsing: %s"%self.parse_string) + self.tokenize() + if self.debug: print("Tokens found: %s"%self.token_list) + + try: + parse_tree = self.parse2() + except Exception as e: + raise e + return parse_tree + + def parse2(self): + self.op_stack = Stack() + self.val_stack = Stack() + self.node_stack = Stack() + + self.op_stack.push('$') + self.val_stack.push('$') + try: + ret = self.parse_token_list_rec(self.op_priority['$']) + except Exception as e: + raise e + + return ret + + def __str__(self): + return str(self.token_list) From 17eae59b1b709f05fde10ad7616fa90772d42a9c Mon Sep 17 00:00:00 2001 From: DeLaVlag Date: Thu, 18 Jun 2020 22:17:37 +0200 Subject: [PATCH 02/10] modelgen + single cuda_run implemented --- TVB_testsuit/benchAll.sh | 20 +- TVB_testsuit/cuda_run.py | 13 +- .../{tvbRegCudaNumba.py => cuda_setup.py} | 240 ++---------------- TVB_testsuit/runthings | 8 +- dsl_cuda/LEMS2CUDA.py | 46 ++-- 5 files changed, 73 insertions(+), 254 deletions(-) rename TVB_testsuit/{tvbRegCudaNumba.py => cuda_setup.py} (51%) diff --git a/TVB_testsuit/benchAll.sh b/TVB_testsuit/benchAll.sh index 692305dacf..7d48ffae8b 100755 --- a/TVB_testsuit/benchAll.sh +++ b/TVB_testsuit/benchAll.sh @@ -1,17 +1,15 @@ #!/usr/bin/env bash -#BSUB -q normal -#BSUB -W 00:30 -#BSUB -n 1 -#BSUB -R "span[ptile=1]" -#BSUB -gpu "num=1:j_exclusive=yes" -##BSUB -e "./error.%J.er" -##BSUB -o "./output_%J.out" -#BSUB -e "./error.er" -#BSUB -o "./output.out" -#BSUB -J testbench +#SBATCH --partition=dp-esb +#SBATCH -A type1_1 +#SBATCH -N 1 +#SBATCH -n 1 +#SBATCH -o output.out +#SBATCH -e ./error.er +#SBATCH --time=00:30:00 +#SBATCH -J benchDSL # Run the program +srun python ./cuda_setup.py --model mdlrun --bench bencharg -mpirun python ./tvbRegCudaNumba.py -b bencharg diff --git a/TVB_testsuit/cuda_run.py b/TVB_testsuit/cuda_run.py index 295074e93c..5c4de59231 100755 --- a/TVB_testsuit/cuda_run.py +++ b/TVB_testsuit/cuda_run.py @@ -71,8 +71,8 @@ def make_gpu_data(self, data):#{{{ return gpu_data#}}} - def run_simulation(self, weights, lengths, params_matrix, couplings, speeds, logger, args, n_nodes, n_work_items, n_params, nstep, n_inner_steps, - buf_len, states, dt, min_speed, pop): + def run_simulation(self, weights, lengths, params_matrix, speeds, logger, args, n_nodes, n_work_items, n_params, nstep, n_inner_steps, + buf_len, states, dt, min_speed): # setup data#{{{ data = { 'weights': weights, 'lengths': lengths, 'params': params_matrix.T } @@ -90,10 +90,8 @@ def run_simulation(self, weights, lengths, params_matrix, couplings, speeds, log # setup CUDA stuff#{{{ step_fn = self.make_kernel( source_file=args.filename, - # warp_size=32, - # block_dim_x=args.n_coupling, - warp_size=8, - block_dim_x=8, + warp_size=32, + block_dim_x=args.n_coupling, # ext_options=preproccesor_defines, # caching=args.caching, args=args, @@ -113,8 +111,7 @@ def run_simulation(self, weights, lengths, params_matrix, couplings, speeds, log #}}} # adjust gridDim to keep block size <= 1024 {{{ - # block_size_lim = 1024 - block_size_lim = 64 + block_size_lim = 1024 n_coupling_per_block = block_size_lim // args.node_threads n_coupling_blocks = args.n_coupling // n_coupling_per_block if n_coupling_blocks == 0: diff --git a/TVB_testsuit/tvbRegCudaNumba.py b/TVB_testsuit/cuda_setup.py similarity index 51% rename from TVB_testsuit/tvbRegCudaNumba.py rename to TVB_testsuit/cuda_setup.py index 26d3fc8126..b91d3f5afe 100755 --- a/TVB_testsuit/tvbRegCudaNumba.py +++ b/TVB_testsuit/cuda_setup.py @@ -1,17 +1,7 @@ from tvb.simulator.lab import * -# from tvb.datatypes import connectivity -# from tvb.simulator import integrators -# from tvb.simulator import coupling import numpy as np import numpy.random as rgn -import matplotlib.pyplot as plt import math -import sys -import os - -from numpy import corrcoef -import seaborn as sns - import time import logging import itertools @@ -23,22 +13,22 @@ parent_dir = os.path.dirname(current_dir) sys.path.insert(0, parent_dir) -sys.path.append("{}{}".format(parent_dir, '/NeuroML/')) +# sys.path.append("{}{}".format(parent_dir, '/NeuroML/')) +# print('pd', parent_dir) +# for p in sys.path: +# print('sp', p) -print(sys.path) +# from dsl_cuda import LEMS2CUDA np.set_printoptions(threshold=sys.maxsize) -# for numexpr package missing and no permissions to install: -# clone package, copy to hpc, build with $ python setup.py build, copy numexpr folder from build/lib.linux-ppc64le-3.6 to project root -# and do: export PATH=$PATH:/\$PROJECT_cpcp0/vandervlag/all-benchmarking/numexpr/build/lib.linux-ppc64le-3.6/numexpr // preferrable pythonpath -# same for tvb-data dependancy: move build directory (python setyp.py build) to root - -# set global logger level in tvb.logger.library_logger.conf - - rgn.seed(79) +# class testthis: +# +# def __init__(self): +# self.args = self.parse_args() + class TVB_test: @@ -75,26 +65,6 @@ def tvb_connectivity(self, speed, global_coupling, dt=0.1): white_matter_coupling = coupling.Linear(a=global_coupling) return white_matter, white_matter_coupling - def tvb_python_model(self): - whatmodel=self.args.model.lower() - print(whatmodel) - - switcher = { - 'kuramoto': models.Kuramoto, - 'oscillator': models.Generic2dOscillator, - 'wongwang': models.ReducedWongWang, - # 'montbrio': models.Montbrio, - 'epileptor': models.Epileptor - } - modelexe = switcher.get(whatmodel, 'invalid model') - # print(modelexe) - populations = modelexe() - - # populations = models.Kuramoto() - populations.configure() - populations.omega = np.array([self.omega]) - return populations - def parse_args(self): # {{{ parser = argparse.ArgumentParser(description='Run parameter sweep.') parser.add_argument('-c', '--n_coupling', help='num grid points for coupling parameter', default=32, type=int) @@ -108,11 +78,6 @@ def parse_args(self): # {{{ # help="caching strategy for j_node loop (default shuffle)", # default='none' # ) - # parser.add_argument('--dataset', - # choices=['hcp', 'sep'], - # help="dataset to use (hcp: 100 nodes, sep: 645 nodes", - # default='hcp' - # ) parser.add_argument('--node_threads', default=1, type=int) parser.add_argument('--model', choices=['Rwongwang', 'Kuramoto', 'Epileptor', 'Oscillator', \ @@ -124,36 +89,12 @@ def parse_args(self): # {{{ parser.add_argument('--filename', default="kuramoto_network.c", type=str, help="Filename to use as GPU kernel definition") - # parser.add_argument("bench", default="all", nargs='*', choices=["noop", "scatter", "gather", "all"], help="Which sub-set of kernel to run") parser.add_argument('-b', '--bench', default="regular", type=str, help="What to bench: regular, numba, cuda") args = parser.parse_args() return args - # numba load - def make_data(self): - c = network.Connectivity.hcp0() - return c.nnode, c.lengths, c.nnz, c.row, c.col, c.wnz, c.nz, c.weights - - # cuda load - def load_connectome(self, dataset): - # load connectome & normalize - if dataset == 'hcp': - npz = np.load('data/hcp0.npz') - weights = npz['weights'].astype(np.float32) - lengths = npz['lengths'].astype(np.float32) - elif dataset == 'sep': - npz = np.load('sep.npz') - weights = npz['weights'].astype(np.float32) - lengths = npz['lengths'].astype(np.float32) - else: - raise ValueError('unknown dataset name %r' % (dataset,)) - # weights /= {'N':2e3, 'Nfa': 1e3, 'FA': 1.0}[mattype] - weights /= weights.max() - assert (weights <= 1.0).all() - return weights, lengths - def expand_params(self, couplings, speeds): # {{{ # the params array is transformed into a 2d array # by first creating tuples of (speed, coup) and arrayfying then @@ -169,26 +110,6 @@ def setup_params(self, nc, ns): # {{{ speeds = np.logspace(0.0, 2.0, ns) return couplings, speeds # }}} - def calculate_FC(self, timeseries): - return corrcoef(timeseries.T) - - def correlation_SC_FC(self, SC, FC): - return corrcoef(FC.ravel(), SC.ravel())[0, 1] - - def plot_SC_FC(self, SC, FC, tag): - # print(FC) - fig, ax = plt.subplots(ncols=2, figsize=(12, 3)) - sns.heatmap((FC), xticklabels='', - yticklabels='', ax=ax[0], - cmap='coolwarm') - sns.heatmap(SC / SC.max(), xticklabels='', yticklabels='', - ax=ax[1], cmap='coolwarm', vmin=0, vmax=1) # - r = self.correlation_SC_FC(SC, FC) - ax[0].set_title('simulated FC. \n(SC-FC r = %1.4s )' % r) - ax[1].set_title('SC') - # plt.savefig("FC_SC_"+tag+".png") - return r - # Todo: check if this function work. derr_speed > 500 and derr_coupl < -1500 evaluate to false for pyCuda runs def check_results(self, n_nodes, n_work_items, tavg, weights, speeds, couplings, logger, args): r, c = np.triu_indices(n_nodes, 1) @@ -218,68 +139,13 @@ def check_results(self, n_nodes, n_work_items, tavg, weights, speeds, couplings, logger.info('result OK') - def regular(self, logger, pop): - logger.info('start regular TVB run') - logger.info('model to run %s', pop) - # Initialize Model - model = self.tvb_python_model() - # Initialize Monitors - monitorsen = (monitors.TemporalAverage(period=self.period)) - # Initialize Simulator - sim = simulator.Simulator(model=model, connectivity=self.connectivity, coupling=self.coupling, - integrator=self.integrator, - monitors=[monitorsen]) - sim.configure() - (_, tavg_data) = sim.run(simulation_length=self.sim_length)[0] - # print(np.squeeze(np.array(tavg_data)).shape) - # - # FC = self.calculate_FC(np.squeeze(np.array(tavg_data))) - # r = self.plot_SC_FC(self.SC, FC,"regular") - # print(r) - return np.squeeze(tavg_data) - - def numba(self, logger, pop): - logger.info('start Numba run') - from numbacuda_run import NumbaCudaRun - numbacuda = NumbaCudaRun() - trace = numbacuda.run_simulation(dt) - - # - # (numbacuda_FC, python_r) = tvbhpc.simulate_numbacuda() - # print(numbacuda_FC) - # tavg_data = np.transpose(trace, (1, 2, 0)) - # tvbhpc.check_results(n_nodes, n_work_items, tavg_data, weights, speeds, couplings, logger, args) - - # numba kernel based on the c index used for cuda - def numbac(self, logger, pop): - logger.info('start Numba run') - from cindex_numbacuda_run import NumbaCudaRun - numbacuda = NumbaCudaRun() - - threadsperblock = len(self.couplings) - blockspergrid = len(self.speeds) - logger.info('threadsperblock %d', threadsperblock) - logger.info('blockspergrid %d', blockspergrid) - - tavg_data = numbacuda.run_simulation(blockspergrid, threadsperblock, self.n_inner_steps, self.n_nodes, - self.buf_len, self.dt, self.weights, self.lengths, self.params.T, - logger) - logger.info('tavg_data.shape %s', tavg_data.shape) - # logger.info('tavg_data %f', tavg_data) - - # - # (numbacuda_FC, python_r) = tvbhpc.simulate_numbacuda() - # print(numbacuda_FC) - # tavg_data = np.transpose(trace, (1, 2, 0)) - # tvbhpc.check_results(n_nodes, n_work_items, tavg_data, weights, speeds, couplings, logger, args) - def cuda(self, logger, pop): logger.info('start Cuda run') from cuda_run import CudaRun cudarun = CudaRun() - tavg_data = cudarun.run_simulation(self.weights, self.lengths, self.params, self.couplings, self.speeds, logger, + tavg_data = cudarun.run_simulation(self.weights, self.lengths, self.params, self.speeds, logger, self.args, self.n_nodes, self.n_work_items, self.n_params, self.nstep, - self.n_inner_steps, self.buf_len, self.states, self.dt, self.min_speed, pop) + self.n_inner_steps, self.buf_len, self.states, self.dt, self.min_speed) # logger.info('tavg_data %f', tavg_data) # Todo: fix this for cuda @@ -287,67 +153,37 @@ def cuda(self, logger, pop): return tavg_data - def startsim(self, pop, tmpld): + def startsim(self, pop): tic = time.time() - tvbhpc = TVB_test() - - # args = tvbhpc.parse_args() logging.basicConfig(level=logging.DEBUG if self.args.verbose else logging.INFO) logger = logging.getLogger('[tvbBench.py]') - # dimensions#{{{ - # dt, tavg_period = 1.0, 10.0 - # nstep = args.n_time # 4s - # n_inner_steps = int(tavg_period / dt) - # states = 1 - # if args.model == 'rww': - # states = 2 - # TODO buf_len per speed/block logger.info('dt %f', self.dt) logger.info('nstep %d', self.nstep) - # logger.info('caching strategy %r', self.args.caching) logger.info('n_inner_steps %f', self.n_inner_steps) if self.args.test and self.args.n_time % 200: logger.warning('rerun w/ a multiple of 200 time steps (-n 200, -n 400, etc) for testing') # }}} # setup data - # weights = tvbhpc.connectivity.weights logger.info('weights.shape %s', self.weights.shape) - # lengths = tvbhpc.connectivity.tract_lengths logger.info('lengths.shape %s', self.lengths.shape) - # n_nodes = weights.shape[0] logger.info('n_nodes %d', self.n_nodes) - # couplings and speeds are not derived from the regular TVB connection setup routine. these parameters are swooped every GPU spawn - # nc = args.n_coupling - # ns = args.n_speed + # couplings and speeds are not derived from the regular TVB connection setup routine. + # these parameters are swooped every GPU spawn logger.info('single connectome, %d x %d parameter space', self.ns, self.nc) logger.info('%d total num threads', self.ns * self.nc) - # couplings, speeds = tvbhpc.setup_params(nc=nc, ns=ns) - # params = tvbhpc.expand_params(couplings, speeds) - # # params = tvbhpc.expand_params(tvbhpc.coupling, tvbhpc.connectivity.speed) - # logger.info('coupling %s', couplings) - # logger.info('connectivity.speed %s', speeds) - # logger.info('coupling %s', (tvbhpc.coupling.pre)) - # logger.info('connectivity.speed %s', dir(tvbhpc.connectivity.speed)) - # logger.info('%f', params.T) - - # n_work_items, n_params = params.shape - # min_speed = speeds.min() - # buf_len_ = ((lengths / min_speed / dt).astype('i').max() + 1) logger.info('min_speed %f', self.min_speed) - # buf_len = 2**np.argwhere(2**np.r_[:30] > buf_len_)[0][0] # use next power of 2 logger.info('real buf_len %d, using power of 2 %d', self.buf_len_, self.buf_len) tac = time.time() logger.info("Setup in: {}".format(tac - tic)) - benchwhat = self.args.bench - self.args.filename = "{}{}{}{}".format(parent_dir, '/dsl_cuda/CUDAmodels/', self.args.model.lower(), '.c') + # self.args.filename = os.path.join(os.path.dirname(tvb.__file__), 'dsl_cuda', 'CUDAmodels', self.args.model.lower() + '.c') + logger.info('modellow %s', self.args.model.lower()) - logger.info('modellow %s', 'wongwang' in self.args.model.lower()) if ('kuramoto' in self.args.model.lower()): self.states = 1 @@ -359,43 +195,16 @@ def startsim(self, pop, tmpld): self.states = 2 elif 'epileptor' in self.args.model.lower(): self.states = 6 - logger.info('number of states %d', self.states) - - # locals()[benchwhat]() - logger.info('benchwhat: %s', benchwhat) - # def bencher(benchwhat): - switcher = { - 'regular': self.regular, - 'numba': self.numba, - 'numbac': self.numbac, - 'cuda': self.cuda - } - func = switcher.get(benchwhat, 'invalid bench choice') - logger.info('func %s', func) - # quick and dirty comparison between old and templated version - if tmpld: - pop = pop + 'T' - # for k in range(2): - # if k == 0: - # pop = pop + 'T' - # print(pop) - # tavg0 = func(logger, pop) - # if k == 1: - # pop = pop + 'T' - # print(pop) - # tavg1 = func(logger, pop) + logger.info('number of states %d', self.states) logger.info('filename %s', self.args.filename) logger.info('model %s', self.args.model) - tavg = func(logger, pop) - # print('tavg0', tavg0.shape, '\ntavg1', tavg1.shape) - # print('coercoef=', corrcoef(tavg0.ravel(), tavg1.ravel())[0, 1]) + + tavg = self.cuda(logger, pop) toc = time.time() - print("Finished python simulation successfully in: {}".format(toc - tac)) elapsed = toc - tic - # inform about time - logger.info('elapsed time %0.3f', elapsed) + logger.info('Finished python simulation successfully in: %0.3f', elapsed) logger.info('%0.3f M step/s', 1e-6 * self.nstep * self.n_inner_steps * self.n_work_items / elapsed) logger.info('finished') @@ -403,6 +212,9 @@ def startsim(self, pop, tmpld): if __name__ == '__main__': + zelf = TVB_test() - # zelf.args.filename = "../NeuroML/CUDAmodels/network.no_defines.c" - tavg = zelf.startsim('Kuramoto', tmpld=0) + # zelf = testthis() + LEMS2CUDA.cuda_templating(zelf.args.model, '../dsl_cuda/XMLmodels/') + + tavg = zelf.startsim(zelf.args.model) diff --git a/TVB_testsuit/runthings b/TVB_testsuit/runthings index cb4515fd2a..147359db22 100755 --- a/TVB_testsuit/runthings +++ b/TVB_testsuit/runthings @@ -2,7 +2,7 @@ rm error.er rm output.out -sed "s/bencharg/$1/" benchAll.sh | bsub +sed -e "s/bencharg/${1}/;s/mdlrun/${2}/" benchAll.sh | sbatch #sleep 2 while [ ! -f ./error.er ] ; @@ -12,8 +12,8 @@ done if [ $1 == "regular" ]; then - tail -f ./output.out | grep -i -E "INFO|WARNING|ERROR" - tail -f ./error.er + #tail -f ./output.out | grep -i -E "INFO|WARNING|ERROR" + tail -n 50 -f ./error.er else - tail -f ./error.er + tail -n 50 -f ./error.er fi diff --git a/dsl_cuda/LEMS2CUDA.py b/dsl_cuda/LEMS2CUDA.py index d2bb7019c8..c40b7f762e 100755 --- a/dsl_cuda/LEMS2CUDA.py +++ b/dsl_cuda/LEMS2CUDA.py @@ -3,20 +3,24 @@ import os import sys +import inspect for p in sys.path: - print(p) + print('spL2C', p) -import dsl -sys.path.append("{}".format(os.path.dirname(dsl.__file__))) +# import dsl +# sys.path.append("{}".format(os.path.dirname(dsl.__file__))) +# # sys.path.append("{}".format(os.path.dirname(__file__))) +# x = os.path.join('../../', os.path.dirname(__file__)) +# print('dsl', x) -from lems.model.model import Model +current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +parent_dir = os.path.dirname(current_dir) +sys.path.insert(0, parent_dir) + +print('cr', inspect.currentframe()) -# model file location -# model_filename = 'Oscillator' -# model_filename = 'Kuramoto' -# model_filename = 'Rwongwang' -model_filename = 'Epileptor' +from lems.model.model import Model def default_lems_folder(): @@ -34,10 +38,10 @@ def default_template(): template = Template(filename=tmp_filename) return template -def load_model(model_filename): +def load_model(model_filename, folder=None): "Load model from filename" - fp_xml = lems_file(model_filename) + fp_xml = lems_file(model_filename, folder) model = Model() model.import_from_file(fp_xml) @@ -45,12 +49,12 @@ def load_model(model_filename): return model -def render_model(model_name, template=None): +def render_model(model_name, template=None, folder=None): # drift dynamics # modelist = list() # modelist.append(model.component_types[modelname]) - model = load_model(model_name) + model = load_model(model_name, folder) template = template or default_template() modellist = model.component_types[model_name] @@ -103,16 +107,24 @@ def render_model(model_name, template=None): return model_str -def cuda_templating(model_filename): +def cuda_templating(model_filename, folder=None): - modelfile = "{}{}{}{}".format(os.path.dirname(dsl.__file__), '/dsl_cuda/CUDAmodels/', model_filename.lower(), '.c') + # modelfile = "{}{}{}{}".format(os.path.dirname(dsl.__file__), '/dsl_cuda/CUDAmodels/', model_filename.lower(), '.c') + # modelfile = os.path.join(os.path.dirname(dsl.__file__), 'dsl_cuda', 'CUDAmodels', model_filename.lower() + '.c') + # print('f', os.path.dirname(__file__)) + modelfile = os.path.join(os.path.dirname(__file__), 'CUDAmodels', model_filename.lower() + '.c') # start templating - model_str = render_model(model_filename, template=default_template()) + model_str = render_model(model_filename, template=default_template(), folder=folder) # write template to file with open(modelfile, "w") as f: f.writelines(model_str) +if __name__ == '__main__': -cuda_templating(model_filename) \ No newline at end of file + # model_filename = 'Oscillator' + # model_filename = 'Kuramoto' + # model_filename = 'Rwongwang' + model_filename = 'Epileptor' + cuda_templating(model_filename) From 9c3c41abb67ad42e07cd35a5cbc64425dbc56a80 Mon Sep 17 00:00:00 2001 From: DeLaVlag Date: Fri, 19 Jun 2020 16:16:29 +0200 Subject: [PATCH 03/10] lots of refactoring --- README.md | 6 +- dsl_cuda/CUDAmodels/network.no_defines.c | 285 -------- dsl_cuda/LEMS2CUDA.py | 42 +- .../example}/__init__.py | 0 .../example}/benchAll.sh | 0 .../example}/cuda_run.py | 9 - .../example}/cuda_setup.py | 84 +-- {TVB_testsuit => dsl_cuda/example}/runthings | 0 dsl_cuda/example/tvb/__init__.py | 43 ++ dsl_cuda/example/tvb/basic/__init__.py | 29 + dsl_cuda/example/tvb/basic/config/__init__.py | 0 .../example/tvb/basic/config/environment.py | 166 +++++ .../tvb/basic/config/profile_settings.py | 198 ++++++ dsl_cuda/example/tvb/basic/config/settings.py | 339 ++++++++++ dsl_cuda/example/tvb/basic/config/stored.py | 131 ++++ dsl_cuda/example/tvb/basic/config/tvb.version | 1 + dsl_cuda/example/tvb/basic/config/utils.py | 50 ++ dsl_cuda/example/tvb/basic/exceptions.py | 52 ++ dsl_cuda/example/tvb/basic/logger/__init__.py | 0 dsl_cuda/example/tvb/basic/logger/builder.py | 101 +++ .../tvb/basic/logger/library_logger.conf | 78 +++ .../tvb/basic/logger/library_logger_test.conf | 68 ++ .../tvb/basic/logger/simple_handler.py | 53 ++ .../example/tvb/basic/neotraits/__init__.py | 41 ++ dsl_cuda/example/tvb/basic/neotraits/_attr.py | 618 ++++++++++++++++++ dsl_cuda/example/tvb/basic/neotraits/_core.py | 257 ++++++++ .../tvb/basic/neotraits/_declarative_base.py | 277 ++++++++ dsl_cuda/example/tvb/basic/neotraits/api.py | 37 ++ dsl_cuda/example/tvb/basic/neotraits/ex.py | 59 ++ dsl_cuda/example/tvb/basic/neotraits/info.py | 165 +++++ dsl_cuda/example/tvb/basic/profile.py | 165 +++++ dsl_cuda/example/tvb/basic/readers.py | 260 ++++++++ 32 files changed, 3230 insertions(+), 384 deletions(-) delete mode 100755 dsl_cuda/CUDAmodels/network.no_defines.c rename {TVB_testsuit => dsl_cuda/example}/__init__.py (100%) rename {TVB_testsuit => dsl_cuda/example}/benchAll.sh (100%) rename {TVB_testsuit => dsl_cuda/example}/cuda_run.py (95%) rename {TVB_testsuit => dsl_cuda/example}/cuda_setup.py (85%) rename {TVB_testsuit => dsl_cuda/example}/runthings (100%) create mode 100644 dsl_cuda/example/tvb/__init__.py create mode 100644 dsl_cuda/example/tvb/basic/__init__.py create mode 100644 dsl_cuda/example/tvb/basic/config/__init__.py create mode 100644 dsl_cuda/example/tvb/basic/config/environment.py create mode 100644 dsl_cuda/example/tvb/basic/config/profile_settings.py create mode 100644 dsl_cuda/example/tvb/basic/config/settings.py create mode 100644 dsl_cuda/example/tvb/basic/config/stored.py create mode 100644 dsl_cuda/example/tvb/basic/config/tvb.version create mode 100644 dsl_cuda/example/tvb/basic/config/utils.py create mode 100644 dsl_cuda/example/tvb/basic/exceptions.py create mode 100644 dsl_cuda/example/tvb/basic/logger/__init__.py create mode 100644 dsl_cuda/example/tvb/basic/logger/builder.py create mode 100644 dsl_cuda/example/tvb/basic/logger/library_logger.conf create mode 100644 dsl_cuda/example/tvb/basic/logger/library_logger_test.conf create mode 100644 dsl_cuda/example/tvb/basic/logger/simple_handler.py create mode 100644 dsl_cuda/example/tvb/basic/neotraits/__init__.py create mode 100644 dsl_cuda/example/tvb/basic/neotraits/_attr.py create mode 100644 dsl_cuda/example/tvb/basic/neotraits/_core.py create mode 100644 dsl_cuda/example/tvb/basic/neotraits/_declarative_base.py create mode 100644 dsl_cuda/example/tvb/basic/neotraits/api.py create mode 100644 dsl_cuda/example/tvb/basic/neotraits/ex.py create mode 100644 dsl_cuda/example/tvb/basic/neotraits/info.py create mode 100644 dsl_cuda/example/tvb/basic/profile.py create mode 100644 dsl_cuda/example/tvb/basic/readers.py diff --git a/README.md b/README.md index 084ed4387d..2ca79f9b46 100755 --- a/README.md +++ b/README.md @@ -180,7 +180,11 @@ for (unsigned int j_node = 0; j_node < n_node; j_node++) ``` -# Running +# Running an example +To run an example of a GPU generated model according to an existing or home-created xml file execute: +./runthings cuda [modelname] located in /tvb-hpc/dsl/dsl_cuda/example. The cuda parameter indicates a cuda simulation +is to be started and the [modelname] paramaters is the model that needs to be simulated. + Place model file in directory and execute cuda_templating('modelname') function. Resulting model will be placed in the CUDA model directory diff --git a/dsl_cuda/CUDAmodels/network.no_defines.c b/dsl_cuda/CUDAmodels/network.no_defines.c deleted file mode 100755 index dfc106cd08..0000000000 --- a/dsl_cuda/CUDAmodels/network.no_defines.c +++ /dev/null @@ -1,285 +0,0 @@ -#include // for printf -#include -#include - -#define PI_2 (2 * M_PI_F) - -// buffer length defaults to the argument to the integrate kernel -// but if it's known at compile time, it can be provided which allows -// compiler to change i%n to i&(n-1) if n is a power of two. -#ifndef NH -#define NH nh -#endif - -#ifndef WARP_SIZE -#define WARP_SIZE 32 -#endif - -#ifdef RAND123/*{{{*/ -#include "Random123/threefry.h" -#include "Random123/boxmuller.hpp" - -struct rng_state -{ - threefry4x32_ctr_t ctr; - threefry4x32_key_t key; - long long int seed; - float out[4]; -}; - -__device__ void rng_gen_normal(struct rng_state *r) -{ - threefry4x32_ctr_t result; - r123::float2 normal; - - ++r->ctr.v[0]; - - result = threefry4x32(r->ctr, r->key); - - normal = r123::boxmuller(result.v[0], result.v[1]); - - r->out[0] = normal.x; - r->out[1] = normal.y; - - normal = r123::boxmuller(result.v[2], result.v[3]); - r->out[2] = normal.x; - r->out[3] = normal.y; -} - -__device__ void rng_init(struct rng_state *r, int seed1, int seed2) -{ - r->ctr[0] = 0; - r->ctr[1] = 0; - r->ctr[2] = seed1; - r->ctr[3] = seed2; -} - -__device__ float rng_next_normal(struct rng_state *r) -{ - const int count = r->ctr.v[0] % 4; - if (count == 0) - rng_gen_normal(r); - return r->out[count]; -} -#endif //RANDOM123/*}}}*/ - -#ifdef CURAND/*{{{*/ -#include -#include -#endif //CURAND/*}}}*/ - -__device__ float wrap_2_pi_(float x)/*{{{*/ -{ - bool neg_mask = x < 0.0f; - bool pos_mask = !neg_mask; - // fmodf diverges 51% of time - float pos_val = fmodf(x, PI_2); - float neg_val = PI_2 - fmodf(-x, PI_2); - return neg_mask * neg_val + pos_mask * pos_val; -}/*}}}*/ - -__device__ float wrap_2_pi(float x) // not divergent/*{{{*/ -{ - bool lt_0 = x < 0.0f; - bool gt_2pi = x > PI_2; - return (x + PI_2)*lt_0 + x*(!lt_0)*(!gt_2pi) + (x - PI_2)*gt_2pi; -}/*}}}*/ - -__global__ void integrate(/*{{{*/ - // config - unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, - float dt, float speed, - float * __restrict__ weights, - float * __restrict__ lengths, - float * __restrict__ params_pwi, // pwi: per work item - // state - float * __restrict__ state_pwi, - // outputs - float * __restrict__ tavg_pwi - ) -{/*}}}*/ - - // work id & size/*{{{*/ - const unsigned int id = (blockIdx.y * gridDim.x + blockIdx.x) * blockDim.x + threadIdx.x; - const unsigned int size = blockDim.x * gridDim.x * gridDim.y;/*}}}*/ - - // ND array accessors (TODO autogen from py shape info)/*{{{*/ -#define params(i_par) (params_pwi[(size * (i_par)) + id]) -#define state(time, i_node) (state_pwi[((time) * n_node + (i_node))*size + id]) -#define tavg(i_node) (tavg_pwi[((i_node) * size) + id])/*}}}*/ - - // unpack params/*{{{*/ - const float coupling_value = params(1); - const float speed_value = params(0);/*}}}*/ - - // derived/*{{{*/ - const float rec_n = 1.0f / n_node; - const float rec_speed_dt = 1.0f / speed_value / dt; - const float omega = 10.0 * 2.0 * M_PI_F / 1e3; - const float sig = sqrt(dt) * sqrt(2.0 * 1e-5);/*}}}*/ - - curandState s; - curand_init(id * (blockDim.x * gridDim.x * gridDim.y), 0, 0, &s); - - for (unsigned int i_node = 0; i_node < n_node; i_node++) - tavg(i_node) = 0.0f; - - for (unsigned int t = i_step; t < (i_step + n_step); t++) { - for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) { - if(i_node >= n_node) continue; - - float theta_i = state(t % NH, i_node); - unsigned int i_n = i_node * n_node; - float sum = 0.0f; - - for (unsigned int j_node = 0; j_node < n_node; j_node++) { - float wij = weights[i_n + j_node]; // nb. not coalesced - if(lengths[i_n + j_node]>0 && i_node>0 && threadIdx.x == 0) - // printf("%d %d %d %f\n", t, i_n + j_node, i_node, lengths[i_n + j_node]); - - if (wij == 0.0) continue; - unsigned int dij = lengths[i_n + j_node] * rec_speed_dt; - unsigned time = (t - dij + NH) % NH; - float theta_j = state_pwi[(time * n_node + j_node)*size + id]; - sum += wij * sin(theta_j - theta_i); - } // j_node - theta_i += dt * (omega + coupling_value * rec_n * sum); - - theta_i += sig * curand_normal2(&s).x; - - theta_i = wrap_2_pi(theta_i); - state((t + 1) % NH, i_node) = theta_i; - tavg(i_node) += sin(theta_i); - - // sync across warps executing nodes for single sim, before going on to next time step - __syncthreads(); - - } // for i_node - } // for t - -// cleanup macros/*{{{*/ -#undef params -#undef state -#undef tavg/*}}}*/ - -} // kernel integrate - -// const float w_plus=1.4f; -// const float a_E=310.0f; -// const float b_E=125.0f; -// const float d_E=0.16f; -// const float a_I=615.0f; -// const float b_I=177.0f; -// const float d_I=0.087f; -// // const float gamma_E=0.641f / 1000.0f; -// // const float tau_E=100.0f; -// // const float tau_I=10.0f; -// const float I_0=0.382f; -// const float w_E=1.0f; -// const float w_I=0.7f; -// // const float gamma_I= 1.0f / 1000.0f; -// const float min_d_E = (-1.0f * d_E); -// const float min_d_I = (-1.0f * d_I); -// // const float imintau_E = (-1.0f / tau_E); -// // const float imintau_I = (-1.0f / tau_I); -// const float w_E__I_0 = (w_E * I_0); -// const float w_I__I_0 = (w_I * I_0); - -// __global__ void integrate_wongwang( -// // config -// unsigned int i_step, unsigned int n_node, unsigned int nh, unsigned int n_step, unsigned int n_params, -// float dt, float speed, -// float * weights, -// float * lengths, -// float * params_pwi, // pwi: per work item -// // state -// float * state_pwi, -// // outputs -// float * tavg_pwi -// ) -// { -// // const int i_step_dev = i_step; -// // const int n_node_dev = n_node; -// // work id & size -// const unsigned int id = (blockIdx.x * blockDim.x) + threadIdx.x; -// const unsigned int size = blockDim.x * gridDim.x; - -// // ND array accessors (TODO autogen from py shape info) -// #define params(i_par) (params_pwi[(size * (i_par)) + id]) -// #define state(time, i_node) (state_pwi[((time) *2 * n_node + (i_node))*(size) + id]) -// #define tavg(i_node) (tavg_pwi[((i_node) * size) + id]) - -// // unpack params -// const float G = params(1); -// const float J_NMDA = params(0); -// const float G_J_NMDA = G*J_NMDA; -// // derived - -// const float w_plus__J_NMDA = (w_plus * J_NMDA); -// const float sig = sqrt(dt) * sqrt(2.0 * 1e-5); -// // We have three variables which could be changed here. Actually 4 -// // G (the global coupling), sigma (the noise), J_NMDA(the excitatory synaptic coupling) and J_i(the inner inhibition for each region) -// // For now we are making things simple and only change two parameters, G and J_NMDA. -// #ifdef RAND123 -// // rng -// struct rng_state rng; -// rng_init(&rng, id, i_step); -// #endif -// #ifdef CURAND -// curandState s; -// curand_init(id * (blockDim.x * gridDim.x), 0, 0, &s); -// #endif -// float tmp_I_E; -// float tmp_H_E; -// float tmp_I_I; -// float tmp_H_I; -// float sum; - - -// for (unsigned int i_node = 0; i_node < n_node; i_node++) -// tavg(i_node) = 0.0f; - -// for (unsigned int t = i_step; t < (i_step + n_step); t++) -// { -// for (unsigned int i_node = 0; i_node < n_node; i_node++) -// { -// sum = 0.0f; -// float S_E = state((t) % nh, i_node); -// float S_I = state((t) % nh, i_node + n_node); -// for (unsigned int j_node = 0; j_node < n_node; j_node++) -// { -// //we are not considering delays in this model -// float wij = G_J_NMDA*weights[(i_node*n_node) + j_node]; // nb. not coalesced -// if (wij == 0.0) -// continue; -// sum += wij * state((t) % nh, j_node); //of J -// } -// // external Input set to 0, no task evoked activity -// tmp_I_E = S_I; // Inner inhibition set to 1 -// tmp_I_E = sum - tmp_I_E ; -// tmp_I_E = ((w_E__I_0)+(w_plus__J_NMDA * S_E)) + tmp_I_E ; -// tmp_I_E = a_E * tmp_I_E ; -// tmp_I_E = tmp_I_E - b_E; -// tmp_I_E = (a_E * (((w_E__I_0)+(w_plus__J_NMDA * S_E))+( sum-(S_I))))-b_E; -// tmp_H_E = tmp_I_E/(1-expf(min_d_E * tmp_I_E)); -// //meanFR[i] += tmp_H_E; Not storing mean firing rate -// // r_Edd_i = tmp_H_E; not observing the firing rate for now -// tmp_I_I = (a_I*(((w_I__I_0)+(J_NMDA * S_E))-( S_I)))-b_I; -// tmp_H_I = tmp_I_I/(1-expf(min_d_I*tmp_I_I)); -// // r_I[i] = tmp_H_I; not observing the firing rate for now - -// #ifdef RAND123 -// S_E = ((sig * rng_next_normal(&rng))+S_E)+(dt*((imintau_E* S_E)+(tmp_H_E*((1-S_E)*gamma_E)))); -// S_I = ((sig * rng_next_normal(&rng))+S_I)+(dt*((imintau_I* S_I)+(tmp_H_I*gamma_I))); -// #endif -// #ifdef CURAND -// S_E = ((sig * curand_normal2(&s).x)+S_E)+(dt*((imintau_E* S_E)+(tmp_H_E*((1-S_E)*gamma_E)))); -// S_I = ((sig * curand_normal2(&s).x)+S_I)+(dt*((imintau_I* S_I)+(tmp_H_I*gamma_I))); -// #endif -// state((t+1) % nh, i_node) = S_E; -// state((t+1) % nh, i_node+(n_node)) = S_I; -// tavg(i_node) += S_E + S_I; -// } // for i_node -// } // for t -// } // kernel integrate -// vim: sw=4 sts=4 ts=8 et ai diff --git a/dsl_cuda/LEMS2CUDA.py b/dsl_cuda/LEMS2CUDA.py index c40b7f762e..7ba5a63d0f 100755 --- a/dsl_cuda/LEMS2CUDA.py +++ b/dsl_cuda/LEMS2CUDA.py @@ -1,30 +1,15 @@ -# from models import G2DO from mako.template import Template import os import sys -import inspect - -for p in sys.path: - print('spL2C', p) - -# import dsl -# sys.path.append("{}".format(os.path.dirname(dsl.__file__))) -# # sys.path.append("{}".format(os.path.dirname(__file__))) -# x = os.path.join('../../', os.path.dirname(__file__)) -# print('dsl', x) - -current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) -parent_dir = os.path.dirname(current_dir) -sys.path.insert(0, parent_dir) - -print('cr', inspect.currentframe()) +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.pardir)) from lems.model.model import Model def default_lems_folder(): here = os.path.dirname(os.path.abspath(__file__)) + print('\n here', here, '\n') xmlpath = os.path.join(here, 'XMLmodels') return xmlpath @@ -50,9 +35,6 @@ def load_model(model_filename, folder=None): return model def render_model(model_name, template=None, folder=None): - # drift dynamics - # modelist = list() - # modelist.append(model.component_types[modelname]) model = load_model(model_name, folder) template = template or default_template() @@ -61,31 +43,17 @@ def render_model(model_name, template=None, folder=None): # coupling functionality couplinglist = list() - # couplinglist.append(model.component_types['coupling_function_pop1']) for i, cplists in enumerate(model.component_types): if 'coupling' in cplists.name: couplinglist.append(cplists) - # collect all signal amplification factors per state variable. - # signalampl = list() - # for i, sig in enumerate(modellist.dynamics.derived_variables): - # if 'sig' in sig.name: - # signalampl.append(sig) - # collect total number of exposures combinations. expolist = list() for i, expo in enumerate(modellist.exposures): for chc in expo.choices: expolist.append(chc) - # print((couplinglist[0].dynamics.derived_variables['pre'].expression)) - # - # for m in range(len(couplinglist)): - # # print((m)) - # for k in (couplinglist[m].functions): - # print(k) - # only check whether noise is there, if so then activate it noisepresent=False for ct in (model.component_types): @@ -93,7 +61,6 @@ def render_model(model_name, template=None, folder=None): noisepresent=True # start templating - # template = Template(filename='tmpl8_CUDA.py') model_str = template.render( modelname=model_name, const=modellist.constants, @@ -109,10 +76,7 @@ def render_model(model_name, template=None, folder=None): def cuda_templating(model_filename, folder=None): - # modelfile = "{}{}{}{}".format(os.path.dirname(dsl.__file__), '/dsl_cuda/CUDAmodels/', model_filename.lower(), '.c') - # modelfile = os.path.join(os.path.dirname(dsl.__file__), 'dsl_cuda', 'CUDAmodels', model_filename.lower() + '.c') - # print('f', os.path.dirname(__file__)) - modelfile = os.path.join(os.path.dirname(__file__), 'CUDAmodels', model_filename.lower() + '.c') + modelfile = os.path.join((os.path.dirname(os.path.abspath(__file__))), 'CUDAmodels', model_filename.lower() + '.c') # start templating model_str = render_model(model_filename, template=default_template(), folder=folder) diff --git a/TVB_testsuit/__init__.py b/dsl_cuda/example/__init__.py similarity index 100% rename from TVB_testsuit/__init__.py rename to dsl_cuda/example/__init__.py diff --git a/TVB_testsuit/benchAll.sh b/dsl_cuda/example/benchAll.sh similarity index 100% rename from TVB_testsuit/benchAll.sh rename to dsl_cuda/example/benchAll.sh diff --git a/TVB_testsuit/cuda_run.py b/dsl_cuda/example/cuda_run.py similarity index 95% rename from TVB_testsuit/cuda_run.py rename to dsl_cuda/example/cuda_run.py index 5c4de59231..f96121cd4a 100755 --- a/TVB_testsuit/cuda_run.py +++ b/dsl_cuda/example/cuda_run.py @@ -1,20 +1,12 @@ #!/usr/bin/env python3 from __future__ import print_function -import sys -import numpy as np import os.path import numpy as np -import itertools -import pycuda.autoinit import pycuda.driver as drv from pycuda.compiler import SourceModule import pycuda.gpuarray as gpuarray -import pytools import time -import argparse -import logging -import scipy.io as io here = os.path.dirname(os.path.abspath(__file__)) @@ -124,7 +116,6 @@ def run_simulation(self, weights, lengths, params_matrix, speeds, logger, args, assert n_coupling_per_block * n_coupling_blocks == args.n_coupling #}}} logger.info('gpu_data[lengts] %s', gpu_data['lengths'].shape) logger.info('nnodes %r', n_nodes) - # logger.info('gpu_data[lengths] %r', gpu_data['lengths']) # run simulation#{{{ logger.info('submitting work') diff --git a/TVB_testsuit/cuda_setup.py b/dsl_cuda/example/cuda_setup.py similarity index 85% rename from TVB_testsuit/cuda_setup.py rename to dsl_cuda/example/cuda_setup.py index b91d3f5afe..ea261cebf1 100755 --- a/TVB_testsuit/cuda_setup.py +++ b/dsl_cuda/example/cuda_setup.py @@ -6,30 +6,13 @@ import logging import itertools import argparse +import os, sys -import os, sys, inspect - -current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) -parent_dir = os.path.dirname(current_dir) -sys.path.insert(0, parent_dir) - -# sys.path.append("{}{}".format(parent_dir, '/NeuroML/')) -# print('pd', parent_dir) -# for p in sys.path: -# print('sp', p) - -# from dsl_cuda import LEMS2CUDA - -np.set_printoptions(threshold=sys.maxsize) +sys.path.insert(0, os.path.join(os.getcwd(), os.pardir)) +import LEMS2CUDA rgn.seed(79) -# class testthis: -# -# def __init__(self): -# self.args = self.parse_args() - - class TVB_test: def __init__(self): @@ -139,25 +122,38 @@ def check_results(self, n_nodes, n_work_items, tavg, weights, speeds, couplings, logger.info('result OK') - def cuda(self, logger, pop): + def start_cuda(self, logger): logger.info('start Cuda run') from cuda_run import CudaRun cudarun = CudaRun() tavg_data = cudarun.run_simulation(self.weights, self.lengths, self.params, self.speeds, logger, self.args, self.n_nodes, self.n_work_items, self.n_params, self.nstep, self.n_inner_steps, self.buf_len, self.states, self.dt, self.min_speed) - # logger.info('tavg_data %f', tavg_data) # Todo: fix this for cuda - self.check_results(self.n_nodes, self.n_work_items, tavg_data, self.weights, self.speeds, self.couplings, logger, self.args) + # self.check_results(self.n_nodes, self.n_work_items, tavg_data, self.weights, self.speeds, self.couplings, logger, self.args) - return tavg_data + def set_CUDAmodel_dir(self): + self.args.filename = os.path.join((os.path.dirname(os.path.abspath(__file__))), os.pardir,'CUDAmodels', + self.args.model.lower() + '.c') - def startsim(self, pop): + def set_states(self): + if 'kuramoto' in self.args.model.lower(): + self.states = 1 + elif 'oscillator' in self.args.model.lower(): + self.states = 2 + elif 'wongwang' in self.args.model.lower(): + self.states = 2 + elif 'montbrio' in self.args.model.lower(): + self.states = 2 + elif 'epileptor' in self.args.model.lower(): + self.states = 6 + + def startsim(self): tic = time.time() logging.basicConfig(level=logging.DEBUG if self.args.verbose else logging.INFO) - logger = logging.getLogger('[tvbBench.py]') + logger = logging.getLogger('[TVB_CUDA]') logger.info('dt %f', self.dt) logger.info('nstep %d', self.nstep) @@ -177,30 +173,18 @@ def startsim(self, pop): logger.info('min_speed %f', self.min_speed) logger.info('real buf_len %d, using power of 2 %d', self.buf_len_, self.buf_len) - tac = time.time() - logger.info("Setup in: {}".format(tac - tic)) - - self.args.filename = "{}{}{}{}".format(parent_dir, '/dsl_cuda/CUDAmodels/', self.args.model.lower(), '.c') - # self.args.filename = os.path.join(os.path.dirname(tvb.__file__), 'dsl_cuda', 'CUDAmodels', self.args.model.lower() + '.c') + self.set_CUDAmodel_dir() - logger.info('modellow %s', self.args.model.lower()) - - if ('kuramoto' in self.args.model.lower()): - self.states = 1 - elif 'oscillator' in self.args.model.lower(): - self.states = 2 - elif 'wongwang' in self.args.model.lower(): - self.states = 2 - elif 'montbrio' in self.args.model.lower(): - self.states = 2 - elif 'epileptor' in self.args.model.lower(): - self.states = 6 + self.set_states() logger.info('number of states %d', self.states) logger.info('filename %s', self.args.filename) logger.info('model %s', self.args.model) - tavg = self.cuda(logger, pop) + tac = time.time() + logger.info("Setup in: {}".format(tac - tic)) + + self.start_cuda(logger) toc = time.time() elapsed = toc - tic @@ -208,13 +192,13 @@ def startsim(self, pop): logger.info('%0.3f M step/s', 1e-6 * self.nstep * self.n_inner_steps * self.n_work_items / elapsed) logger.info('finished') - return tavg - if __name__ == '__main__': - zelf = TVB_test() - # zelf = testthis() - LEMS2CUDA.cuda_templating(zelf.args.model, '../dsl_cuda/XMLmodels/') + example = TVB_test() + + # start templating the model specified on cli + LEMS2CUDA.cuda_templating(example.args.model, '../../dsl_cuda/XMLmodels/') - tavg = zelf.startsim(zelf.args.model) + # start simulation with templated model + example.startsim() diff --git a/TVB_testsuit/runthings b/dsl_cuda/example/runthings similarity index 100% rename from TVB_testsuit/runthings rename to dsl_cuda/example/runthings diff --git a/dsl_cuda/example/tvb/__init__.py b/dsl_cuda/example/tvb/__init__.py new file mode 100644 index 0000000000..3805159484 --- /dev/null +++ b/dsl_cuda/example/tvb/__init__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +We want tvb package to extend over at least 2 folders: +simulator_library and tvb_framework. +""" + +from pkgutil import extend_path + +try: + __path__ = extend_path(__path__, __name__) + +except NameError: + ## Ignore __path__ not defined when called from sphinx + __path__ = [__name__] \ No newline at end of file diff --git a/dsl_cuda/example/tvb/basic/__init__.py b/dsl_cuda/example/tvb/basic/__init__.py new file mode 100644 index 0000000000..ab250309ae --- /dev/null +++ b/dsl_cuda/example/tvb/basic/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# diff --git a/dsl_cuda/example/tvb/basic/config/__init__.py b/dsl_cuda/example/tvb/basic/config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dsl_cuda/example/tvb/basic/config/environment.py b/dsl_cuda/example/tvb/basic/config/environment.py new file mode 100644 index 0000000000..15a2d3bd7b --- /dev/null +++ b/dsl_cuda/example/tvb/basic/config/environment.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +Environment related checks or operations are to be defined here. + +.. moduleauthor:: Lia Domide +.. moduleauthor:: Mihai Andrei +""" + +import os +import sys +from subprocess import Popen, PIPE +from tvb.basic.config.settings import VersionSettings + + +class Environment(object): + + def is_framework_present(self): + """ + :return: True when framework classes are present and can be imported. + """ + framework_present = True + try: + from tvb.config.profile_settings import WebSettingsProfile + except ImportError: + framework_present = False + + return framework_present + + @staticmethod + def is_distribution(): + """ + Return True when TVB is used with Python installed natively (with GitHub clone, SVN, pip or conda) + """ + svn_variable = 'SVN_REVISION' + if svn_variable in os.environ: + # Usage in Hudson build + return False + + try: + import tvb_bin + except ImportError: + # No tvb_bin, it means usage from pip or conda + return False + + try: + _proc = Popen(["svnversion", "."], stdout=PIPE, stderr=PIPE) + version = VersionSettings.parse_svn_version(_proc.communicate()[0]) + if version: + # usage from SVN + return False + except Exception: + # Usage from tvb_distribution + return True + + def is_linux_deployment(self): + """ + Return True if current run is not development and is running on Linux. + """ + return self.is_linux() and self.is_distribution() + + def is_mac_deployment(self): + """ + Return True if current run is not development and is running on Mac OS X + """ + return self.is_mac() and self.is_distribution() + + def is_windows_deployment(self): + """ + Return True if current run is not development and is running on Windows. + """ + return self.is_windows() and self.is_distribution() + + def is_linux(self): + return not self.is_windows() and not self.is_mac() + + @staticmethod + def is_mac(): + return sys.platform == 'darwin' + + @staticmethod + def is_windows(): + return sys.platform.startswith('win') + + def get_library_folder(self, default_mac): + """ + Return top level library folder. Will be use for setting paths + """ + if self.is_windows_deployment(): + return os.path.dirname(sys.executable) + if self.is_mac_deployment(): + return os.path.dirname(default_mac) + if self.is_linux_deployment(): + return os.path.dirname(sys.executable) + + def setup_tk_tcl_environ(self, root_folder): + """ + Given a root folder to look in, find the required configuration files for TCL/TK and set the proper + environmental variables so everything works fine in the distribution package. + + :param root_folder: the top folder from which to start looking for the required configuration files + """ + tk_folder = self._find_file('tk.tcl', root_folder) + if tk_folder: + os.environ['TK_LIBRARY'] = tk_folder + + tcl_folder = self._find_file('init.tcl', root_folder) + if tcl_folder: + os.environ['TCL_LIBRARY'] = tcl_folder + + def _find_file(self, target_file, root_folder): + """ + Search for a file in a folder directory. Return the folder in which the file can be found. + + :param target_file: the name of the file that is searched + :param root_folder: the top lever folder from which to start searching in all it's subdirectories + :returns: the name of the folder in which the file can be found + """ + for root, _, files in os.walk(root_folder): + for file_n in files: + if file_n == target_file: + return root + + def setup_python_path(self, *paths): + """ + Set PYTHONPATH + :param paths: list of absolute folder paths to join. + """ + os.environ['PYTHONPATH'] = os.pathsep.join(paths) + + def append_to_path(self, *paths): + """ + Set PATH + :param paths: list of absolute folder paths to join and add BEFORE the current PATH + """ + paths = list(paths) + paths.append(os.environ.get('PATH', '')) + os.environ['PATH'] = os.pathsep.join(paths) diff --git a/dsl_cuda/example/tvb/basic/config/profile_settings.py b/dsl_cuda/example/tvb/basic/config/profile_settings.py new file mode 100644 index 0000000000..e635a32543 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/config/profile_settings.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +Prepare TVB settings to be grouped under various profile classes. + +.. moduleauthor:: Lia Domide +""" +import os +import sys +from tvb.basic.config import stored +from tvb.basic.config.environment import Environment +from tvb.basic.config.settings import ClusterSettings, DBSettings, VersionSettings, WebSettings + + +class BaseSettingsProfile(object): + TVB_USER_HOME = os.environ.get('TVB_USER_HOME', '~') + + TVB_CONFIG_FILE = os.path.expanduser(os.path.join(TVB_USER_HOME, '.tvb.configuration')) + + DEFAULT_STORAGE = os.path.expanduser(os.path.join(TVB_USER_HOME, 'TVB' + os.sep)) + FIRST_RUN_STORAGE = os.path.expanduser(os.path.join(TVB_USER_HOME, '.tvb-temp')) + + LOGGER_CONFIG_FILE_NAME = "logger_config.conf" + + # Access rights for TVB generated files/folders. + ACCESS_MODE_TVB_FILES = 0o744 + + # Number used for estimation of TVB used storage space + MAGIC_NUMBER = 9 + + def __init__(self, web_enabled=True): + + self.manager = stored.SettingsManager(self.TVB_CONFIG_FILE) + + # Actual storage of all TVB related files + self.TVB_STORAGE = self.manager.get_attribute(stored.KEY_STORAGE, self.FIRST_RUN_STORAGE, str) + self.TVB_LOG_FOLDER = os.path.join(self.TVB_STORAGE, "logs") + self.TVB_TEMP_FOLDER = os.path.join(self.TVB_STORAGE, "TEMP") + + self.env = Environment() + self.cluster = ClusterSettings(self.manager) + self.web = WebSettings(self.manager, web_enabled) + self.db = DBSettings(self.manager, self.DEFAULT_STORAGE, self.TVB_STORAGE) + self.version = VersionSettings(self.manager, self.BIN_FOLDER) + + self.EXTERNALS_FOLDER_PARENT = os.path.dirname(self.BIN_FOLDER) + if not self.env.is_distribution(): + self.EXTERNALS_FOLDER_PARENT = os.path.dirname(self.EXTERNALS_FOLDER_PARENT) + + # The path to the matlab executable (if existent). Otherwise just return an empty string. + value = self.manager.get_attribute(stored.KEY_MATLAB_EXECUTABLE, '', str) or '' + if value == 'None': + value = '' + self.MATLAB_EXECUTABLE = value + + # Maximum number of vertices acceptable o be part of a surface at import time. + self.MAX_SURFACE_VERTICES_NUMBER = self.manager.get_attribute(stored.KEY_MAX_NR_SURFACE_VERTEX, 300000, int) + # Max number of ops that can be scheduled from UI in a PSE. To be correlated with the oarsub limitations + self.MAX_RANGE_NUMBER = self.manager.get_attribute(stored.KEY_MAX_RANGE_NR, 2000, int) + # Max number of threads in the pool of ops running in parallel. TO be correlated with CPU cores + self.MAX_THREADS_NUMBER = self.manager.get_attribute(stored.KEY_MAX_THREAD_NR, 4, int) + # The maximum disk space that can be used by one single user, in KB. + self.MAX_DISK_SPACE = self.manager.get_attribute(stored.KEY_MAX_DISK_SPACE_USR, 5 * 1024 * 1024, int) + + @property + def BIN_FOLDER(self): + """ + Return path towards tvb_bin location. It will be used in some environment for determining the starting point + """ + try: + import tvb_bin + return os.path.dirname(os.path.abspath(tvb_bin.__file__)) + except ImportError: + return "." + + @property + def PYTHON_INTERPRETER_PATH(self): + """ + Get Python path, based on current environment. + """ + if self.env.is_mac_deployment(): + return os.path.join(os.path.dirname(sys.executable), "python") + + return sys.executable + + def prepare_for_operation_mode(self): + """ + Overwrite PostgreSQL number of connections when executed in the context of a node. + """ + self.db.MAX_CONNECTIONS = self.db.MAX_ASYNC_CONNECTIONS + self.cluster.IN_OPERATION_EXECUTION_PROCESS = True + + def initialize_profile(self): + """ + Make sure tvb folders are created. + """ + if not os.path.exists(self.TVB_LOG_FOLDER): + os.makedirs(self.TVB_LOG_FOLDER) + + if not os.path.exists(self.TVB_TEMP_FOLDER): + os.makedirs(self.TVB_TEMP_FOLDER) + + if not os.path.exists(self.TVB_STORAGE): + os.makedirs(self.TVB_STORAGE) + + def initialize_for_deployment(self): + + library_folder = self.env.get_library_folder(self.BIN_FOLDER) + + if self.env.is_windows_deployment(): + self.env.setup_python_path(library_folder, os.path.join(library_folder, 'lib-tk')) + self.env.append_to_path(library_folder) + self.env.setup_tk_tcl_environ(library_folder) + + if self.env.is_mac_deployment(): + # MacOS package structure is in the form: + # Contents/Resorces/lib/python2.7/tvb . PYTHONPATH needs to be set + # at the level Contents/Resources/lib/python2.7/ and the root path + # from where to start looking for TK and TCL up to Contents/ + tcl_root = os.path.dirname(os.path.dirname(os.path.dirname(library_folder))) + self.env.setup_tk_tcl_environ(tcl_root) + + self.env.setup_python_path(library_folder, os.path.join(library_folder, 'site-packages.zip'), + os.path.join(library_folder, 'lib-dynload')) + + if self.env.is_linux_deployment(): + # Note that for the Linux package some environment variables like LD_LIBRARY_PATH, + # LD_RUN_PATH, PYTHONPATH and PYTHONHOME are set also in the startup scripts. + self.env.setup_python_path(library_folder, os.path.join(library_folder, 'lib-tk')) + self.env.setup_tk_tcl_environ(library_folder) + + # Correctly set MatplotLib Path, before start. + mpl_data_path_maybe = os.path.join(library_folder, 'mpl-data') + try: + os.stat(mpl_data_path_maybe) + os.environ['MATPLOTLIBDATA'] = mpl_data_path_maybe + except: + pass + + +class LibrarySettingsProfile(BaseSettingsProfile): + """ + Profile used when scientific library is used without storage and without web UI. + """ + + TVB_STORAGE = os.path.expanduser(os.path.join("~", "TVB" + os.sep)) + LOGGER_CONFIG_FILE_NAME = "library_logger.conf" + + def __init__(self): + super(LibrarySettingsProfile, self).__init__(False) + + +class TestLibraryProfile(LibrarySettingsProfile): + """ + Profile for library unit-tests. + """ + + LOGGER_CONFIG_FILE_NAME = "library_logger_test.conf" + + def __init__(self): + super(TestLibraryProfile, self).__init__() + self.TVB_LOG_FOLDER = "TEST_OUTPUT" + + +class MATLABLibraryProfile(LibrarySettingsProfile): + """ + Profile use library use from MATLAB. + """ + + LOGGER_CONFIG_FILE_NAME = None diff --git a/dsl_cuda/example/tvb/basic/config/settings.py b/dsl_cuda/example/tvb/basic/config/settings.py new file mode 100644 index 0000000000..a586788e9a --- /dev/null +++ b/dsl_cuda/example/tvb/basic/config/settings.py @@ -0,0 +1,339 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +TVB Raw Settings are defined here, grouped by their category of usage (e.g. cluster related, web related, etc). +Do not instantiate these classes directly, but rather use them through TvpProfile.current instance. + +.. moduleauthor:: Lia Domide +""" + +import os +from subprocess import Popen, PIPE +from tvb.basic.config import stored + + + +class VersionSettings(object): + """ + Gather settings related to various version numbers of TVB application + """ + + # Current release number + BASE_VERSION = "2.0" + + # Current DB version. Increment this and create a new xxx_update_db.py migrate script + DB_STRUCTURE_VERSION = 18 + + # This is the version of the data stored in H5 and XML files + # and should be used by next versions to know how to import + # data in TVB format, in case data structure changes. + # Value should be updated every time data structure is changed. + DATA_VERSION = 5 + DATA_VERSION_ATTRIBUTE = "Data_version" + + # This is the version of the tvb project. + # It should be updated every time the project structure changes + # Should this be sync-ed with data version changes? + PROJECT_VERSION = 3 + + + def __init__(self, manager, bin_folder): + + # Used for reading the version file from it + self.BIN_FOLDER = bin_folder + + # Concatenate BASE_VERSION with svn revision number + self.CURRENT_VERSION = self.BASE_VERSION + '-' + str(self.SVN_VERSION) + + # The version up until we done the upgrade properly for the file data storage. + self.DATA_CHECKED_TO_VERSION = manager.get_attribute(stored.KEY_LAST_CHECKED_FILE_VERSION, 1, int) + + # The version up until we done the upgrade properly for the file data storage. + self.CODE_CHECKED_TO_VERSION = manager.get_attribute(stored.KEY_LAST_CHECKED_CODE_VERSION, -1, int) + + + @property + def SVN_VERSION(self): + """Current SVN version in the package running now.""" + svn_variable = 'SVN_REVISION' + if svn_variable in os.environ: + return os.environ[svn_variable] + + try: + _proc = Popen(["svnversion", "."], stdout=PIPE, stderr=PIPE) + return self.parse_svn_version(_proc.communicate()[0]) + except Exception: + pass + + try: + import tvb.basic.config + config_folder = os.path.dirname(os.path.abspath(tvb.basic.config.__file__)) + with open(os.path.join(config_folder, 'tvb.version'), 'r') as version_file: + return self.parse_svn_version(version_file.read()) + except Exception: + pass + + raise ValueError('cannot determine TVB revision number') + + + @staticmethod + def parse_svn_version(version_string): + if ':' in version_string: + version_string = version_string.split(':')[1] + + number = ''.join([ch for ch in version_string if ch.isdigit()]) + return int(number) + + + +class ClusterSettings(object): + """ + Cluster related settings. + """ + SCHEDULER_OAR = "oar" + SCHEDULER_SLURM = "slurm" + + # Specify if the current process is executing an operation (via clusterLauncher) + IN_OPERATION_EXECUTION_PROCESS = False + + _CACHED_IS_RUNNING_ON_CLUSTER = None + _CACHED_NODE_NAME = None + + + def __init__(self, manager): + self.IS_DEPLOY = manager.get_attribute(stored.KEY_CLUSTER, False, eval) + self.CLUSTER_SCHEDULER = manager.get_attribute(stored.KEY_CLUSTER_SCHEDULER, self.SCHEDULER_OAR) + self.ACCEPTED_SCHEDULERS = {self.SCHEDULER_OAR: self.SCHEDULER_OAR, + self.SCHEDULER_SLURM: self.SCHEDULER_SLURM} + + @property + def SCHEDULE_COMMAND(self): + if self.CLUSTER_SCHEDULER == self.SCHEDULER_OAR: + return 'oarsub -q tvb -S "/home/tvbadmin/clusterLauncher %s %s" -l walltime=%s' + return 'sbatch /home/tvbadmin/clusterLauncher %s %s %s' + + @property + def STOP_COMMAND(self): + if self.CLUSTER_SCHEDULER == self.SCHEDULER_OAR: + return 'oardel %s' + return 'scancel %s' + + @property + def STATUS_COMMAND(self): + if self.CLUSTER_SCHEDULER == self.SCHEDULER_OAR: + return 'oarstat %s' + return 'squeue -j %s' + + @property + def JOB_ID_STRING(self): + if self.CLUSTER_SCHEDULER == self.SCHEDULER_OAR: + return 'OAR_JOB_ID=' + return 'Submitted batch job ' + + @property + def NODE_ENV(self): + if self.CLUSTER_SCHEDULER == self.SCHEDULER_OAR: + return 'OAR_NODEFILE' + return 'SLURM_NODEID' + + @property + def IS_RUNNING_ON_CLUSTER_NODE(self): + """ + Returns True if current execution happens on cluster node. + Even when IS_DEPLOY is True, this call will return False for the web machine. + """ + if self._CACHED_IS_RUNNING_ON_CLUSTER is None: + self._CACHED_IS_RUNNING_ON_CLUSTER = self.CLUSTER_NODE_NAME is not None + + return self._CACHED_IS_RUNNING_ON_CLUSTER + + + @property + def CLUSTER_NODE_NAME(self): + """ + :return the name of the cluster on which TVB code is executed. + If code is executed on a normal machine (not cluster node) returns None + """ + # Check if the name wasn't computed before. + if self._CACHED_NODE_NAME is None: + # Read env variable which contains path the the file containing node name + env_oar_nodefile = os.getenv(self.NODE_ENV) + if env_oar_nodefile is not None and len(env_oar_nodefile) > 0 and os.path.exists(env_oar_nodefile): + # Read node name from file (valid for OAR cluster) + with open(env_oar_nodefile, 'r') as f: + node_name = f.read() + else: + # Valid for SLURM clusters + node_name = os.getenv(self.NODE_ENV) + + if node_name is not None and len(node_name.strip()) > 0: + self._CACHED_NODE_NAME = node_name.strip() + return self._CACHED_NODE_NAME + else: + return self._CACHED_NODE_NAME + + return None + + + +class WebSettings(object): + """ + Web related specifications + """ + + ENABLED = False + LOCALHOST = "127.0.0.1" + RENDER_HTML = True + VISUALIZERS_ROOT = "tvb.interfaces.web.templates.genshi.visualizers" + VISUALIZERS_URL_PREFIX = "/flow/read_datatype_attribute/" + + + def __init__(self, manager, enabled): + + self.ENABLED = enabled + self.admin = WebAdminSettings(manager) + + self.CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + if enabled: + try: + import tvb.interfaces + self.CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(tvb.interfaces.__file__))) + except ImportError: + pass + else: + self.VISUALIZERS_URL_PREFIX = "" + + self.SERVER_PORT = manager.get_attribute(stored.KEY_PORT, 8080, int) + + # Compute reference towards the current web application, valid FROM localhost + server_IP = manager.get_attribute(stored.KEY_IP, self.LOCALHOST) + self.BASE_LOCAL_URL = "http://%s:%s/" % (server_IP, str(self.SERVER_PORT)) + + # Compute PUBLIC reference towards the current web application, valid FROM outside + self.BASE_URL = manager.get_attribute(stored.KEY_URL_WEB, self.BASE_LOCAL_URL) + + # URL for reading current available version information. + default = "http://www.thevirtualbrain.org/tvb/zwei/action/serialize-version?version=1&type=json" + self.URL_TVB_VERSION = manager.get_attribute(stored.KEY_URL_VERSION, default) + + self.TEMPLATE_ROOT = os.path.join(self.CURRENT_DIR, 'interfaces', 'web', 'templates', 'genshi') + self.CHERRYPY_CONFIGURATION = {'global': {'server.socket_host': '0.0.0.0', + 'server.socket_port': self.SERVER_PORT, + 'server.thread_pool': 20, + 'engine.autoreload_on': False, + 'server.max_request_body_size': 3221225472 # 3 GB + }, + '/': {'tools.encode.on': True, + 'tools.encode.encoding': 'utf-8', + 'tools.decode.on': True, + 'tools.gzip.on': True, + 'tools.gzip.mime_types': ['text/html', 'text/plain', + 'text/javascript', 'text/css', + 'application/x.ndarray'], + 'tools.sessions.on': True, + 'tools.sessions.storage_type': 'ram', + 'tools.sessions.timeout': 600, # 10 hours + 'response.timeout': 1000000, + 'tools.sessions.locking': 'explicit', + 'tools.upload.on': True, # Tool to check upload content size + 'tools.cleanup.on': True # Tool to clean up files on disk + }, + '/static': {'tools.staticdir.root': self.CURRENT_DIR, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': os.path.join('interfaces', 'web', 'static') + }, + '/statichelp': {'tools.staticdir.root': self.CURRENT_DIR, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': os.path.join('interfaces', 'web', + 'static', 'help') + }, + '/static_view': {'tools.staticdir.root': self.CURRENT_DIR, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': os.path.join('interfaces', 'web', + 'templates', 'genshi', + 'visualizers'), + }, + } + + + +class WebAdminSettings(object): + """ + Setting related to the default users of web-tvb + """ + + SYSTEM_USER_NAME = 'TVB system' + DEFAULT_ADMIN_EMAIL = 'jira.tvb@gmail.com' + ADMINISTRATOR_BLANK_PWD = 'pass' + + + def __init__(self, manager): + # Give name for the Admin user, first created. + self.ADMINISTRATOR_NAME = manager.get_attribute(stored.KEY_ADMIN_NAME, 'admin') + + # Admin's password used when creating first user (default is MD5 for 'pass') + self.ADMINISTRATOR_PASSWORD = manager.get_attribute(stored.KEY_ADMIN_PWD, '1a1dc91c907325c69271ddf0c944bc72') + + # Admin's email used when creating first user + self.ADMINISTRATOR_EMAIL = manager.get_attribute(stored.KEY_ADMIN_EMAIL, self.DEFAULT_ADMIN_EMAIL) + + + +class DBSettings(object): + + # Overwrite number of connections to the DB. + # Otherwise might reach PostgreSQL limit when launching multiple concurrent operations. + # MAX_CONNECTION default value will be used for WEB + # When launched on cluster, the MAX_ASYNC_CONNECTIONS overwrites MAX_ONNECTIONS value + MAX_CONNECTIONS = 20 + MAX_ASYNC_CONNECTIONS = 2 + + # Nested transactions are not supported by all databases and not really necessary in TVB so far so + # we don't support them yet. However when running tests we can use them to out advantage to rollback + # any database changes between tests. + ALLOW_NESTED_TRANSACTIONS = False + + def __init__(self, manager, default_storage, current_storage): + # A dictionary with accepted db's and their default URLS + default_pg = 'postgresql+psycopg2://postgres:root@127.0.0.1:5432/tvb?user=postgres&password=postgres' + default_lite = 'sqlite:///' + os.path.join(default_storage, 'tvb-database.db') + self.ACEEPTED_DBS = {'postgres': manager.get_attribute(stored.KEY_DB_URL, default_pg), + 'sqlite': manager.get_attribute(stored.KEY_DB_URL, default_lite)} + + # Currently selected database (must be a key in ACCEPTED_DBS) + self.SELECTED_DB = manager.get_attribute(stored.KEY_SELECTED_DB, 'sqlite') + + # Used DB url: IP,PORT. The DB needs to be created in advance. + default_lite = 'sqlite:///' + os.path.join(current_storage, "tvb-database.db") + self.DB_URL = manager.get_attribute(stored.KEY_DB_URL, default_lite) + + # Upgrade/Downgrade repository + self.DB_VERSIONING_REPO = os.path.join(current_storage, 'db_repo') diff --git a/dsl_cuda/example/tvb/basic/config/stored.py b/dsl_cuda/example/tvb/basic/config/stored.py new file mode 100644 index 0000000000..d8806be8f9 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/config/stored.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +Manages reading and writing settings in file + +.. moduleauthor:: Lia Domide +.. moduleauthor:: Bogdan Neacsa + +""" + +import os + +# File keys +KEY_ADMIN_NAME = 'ADMINISTRATOR_NAME' +KEY_ADMIN_PWD = 'ADMINISTRATOR_PASSWORD' +KEY_ADMIN_EMAIL = 'ADMINISTRATOR_EMAIL' +KEY_STORAGE = 'TVB_STORAGE' +KEY_MAX_DISK_SPACE_USR = 'USR_DISK_SPACE' +# During the introspection phase, it is checked if either Matlab or +# octave are installed and available trough the system PATH variable +# If so, they will be used for some analyzers +KEY_MATLAB_EXECUTABLE = 'MATLAB_EXECUTABLE' +KEY_IP = 'SERVER_IP' +KEY_PORT = 'WEB_SERVER_PORT' +KEY_URL_WEB = 'URL_WEB' +KEY_SELECTED_DB = 'SELECTED_DB' +KEY_DB_URL = 'URL_VALUE' +KEY_URL_VERSION = 'URL_TVB_VERSION' +KEY_CLUSTER = 'DEPLOY_CLUSTER' +KEY_CLUSTER_SCHEDULER = 'CLUSTER_SCHEDULER' +KEY_MAX_THREAD_NR = 'MAXIMUM_NR_OF_THREADS' +KEY_MAX_RANGE_NR = 'MAXIMUM_NR_OF_OPS_IN_RANGE' +KEY_MAX_NR_SURFACE_VERTEX = 'MAXIMUM_NR_OF_VERTICES_ON_SURFACE' +KEY_LAST_CHECKED_FILE_VERSION = 'LAST_CHECKED_FILE_VERSION' +KEY_LAST_CHECKED_CODE_VERSION = 'LAST_CHECKED_CODE_VERSION' +KEY_FILE_STORAGE_UPDATE_STATUS = 'FILE_STORAGE_UPDATE_STATUS' + + +class SettingsManager(object): + def __init__(self, config_file_location): + self.config_file_location = config_file_location + self.stored_settings = self._read_config_file() + + def _read_config_file(self): + """ + Get data from the configurations file in the form of a dictionary. + Return empty dictionary if file not present. + """ + if not os.path.exists(self.config_file_location): + return {} + + config_dict = {} + with open(self.config_file_location, 'r') as cfg_file: + data = cfg_file.read() + entries = [line for line in data.split('\n') if not line.startswith('#') and len(line.strip()) > 0] + for one_entry in entries: + name, value = one_entry.split('=', 1) + config_dict[name] = value + return config_dict + + def add_entries_to_config_file(self, input_data): + """ + Add to the dictionary of settings already existent in the settings file. + + :param input_data: A dictionary of pairs that need to be added to the config file. + """ + config_dict = self._read_config_file() + if config_dict is None: + config_dict = {} + + for entry in input_data: + config_dict[entry] = input_data[entry] + + with open(self.config_file_location, 'w') as file_writer: + for key in config_dict: + file_writer.write(key + '=' + str(config_dict[key]) + '\n') + + self.stored_settings = self._read_config_file() + + def write_config_data(self, config_dict): + """ + Overwrite anything already existent in the config file + """ + with open(self.config_file_location, 'w') as file_writer: + for key in config_dict: + file_writer.write(key + '=' + str(config_dict[key]) + '\n') + + self.stored_settings = self._read_config_file() + + def get_attribute(self, key, default=None, dtype=str): + """ + Get a cfg attribute that could also be found in the settings file. + """ + try: + if key in self.stored_settings: + return dtype(self.stored_settings[key]) + except ValueError: + # Invalid convert operation. + return default + return default + + def is_first_run(self): + return self.stored_settings is None or len(self.stored_settings) <= 2 diff --git a/dsl_cuda/example/tvb/basic/config/tvb.version b/dsl_cuda/example/tvb/basic/config/tvb.version new file mode 100644 index 0000000000..ab116fdfd6 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/config/tvb.version @@ -0,0 +1 @@ +Revision: 8898 \ No newline at end of file diff --git a/dsl_cuda/example/tvb/basic/config/utils.py b/dsl_cuda/example/tvb/basic/config/utils.py new file mode 100644 index 0000000000..af449980b5 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/config/utils.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +Helper tools, for the configuration area. + +.. moduleauthor:: Bogdan Neacsa +.. moduleauthor:: marmaduke + +""" + + +class EnhancedDictionary(dict): + """ + A dictionary like class that provides easy access to configuration values. + """ + + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + self[key] = value + diff --git a/dsl_cuda/example/tvb/basic/exceptions.py b/dsl_cuda/example/tvb/basic/exceptions.py new file mode 100644 index 0000000000..55bad2fd13 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/exceptions.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# +""" +.. moduleauthor:: Calin Pavel +""" + + +class TVBException(Exception): + """ + Base class for all TVB exceptions. + """ + + def __init__(self, message, parent_exception=None): + Exception.__init__(self, message, parent_exception) + self.message = str(message) + + def __str__(self): + return self.message + + +class ValidationException(TVBException): + """ + Exception class for problems that occurs during MappedType + validation before storing it into DB. + """ diff --git a/dsl_cuda/example/tvb/basic/logger/__init__.py b/dsl_cuda/example/tvb/basic/logger/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dsl_cuda/example/tvb/basic/logger/builder.py b/dsl_cuda/example/tvb/basic/logger/builder.py new file mode 100644 index 0000000000..71b17e431f --- /dev/null +++ b/dsl_cuda/example/tvb/basic/logger/builder.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +Singleton logging builder. + + +.. moduleauthor:: Calin Pavel +.. moduleauthor:: Bogdan Neacsa +.. moduleauthor:: Lia Domide +.. moduleauthor:: Stuart A. Knock +.. moduleauthor:: Marmaduke Woodman + +""" + +import os +import weakref +import logging +import logging.config +from tvb.basic.profile import TvbProfile +from tvb.basic.config.profile_settings import MATLABLibraryProfile + + +class LoggerBuilder(object): + """ + Class taking care of uniform Python logger initialization. + It uses the Python native logging package. + It's purpose is just to offer a common mechanism for initializing all modules in a package. + """ + + def __init__(self, config_root): + """ + Prepare Python logger based on a configuration file. + :param: config_root - current package to configure logger for it. + + """ + if not isinstance(TvbProfile.current, MATLABLibraryProfile): + config_file_name = TvbProfile.current.LOGGER_CONFIG_FILE_NAME + package = __import__(config_root, globals(), locals(), ['__init__'], 0) + package_path = package.__path__[0] + # Specify logging configuration file for current package. + logging.config.fileConfig(os.path.join(package_path, config_file_name), disable_existing_loggers=False) + else: + logging.basicConfig(level=logging.DEBUG) + self._loggers = weakref.WeakValueDictionary() + + def build_logger(self, parent_module): + """ + Build a logger instance and return it + """ + self._loggers[parent_module] = logger = logging.getLogger(parent_module) + return logger + + def set_loggers_level(self, level): + for logger in self._loggers.values(): + logger.setLevel(level) + + +# We make sure a single instance of logger-builder is created. +if "GLOBAL_LOGGER_BUILDER" not in globals(): + + if TvbProfile.is_library_mode(): + GLOBAL_LOGGER_BUILDER = LoggerBuilder('tvb.basic.logger') + else: + GLOBAL_LOGGER_BUILDER = LoggerBuilder('tvb.config.logger') + + +def get_logger(parent_module=''): + """ + Function to retrieve a new Python logger instance for current module. + + :param parent_module: module name for which to create logger. + """ + return GLOBAL_LOGGER_BUILDER.build_logger(parent_module) diff --git a/dsl_cuda/example/tvb/basic/logger/library_logger.conf b/dsl_cuda/example/tvb/basic/logger/library_logger.conf new file mode 100644 index 0000000000..16ad501154 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/logger/library_logger.conf @@ -0,0 +1,78 @@ +############################################ +## TVB - logging configuration. ## +############################################ +[loggers] +keys=root, tvb, tvb_basic_datatypes, tvb_basic_config, tvb_simulator, tvb_simulator_monitors + +[handlers] +keys=consoleHandler,fileHandler + +[formatters] +keys=simpleFormatter + +[logger_root] +## was: level=DEBUG, mv +level=INFO +handlers=consoleHandler, fileHandler +propagate=0 + +############################################ +## tvb specific logging ## +############################################ +[logger_tvb] +level=INFO +handlers=consoleHandler, fileHandler +qualname=tvb +propagate=0 + +[logger_tvb_basic_datatypes] +## was: level=WARNING mv +level=INFO +handlers=consoleHandler, fileHandler +qualname=tvb.datatypes +propagate=0 + +[logger_tvb_basic_config] +## was: level=WARNING mv +level=INFO +handlers=consoleHandler, fileHandler +qualname=tvb.basic.config +propagate=0 + +[logger_tvb_simulator] +## was: level=WARNING mv +level=INFO +handlers=consoleHandler, fileHandler +qualname=tvb.simulator +propagate=0 + +[logger_tvb_simulator_monitors] +level=INFO +handlers=consoleHandler, fileHandler +qualname=tvb.simulator.monitors +propagate=0 + +############################################ +## Handlers ## +############################################ + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=simpleFormatter +args=(sys.stderr,) + +[handler_fileHandler] +class=tvb.basic.logger.simple_handler.SimpleTimedRotatingFileHandler +level=INFO +formatter=simpleFormatter +# Generate a new file every midnight and keep logs for 30 days +args=('library.log', 'midnight', 1, 30) + +############################################ +## Formatters ## +############################################ + +[formatter_simpleFormatter] +format=%(asctime)s - %(levelname)s - %(name)s - %(message)s +datefmt= diff --git a/dsl_cuda/example/tvb/basic/logger/library_logger_test.conf b/dsl_cuda/example/tvb/basic/logger/library_logger_test.conf new file mode 100644 index 0000000000..7d6387b100 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/logger/library_logger_test.conf @@ -0,0 +1,68 @@ +############################################ +## TVB - logging configuration. ## +############################################ +[loggers] +keys=root, tvb, tvb_basic_datatypes, tvb_basic_config, tvb_simulator + +[handlers] +keys=consoleHandler,fileHandler + +[formatters] +keys=simpleFormatter + +[logger_root] +level=DEBUG +handlers=fileHandler +propagate=0 + +############################################ +## tvb specific logging ## +############################################ +[logger_tvb] +level=INFO +handlers=fileHandler +qualname=tvb +propagate=0 + +[logger_tvb_basic_datatypes] +level=WARNING +handlers=fileHandler +qualname=tvb.datatypes +propagate=0 + +[logger_tvb_basic_config] +level=WARNING +handlers=fileHandler +qualname=tvb.basic.config +propagate=0 + +[logger_tvb_simulator] +level=WARNING +handlers=fileHandler +qualname=tvb.simulator +propagate=0 + +############################################ +## Handlers ## +############################################ + +[handler_consoleHandler] +class=StreamHandler +level=ERROR +formatter=simpleFormatter +args=(sys.stdout,) + +[handler_fileHandler] +class=tvb.basic.logger.simple_handler.SimpleTimedRotatingFileHandler +level=INFO +formatter=simpleFormatter +# Generate a new file every midnight and keep logs for 30 days +args=('library.log', 'midnight', 1, 30) + +############################################ +## Formatters ## +############################################ + +[formatter_simpleFormatter] +format=%(asctime)s - %(levelname)s - %(name)s - %(message)s +datefmt= diff --git a/dsl_cuda/example/tvb/basic/logger/simple_handler.py b/dsl_cuda/example/tvb/basic/logger/simple_handler.py new file mode 100644 index 0000000000..f7630a7f15 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/logger/simple_handler.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# +""" +This module contains a simple file handlers used to log messages +for different parts of application. + +.. moduleauthor:: Calin Pavel +""" + +import os +from logging.handlers import TimedRotatingFileHandler +from tvb.basic.profile import TvbProfile + + +class SimpleTimedRotatingFileHandler(TimedRotatingFileHandler): + """ + This is a custom rotating file handler which computes the full path for log file + depending on the TVB configuration. + """ + + def __init__(self, filename, when='h', interval=1, backupCount=0): + """ + Only set our logging path, and call superclass. + """ + log_file = os.path.join(TvbProfile.current.TVB_LOG_FOLDER, filename) + TimedRotatingFileHandler.__init__(self, log_file, when, interval, backupCount) diff --git a/dsl_cuda/example/tvb/basic/neotraits/__init__.py b/dsl_cuda/example/tvb/basic/neotraits/__init__.py new file mode 100644 index 0000000000..c7c8d24352 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/neotraits/__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +Neotraits is a framework that lets you declare class attributes with type checking and introspection abilities. +The public api is in the neotraits.api module +""" + +# note: the api is in it's own module. +# This is a bit less convenient. +# But it prevents the annoying situation where +# importing neotraits.some_module will run __init__ +# and that one will import most modules in order to provide the api. +# That is not ideal because circular imports become likely and over-importing can slow startup. diff --git a/dsl_cuda/example/tvb/basic/neotraits/_attr.py b/dsl_cuda/example/tvb/basic/neotraits/_attr.py new file mode 100644 index 0000000000..4399b4dde2 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/neotraits/_attr.py @@ -0,0 +1,618 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +This private module implements concrete declarative attributes +""" +import types +import collections +import numpy +import logging +from ._declarative_base import _Attr +from .ex import TraitValueError, TraitTypeError, TraitAttributeError +import sys + +if sys.version_info[0] == 3: + import typing + if typing.TYPE_CHECKING: + from ._core import HasTraits + from tvb.basic.neotraits._declarative_base import MetaType + +# a logger for the whole traits system +log = logging.getLogger('tvb.traits') + + +class Attr(_Attr): + """ + An Attr declares the following about the attribute it describes: + * the type + * a default value shared by all instances + * if the value might be missing + * documentation + It will resolve to attributes on the instance. + """ + + # This class is a python data descriptor. + # For an introduction see https://docs.python.org/2/howto/descriptor.html + + def __init__( + self, field_type, default=None, doc='', label='', required=True, final=False, choices=None + ): + # type: (type, typing.Any, str, str, bool, bool, typing.Optional[tuple]) -> None + """ + :param field_type: the python type of this attribute + :param default: A shared default value. Behaves like class level attribute assignment. + Take care with mutable defaults. + :param doc: Documentation for this field. + :param label: A short description. + :param required: required fields should not be None. + :param final: Final fields can only be assigned once. + :param choices: A tuple of the values that this field is allowed to take. + """ + super(Attr, self).__init__() + self.field_type = field_type + self.default = default + self.doc = doc + self.label = label + self.required = bool(required) + self.final = bool(final) + self.choices = choices + + + def __validate(self, value): + """ check field_type and choices """ + if not isinstance(value, self.field_type): + raise TraitTypeError("Attribute can't be set to an instance of {}".format(type(value)), attr=self) + if self.choices is not None: + if value not in self.choices: + raise TraitValueError("Value {!r} must be one of {}".format(value, self.choices), attr=self) + + # subclass api + + def _post_bind_validate(self): + # type: () -> None + """ + Validates this instance of Attr. + This is called just after field_name is set, by MetaType. + We do checks here and not in init in order to give better error messages. + Attr should be considered initialized only after this has run + """ + if not isinstance(self.field_type, type): + msg = 'Field_type must be a type not {!r}. Did you mean to declare a default?'.format( + self.field_type + ) + raise TraitTypeError(msg, attr=self) + + skip_default_checks = self.default is None or isinstance(self.default, types.FunctionType) + + if not skip_default_checks: + self.__validate(self.default) + + # heuristic check for mutability. might be costly. hasattr(__hash__) is fastest but less reliable + try: + hash(self.default) + except TypeError: + log.warning('Field seems mutable and has a default value. ' + 'Consider using a lambda as a value factory \n attribute {}'.format(self)) + # we do not check here if we have a value for a required field + # it is too early for that, owner.__init__ has not run yet + + + def _validate_set(self, instance, value): + # type: ('HasTraits', typing.Any) -> typing.Any + """ + Called before updating the value of an attribute. + It checks the type *AND* returns the valid value. + You can override this for further checks. Call super to retain this check. + Raise if checks fail. + You should return a cleaned up value if validation passes + """ + if value is None: + if self.required: + raise TraitValueError("Attribute is required. Can't set to None", attr=self) + else: + return value + + self.__validate(value) + return value + + + # descriptor protocol + + def __get__(self, instance, owner): + # type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Any + self._assert_have_field_name() + if instance is None: + # called from class, not an instance + return self + # data is stored on the instance in a field with the same name + # If field is not on the instance yet, return the class level default + # (this attr instance is a class field, so the default is for the class) + # This is consistent with how class fields work before they are assigned and become instance bound + if self.field_name not in instance.__dict__: + if isinstance(self.default, types.FunctionType): + default = self.default() + else: + default = self.default + + # Unless we store the default on the instance, this will keep returning self.default() + # when the default is a function. So if the default is mutable, any changes to it are + # lost as a new one is created every time. + instance.__dict__[self.field_name] = default + + value = instance.__dict__[self.field_name] + if self.required and value is None: + raise TraitAttributeError('required attribute referenced before assignment. ' + 'Use a default or assign a value before reading it', attr=self) + return value + + + def __set__(self, instance, value): + # type: ('HasTraits', typing.Any) -> None + self._assert_have_field_name() + + if self.final: + # non-set to set final transition happens when instance stored value becomes not none + # getattr will call __get__. We want that in order to allow the default to be set. + # If __set__ is called before a __get__ then no defaults have been assigned. + # subtlety: if the value of this final field is not set then __get__ will raise + # getattr with a default value swallows that exception and returns false + present_value = getattr(instance, self.field_name, None) + if present_value is not None: + raise TraitAttributeError("can't write final attribute") + + value = self._validate_set(instance, value) + + instance.__dict__[self.field_name] = value + + + def _defined_on_str_helper(self): + if self.owner is not None: + return '{}.{}.{} = {}'.format( + self.owner.__module__, + self.owner.__name__, + self.field_name, + type(self).__name__ + ) + else: + return '{}'.format(type(self).__name__) + + + def __str__(self): + return '{}(field_type={}, default={!r}, required={})'.format( + self._defined_on_str_helper(), self.field_type, self.default, self.required + ) + + + +class Final(Attr): + """ + An attribute that can only be set once. + If a default is provided it counts as a set, so it cannot be written to. + Note that if the default is a mutable type, the value is shared with all instances + of the owning class. + We cannot enforce true constancy in python + """ + + def __init__(self, default=None, field_type=None, doc='', label=''): + """ + :param default: The constant value + """ + # it would be nice if we could turn the default immutable. But this is unreasonable work in python + # maybe a deep copy? + if default is not None: + field_type = type(default) + + if default is None and field_type is None: + raise ValueError('Either a default or a field_type is required') + + super(Final, self).__init__( + field_type=field_type, default=default, doc=doc, label=label, required=True, final=True + ) + + +class List(Attr): + """ + The attribute is a list of values. + Choices and type are reinterpreted as applying not to the list but to the elements of it + """ + + def __init__(self, of=object, default=(), doc='', label='', final=False, choices=None): + # type: (type, tuple, str, str, bool, typing.Optional[tuple]) -> None + super(List, self).__init__( + field_type=collections.Sequence, + default=default, + doc=doc, + label=label, + required=True, + final=final, + choices=None, + ) + self.element_type = of + self.element_choices = choices + + + def __validate_elements(self, value): + """ check that all elements are of the declared type and one of the declared choices """ + for i, el in enumerate(value): + if not isinstance(el, self.element_type): + raise TraitTypeError("value[{}] can't be of type {}".format(i, type(el)), attr=self) + + if self.element_choices is not None: + for i, el in enumerate(value): + if el not in self.element_choices: + raise TraitValueError( + "value[{}]=={!r} must be one of {}".format(i, el, self.element_choices), + attr=self + ) + + + def _post_bind_validate(self): + super(List, self)._post_bind_validate() + # check that the default contains elements of the declared type + self.__validate_elements(self.default) + + + def _validate_set(self, instance, value): + value = super(List, self)._validate_set(instance, value) + if value is None: + # value is optional and missing, nothing to do here + return + self.__validate_elements(value) + return value + + + # __get__ __set__ here only for typing purposes, for better ide checking and autocomplete + + + def __get__(self, instance, owner): + # type: (typing.Optional[HasTraits], MetaType) -> typing.Sequence + return super(List, self).__get__(instance, owner) + + + def __set__(self, instance, value): + # type: (HasTraits, typing.Sequence) -> None + super(List, self).__set__(instance, value) + + def __str__(self): + return '{}(of={}, default={!r}, required={})'.format( + self._defined_on_str_helper(), self.element_type, self.default, self.required + ) + + +class _Number(Attr): + + def __validate(self, value): + """ value should be safely cast to field type and choices must be enforced """ + if not isinstance(value, (int, float, complex, numpy.number)): + # we have to check that the value is numeric before the can_cast check + # as can_cast works with dtype strings as well + # can_cast('i8', 'i32') + raise TraitTypeError("can't be set to {!r}. Need a number.".format(value), attr=self) + if not numpy.can_cast(value, self.field_type, 'safe'): + raise TraitTypeError("can't be set to {!r}. No safe cast.".format(value), attr=self) + if self.choices is not None: + if value not in self.choices: + raise TraitValueError("value {!r} must be one of {}".format(value, self.choices), attr=self) + + def _post_bind_validate(self): + if self.default is not None: + self.__validate(self.default) + + + def _validate_set(self, instance, value): + if value is None: + if self.required: + raise TraitValueError("is required. Can't set to None", attr=self) + else: + return value + + self.__validate(value) + return self.field_type(value) + + + +class Int(_Number): + """ + Declares an integer + This is different from Attr(field_type=int). + The former enforces int subtypes + This allows all integer types, including numpy ones that can be safely cast to the declared type + according to numpy rules + """ + + def __init__( + self, field_type=int, default=0, doc='', label='', required=True, final=False, choices=None + ): + super(_Number, self).__init__( + field_type=field_type, + default=default, + doc=doc, + label=label, + required=required, + final=final, + choices=choices, + ) + + def _post_bind_validate(self): + if not issubclass(self.field_type, (int, numpy.integer)): + msg = 'field_type must be a python int or a numpy.integer not {!r}.'.format(self.field_type) + raise TraitTypeError(msg, attr=self) + # super call after the field_type check above + super(Int, self)._post_bind_validate() + + + +class Float(_Number): + """ + Declares a float. + This is different from Attr(field_type=float). + The former enforces float subtypes. + This allows any type that can be safely cast to the declared float type + according to numpy rules. + + Reading and writing this attribute is slower than a plain python attribute. + In performance sensitive code you might want to use plain python attributes + or even better local variables. + """ + + def __init__( + self, field_type=float, default=0, doc='', label='', required=True, final=False, choices=None + ): + super(_Number, self).__init__( + field_type=field_type, + default=default, + doc=doc, + label=label, + required=required, + final=final, + choices=choices, + ) + + def _post_bind_validate(self): + if not issubclass(self.field_type, (float, numpy.floating)): + msg = 'field_type must be a python float or a numpy.floating not {!r}.'.format(self.field_type) + raise TraitTypeError(msg, attr=self) + # super call after the field_type check above + super(Float, self)._post_bind_validate() + + + +class Dim(Final): + """ + A symbol that defines a dimension in a numpy array shape. + It can only be set once. It is an int. + Dimensions have to be set before any NArrays that reference them are used. + """ + + any = object() # sentinel + + def __init__(self, doc=''): + super(Dim, self).__init__(field_type=int, doc=doc) + + + +class NArray(Attr): + """ + Declares a numpy array. + dtype enforces the dtype. The default dtype is float32. + An optional symbolic shape can be given, as a tuple of Dim attributes from the owning class. + The shape will be enforced, but no broadcasting will be done. + domain declares what values are allowed in this array. + It can be any object that can be checked for membership + Defaults are checked if they are in the declared domain. + For performance reasons this does not happen on every attribute set. + """ + + def __init__( + self, + default=None, + required=True, + doc='', + label='', + dtype=numpy.float, + shape=None, + dim_names=(), + domain=None, + ): + # type: (numpy.ndarray, bool, str, str, typing.Union[numpy.dtype, type, str], typing.Optional[typing.Tuple[Dim, ...]], typing.Tuple[str, ...], typing.Container) -> None + """ + :param dtype: The numpy datatype. Defaults to float64. This is checked by neotraits. + :param shape: An optional symbolic shape, tuple of Dim's declared on the owning class + :param dim_names: Optional names for the names of the dimensions + :param domain: Any type that can be checked for membership like xrange. + Represents the expected domain of the values in the array. + """ + + self.dtype = numpy.dtype(dtype) + + super(NArray, self).__init__( + field_type=numpy.ndarray, default=default, required=required, doc=doc, label=label + ) + self.shape = shape + self.domain = domain # anything that supports 3.1 in domain + self.dim_names = dim_names + + if self.shape is not None: # we have a shape + if self.dim_names: # and dim_names + # ensure that len(shape) == len(dim_names) + if len(self.shape) != len(self.dim_names): + raise TraitValueError('shape contradicts dim_names', attr=self) + + # maybe a over zealous type check + for d in self.shape: + if d is not Dim.any and type(d) != Dim: + raise TraitValueError("shape elements must be Dim's not {}".format(type(d)), attr=self) + self.ndim = len(self.shape) + elif self.dim_names: # no shape but dim_names + self.ndim = len(self.dim_names) + else: + self.ndim = None + + def __validate(self, value): + """ check that ndim's and dtypes match""" + if self.ndim is not None and value.ndim != self.ndim: + raise TraitValueError("can't be set to an array with ndim {}".format(value.ndim), attr=self) + + if not numpy.can_cast(value.dtype, self.dtype, 'safe'): + raise TraitTypeError("can't be set to an array of dtype {}".format(value.dtype), attr=self) + + + def _post_bind_validate(self): + if self.default is None: + return + if not isinstance(self.default, numpy.ndarray): + msg = 'default {} should be a numpy.ndarray'.format(self.default) + raise TraitTypeError(msg, attr=self) + + self.__validate(self.default) + + # we make the default a read only array + self.default.setflags(write=False) + + # check that the default array values are in the declared domain + # this may be expensive + if self.domain is not None and self.default is not None: + for e in self.default.flat: + if e not in self.domain: + msg = 'default contains values out of the declared domain. Ex {}'.format(e) + # log.warning('{} \n attribute {}'.format(msg, self)) broken by mv + + break + + + def _lookup_expected_shape(self, instance): + """ look up expected shape on the instance """ + expected_shape = [] + + for dim_attr in self.shape: + if dim_attr is Dim.any: + expected_dim = Dim.any + else: + try: + # invoke Dim's __get__(instance) + expected_dim = getattr(instance, dim_attr.field_name) + except TraitAttributeError: + # re-raise with a better error message + msg = "Narray's shape references undefined dimension <{}>. " \ + "Set it before accessing this array" + raise TraitAttributeError(msg.format(dim_attr.field_name), attr=self) + expected_shape.append(expected_dim) + return expected_shape + + + def _validate_set(self, instance, value): + value = super(NArray, self)._validate_set(instance, value) + if value is None: + # value is optional and missing, nothing to do here + return + self.__validate(value) + # we should know here the concrete shape + # check it + + if self.shape is not None: + expected_shape = self._lookup_expected_shape(instance) + + for expected_dim, value_dim in zip(expected_shape, value.shape): + if expected_dim is Dim.any: + continue + if value_dim != expected_dim: + raise TraitValueError( + 'Shape mismatch. Expected {}. Given {}. Not broadcasting'.format( + expected_shape, value.shape + ) + ) + + return value.astype(self.dtype) + + + # here only for typing purposes, so ide's can get better suggestions + def __get__(self, instance, owner): + # type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Union[numpy.ndarray, 'NArray'] + return super(NArray, self).__get__(instance, owner) + + def __set__(self, instance, value): + # type: (HasTraits, numpy.ndarray) -> None + super(NArray, self).__set__(instance, value) + + def __str__(self): + return '{}(label={!r}, dtype={}, default={!r}, dim_names={}, ndim={}, required={})'.format( + self._defined_on_str_helper(), + self.label, + self.dtype, + self.default, + self.dim_names, + self.ndim, + self.required, + ) + + +class Range(object): + """ + Defines a domain like the one that numpy.arange generates + Points are precisely equidistant but the largest point is <= hi + """ + + def __init__(self, lo, hi, step=1.0): + self.lo = lo + self.hi = hi + self.step = step + + def __contains__(self, item): + """ true if item between lo and high. ignores the step""" + return self.lo <= item < self.hi + + def to_array(self): + return numpy.arange(self.lo, self.hi, self.step) + + def __repr__(self): + return 'Range(lo={}, hi={}, step={})'.format(self.lo, self.hi, self.step) + + +class LinspaceRange(object): + """ + Defines a domain with precise endpoints but the points are not precisely equidistant + Similar to numpy.linspace + """ + + def __init__(self, lo, hi, npoints=50): + self.lo = lo + self.hi = hi + self.npoints = npoints + + def __contains__(self, item): + """ true if item between lo and high. ignores the step""" + return self.lo <= item < self.hi + + def to_array(self): + return numpy.linspace(self.lo, self.hi, self.npoints) + + def __repr__(self): + return 'LinspaceRange(lo={}, hi={}, step={})'.format(self.lo, self.hi, self.npoints) diff --git a/dsl_cuda/example/tvb/basic/neotraits/_core.py b/dsl_cuda/example/tvb/basic/neotraits/_core.py new file mode 100644 index 0000000000..7da40f3a8a --- /dev/null +++ b/dsl_cuda/example/tvb/basic/neotraits/_core.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +This module implements neotraits. +It is private only to shield public usage of the imports and logger. +""" +import sys +import uuid +import numpy +from six import add_metaclass +from ._attr import Attr +from ._declarative_base import _Property, MetaType +from .info import trait_object_str, trait_object_repr_html, narray_summary_info +from .ex import TraitAttributeError, TraitTypeError, TraitError +from tvb.basic.logger.builder import get_logger + +if sys.version_info[0] == 3: + import typing + + + +class CachedTraitProperty(_Property): + # This is a *non-data* descriptor + # Once a field with the same name exists on the instance it will + # take precedence before this non-data descriptor + # This means that after the first __get__ which sets a same-name instance attribute + # this will not be called again. Thus this is a cache. + # To refresh the cache one could delete the instance attr. + + def __get__(self, instance, owner): + # type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Any + if instance is None: + return self + ret = self.fget(instance) + # mhtodo the error messages generated by this will be confusing + # noinspection PyProtectedMember + ret = self.attr._validate_set(instance, ret) + # set the instance same-named attribute which becomes the cache + setattr(instance, self.attr.field_name, ret) + return ret + + + +class TraitProperty(_Property): + + def __get__(self, instance, owner): + # type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Any + if instance is None: + return self + ret = self.fget(instance) + # mhtodo the error messages generated by this will be confusing + # noinspection PyProtectedMember + ret = self.attr._validate_set(instance, ret) + return ret + + def setter(self, fset): + # return a copy of self that has fset. It will overwrite the current one in the + # owning class as the attributes have the same name and the setter comes after the getter + return type(self)(self.fget, self.attr, fset) + + def __set__(self, instance, value): + # type: ('HasTraits', typing.Any) -> None + if self.fset is None: + raise TraitAttributeError("Can't set attribute. Property is read only. In " + str(self)) + # mhtodo the error messages generated by this will be confusing + # noinspection PyProtectedMember + value = self.attr._validate_set(instance, value) + self.fset(instance, value) + + def __delete__(self, instance): + raise TraitAttributeError("can't delete a traitproperty") + + def __str__(self): + return 'TraitProperty(attr={}, fget={}'.format(self.attr, self.fget) + + + +def trait_property(attr): + # type: (Attr) -> typing.Callable[[typing.Callable], TraitProperty] + """ + A read only property that has a declarative attribute associated with. + :param attr: the declarative attribute that describes this property + """ + if not isinstance(attr, Attr): + raise TypeError('@trait_property(attr) attribute argument required.') + + def deco(func): + return TraitProperty(func, attr) + return deco + + +def cached_trait_property(attr): + # type: (Attr) -> typing.Callable[[typing.Callable], CachedTraitProperty] + """ + A lazy evaluated attribute. + Transforms the decorated method into a cached property. + The method will be called once to compute a value. + The value will be stored in an instance attribute with + the same name as the decorated function. + :param attr: the declarative attribute that describes this property + """ + if not isinstance(attr, Attr): + raise TypeError('@cached_trait_property(attr) attribute argument required.') + + def deco(func): + return CachedTraitProperty(func, attr) + return deco + + +@add_metaclass(MetaType) +class HasTraits(object): + + # The base __init__ and __str__ rely upon metadata gathered by MetaType + # we could have injected these in MetaType, but we don't need meta powers + # this is simpler to grok + + gid = Attr(field_type=uuid.UUID) + + def __init__(self, **kwargs): + """ + The default init accepts kwargs for all declarative attrs + and sets them to the given values + """ + # cls just to emphasise that the metadata is on the class not on instances + cls = type(self) + + # defined before the kwargs loop, so that a title or gid Attr can overwrite this defaults + + self.gid = uuid.uuid4() + """ + gid identifies a specific instance of the hastraits + it is used by serializers as an identifier. + For non-datatype HasTraits this is less usefull but still + provides a unique id for example for a model configuration + """ # these strings are interpreted as docstrings by many tools, not by python though + + self.title = '{} gid: {}'.format(self.__class__.__name__, self.gid) + """ a generic name that the user can set to easily recognize the instance """ + + for k, v in kwargs.items(): + if k not in cls.declarative_attrs: + raise TraitTypeError( + 'Valid kwargs for type {!r} are: {}. You have given: {!r}'.format( + cls, repr(cls.declarative_attrs), k + ) + ) + setattr(self, k, v) + + self.tags = {} + """ + a generic collections of tags. The trait system is not using them + nor should any other code. They should not alter behaviour + They should describe the instance for the user + """ + + self.log = get_logger(self.__class__.__module__) + + + def __str__(self): + return trait_object_str(self) + + + def _repr_html_(self): + return trait_object_repr_html(self) + + def tag(self, tag_name, tag_value=None): + # type: (str, str) -> None + """ + Add a tag to this trait instance. + The tags are for user to recognize and categorize the instances + They should never influence the behaviour of the program + :param tag_name: an arbitrary tag + :param tag_value: an optional tag value + """ + self.tags[str(tag_name)] = str(tag_value) + + + def validate(self): + """ + Check that the internal invariants of this class are satisfied. + Not meant to ensure that that is the case. + Use configure for that. + The default configure calls this before it returns. + It complains about missing required attrs + Can be overridden in subclasses + """ + cls = type(self) + + for k in cls.declarative_attrs: + # read all declarative attributes. This will trigger errors if they are + # in an invalid state, like beeing required but not set + getattr(self, k) + + + def configure(self, *args, **kwargs): + """ + Ensures that invariant of the class are satisfied. + Override to compute uninitialized state of the class. + """ + self.validate() + + + def summary_info(self): + # type: () -> typing.Dict[str, str] + """ + A more structured __str__ + A 2 column table represented as a dict of str->str + The default __str__ and html representations of this object are derived from + this table. + Override this method and return such a table filled with instance information + that informs the user about your instance + """ + cls = type(self) + ret = {'Type': cls.__name__} + if self.title: + ret['title'] = str(self.title) + + for aname in cls.declarative_attrs: + try: + attr_field = getattr(self, aname) + if isinstance(attr_field, numpy.ndarray): + ret.update(narray_summary_info(attr_field, ar_name=aname)) + elif isinstance(attr_field, HasTraits): + ret[aname] = attr_field.title + else: + ret[aname] = repr(attr_field) + except TraitError: + ret[aname] = 'unavailable' + return ret diff --git a/dsl_cuda/example/tvb/basic/neotraits/_declarative_base.py b/dsl_cuda/example/tvb/basic/neotraits/_declarative_base.py new file mode 100644 index 0000000000..d9037bf70b --- /dev/null +++ b/dsl_cuda/example/tvb/basic/neotraits/_declarative_base.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +This private module implements the neotraits declarative machinery. +The basic Attribute Property and their automatic discovery by the Metaclass. +""" +import abc +import inspect +import logging + +from tvb.basic.neotraits.ex import TraitTypeError, TraitAttributeError +from tvb.basic.neotraits.info import auto_docstring + +import sys + +if sys.version_info[0] == 3: + import typing + +# a logger for the whole traits system +log = logging.getLogger('tvb.traits') + + +class _Attr(object): + """ + A private base class of Attributes + Contains the minimum expected functionality by the declarative system. + """ + def __init__(self): + self.field_name = None # type: typing.Optional[str] # to be set by metaclass + self.owner = None # type: typing.Optional[MetaType] # to be set by metaclass + + def _assert_have_field_name(self): + """ check that the fields we expect the be set by metaclass have been set """ + if self.field_name is None: + # this is the case if the descriptor is not in a class of type MetaType + raise AttributeError("Declarative attributes can only be declared in subclasses of HasTraits") + + # subclass api + + def _post_bind_validate(self): + # type: () -> None + """ + Validates this instance of Attr. + This is called just after field_name is set, by MetaType. + We do checks here and not in init in order to give better error messages. + Attr should be considered initialized only after this has run + """ + + # descriptor protocol + + def __get__(self, instance, owner): + # type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Any + self._assert_have_field_name() + if instance is None: + # called from class, not an instance + return self + # data is stored on the instance in a field with the same name + return instance.__dict__[self.field_name] + + + def __set__(self, instance, value): + # type: ('HasTraits', typing.Any) -> None + self._assert_have_field_name() + instance.__dict__[self.field_name] = value + + + def __delete__(self, instance): + raise TraitAttributeError("can't be deleted", attr=self) + + # A modest attempt of making Attr immutable + + def __setattr__(self, key, value): + """ After owner is set disallow any field assignment """ + if getattr(self, 'owner', None) is not None: + raise TraitAttributeError( + "Can't change an Attr after it has been bound to a class." + "Reusing Attr instances for different fields is not supported." + ) + super(_Attr, self).__setattr__(key, value) + + + def __delattr__(self, item): + raise TraitAttributeError("Deleting an Attr field is not supported.") + + + +class _Property(object): + def __init__(self, fget, attr, fset=None): + # type: (typing.Callable, _Attr, typing.Optional[typing.Callable]) -> None + self.fget = fget + self.fset = fset + self.__doc__ = fget.__doc__ + self.attr = attr + + + +class MetaType(abc.ABCMeta): + """ + Metaclass for the declarative traits. + We inherit ABCMeta so that the users may use @abstractmethod without having to + deal with 2 meta-classes. + Even though we do this we don't support the dynamic registration of subtypes to these abc's + """ + + # This is a python metaclass. + # For an introduction see https://docs.python.org/2/reference/datamodel.html + + # here to avoid some hasattr; is None etc checks. And to make pycharm happy + # should be harmless and shadowed by _declarative_attrs on the returned classes + _own_declarative_attrs = () # type: typing.Tuple[str, ...] # name of all declarative fields on this class + _own_declarative_props = () # type: typing.Tuple[str, ...] + + # A record of all the classes we have created. + # note: As this holds references and not weakrefs it will prevent class garbage collection. + # Deleting classes would break get_known_subclasses and this cache + __classes = {} # type: typing.Dict[str, type] + + + def get_known_subclasses(cls, include_abstract=False, include_itself=False): + # type: (bool) -> typing.Dict[str, typing.Type[MetaType]] + """ + Returns all subclasses that exist *now*. + New subclasses can be created after this call, + after importing a new module or dynamically creating subclasses. + Use with care. Use after most relevant modules have been imported. + """ + ret = {} + + for k, c in cls.__classes.items(): + if issubclass(c, cls): + if inspect.isabstract(c) and not include_abstract: + continue + if c == cls and not include_itself: + continue + ret.update({k: c}) + return ret + + def __walk_mro_inherit_declarations(cls, declaration): + ret = [] + for super_cls in cls.mro(): + if isinstance(super_cls, MetaType): + for attr_name in getattr(super_cls, declaration): + if attr_name not in ret: # attr was overridden, don't duplicate + ret.append(attr_name) + return tuple(ret) + + @property + def declarative_attrs(cls): + # type: () -> typing.Tuple[str, ...] + """ + Gathers all the declared attributes, including the ones declared in superclasses. + This is a meta-property common to all classes with this metatype + """ + # We walk the mro here. This is in contrast with _own_declarative_attrs which is + # not computed but cached by the metaclass on the class. + # Caching is faster, but comes with the cost of taking care of the caches validity + return cls.__walk_mro_inherit_declarations('_own_declarative_attrs') + + @property + def declarative_props(cls): + # type: () -> typing.Tuple[str, ...] + """ + Gathers all the declared props, including the ones declared in superclasses. + This is a meta-property common to all classes with this metatype + """ + return cls.__walk_mro_inherit_declarations('_own_declarative_props') + + # here only to have a similar invocation like declarative_attrs + # namely type(traited_instance).own_declarative_attrs + # consider the traited_instance._own_declarative_attrs discouraged + @property + def own_declarative_attrs(cls): + return cls._own_declarative_attrs + + + def __new__(mcs, type_name, bases, namespace): + """ + Gathers the names of all declarative fields. + Tell each Attr of the name of the field it is bound to. + """ + # gather all declarative attributes defined in the class to be constructed. + # validate all declarations before constructing the new type + attrs = [] + props = [] + + for k, v in namespace.items(): + if isinstance(v, _Attr): + attrs.append(k) + elif isinstance(v, _Property): + props.append(k) + + # record the names of the declarative attrs in the _own_declarative_attrs field + if '_own_declarative_attrs' in namespace: + raise TraitTypeError('class attribute _own_declarative_attrs is reserved in traited classes') + if '_own_declarative_props' in namespace: + raise TraitTypeError('class attribute _own_declarative_props is reserved in traited classes') + + namespace['_own_declarative_attrs'] = tuple(attrs) + namespace['_own_declarative_props'] = tuple(props) + # construct the class + cls = super(MetaType, mcs).__new__(mcs, type_name, bases, namespace) + + # inform the Attr instances about the class their are bound to + for attr_name in attrs: + v = namespace[attr_name] + v.field_name = attr_name + v.owner = cls + # noinspection PyProtectedMember + v._post_bind_validate() + + # do the same for props. + for prop_name in props: + v = namespace[prop_name].attr + v.field_name = prop_name + v.owner = cls + # noinspection PyProtectedMember + v._post_bind_validate() + + # update docstring. Note that this is only possible if cls was created by a metatype in python + setattr(cls, '__doc__', auto_docstring(cls)) + + # update the HasTraits class registry + mcs.__classes.update({cls.__name__: cls}) + return cls + + + # note: Any methods defined here are metamethods, visible from all classes with this metatype + # AClass.field if not found on AClass will be looked up on the metaclass + # If you just want a regular private method here name it def __lala(cls) + # double __ not for any privacy but to reduce namespace pollution + # double __ will mangle the names and make such lookups fail + + # warn about dynamic Attributes + + + def __setattr__(self, key, value): + """ + Complain if TraitedClass.a = Attr() + Traits assumes that all attributes are statically declared in the class body + """ + if isinstance(value, _Attr): + log.warning('dynamically assigned Attributes are not supported') + super(MetaType, self).__setattr__(key, value) + + + def __delattr__(self, item): + if isinstance(getattr(self, item, None), _Attr): + log.warning('Dynamically removing Attributes is not supported') + super(MetaType, self).__delattr__(item) diff --git a/dsl_cuda/example/tvb/basic/neotraits/api.py b/dsl_cuda/example/tvb/basic/neotraits/api.py new file mode 100644 index 0000000000..de159f1852 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/neotraits/api.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +The public api of the neotraits package. +""" + +from ._core import HasTraits, trait_property, cached_trait_property +from .info import narray_describe, narray_summary_info +from ._attr import Attr, Int, Float, NArray, Final, List, Range, LinspaceRange, Dim diff --git a/dsl_cuda/example/tvb/basic/neotraits/ex.py b/dsl_cuda/example/tvb/basic/neotraits/ex.py new file mode 100644 index 0000000000..81f04577e5 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/neotraits/ex.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + + +class TraitError(Exception): + def __init__(self, msg='', trait=None, attr=None): + self.trait = trait + self.attr = attr + super(TraitError, self).__init__(msg) + + + def __str__(self): + lines = [self.args[0]] + if self.attr: + lines.append(' attribute {}'.format(self.attr)) + if self.trait: + lines.append(' class {}'.format(type(self.trait).__name__)) + + return '\n'.join(lines) + + + +class TraitAttributeError(TraitError, AttributeError): + pass + + +class TraitValueError(TraitError, ValueError): + pass + + +class TraitTypeError(TraitError, TypeError): + pass diff --git a/dsl_cuda/example/tvb/basic/neotraits/info.py b/dsl_cuda/example/tvb/basic/neotraits/info.py new file mode 100644 index 0000000000..e8e51d8719 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/neotraits/info.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +Functions that inform a user about the state of a traited class or object. + +Some of these functions are here so that they won't clutter the core trait implementation. +""" + +import numpy +import sys + +if sys.version_info[0] == 3: + import typing + + +def auto_docstring(cls): + """ generate a docstring for the new class in which the Attrs are documented """ + header = 'Traited class [{}.{}]'.format(cls.__module__, cls.__name__) + + doc = [ + header, + len(header) * '^', + '', + ] + if cls.__doc__ is not None: + doc.extend([cls.__doc__, '']) + + doc.extend([ + 'Attributes declared', + '"""""""""""""""""""', + '' + ]) + # a rst definition list for all attributes + for attr_name in cls.declarative_attrs: + attr = getattr(cls, attr_name) + # the standard repr of the attribute + doc.append('{} : {}'.format(attr_name, str(attr))) + # and now the doc property + for line in attr.doc.splitlines(): + doc.append(' ' + line.lstrip()) + doc.append('') + + if cls.declarative_props: + doc.extend([ + '', + 'Properties declared', + '"""""""""""""""""""', + '' + ]) + + for prop_name in cls.declarative_props: + prop = getattr(cls, prop_name) + # the standard repr + doc.append(' {} : {}'.format(prop_name, str(prop))) + # now fish the docstrings + for line in prop.attr.doc.splitlines(): + doc.append(' ' + line.lstrip()) + if prop.fget.__doc__ is not None: + for line in prop.fget.__doc__.splitlines(): + doc.append(' ' + line.lstrip()) + + doc = '\n'.join(doc) + + return doc + + +def narray_summary_info(ar, ar_name='', omit_shape=False): + # type: (numpy.ndarray, str, bool) -> typing.Dict[str, str] + """ + A 2 column table represented as a dict of str->str + """ + if ar is None: + return {'is None': 'True'} + + ret = {} + if not omit_shape: + ret.update({'shape': str(ar.shape), 'dtype': str(ar.dtype)}) + + if ar.size == 0: + ret['is empty'] = 'True' + return ret + + if ar.dtype.kind in 'iufc': + has_nan = numpy.isnan(ar).any() + if has_nan: + ret['has NaN'] = 'True' + ret['[min, median, max]'] = '[{:g}, {:g}, {:g}]'.format(ar.min(), numpy.median(ar), ar.max()) + + if ar_name: + return {ar_name + ' ' + k: v for k, v in ret.items()} + else: + return ret + + +def narray_describe(ar): + # type: (numpy.ndarray) -> str + summary = narray_summary_info(ar) + ret = [] + for k in sorted(summary): + ret.append('{:<12}{}'.format(k, summary[k])) + return '\n'.join(ret) + + +# these are here and not on HasTraits just so that that class is not +# complicated by irrelevant string formatting + + +def trait_object_str(self): + cls = type(self) + summary = self.summary_info() + result = ['{} ('.format(cls.__name__)] + maxlenk = max(len(k) for k in summary) + + for k in sorted(summary): + result.append(' {:.<{}} {}'.format(k + ' ', maxlenk, summary[k])) + result.append(')') + return '\n'.join(result) + + +def trait_object_repr_html(self): + cls = type(self) + result = [ + '', + '

{}

'.format(cls.__name__), + '', + '', + ] + + summary = self.summary_info() + + for k in sorted(summary): + row_fmt = '' + result.append(row_fmt.format(k, summary[k])) + + result += ['
value
{}
{}
'] + + return '\n'.join(result) diff --git a/dsl_cuda/example/tvb/basic/profile.py b/dsl_cuda/example/tvb/basic/profile.py new file mode 100644 index 0000000000..3263c5e755 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/profile.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +TVB Profile Manager (top level in TVB profile & settings). + +This class is responsible for referring towards application settings, +based on current running environment (e.g. dev vs deployment), or developer profile choice (e.g. web vs console). + +.. moduleauthor:: Lia Domide +.. moduleauthor:: Mihai Andrei +.. moduleauthor:: Bogdan Neacsa + +""" + +import sys +import importlib +from tvb.basic.config.environment import Environment +from tvb.basic.config.profile_settings import BaseSettingsProfile + + +class TvbProfile(object): + """ + ENUM-like class with current TVB profile and accepted values. + """ + + LIBRARY_PROFILE = "LIBRARY_PROFILE" + COMMAND_PROFILE = "COMMAND_PROFILE" + WEB_PROFILE = "WEB_PROFILE" + MATLAB_PROFILE = "MATLAB_PROFILE" + + TEST_LIBRARY_PROFILE = "TEST_LIBRARY_PROFILE" + TEST_POSTGRES_PROFILE = "TEST_POSTGRES_PROFILE" + TEST_SQLITE_PROFILE = "TEST_SQLITE_PROFILE" + + ALL = [LIBRARY_PROFILE, COMMAND_PROFILE, WEB_PROFILE, MATLAB_PROFILE, + TEST_POSTGRES_PROFILE, TEST_SQLITE_PROFILE, TEST_LIBRARY_PROFILE] + + REGISTERED_PROFILES = {} + + CURRENT_PROFILE_NAME = None + + current = BaseSettingsProfile(False) + env = Environment() + + @classmethod + def set_profile(cls, selected_profile, in_operation=False, run_init=True): + """ + Sets TVB profile and do related initializations. + """ + + # Ensure Python is using UTF-8 encoding (otherwise default encoding is ASCII) + # We should make sure UTF-8 gets set before reading from any TVB files + # e.g. TVB_STORAGE will differ if the .tvb.configuration file contains non-ascii bytes + # most of the comments in the simulator are having pieces outside of ascii coverage + if not cls.env.is_distribution() and sys.getdefaultencoding().lower() != 'utf-8': + old_out = sys.stdout + if sys.version_info[0] < 3: + reload(sys) + sys.setdefaultencoding("utf-8") + else: + importlib.reload(sys) + sys.setdefaultencoding('utf-8') + sys.stdout = old_out + + if selected_profile is not None: + cls._load_framework_profiles(selected_profile) + cls._build_profile_class(selected_profile, in_operation, run_init) + + @classmethod + def _build_profile_class(cls, selected_profile, in_operation=False, run_init=True): + """ + :param selected_profile: Profile name to be loaded. + """ + + if selected_profile in cls.REGISTERED_PROFILES: + current_class = cls.REGISTERED_PROFILES[selected_profile] + + cls.current = current_class() + cls.CURRENT_PROFILE_NAME = selected_profile + + if in_operation: + # set flags IN_OPERATION, before initialize** calls, to avoid LoggingBuilder being created there + cls.current.prepare_for_operation_mode() + + if cls.env.is_distribution(): + # initialize deployment first, because in case of a contributor setup this tried to reload + # and initialize_profile loads already too many tvb modules, + # making the reload difficult and prone to more failures + cls.current.initialize_for_deployment() + if run_init: + cls.current.initialize_profile() + + else: + msg = "Invalid profile name %r, expected one of %r" + msg %= (selected_profile, cls.ALL) + raise Exception(msg) + + @classmethod + def _load_framework_profiles(cls, new_profile): + + from tvb.basic.config.profile_settings import LibrarySettingsProfile, TestLibraryProfile, MATLABLibraryProfile + cls.REGISTERED_PROFILES[TvbProfile.LIBRARY_PROFILE] = LibrarySettingsProfile + cls.REGISTERED_PROFILES[TvbProfile.TEST_LIBRARY_PROFILE] = TestLibraryProfile + cls.REGISTERED_PROFILES[TvbProfile.MATLAB_PROFILE] = MATLABLibraryProfile + + if not cls.is_library_mode(new_profile): + try: + from tvb.config.profile_settings import CommandSettingsProfile, WebSettingsProfile + from tvb.config.profile_settings import TestPostgresProfile, TestSQLiteProfile + + cls.REGISTERED_PROFILES[TvbProfile.COMMAND_PROFILE] = CommandSettingsProfile + cls.REGISTERED_PROFILES[TvbProfile.WEB_PROFILE] = WebSettingsProfile + cls.REGISTERED_PROFILES[TvbProfile.TEST_POSTGRES_PROFILE] = TestPostgresProfile + cls.REGISTERED_PROFILES[TvbProfile.TEST_SQLITE_PROFILE] = TestSQLiteProfile + + except ImportError: + pass + + @staticmethod + def is_library_mode(new_profile=None): + + lib_profiles = [TvbProfile.LIBRARY_PROFILE, TvbProfile.TEST_LIBRARY_PROFILE] + result = (new_profile in lib_profiles + or (new_profile is None and TvbProfile.CURRENT_PROFILE_NAME in lib_profiles) + or not TvbProfile.env.is_framework_present()) + + # Make sure default settings are not failing because we are not finding some modules + if (new_profile is None and TvbProfile.CURRENT_PROFILE_NAME is None and + not TvbProfile.env.is_framework_present()): + TvbProfile.set_profile(TvbProfile.LIBRARY_PROFILE) + + return result + + @staticmethod + def is_first_run(): + + return TvbProfile.current.manager.is_first_run() diff --git a/dsl_cuda/example/tvb/basic/readers.py b/dsl_cuda/example/tvb/basic/readers.py new file mode 100644 index 0000000000..ff50a79a75 --- /dev/null +++ b/dsl_cuda/example/tvb/basic/readers.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +# +# +# TheVirtualBrain-Scientific Package. This package holds all simulators, and +# analysers necessary to run brain-simulations. You can use it stand alone or +# in conjunction with TheVirtualBrain-Framework Package. See content of the +# documentation-folder for more details. See also http://www.thevirtualbrain.org +# +# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others +# +# This program is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You should have received a copy of the GNU General Public License along with this +# program. If not, see . +# +# +# CITATION: +# When using The Virtual Brain for scientific publications, please cite it as follows: +# +# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, +# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) +# The Virtual Brain: a simulator of primate brain network dynamics. +# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) +# +# + +""" +This module contains basic reading mechanism for default DataType fields. + +.. moduleauthor:: Lia Domide +""" + +try: + H5PY_SUPPORT = True + import h5py as hdf5 +except ImportError: + H5PY_SUPPORT = False + +import os +import numpy +import zipfile +import uuid +from tempfile import gettempdir +from scipy import io as scipy_io +from tvb.basic.logger.builder import get_logger + + + +class H5Reader(object): + """ + Read one or many numpy arrays from a H5 file. + """ + + def __init__(self, h5_path): + + self.logger = get_logger(__name__) + if H5PY_SUPPORT: + self.hfd5_source = hdf5.File(h5_path, 'r', libver='latest') + else: + self.logger.warning("You need h5py properly installed in order to load from a HDF5 source.") + + + def read_field(self, field, log_exception=True): + + try: + return self.hfd5_source['/' + field][()] + except Exception: + if log_exception: + self.logger.exception("Could not read from %s field" % field) + raise ReaderException("Could not read from %s field" % field) + + + def read_optional_field(self, field): + try: + return self.read_field(field, log_exception=False) + except ReaderException: + return None + + + +class FileReader(object): + """ + Read one or multiple numpy arrays from a text/bz2 file. + """ + + def __init__(self, file_path): + + self.logger = get_logger(__name__) + self.file_path = file_path + self.file_stream = file_path + + + def read_array(self, dtype=numpy.float64, skip_rows=0, use_cols=None, matlab_data_name=None): + + self.logger.debug("Starting to read from: " + str(self.file_path)) + + try: + # Try to read H5: + if self.file_path.endswith('.h5'): + self.logger.error("Not yet implemented read from a ZIP of H5 files!") + return numpy.array([]) + + # Try to read NumPy: + if self.file_path.endswith('.txt') or self.file_path.endswith('.bz2'): + return self._read_text(self.file_stream, dtype, skip_rows, use_cols) + + if self.file_path.endswith('.npz') or self.file_path.endswith(".npy"): + return numpy.load(self.file_stream) + + # Try to read Matlab format: + return self._read_matlab(self.file_stream, matlab_data_name) + + except Exception as e: + msg = "Could not read from %s file \n %s" % (self.file_path, e) + self.logger.exception(msg) + raise ReaderException(msg) + + + def _read_text(self, file_stream, dtype, skip_rows, use_cols): + + array_result = numpy.loadtxt(file_stream, dtype=dtype, skiprows=skip_rows, usecols=use_cols) + return array_result + + + def _read_matlab(self, file_stream, matlab_data_name=None): + + if self.file_path.endswith(".mtx"): + return scipy_io.mmread(file_stream) + + if self.file_path.endswith(".mat"): + matlab_data = scipy_io.matlab.loadmat(file_stream) + return matlab_data[matlab_data_name] + + + def read_gain_from_brainstorm(self): + + if not self.file_path.endswith('.mat'): + raise ReaderException("Brainstorm format is expected in a Matlab file not %s" % self.file_path) + + mat = scipy_io.loadmat(self.file_stream) + expected_fields = ['Gain', 'GridLoc', 'GridOrient'] + + for field in expected_fields: + if field not in mat.keys(): + raise ReaderException("Brainstorm format is expecting field %s" % field) + + gain, loc, ori = (mat[field] for field in expected_fields) + return (gain.reshape((gain.shape[0], -1, 3)) * ori).sum(axis=-1) + + + +class ZipReader(object): + """ + Read one or many numpy arrays from a ZIP archive. + """ + + def __init__(self, zip_path): + + self.logger = get_logger(__name__) + self.zip_archive = zipfile.ZipFile(zip_path) + + def has_file_like(self, file_name): + for actual_name in self.zip_archive.namelist(): + if file_name in actual_name: + return True + return False + + def read_array_from_file(self, file_name, dtype=numpy.float64, skip_rows=0, use_cols=None, matlab_data_name=None): + + matching_file_name = None + for actual_name in self.zip_archive.namelist(): + if file_name in actual_name and not actual_name.startswith("__MACOSX"): + matching_file_name = actual_name + break + + if matching_file_name is None: + # broken by mv + # self.logger.warning("File %r not found in ZIP." % file_name) + raise ReaderException("File %r not found in ZIP." % file_name) + + zip_entry = self.zip_archive.open(matching_file_name, 'r') + + if matching_file_name.endswith(".bz2"): + temp_file = copy_zip_entry_into_temp(zip_entry, matching_file_name) + file_reader = FileReader(temp_file) + result = file_reader.read_array(dtype, skip_rows, use_cols, matlab_data_name) + os.remove(temp_file) + return result + + file_reader = FileReader(matching_file_name) + file_reader.file_stream = zip_entry + return file_reader.read_array(dtype, skip_rows, use_cols, matlab_data_name) + + + def read_optional_array_from_file(self, file_name, dtype=numpy.float64, skip_rows=0, + use_cols=None, matlab_data_name=None): + try: + return self.read_array_from_file(file_name, dtype, skip_rows, use_cols, matlab_data_name) + except ReaderException: + return numpy.array([], dtype=dtype) + + + +class ReaderException(Exception): + pass + + + +def try_get_absolute_path(relative_module, file_suffix): + """ + :param relative_module: python module to be imported. When import of this fails, we will return the file_suffix + :param file_suffix: In case this is already an absolute path, return it immediately, + otherwise append it after the module path + :return: Try to build an absolute path based on a python module and a file-suffix + """ + + result_full_path = file_suffix + + if not os.path.isabs(file_suffix): + + try: + module_import = __import__(relative_module, globals(), locals(), ["__init__"]) + result_full_path = os.path.join(os.path.dirname(module_import.__file__), file_suffix) + + except ImportError: + logger = get_logger(__name__) + logger.exception("Could not import tvb_data Python module for default data-set!") + + return result_full_path + + + +def copy_zip_entry_into_temp(source, file_suffix, buffer_size=1024 * 1024): + """ + Copy a ZIP Entry into a new file created under system temporary folder. + + :param source: ZipEntry + :param file_suffix: String suffix to be added to the temporary file name + :param buffer_size: Buffer size used when copying the file-content + :return: the path towards the new file. + """ + + result_dest_path = os.path.join(gettempdir(), "tvb_" + str(uuid.uuid1()) + file_suffix) + result_dest = open(result_dest_path, 'wb') + + while 1: + copy_buffer = source.read(buffer_size) + if copy_buffer: + result_dest.write(copy_buffer) + else: + break + + source.close() + result_dest.close() + + return result_dest_path From 696f005d86564cd911768205e3bf4e7dd8246574 Mon Sep 17 00:00:00 2001 From: DeLaVlag Date: Mon, 22 Jun 2020 12:18:07 +0200 Subject: [PATCH 04/10] readme updation --- README.md | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2ca79f9b46..aad2a37d8a 100755 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # TVB CUDA model generation using LEMS format This readme describes the usage of the code generation for models defined in LEMS based XML to Cuda (C) format. The LEMS format PR has been adopted and altered to match TVB model names. -In LEMSCUDA.py the function "cuda_templating(Model+'_CUDA')" will start the code generation. -It expects a [model+'_CUDA'].xml file to be present in tvb/dsl_cuda/NeuroML/XMLmodels. -The generated file will be placed in tvb/simulator/models. +In LEMSCUDA.py the function "cuda_templating(Model+'_CUDA', 'path/to/XMLmodels')" will start the code generation. +It expects a [model+'_CUDA'].xml file to be present in ['path/to/XMLmodels']. +The generated file will be placed in ['installpath']'/tvb-hpc/dsl/dsl_cuda/CUDAmodels/'. The produced filename is a lower cased [model].py which contains a class named [model]. -In the directory TVB_testsuite the files to run the models on the GPU can be found. -Execute './runthings cuda Modelname' to start the parameter sweep based simulation. .. moduleauthor:: Michiel. A. van der Vlag + .. moduleauthor:: Marmaduke Woodman + .. moduleauthor:: Sandra Diaz # The CUDA memory model specification ![](GPUmemindex.png) @@ -29,7 +29,7 @@ Mako templating # XML LEMS Definitions Based on http://lems.github.io/LEMS/elements.html but attributes are tuned for TVB CUDA models. -As an example an XML line and its translation to CUDA are given. +As an example an XML line and its translation to CUDA are given below. * Constants\ If domain = 'none' no domain range will be added.\ @@ -181,12 +181,11 @@ for (unsigned int j_node = 0; j_node < n_node; j_node++) # Running an example -To run an example of a GPU generated model according to an existing or home-created xml file execute: -./runthings cuda [modelname] located in /tvb-hpc/dsl/dsl_cuda/example. The cuda parameter indicates a cuda simulation -is to be started and the [modelname] paramaters is the model that needs to be simulated. - -Place model file in directory and execute cuda_templating('modelname') function. Resulting model will be -placed in the CUDA model directory - -# TODO -Add CUDA model validation tests. \ No newline at end of file +Place an xml model file in directory used for your XML model storage and execute "cuda_templating(Model+'_CUDA', +'path/to/XMLmodels')" function. The resulting model will be placed in the CUDA model directory. +The directory 'tvb-hpc/dsl/dsl_cuda/example/' holds an example how to run the model generator and the CUDA model +on a GPU. +From this directory, execute './runthings cuda [Modelname]' to start model generation corresponding to an xml file +and a parameters sweep simulation with the produced model file on a CUDA enabled machine. +The cuda parameter indicates a cuda simulation is to be started and the [modelname] paramater is the model +that is the target of simulation. From b52026f9beecdc5c954700c06610fdd03a8775a1 Mon Sep 17 00:00:00 2001 From: DeLaVlag Date: Wed, 1 Jul 2020 12:39:07 +0200 Subject: [PATCH 05/10] removed tvb.basic --- dsl_cuda/example/tvb/__init__.py | 43 -- dsl_cuda/example/tvb/basic/__init__.py | 29 - dsl_cuda/example/tvb/basic/config/__init__.py | 0 .../example/tvb/basic/config/environment.py | 166 ----- .../tvb/basic/config/profile_settings.py | 198 ------ dsl_cuda/example/tvb/basic/config/settings.py | 339 ---------- dsl_cuda/example/tvb/basic/config/stored.py | 131 ---- dsl_cuda/example/tvb/basic/config/tvb.version | 1 - dsl_cuda/example/tvb/basic/config/utils.py | 50 -- dsl_cuda/example/tvb/basic/exceptions.py | 52 -- dsl_cuda/example/tvb/basic/logger/__init__.py | 0 dsl_cuda/example/tvb/basic/logger/builder.py | 101 --- .../tvb/basic/logger/library_logger.conf | 78 --- .../tvb/basic/logger/library_logger_test.conf | 68 -- .../tvb/basic/logger/simple_handler.py | 53 -- .../example/tvb/basic/neotraits/__init__.py | 41 -- dsl_cuda/example/tvb/basic/neotraits/_attr.py | 618 ------------------ dsl_cuda/example/tvb/basic/neotraits/_core.py | 257 -------- .../tvb/basic/neotraits/_declarative_base.py | 277 -------- dsl_cuda/example/tvb/basic/neotraits/api.py | 37 -- dsl_cuda/example/tvb/basic/neotraits/ex.py | 59 -- dsl_cuda/example/tvb/basic/neotraits/info.py | 165 ----- dsl_cuda/example/tvb/basic/profile.py | 165 ----- dsl_cuda/example/tvb/basic/readers.py | 260 -------- 24 files changed, 3188 deletions(-) delete mode 100644 dsl_cuda/example/tvb/__init__.py delete mode 100644 dsl_cuda/example/tvb/basic/__init__.py delete mode 100644 dsl_cuda/example/tvb/basic/config/__init__.py delete mode 100644 dsl_cuda/example/tvb/basic/config/environment.py delete mode 100644 dsl_cuda/example/tvb/basic/config/profile_settings.py delete mode 100644 dsl_cuda/example/tvb/basic/config/settings.py delete mode 100644 dsl_cuda/example/tvb/basic/config/stored.py delete mode 100644 dsl_cuda/example/tvb/basic/config/tvb.version delete mode 100644 dsl_cuda/example/tvb/basic/config/utils.py delete mode 100644 dsl_cuda/example/tvb/basic/exceptions.py delete mode 100644 dsl_cuda/example/tvb/basic/logger/__init__.py delete mode 100644 dsl_cuda/example/tvb/basic/logger/builder.py delete mode 100644 dsl_cuda/example/tvb/basic/logger/library_logger.conf delete mode 100644 dsl_cuda/example/tvb/basic/logger/library_logger_test.conf delete mode 100644 dsl_cuda/example/tvb/basic/logger/simple_handler.py delete mode 100644 dsl_cuda/example/tvb/basic/neotraits/__init__.py delete mode 100644 dsl_cuda/example/tvb/basic/neotraits/_attr.py delete mode 100644 dsl_cuda/example/tvb/basic/neotraits/_core.py delete mode 100644 dsl_cuda/example/tvb/basic/neotraits/_declarative_base.py delete mode 100644 dsl_cuda/example/tvb/basic/neotraits/api.py delete mode 100644 dsl_cuda/example/tvb/basic/neotraits/ex.py delete mode 100644 dsl_cuda/example/tvb/basic/neotraits/info.py delete mode 100644 dsl_cuda/example/tvb/basic/profile.py delete mode 100644 dsl_cuda/example/tvb/basic/readers.py diff --git a/dsl_cuda/example/tvb/__init__.py b/dsl_cuda/example/tvb/__init__.py deleted file mode 100644 index 3805159484..0000000000 --- a/dsl_cuda/example/tvb/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -We want tvb package to extend over at least 2 folders: -simulator_library and tvb_framework. -""" - -from pkgutil import extend_path - -try: - __path__ = extend_path(__path__, __name__) - -except NameError: - ## Ignore __path__ not defined when called from sphinx - __path__ = [__name__] \ No newline at end of file diff --git a/dsl_cuda/example/tvb/basic/__init__.py b/dsl_cuda/example/tvb/basic/__init__.py deleted file mode 100644 index ab250309ae..0000000000 --- a/dsl_cuda/example/tvb/basic/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# diff --git a/dsl_cuda/example/tvb/basic/config/__init__.py b/dsl_cuda/example/tvb/basic/config/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dsl_cuda/example/tvb/basic/config/environment.py b/dsl_cuda/example/tvb/basic/config/environment.py deleted file mode 100644 index 15a2d3bd7b..0000000000 --- a/dsl_cuda/example/tvb/basic/config/environment.py +++ /dev/null @@ -1,166 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -Environment related checks or operations are to be defined here. - -.. moduleauthor:: Lia Domide -.. moduleauthor:: Mihai Andrei -""" - -import os -import sys -from subprocess import Popen, PIPE -from tvb.basic.config.settings import VersionSettings - - -class Environment(object): - - def is_framework_present(self): - """ - :return: True when framework classes are present and can be imported. - """ - framework_present = True - try: - from tvb.config.profile_settings import WebSettingsProfile - except ImportError: - framework_present = False - - return framework_present - - @staticmethod - def is_distribution(): - """ - Return True when TVB is used with Python installed natively (with GitHub clone, SVN, pip or conda) - """ - svn_variable = 'SVN_REVISION' - if svn_variable in os.environ: - # Usage in Hudson build - return False - - try: - import tvb_bin - except ImportError: - # No tvb_bin, it means usage from pip or conda - return False - - try: - _proc = Popen(["svnversion", "."], stdout=PIPE, stderr=PIPE) - version = VersionSettings.parse_svn_version(_proc.communicate()[0]) - if version: - # usage from SVN - return False - except Exception: - # Usage from tvb_distribution - return True - - def is_linux_deployment(self): - """ - Return True if current run is not development and is running on Linux. - """ - return self.is_linux() and self.is_distribution() - - def is_mac_deployment(self): - """ - Return True if current run is not development and is running on Mac OS X - """ - return self.is_mac() and self.is_distribution() - - def is_windows_deployment(self): - """ - Return True if current run is not development and is running on Windows. - """ - return self.is_windows() and self.is_distribution() - - def is_linux(self): - return not self.is_windows() and not self.is_mac() - - @staticmethod - def is_mac(): - return sys.platform == 'darwin' - - @staticmethod - def is_windows(): - return sys.platform.startswith('win') - - def get_library_folder(self, default_mac): - """ - Return top level library folder. Will be use for setting paths - """ - if self.is_windows_deployment(): - return os.path.dirname(sys.executable) - if self.is_mac_deployment(): - return os.path.dirname(default_mac) - if self.is_linux_deployment(): - return os.path.dirname(sys.executable) - - def setup_tk_tcl_environ(self, root_folder): - """ - Given a root folder to look in, find the required configuration files for TCL/TK and set the proper - environmental variables so everything works fine in the distribution package. - - :param root_folder: the top folder from which to start looking for the required configuration files - """ - tk_folder = self._find_file('tk.tcl', root_folder) - if tk_folder: - os.environ['TK_LIBRARY'] = tk_folder - - tcl_folder = self._find_file('init.tcl', root_folder) - if tcl_folder: - os.environ['TCL_LIBRARY'] = tcl_folder - - def _find_file(self, target_file, root_folder): - """ - Search for a file in a folder directory. Return the folder in which the file can be found. - - :param target_file: the name of the file that is searched - :param root_folder: the top lever folder from which to start searching in all it's subdirectories - :returns: the name of the folder in which the file can be found - """ - for root, _, files in os.walk(root_folder): - for file_n in files: - if file_n == target_file: - return root - - def setup_python_path(self, *paths): - """ - Set PYTHONPATH - :param paths: list of absolute folder paths to join. - """ - os.environ['PYTHONPATH'] = os.pathsep.join(paths) - - def append_to_path(self, *paths): - """ - Set PATH - :param paths: list of absolute folder paths to join and add BEFORE the current PATH - """ - paths = list(paths) - paths.append(os.environ.get('PATH', '')) - os.environ['PATH'] = os.pathsep.join(paths) diff --git a/dsl_cuda/example/tvb/basic/config/profile_settings.py b/dsl_cuda/example/tvb/basic/config/profile_settings.py deleted file mode 100644 index e635a32543..0000000000 --- a/dsl_cuda/example/tvb/basic/config/profile_settings.py +++ /dev/null @@ -1,198 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -Prepare TVB settings to be grouped under various profile classes. - -.. moduleauthor:: Lia Domide -""" -import os -import sys -from tvb.basic.config import stored -from tvb.basic.config.environment import Environment -from tvb.basic.config.settings import ClusterSettings, DBSettings, VersionSettings, WebSettings - - -class BaseSettingsProfile(object): - TVB_USER_HOME = os.environ.get('TVB_USER_HOME', '~') - - TVB_CONFIG_FILE = os.path.expanduser(os.path.join(TVB_USER_HOME, '.tvb.configuration')) - - DEFAULT_STORAGE = os.path.expanduser(os.path.join(TVB_USER_HOME, 'TVB' + os.sep)) - FIRST_RUN_STORAGE = os.path.expanduser(os.path.join(TVB_USER_HOME, '.tvb-temp')) - - LOGGER_CONFIG_FILE_NAME = "logger_config.conf" - - # Access rights for TVB generated files/folders. - ACCESS_MODE_TVB_FILES = 0o744 - - # Number used for estimation of TVB used storage space - MAGIC_NUMBER = 9 - - def __init__(self, web_enabled=True): - - self.manager = stored.SettingsManager(self.TVB_CONFIG_FILE) - - # Actual storage of all TVB related files - self.TVB_STORAGE = self.manager.get_attribute(stored.KEY_STORAGE, self.FIRST_RUN_STORAGE, str) - self.TVB_LOG_FOLDER = os.path.join(self.TVB_STORAGE, "logs") - self.TVB_TEMP_FOLDER = os.path.join(self.TVB_STORAGE, "TEMP") - - self.env = Environment() - self.cluster = ClusterSettings(self.manager) - self.web = WebSettings(self.manager, web_enabled) - self.db = DBSettings(self.manager, self.DEFAULT_STORAGE, self.TVB_STORAGE) - self.version = VersionSettings(self.manager, self.BIN_FOLDER) - - self.EXTERNALS_FOLDER_PARENT = os.path.dirname(self.BIN_FOLDER) - if not self.env.is_distribution(): - self.EXTERNALS_FOLDER_PARENT = os.path.dirname(self.EXTERNALS_FOLDER_PARENT) - - # The path to the matlab executable (if existent). Otherwise just return an empty string. - value = self.manager.get_attribute(stored.KEY_MATLAB_EXECUTABLE, '', str) or '' - if value == 'None': - value = '' - self.MATLAB_EXECUTABLE = value - - # Maximum number of vertices acceptable o be part of a surface at import time. - self.MAX_SURFACE_VERTICES_NUMBER = self.manager.get_attribute(stored.KEY_MAX_NR_SURFACE_VERTEX, 300000, int) - # Max number of ops that can be scheduled from UI in a PSE. To be correlated with the oarsub limitations - self.MAX_RANGE_NUMBER = self.manager.get_attribute(stored.KEY_MAX_RANGE_NR, 2000, int) - # Max number of threads in the pool of ops running in parallel. TO be correlated with CPU cores - self.MAX_THREADS_NUMBER = self.manager.get_attribute(stored.KEY_MAX_THREAD_NR, 4, int) - # The maximum disk space that can be used by one single user, in KB. - self.MAX_DISK_SPACE = self.manager.get_attribute(stored.KEY_MAX_DISK_SPACE_USR, 5 * 1024 * 1024, int) - - @property - def BIN_FOLDER(self): - """ - Return path towards tvb_bin location. It will be used in some environment for determining the starting point - """ - try: - import tvb_bin - return os.path.dirname(os.path.abspath(tvb_bin.__file__)) - except ImportError: - return "." - - @property - def PYTHON_INTERPRETER_PATH(self): - """ - Get Python path, based on current environment. - """ - if self.env.is_mac_deployment(): - return os.path.join(os.path.dirname(sys.executable), "python") - - return sys.executable - - def prepare_for_operation_mode(self): - """ - Overwrite PostgreSQL number of connections when executed in the context of a node. - """ - self.db.MAX_CONNECTIONS = self.db.MAX_ASYNC_CONNECTIONS - self.cluster.IN_OPERATION_EXECUTION_PROCESS = True - - def initialize_profile(self): - """ - Make sure tvb folders are created. - """ - if not os.path.exists(self.TVB_LOG_FOLDER): - os.makedirs(self.TVB_LOG_FOLDER) - - if not os.path.exists(self.TVB_TEMP_FOLDER): - os.makedirs(self.TVB_TEMP_FOLDER) - - if not os.path.exists(self.TVB_STORAGE): - os.makedirs(self.TVB_STORAGE) - - def initialize_for_deployment(self): - - library_folder = self.env.get_library_folder(self.BIN_FOLDER) - - if self.env.is_windows_deployment(): - self.env.setup_python_path(library_folder, os.path.join(library_folder, 'lib-tk')) - self.env.append_to_path(library_folder) - self.env.setup_tk_tcl_environ(library_folder) - - if self.env.is_mac_deployment(): - # MacOS package structure is in the form: - # Contents/Resorces/lib/python2.7/tvb . PYTHONPATH needs to be set - # at the level Contents/Resources/lib/python2.7/ and the root path - # from where to start looking for TK and TCL up to Contents/ - tcl_root = os.path.dirname(os.path.dirname(os.path.dirname(library_folder))) - self.env.setup_tk_tcl_environ(tcl_root) - - self.env.setup_python_path(library_folder, os.path.join(library_folder, 'site-packages.zip'), - os.path.join(library_folder, 'lib-dynload')) - - if self.env.is_linux_deployment(): - # Note that for the Linux package some environment variables like LD_LIBRARY_PATH, - # LD_RUN_PATH, PYTHONPATH and PYTHONHOME are set also in the startup scripts. - self.env.setup_python_path(library_folder, os.path.join(library_folder, 'lib-tk')) - self.env.setup_tk_tcl_environ(library_folder) - - # Correctly set MatplotLib Path, before start. - mpl_data_path_maybe = os.path.join(library_folder, 'mpl-data') - try: - os.stat(mpl_data_path_maybe) - os.environ['MATPLOTLIBDATA'] = mpl_data_path_maybe - except: - pass - - -class LibrarySettingsProfile(BaseSettingsProfile): - """ - Profile used when scientific library is used without storage and without web UI. - """ - - TVB_STORAGE = os.path.expanduser(os.path.join("~", "TVB" + os.sep)) - LOGGER_CONFIG_FILE_NAME = "library_logger.conf" - - def __init__(self): - super(LibrarySettingsProfile, self).__init__(False) - - -class TestLibraryProfile(LibrarySettingsProfile): - """ - Profile for library unit-tests. - """ - - LOGGER_CONFIG_FILE_NAME = "library_logger_test.conf" - - def __init__(self): - super(TestLibraryProfile, self).__init__() - self.TVB_LOG_FOLDER = "TEST_OUTPUT" - - -class MATLABLibraryProfile(LibrarySettingsProfile): - """ - Profile use library use from MATLAB. - """ - - LOGGER_CONFIG_FILE_NAME = None diff --git a/dsl_cuda/example/tvb/basic/config/settings.py b/dsl_cuda/example/tvb/basic/config/settings.py deleted file mode 100644 index a586788e9a..0000000000 --- a/dsl_cuda/example/tvb/basic/config/settings.py +++ /dev/null @@ -1,339 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -TVB Raw Settings are defined here, grouped by their category of usage (e.g. cluster related, web related, etc). -Do not instantiate these classes directly, but rather use them through TvpProfile.current instance. - -.. moduleauthor:: Lia Domide -""" - -import os -from subprocess import Popen, PIPE -from tvb.basic.config import stored - - - -class VersionSettings(object): - """ - Gather settings related to various version numbers of TVB application - """ - - # Current release number - BASE_VERSION = "2.0" - - # Current DB version. Increment this and create a new xxx_update_db.py migrate script - DB_STRUCTURE_VERSION = 18 - - # This is the version of the data stored in H5 and XML files - # and should be used by next versions to know how to import - # data in TVB format, in case data structure changes. - # Value should be updated every time data structure is changed. - DATA_VERSION = 5 - DATA_VERSION_ATTRIBUTE = "Data_version" - - # This is the version of the tvb project. - # It should be updated every time the project structure changes - # Should this be sync-ed with data version changes? - PROJECT_VERSION = 3 - - - def __init__(self, manager, bin_folder): - - # Used for reading the version file from it - self.BIN_FOLDER = bin_folder - - # Concatenate BASE_VERSION with svn revision number - self.CURRENT_VERSION = self.BASE_VERSION + '-' + str(self.SVN_VERSION) - - # The version up until we done the upgrade properly for the file data storage. - self.DATA_CHECKED_TO_VERSION = manager.get_attribute(stored.KEY_LAST_CHECKED_FILE_VERSION, 1, int) - - # The version up until we done the upgrade properly for the file data storage. - self.CODE_CHECKED_TO_VERSION = manager.get_attribute(stored.KEY_LAST_CHECKED_CODE_VERSION, -1, int) - - - @property - def SVN_VERSION(self): - """Current SVN version in the package running now.""" - svn_variable = 'SVN_REVISION' - if svn_variable in os.environ: - return os.environ[svn_variable] - - try: - _proc = Popen(["svnversion", "."], stdout=PIPE, stderr=PIPE) - return self.parse_svn_version(_proc.communicate()[0]) - except Exception: - pass - - try: - import tvb.basic.config - config_folder = os.path.dirname(os.path.abspath(tvb.basic.config.__file__)) - with open(os.path.join(config_folder, 'tvb.version'), 'r') as version_file: - return self.parse_svn_version(version_file.read()) - except Exception: - pass - - raise ValueError('cannot determine TVB revision number') - - - @staticmethod - def parse_svn_version(version_string): - if ':' in version_string: - version_string = version_string.split(':')[1] - - number = ''.join([ch for ch in version_string if ch.isdigit()]) - return int(number) - - - -class ClusterSettings(object): - """ - Cluster related settings. - """ - SCHEDULER_OAR = "oar" - SCHEDULER_SLURM = "slurm" - - # Specify if the current process is executing an operation (via clusterLauncher) - IN_OPERATION_EXECUTION_PROCESS = False - - _CACHED_IS_RUNNING_ON_CLUSTER = None - _CACHED_NODE_NAME = None - - - def __init__(self, manager): - self.IS_DEPLOY = manager.get_attribute(stored.KEY_CLUSTER, False, eval) - self.CLUSTER_SCHEDULER = manager.get_attribute(stored.KEY_CLUSTER_SCHEDULER, self.SCHEDULER_OAR) - self.ACCEPTED_SCHEDULERS = {self.SCHEDULER_OAR: self.SCHEDULER_OAR, - self.SCHEDULER_SLURM: self.SCHEDULER_SLURM} - - @property - def SCHEDULE_COMMAND(self): - if self.CLUSTER_SCHEDULER == self.SCHEDULER_OAR: - return 'oarsub -q tvb -S "/home/tvbadmin/clusterLauncher %s %s" -l walltime=%s' - return 'sbatch /home/tvbadmin/clusterLauncher %s %s %s' - - @property - def STOP_COMMAND(self): - if self.CLUSTER_SCHEDULER == self.SCHEDULER_OAR: - return 'oardel %s' - return 'scancel %s' - - @property - def STATUS_COMMAND(self): - if self.CLUSTER_SCHEDULER == self.SCHEDULER_OAR: - return 'oarstat %s' - return 'squeue -j %s' - - @property - def JOB_ID_STRING(self): - if self.CLUSTER_SCHEDULER == self.SCHEDULER_OAR: - return 'OAR_JOB_ID=' - return 'Submitted batch job ' - - @property - def NODE_ENV(self): - if self.CLUSTER_SCHEDULER == self.SCHEDULER_OAR: - return 'OAR_NODEFILE' - return 'SLURM_NODEID' - - @property - def IS_RUNNING_ON_CLUSTER_NODE(self): - """ - Returns True if current execution happens on cluster node. - Even when IS_DEPLOY is True, this call will return False for the web machine. - """ - if self._CACHED_IS_RUNNING_ON_CLUSTER is None: - self._CACHED_IS_RUNNING_ON_CLUSTER = self.CLUSTER_NODE_NAME is not None - - return self._CACHED_IS_RUNNING_ON_CLUSTER - - - @property - def CLUSTER_NODE_NAME(self): - """ - :return the name of the cluster on which TVB code is executed. - If code is executed on a normal machine (not cluster node) returns None - """ - # Check if the name wasn't computed before. - if self._CACHED_NODE_NAME is None: - # Read env variable which contains path the the file containing node name - env_oar_nodefile = os.getenv(self.NODE_ENV) - if env_oar_nodefile is not None and len(env_oar_nodefile) > 0 and os.path.exists(env_oar_nodefile): - # Read node name from file (valid for OAR cluster) - with open(env_oar_nodefile, 'r') as f: - node_name = f.read() - else: - # Valid for SLURM clusters - node_name = os.getenv(self.NODE_ENV) - - if node_name is not None and len(node_name.strip()) > 0: - self._CACHED_NODE_NAME = node_name.strip() - return self._CACHED_NODE_NAME - else: - return self._CACHED_NODE_NAME - - return None - - - -class WebSettings(object): - """ - Web related specifications - """ - - ENABLED = False - LOCALHOST = "127.0.0.1" - RENDER_HTML = True - VISUALIZERS_ROOT = "tvb.interfaces.web.templates.genshi.visualizers" - VISUALIZERS_URL_PREFIX = "/flow/read_datatype_attribute/" - - - def __init__(self, manager, enabled): - - self.ENABLED = enabled - self.admin = WebAdminSettings(manager) - - self.CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - if enabled: - try: - import tvb.interfaces - self.CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(tvb.interfaces.__file__))) - except ImportError: - pass - else: - self.VISUALIZERS_URL_PREFIX = "" - - self.SERVER_PORT = manager.get_attribute(stored.KEY_PORT, 8080, int) - - # Compute reference towards the current web application, valid FROM localhost - server_IP = manager.get_attribute(stored.KEY_IP, self.LOCALHOST) - self.BASE_LOCAL_URL = "http://%s:%s/" % (server_IP, str(self.SERVER_PORT)) - - # Compute PUBLIC reference towards the current web application, valid FROM outside - self.BASE_URL = manager.get_attribute(stored.KEY_URL_WEB, self.BASE_LOCAL_URL) - - # URL for reading current available version information. - default = "http://www.thevirtualbrain.org/tvb/zwei/action/serialize-version?version=1&type=json" - self.URL_TVB_VERSION = manager.get_attribute(stored.KEY_URL_VERSION, default) - - self.TEMPLATE_ROOT = os.path.join(self.CURRENT_DIR, 'interfaces', 'web', 'templates', 'genshi') - self.CHERRYPY_CONFIGURATION = {'global': {'server.socket_host': '0.0.0.0', - 'server.socket_port': self.SERVER_PORT, - 'server.thread_pool': 20, - 'engine.autoreload_on': False, - 'server.max_request_body_size': 3221225472 # 3 GB - }, - '/': {'tools.encode.on': True, - 'tools.encode.encoding': 'utf-8', - 'tools.decode.on': True, - 'tools.gzip.on': True, - 'tools.gzip.mime_types': ['text/html', 'text/plain', - 'text/javascript', 'text/css', - 'application/x.ndarray'], - 'tools.sessions.on': True, - 'tools.sessions.storage_type': 'ram', - 'tools.sessions.timeout': 600, # 10 hours - 'response.timeout': 1000000, - 'tools.sessions.locking': 'explicit', - 'tools.upload.on': True, # Tool to check upload content size - 'tools.cleanup.on': True # Tool to clean up files on disk - }, - '/static': {'tools.staticdir.root': self.CURRENT_DIR, - 'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join('interfaces', 'web', 'static') - }, - '/statichelp': {'tools.staticdir.root': self.CURRENT_DIR, - 'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join('interfaces', 'web', - 'static', 'help') - }, - '/static_view': {'tools.staticdir.root': self.CURRENT_DIR, - 'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join('interfaces', 'web', - 'templates', 'genshi', - 'visualizers'), - }, - } - - - -class WebAdminSettings(object): - """ - Setting related to the default users of web-tvb - """ - - SYSTEM_USER_NAME = 'TVB system' - DEFAULT_ADMIN_EMAIL = 'jira.tvb@gmail.com' - ADMINISTRATOR_BLANK_PWD = 'pass' - - - def __init__(self, manager): - # Give name for the Admin user, first created. - self.ADMINISTRATOR_NAME = manager.get_attribute(stored.KEY_ADMIN_NAME, 'admin') - - # Admin's password used when creating first user (default is MD5 for 'pass') - self.ADMINISTRATOR_PASSWORD = manager.get_attribute(stored.KEY_ADMIN_PWD, '1a1dc91c907325c69271ddf0c944bc72') - - # Admin's email used when creating first user - self.ADMINISTRATOR_EMAIL = manager.get_attribute(stored.KEY_ADMIN_EMAIL, self.DEFAULT_ADMIN_EMAIL) - - - -class DBSettings(object): - - # Overwrite number of connections to the DB. - # Otherwise might reach PostgreSQL limit when launching multiple concurrent operations. - # MAX_CONNECTION default value will be used for WEB - # When launched on cluster, the MAX_ASYNC_CONNECTIONS overwrites MAX_ONNECTIONS value - MAX_CONNECTIONS = 20 - MAX_ASYNC_CONNECTIONS = 2 - - # Nested transactions are not supported by all databases and not really necessary in TVB so far so - # we don't support them yet. However when running tests we can use them to out advantage to rollback - # any database changes between tests. - ALLOW_NESTED_TRANSACTIONS = False - - def __init__(self, manager, default_storage, current_storage): - # A dictionary with accepted db's and their default URLS - default_pg = 'postgresql+psycopg2://postgres:root@127.0.0.1:5432/tvb?user=postgres&password=postgres' - default_lite = 'sqlite:///' + os.path.join(default_storage, 'tvb-database.db') - self.ACEEPTED_DBS = {'postgres': manager.get_attribute(stored.KEY_DB_URL, default_pg), - 'sqlite': manager.get_attribute(stored.KEY_DB_URL, default_lite)} - - # Currently selected database (must be a key in ACCEPTED_DBS) - self.SELECTED_DB = manager.get_attribute(stored.KEY_SELECTED_DB, 'sqlite') - - # Used DB url: IP,PORT. The DB needs to be created in advance. - default_lite = 'sqlite:///' + os.path.join(current_storage, "tvb-database.db") - self.DB_URL = manager.get_attribute(stored.KEY_DB_URL, default_lite) - - # Upgrade/Downgrade repository - self.DB_VERSIONING_REPO = os.path.join(current_storage, 'db_repo') diff --git a/dsl_cuda/example/tvb/basic/config/stored.py b/dsl_cuda/example/tvb/basic/config/stored.py deleted file mode 100644 index d8806be8f9..0000000000 --- a/dsl_cuda/example/tvb/basic/config/stored.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -Manages reading and writing settings in file - -.. moduleauthor:: Lia Domide -.. moduleauthor:: Bogdan Neacsa - -""" - -import os - -# File keys -KEY_ADMIN_NAME = 'ADMINISTRATOR_NAME' -KEY_ADMIN_PWD = 'ADMINISTRATOR_PASSWORD' -KEY_ADMIN_EMAIL = 'ADMINISTRATOR_EMAIL' -KEY_STORAGE = 'TVB_STORAGE' -KEY_MAX_DISK_SPACE_USR = 'USR_DISK_SPACE' -# During the introspection phase, it is checked if either Matlab or -# octave are installed and available trough the system PATH variable -# If so, they will be used for some analyzers -KEY_MATLAB_EXECUTABLE = 'MATLAB_EXECUTABLE' -KEY_IP = 'SERVER_IP' -KEY_PORT = 'WEB_SERVER_PORT' -KEY_URL_WEB = 'URL_WEB' -KEY_SELECTED_DB = 'SELECTED_DB' -KEY_DB_URL = 'URL_VALUE' -KEY_URL_VERSION = 'URL_TVB_VERSION' -KEY_CLUSTER = 'DEPLOY_CLUSTER' -KEY_CLUSTER_SCHEDULER = 'CLUSTER_SCHEDULER' -KEY_MAX_THREAD_NR = 'MAXIMUM_NR_OF_THREADS' -KEY_MAX_RANGE_NR = 'MAXIMUM_NR_OF_OPS_IN_RANGE' -KEY_MAX_NR_SURFACE_VERTEX = 'MAXIMUM_NR_OF_VERTICES_ON_SURFACE' -KEY_LAST_CHECKED_FILE_VERSION = 'LAST_CHECKED_FILE_VERSION' -KEY_LAST_CHECKED_CODE_VERSION = 'LAST_CHECKED_CODE_VERSION' -KEY_FILE_STORAGE_UPDATE_STATUS = 'FILE_STORAGE_UPDATE_STATUS' - - -class SettingsManager(object): - def __init__(self, config_file_location): - self.config_file_location = config_file_location - self.stored_settings = self._read_config_file() - - def _read_config_file(self): - """ - Get data from the configurations file in the form of a dictionary. - Return empty dictionary if file not present. - """ - if not os.path.exists(self.config_file_location): - return {} - - config_dict = {} - with open(self.config_file_location, 'r') as cfg_file: - data = cfg_file.read() - entries = [line for line in data.split('\n') if not line.startswith('#') and len(line.strip()) > 0] - for one_entry in entries: - name, value = one_entry.split('=', 1) - config_dict[name] = value - return config_dict - - def add_entries_to_config_file(self, input_data): - """ - Add to the dictionary of settings already existent in the settings file. - - :param input_data: A dictionary of pairs that need to be added to the config file. - """ - config_dict = self._read_config_file() - if config_dict is None: - config_dict = {} - - for entry in input_data: - config_dict[entry] = input_data[entry] - - with open(self.config_file_location, 'w') as file_writer: - for key in config_dict: - file_writer.write(key + '=' + str(config_dict[key]) + '\n') - - self.stored_settings = self._read_config_file() - - def write_config_data(self, config_dict): - """ - Overwrite anything already existent in the config file - """ - with open(self.config_file_location, 'w') as file_writer: - for key in config_dict: - file_writer.write(key + '=' + str(config_dict[key]) + '\n') - - self.stored_settings = self._read_config_file() - - def get_attribute(self, key, default=None, dtype=str): - """ - Get a cfg attribute that could also be found in the settings file. - """ - try: - if key in self.stored_settings: - return dtype(self.stored_settings[key]) - except ValueError: - # Invalid convert operation. - return default - return default - - def is_first_run(self): - return self.stored_settings is None or len(self.stored_settings) <= 2 diff --git a/dsl_cuda/example/tvb/basic/config/tvb.version b/dsl_cuda/example/tvb/basic/config/tvb.version deleted file mode 100644 index ab116fdfd6..0000000000 --- a/dsl_cuda/example/tvb/basic/config/tvb.version +++ /dev/null @@ -1 +0,0 @@ -Revision: 8898 \ No newline at end of file diff --git a/dsl_cuda/example/tvb/basic/config/utils.py b/dsl_cuda/example/tvb/basic/config/utils.py deleted file mode 100644 index af449980b5..0000000000 --- a/dsl_cuda/example/tvb/basic/config/utils.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -Helper tools, for the configuration area. - -.. moduleauthor:: Bogdan Neacsa -.. moduleauthor:: marmaduke - -""" - - -class EnhancedDictionary(dict): - """ - A dictionary like class that provides easy access to configuration values. - """ - - def __getattr__(self, key): - return self[key] - - def __setattr__(self, key, value): - self[key] = value - diff --git a/dsl_cuda/example/tvb/basic/exceptions.py b/dsl_cuda/example/tvb/basic/exceptions.py deleted file mode 100644 index 55bad2fd13..0000000000 --- a/dsl_cuda/example/tvb/basic/exceptions.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# -""" -.. moduleauthor:: Calin Pavel -""" - - -class TVBException(Exception): - """ - Base class for all TVB exceptions. - """ - - def __init__(self, message, parent_exception=None): - Exception.__init__(self, message, parent_exception) - self.message = str(message) - - def __str__(self): - return self.message - - -class ValidationException(TVBException): - """ - Exception class for problems that occurs during MappedType - validation before storing it into DB. - """ diff --git a/dsl_cuda/example/tvb/basic/logger/__init__.py b/dsl_cuda/example/tvb/basic/logger/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dsl_cuda/example/tvb/basic/logger/builder.py b/dsl_cuda/example/tvb/basic/logger/builder.py deleted file mode 100644 index 71b17e431f..0000000000 --- a/dsl_cuda/example/tvb/basic/logger/builder.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -Singleton logging builder. - - -.. moduleauthor:: Calin Pavel -.. moduleauthor:: Bogdan Neacsa -.. moduleauthor:: Lia Domide -.. moduleauthor:: Stuart A. Knock -.. moduleauthor:: Marmaduke Woodman - -""" - -import os -import weakref -import logging -import logging.config -from tvb.basic.profile import TvbProfile -from tvb.basic.config.profile_settings import MATLABLibraryProfile - - -class LoggerBuilder(object): - """ - Class taking care of uniform Python logger initialization. - It uses the Python native logging package. - It's purpose is just to offer a common mechanism for initializing all modules in a package. - """ - - def __init__(self, config_root): - """ - Prepare Python logger based on a configuration file. - :param: config_root - current package to configure logger for it. - - """ - if not isinstance(TvbProfile.current, MATLABLibraryProfile): - config_file_name = TvbProfile.current.LOGGER_CONFIG_FILE_NAME - package = __import__(config_root, globals(), locals(), ['__init__'], 0) - package_path = package.__path__[0] - # Specify logging configuration file for current package. - logging.config.fileConfig(os.path.join(package_path, config_file_name), disable_existing_loggers=False) - else: - logging.basicConfig(level=logging.DEBUG) - self._loggers = weakref.WeakValueDictionary() - - def build_logger(self, parent_module): - """ - Build a logger instance and return it - """ - self._loggers[parent_module] = logger = logging.getLogger(parent_module) - return logger - - def set_loggers_level(self, level): - for logger in self._loggers.values(): - logger.setLevel(level) - - -# We make sure a single instance of logger-builder is created. -if "GLOBAL_LOGGER_BUILDER" not in globals(): - - if TvbProfile.is_library_mode(): - GLOBAL_LOGGER_BUILDER = LoggerBuilder('tvb.basic.logger') - else: - GLOBAL_LOGGER_BUILDER = LoggerBuilder('tvb.config.logger') - - -def get_logger(parent_module=''): - """ - Function to retrieve a new Python logger instance for current module. - - :param parent_module: module name for which to create logger. - """ - return GLOBAL_LOGGER_BUILDER.build_logger(parent_module) diff --git a/dsl_cuda/example/tvb/basic/logger/library_logger.conf b/dsl_cuda/example/tvb/basic/logger/library_logger.conf deleted file mode 100644 index 16ad501154..0000000000 --- a/dsl_cuda/example/tvb/basic/logger/library_logger.conf +++ /dev/null @@ -1,78 +0,0 @@ -############################################ -## TVB - logging configuration. ## -############################################ -[loggers] -keys=root, tvb, tvb_basic_datatypes, tvb_basic_config, tvb_simulator, tvb_simulator_monitors - -[handlers] -keys=consoleHandler,fileHandler - -[formatters] -keys=simpleFormatter - -[logger_root] -## was: level=DEBUG, mv -level=INFO -handlers=consoleHandler, fileHandler -propagate=0 - -############################################ -## tvb specific logging ## -############################################ -[logger_tvb] -level=INFO -handlers=consoleHandler, fileHandler -qualname=tvb -propagate=0 - -[logger_tvb_basic_datatypes] -## was: level=WARNING mv -level=INFO -handlers=consoleHandler, fileHandler -qualname=tvb.datatypes -propagate=0 - -[logger_tvb_basic_config] -## was: level=WARNING mv -level=INFO -handlers=consoleHandler, fileHandler -qualname=tvb.basic.config -propagate=0 - -[logger_tvb_simulator] -## was: level=WARNING mv -level=INFO -handlers=consoleHandler, fileHandler -qualname=tvb.simulator -propagate=0 - -[logger_tvb_simulator_monitors] -level=INFO -handlers=consoleHandler, fileHandler -qualname=tvb.simulator.monitors -propagate=0 - -############################################ -## Handlers ## -############################################ - -[handler_consoleHandler] -class=StreamHandler -level=DEBUG -formatter=simpleFormatter -args=(sys.stderr,) - -[handler_fileHandler] -class=tvb.basic.logger.simple_handler.SimpleTimedRotatingFileHandler -level=INFO -formatter=simpleFormatter -# Generate a new file every midnight and keep logs for 30 days -args=('library.log', 'midnight', 1, 30) - -############################################ -## Formatters ## -############################################ - -[formatter_simpleFormatter] -format=%(asctime)s - %(levelname)s - %(name)s - %(message)s -datefmt= diff --git a/dsl_cuda/example/tvb/basic/logger/library_logger_test.conf b/dsl_cuda/example/tvb/basic/logger/library_logger_test.conf deleted file mode 100644 index 7d6387b100..0000000000 --- a/dsl_cuda/example/tvb/basic/logger/library_logger_test.conf +++ /dev/null @@ -1,68 +0,0 @@ -############################################ -## TVB - logging configuration. ## -############################################ -[loggers] -keys=root, tvb, tvb_basic_datatypes, tvb_basic_config, tvb_simulator - -[handlers] -keys=consoleHandler,fileHandler - -[formatters] -keys=simpleFormatter - -[logger_root] -level=DEBUG -handlers=fileHandler -propagate=0 - -############################################ -## tvb specific logging ## -############################################ -[logger_tvb] -level=INFO -handlers=fileHandler -qualname=tvb -propagate=0 - -[logger_tvb_basic_datatypes] -level=WARNING -handlers=fileHandler -qualname=tvb.datatypes -propagate=0 - -[logger_tvb_basic_config] -level=WARNING -handlers=fileHandler -qualname=tvb.basic.config -propagate=0 - -[logger_tvb_simulator] -level=WARNING -handlers=fileHandler -qualname=tvb.simulator -propagate=0 - -############################################ -## Handlers ## -############################################ - -[handler_consoleHandler] -class=StreamHandler -level=ERROR -formatter=simpleFormatter -args=(sys.stdout,) - -[handler_fileHandler] -class=tvb.basic.logger.simple_handler.SimpleTimedRotatingFileHandler -level=INFO -formatter=simpleFormatter -# Generate a new file every midnight and keep logs for 30 days -args=('library.log', 'midnight', 1, 30) - -############################################ -## Formatters ## -############################################ - -[formatter_simpleFormatter] -format=%(asctime)s - %(levelname)s - %(name)s - %(message)s -datefmt= diff --git a/dsl_cuda/example/tvb/basic/logger/simple_handler.py b/dsl_cuda/example/tvb/basic/logger/simple_handler.py deleted file mode 100644 index f7630a7f15..0000000000 --- a/dsl_cuda/example/tvb/basic/logger/simple_handler.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# -""" -This module contains a simple file handlers used to log messages -for different parts of application. - -.. moduleauthor:: Calin Pavel -""" - -import os -from logging.handlers import TimedRotatingFileHandler -from tvb.basic.profile import TvbProfile - - -class SimpleTimedRotatingFileHandler(TimedRotatingFileHandler): - """ - This is a custom rotating file handler which computes the full path for log file - depending on the TVB configuration. - """ - - def __init__(self, filename, when='h', interval=1, backupCount=0): - """ - Only set our logging path, and call superclass. - """ - log_file = os.path.join(TvbProfile.current.TVB_LOG_FOLDER, filename) - TimedRotatingFileHandler.__init__(self, log_file, when, interval, backupCount) diff --git a/dsl_cuda/example/tvb/basic/neotraits/__init__.py b/dsl_cuda/example/tvb/basic/neotraits/__init__.py deleted file mode 100644 index c7c8d24352..0000000000 --- a/dsl_cuda/example/tvb/basic/neotraits/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -Neotraits is a framework that lets you declare class attributes with type checking and introspection abilities. -The public api is in the neotraits.api module -""" - -# note: the api is in it's own module. -# This is a bit less convenient. -# But it prevents the annoying situation where -# importing neotraits.some_module will run __init__ -# and that one will import most modules in order to provide the api. -# That is not ideal because circular imports become likely and over-importing can slow startup. diff --git a/dsl_cuda/example/tvb/basic/neotraits/_attr.py b/dsl_cuda/example/tvb/basic/neotraits/_attr.py deleted file mode 100644 index 4399b4dde2..0000000000 --- a/dsl_cuda/example/tvb/basic/neotraits/_attr.py +++ /dev/null @@ -1,618 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -This private module implements concrete declarative attributes -""" -import types -import collections -import numpy -import logging -from ._declarative_base import _Attr -from .ex import TraitValueError, TraitTypeError, TraitAttributeError -import sys - -if sys.version_info[0] == 3: - import typing - if typing.TYPE_CHECKING: - from ._core import HasTraits - from tvb.basic.neotraits._declarative_base import MetaType - -# a logger for the whole traits system -log = logging.getLogger('tvb.traits') - - -class Attr(_Attr): - """ - An Attr declares the following about the attribute it describes: - * the type - * a default value shared by all instances - * if the value might be missing - * documentation - It will resolve to attributes on the instance. - """ - - # This class is a python data descriptor. - # For an introduction see https://docs.python.org/2/howto/descriptor.html - - def __init__( - self, field_type, default=None, doc='', label='', required=True, final=False, choices=None - ): - # type: (type, typing.Any, str, str, bool, bool, typing.Optional[tuple]) -> None - """ - :param field_type: the python type of this attribute - :param default: A shared default value. Behaves like class level attribute assignment. - Take care with mutable defaults. - :param doc: Documentation for this field. - :param label: A short description. - :param required: required fields should not be None. - :param final: Final fields can only be assigned once. - :param choices: A tuple of the values that this field is allowed to take. - """ - super(Attr, self).__init__() - self.field_type = field_type - self.default = default - self.doc = doc - self.label = label - self.required = bool(required) - self.final = bool(final) - self.choices = choices - - - def __validate(self, value): - """ check field_type and choices """ - if not isinstance(value, self.field_type): - raise TraitTypeError("Attribute can't be set to an instance of {}".format(type(value)), attr=self) - if self.choices is not None: - if value not in self.choices: - raise TraitValueError("Value {!r} must be one of {}".format(value, self.choices), attr=self) - - # subclass api - - def _post_bind_validate(self): - # type: () -> None - """ - Validates this instance of Attr. - This is called just after field_name is set, by MetaType. - We do checks here and not in init in order to give better error messages. - Attr should be considered initialized only after this has run - """ - if not isinstance(self.field_type, type): - msg = 'Field_type must be a type not {!r}. Did you mean to declare a default?'.format( - self.field_type - ) - raise TraitTypeError(msg, attr=self) - - skip_default_checks = self.default is None or isinstance(self.default, types.FunctionType) - - if not skip_default_checks: - self.__validate(self.default) - - # heuristic check for mutability. might be costly. hasattr(__hash__) is fastest but less reliable - try: - hash(self.default) - except TypeError: - log.warning('Field seems mutable and has a default value. ' - 'Consider using a lambda as a value factory \n attribute {}'.format(self)) - # we do not check here if we have a value for a required field - # it is too early for that, owner.__init__ has not run yet - - - def _validate_set(self, instance, value): - # type: ('HasTraits', typing.Any) -> typing.Any - """ - Called before updating the value of an attribute. - It checks the type *AND* returns the valid value. - You can override this for further checks. Call super to retain this check. - Raise if checks fail. - You should return a cleaned up value if validation passes - """ - if value is None: - if self.required: - raise TraitValueError("Attribute is required. Can't set to None", attr=self) - else: - return value - - self.__validate(value) - return value - - - # descriptor protocol - - def __get__(self, instance, owner): - # type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Any - self._assert_have_field_name() - if instance is None: - # called from class, not an instance - return self - # data is stored on the instance in a field with the same name - # If field is not on the instance yet, return the class level default - # (this attr instance is a class field, so the default is for the class) - # This is consistent with how class fields work before they are assigned and become instance bound - if self.field_name not in instance.__dict__: - if isinstance(self.default, types.FunctionType): - default = self.default() - else: - default = self.default - - # Unless we store the default on the instance, this will keep returning self.default() - # when the default is a function. So if the default is mutable, any changes to it are - # lost as a new one is created every time. - instance.__dict__[self.field_name] = default - - value = instance.__dict__[self.field_name] - if self.required and value is None: - raise TraitAttributeError('required attribute referenced before assignment. ' - 'Use a default or assign a value before reading it', attr=self) - return value - - - def __set__(self, instance, value): - # type: ('HasTraits', typing.Any) -> None - self._assert_have_field_name() - - if self.final: - # non-set to set final transition happens when instance stored value becomes not none - # getattr will call __get__. We want that in order to allow the default to be set. - # If __set__ is called before a __get__ then no defaults have been assigned. - # subtlety: if the value of this final field is not set then __get__ will raise - # getattr with a default value swallows that exception and returns false - present_value = getattr(instance, self.field_name, None) - if present_value is not None: - raise TraitAttributeError("can't write final attribute") - - value = self._validate_set(instance, value) - - instance.__dict__[self.field_name] = value - - - def _defined_on_str_helper(self): - if self.owner is not None: - return '{}.{}.{} = {}'.format( - self.owner.__module__, - self.owner.__name__, - self.field_name, - type(self).__name__ - ) - else: - return '{}'.format(type(self).__name__) - - - def __str__(self): - return '{}(field_type={}, default={!r}, required={})'.format( - self._defined_on_str_helper(), self.field_type, self.default, self.required - ) - - - -class Final(Attr): - """ - An attribute that can only be set once. - If a default is provided it counts as a set, so it cannot be written to. - Note that if the default is a mutable type, the value is shared with all instances - of the owning class. - We cannot enforce true constancy in python - """ - - def __init__(self, default=None, field_type=None, doc='', label=''): - """ - :param default: The constant value - """ - # it would be nice if we could turn the default immutable. But this is unreasonable work in python - # maybe a deep copy? - if default is not None: - field_type = type(default) - - if default is None and field_type is None: - raise ValueError('Either a default or a field_type is required') - - super(Final, self).__init__( - field_type=field_type, default=default, doc=doc, label=label, required=True, final=True - ) - - -class List(Attr): - """ - The attribute is a list of values. - Choices and type are reinterpreted as applying not to the list but to the elements of it - """ - - def __init__(self, of=object, default=(), doc='', label='', final=False, choices=None): - # type: (type, tuple, str, str, bool, typing.Optional[tuple]) -> None - super(List, self).__init__( - field_type=collections.Sequence, - default=default, - doc=doc, - label=label, - required=True, - final=final, - choices=None, - ) - self.element_type = of - self.element_choices = choices - - - def __validate_elements(self, value): - """ check that all elements are of the declared type and one of the declared choices """ - for i, el in enumerate(value): - if not isinstance(el, self.element_type): - raise TraitTypeError("value[{}] can't be of type {}".format(i, type(el)), attr=self) - - if self.element_choices is not None: - for i, el in enumerate(value): - if el not in self.element_choices: - raise TraitValueError( - "value[{}]=={!r} must be one of {}".format(i, el, self.element_choices), - attr=self - ) - - - def _post_bind_validate(self): - super(List, self)._post_bind_validate() - # check that the default contains elements of the declared type - self.__validate_elements(self.default) - - - def _validate_set(self, instance, value): - value = super(List, self)._validate_set(instance, value) - if value is None: - # value is optional and missing, nothing to do here - return - self.__validate_elements(value) - return value - - - # __get__ __set__ here only for typing purposes, for better ide checking and autocomplete - - - def __get__(self, instance, owner): - # type: (typing.Optional[HasTraits], MetaType) -> typing.Sequence - return super(List, self).__get__(instance, owner) - - - def __set__(self, instance, value): - # type: (HasTraits, typing.Sequence) -> None - super(List, self).__set__(instance, value) - - def __str__(self): - return '{}(of={}, default={!r}, required={})'.format( - self._defined_on_str_helper(), self.element_type, self.default, self.required - ) - - -class _Number(Attr): - - def __validate(self, value): - """ value should be safely cast to field type and choices must be enforced """ - if not isinstance(value, (int, float, complex, numpy.number)): - # we have to check that the value is numeric before the can_cast check - # as can_cast works with dtype strings as well - # can_cast('i8', 'i32') - raise TraitTypeError("can't be set to {!r}. Need a number.".format(value), attr=self) - if not numpy.can_cast(value, self.field_type, 'safe'): - raise TraitTypeError("can't be set to {!r}. No safe cast.".format(value), attr=self) - if self.choices is not None: - if value not in self.choices: - raise TraitValueError("value {!r} must be one of {}".format(value, self.choices), attr=self) - - def _post_bind_validate(self): - if self.default is not None: - self.__validate(self.default) - - - def _validate_set(self, instance, value): - if value is None: - if self.required: - raise TraitValueError("is required. Can't set to None", attr=self) - else: - return value - - self.__validate(value) - return self.field_type(value) - - - -class Int(_Number): - """ - Declares an integer - This is different from Attr(field_type=int). - The former enforces int subtypes - This allows all integer types, including numpy ones that can be safely cast to the declared type - according to numpy rules - """ - - def __init__( - self, field_type=int, default=0, doc='', label='', required=True, final=False, choices=None - ): - super(_Number, self).__init__( - field_type=field_type, - default=default, - doc=doc, - label=label, - required=required, - final=final, - choices=choices, - ) - - def _post_bind_validate(self): - if not issubclass(self.field_type, (int, numpy.integer)): - msg = 'field_type must be a python int or a numpy.integer not {!r}.'.format(self.field_type) - raise TraitTypeError(msg, attr=self) - # super call after the field_type check above - super(Int, self)._post_bind_validate() - - - -class Float(_Number): - """ - Declares a float. - This is different from Attr(field_type=float). - The former enforces float subtypes. - This allows any type that can be safely cast to the declared float type - according to numpy rules. - - Reading and writing this attribute is slower than a plain python attribute. - In performance sensitive code you might want to use plain python attributes - or even better local variables. - """ - - def __init__( - self, field_type=float, default=0, doc='', label='', required=True, final=False, choices=None - ): - super(_Number, self).__init__( - field_type=field_type, - default=default, - doc=doc, - label=label, - required=required, - final=final, - choices=choices, - ) - - def _post_bind_validate(self): - if not issubclass(self.field_type, (float, numpy.floating)): - msg = 'field_type must be a python float or a numpy.floating not {!r}.'.format(self.field_type) - raise TraitTypeError(msg, attr=self) - # super call after the field_type check above - super(Float, self)._post_bind_validate() - - - -class Dim(Final): - """ - A symbol that defines a dimension in a numpy array shape. - It can only be set once. It is an int. - Dimensions have to be set before any NArrays that reference them are used. - """ - - any = object() # sentinel - - def __init__(self, doc=''): - super(Dim, self).__init__(field_type=int, doc=doc) - - - -class NArray(Attr): - """ - Declares a numpy array. - dtype enforces the dtype. The default dtype is float32. - An optional symbolic shape can be given, as a tuple of Dim attributes from the owning class. - The shape will be enforced, but no broadcasting will be done. - domain declares what values are allowed in this array. - It can be any object that can be checked for membership - Defaults are checked if they are in the declared domain. - For performance reasons this does not happen on every attribute set. - """ - - def __init__( - self, - default=None, - required=True, - doc='', - label='', - dtype=numpy.float, - shape=None, - dim_names=(), - domain=None, - ): - # type: (numpy.ndarray, bool, str, str, typing.Union[numpy.dtype, type, str], typing.Optional[typing.Tuple[Dim, ...]], typing.Tuple[str, ...], typing.Container) -> None - """ - :param dtype: The numpy datatype. Defaults to float64. This is checked by neotraits. - :param shape: An optional symbolic shape, tuple of Dim's declared on the owning class - :param dim_names: Optional names for the names of the dimensions - :param domain: Any type that can be checked for membership like xrange. - Represents the expected domain of the values in the array. - """ - - self.dtype = numpy.dtype(dtype) - - super(NArray, self).__init__( - field_type=numpy.ndarray, default=default, required=required, doc=doc, label=label - ) - self.shape = shape - self.domain = domain # anything that supports 3.1 in domain - self.dim_names = dim_names - - if self.shape is not None: # we have a shape - if self.dim_names: # and dim_names - # ensure that len(shape) == len(dim_names) - if len(self.shape) != len(self.dim_names): - raise TraitValueError('shape contradicts dim_names', attr=self) - - # maybe a over zealous type check - for d in self.shape: - if d is not Dim.any and type(d) != Dim: - raise TraitValueError("shape elements must be Dim's not {}".format(type(d)), attr=self) - self.ndim = len(self.shape) - elif self.dim_names: # no shape but dim_names - self.ndim = len(self.dim_names) - else: - self.ndim = None - - def __validate(self, value): - """ check that ndim's and dtypes match""" - if self.ndim is not None and value.ndim != self.ndim: - raise TraitValueError("can't be set to an array with ndim {}".format(value.ndim), attr=self) - - if not numpy.can_cast(value.dtype, self.dtype, 'safe'): - raise TraitTypeError("can't be set to an array of dtype {}".format(value.dtype), attr=self) - - - def _post_bind_validate(self): - if self.default is None: - return - if not isinstance(self.default, numpy.ndarray): - msg = 'default {} should be a numpy.ndarray'.format(self.default) - raise TraitTypeError(msg, attr=self) - - self.__validate(self.default) - - # we make the default a read only array - self.default.setflags(write=False) - - # check that the default array values are in the declared domain - # this may be expensive - if self.domain is not None and self.default is not None: - for e in self.default.flat: - if e not in self.domain: - msg = 'default contains values out of the declared domain. Ex {}'.format(e) - # log.warning('{} \n attribute {}'.format(msg, self)) broken by mv - - break - - - def _lookup_expected_shape(self, instance): - """ look up expected shape on the instance """ - expected_shape = [] - - for dim_attr in self.shape: - if dim_attr is Dim.any: - expected_dim = Dim.any - else: - try: - # invoke Dim's __get__(instance) - expected_dim = getattr(instance, dim_attr.field_name) - except TraitAttributeError: - # re-raise with a better error message - msg = "Narray's shape references undefined dimension <{}>. " \ - "Set it before accessing this array" - raise TraitAttributeError(msg.format(dim_attr.field_name), attr=self) - expected_shape.append(expected_dim) - return expected_shape - - - def _validate_set(self, instance, value): - value = super(NArray, self)._validate_set(instance, value) - if value is None: - # value is optional and missing, nothing to do here - return - self.__validate(value) - # we should know here the concrete shape - # check it - - if self.shape is not None: - expected_shape = self._lookup_expected_shape(instance) - - for expected_dim, value_dim in zip(expected_shape, value.shape): - if expected_dim is Dim.any: - continue - if value_dim != expected_dim: - raise TraitValueError( - 'Shape mismatch. Expected {}. Given {}. Not broadcasting'.format( - expected_shape, value.shape - ) - ) - - return value.astype(self.dtype) - - - # here only for typing purposes, so ide's can get better suggestions - def __get__(self, instance, owner): - # type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Union[numpy.ndarray, 'NArray'] - return super(NArray, self).__get__(instance, owner) - - def __set__(self, instance, value): - # type: (HasTraits, numpy.ndarray) -> None - super(NArray, self).__set__(instance, value) - - def __str__(self): - return '{}(label={!r}, dtype={}, default={!r}, dim_names={}, ndim={}, required={})'.format( - self._defined_on_str_helper(), - self.label, - self.dtype, - self.default, - self.dim_names, - self.ndim, - self.required, - ) - - -class Range(object): - """ - Defines a domain like the one that numpy.arange generates - Points are precisely equidistant but the largest point is <= hi - """ - - def __init__(self, lo, hi, step=1.0): - self.lo = lo - self.hi = hi - self.step = step - - def __contains__(self, item): - """ true if item between lo and high. ignores the step""" - return self.lo <= item < self.hi - - def to_array(self): - return numpy.arange(self.lo, self.hi, self.step) - - def __repr__(self): - return 'Range(lo={}, hi={}, step={})'.format(self.lo, self.hi, self.step) - - -class LinspaceRange(object): - """ - Defines a domain with precise endpoints but the points are not precisely equidistant - Similar to numpy.linspace - """ - - def __init__(self, lo, hi, npoints=50): - self.lo = lo - self.hi = hi - self.npoints = npoints - - def __contains__(self, item): - """ true if item between lo and high. ignores the step""" - return self.lo <= item < self.hi - - def to_array(self): - return numpy.linspace(self.lo, self.hi, self.npoints) - - def __repr__(self): - return 'LinspaceRange(lo={}, hi={}, step={})'.format(self.lo, self.hi, self.npoints) diff --git a/dsl_cuda/example/tvb/basic/neotraits/_core.py b/dsl_cuda/example/tvb/basic/neotraits/_core.py deleted file mode 100644 index 7da40f3a8a..0000000000 --- a/dsl_cuda/example/tvb/basic/neotraits/_core.py +++ /dev/null @@ -1,257 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -This module implements neotraits. -It is private only to shield public usage of the imports and logger. -""" -import sys -import uuid -import numpy -from six import add_metaclass -from ._attr import Attr -from ._declarative_base import _Property, MetaType -from .info import trait_object_str, trait_object_repr_html, narray_summary_info -from .ex import TraitAttributeError, TraitTypeError, TraitError -from tvb.basic.logger.builder import get_logger - -if sys.version_info[0] == 3: - import typing - - - -class CachedTraitProperty(_Property): - # This is a *non-data* descriptor - # Once a field with the same name exists on the instance it will - # take precedence before this non-data descriptor - # This means that after the first __get__ which sets a same-name instance attribute - # this will not be called again. Thus this is a cache. - # To refresh the cache one could delete the instance attr. - - def __get__(self, instance, owner): - # type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Any - if instance is None: - return self - ret = self.fget(instance) - # mhtodo the error messages generated by this will be confusing - # noinspection PyProtectedMember - ret = self.attr._validate_set(instance, ret) - # set the instance same-named attribute which becomes the cache - setattr(instance, self.attr.field_name, ret) - return ret - - - -class TraitProperty(_Property): - - def __get__(self, instance, owner): - # type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Any - if instance is None: - return self - ret = self.fget(instance) - # mhtodo the error messages generated by this will be confusing - # noinspection PyProtectedMember - ret = self.attr._validate_set(instance, ret) - return ret - - def setter(self, fset): - # return a copy of self that has fset. It will overwrite the current one in the - # owning class as the attributes have the same name and the setter comes after the getter - return type(self)(self.fget, self.attr, fset) - - def __set__(self, instance, value): - # type: ('HasTraits', typing.Any) -> None - if self.fset is None: - raise TraitAttributeError("Can't set attribute. Property is read only. In " + str(self)) - # mhtodo the error messages generated by this will be confusing - # noinspection PyProtectedMember - value = self.attr._validate_set(instance, value) - self.fset(instance, value) - - def __delete__(self, instance): - raise TraitAttributeError("can't delete a traitproperty") - - def __str__(self): - return 'TraitProperty(attr={}, fget={}'.format(self.attr, self.fget) - - - -def trait_property(attr): - # type: (Attr) -> typing.Callable[[typing.Callable], TraitProperty] - """ - A read only property that has a declarative attribute associated with. - :param attr: the declarative attribute that describes this property - """ - if not isinstance(attr, Attr): - raise TypeError('@trait_property(attr) attribute argument required.') - - def deco(func): - return TraitProperty(func, attr) - return deco - - -def cached_trait_property(attr): - # type: (Attr) -> typing.Callable[[typing.Callable], CachedTraitProperty] - """ - A lazy evaluated attribute. - Transforms the decorated method into a cached property. - The method will be called once to compute a value. - The value will be stored in an instance attribute with - the same name as the decorated function. - :param attr: the declarative attribute that describes this property - """ - if not isinstance(attr, Attr): - raise TypeError('@cached_trait_property(attr) attribute argument required.') - - def deco(func): - return CachedTraitProperty(func, attr) - return deco - - -@add_metaclass(MetaType) -class HasTraits(object): - - # The base __init__ and __str__ rely upon metadata gathered by MetaType - # we could have injected these in MetaType, but we don't need meta powers - # this is simpler to grok - - gid = Attr(field_type=uuid.UUID) - - def __init__(self, **kwargs): - """ - The default init accepts kwargs for all declarative attrs - and sets them to the given values - """ - # cls just to emphasise that the metadata is on the class not on instances - cls = type(self) - - # defined before the kwargs loop, so that a title or gid Attr can overwrite this defaults - - self.gid = uuid.uuid4() - """ - gid identifies a specific instance of the hastraits - it is used by serializers as an identifier. - For non-datatype HasTraits this is less usefull but still - provides a unique id for example for a model configuration - """ # these strings are interpreted as docstrings by many tools, not by python though - - self.title = '{} gid: {}'.format(self.__class__.__name__, self.gid) - """ a generic name that the user can set to easily recognize the instance """ - - for k, v in kwargs.items(): - if k not in cls.declarative_attrs: - raise TraitTypeError( - 'Valid kwargs for type {!r} are: {}. You have given: {!r}'.format( - cls, repr(cls.declarative_attrs), k - ) - ) - setattr(self, k, v) - - self.tags = {} - """ - a generic collections of tags. The trait system is not using them - nor should any other code. They should not alter behaviour - They should describe the instance for the user - """ - - self.log = get_logger(self.__class__.__module__) - - - def __str__(self): - return trait_object_str(self) - - - def _repr_html_(self): - return trait_object_repr_html(self) - - def tag(self, tag_name, tag_value=None): - # type: (str, str) -> None - """ - Add a tag to this trait instance. - The tags are for user to recognize and categorize the instances - They should never influence the behaviour of the program - :param tag_name: an arbitrary tag - :param tag_value: an optional tag value - """ - self.tags[str(tag_name)] = str(tag_value) - - - def validate(self): - """ - Check that the internal invariants of this class are satisfied. - Not meant to ensure that that is the case. - Use configure for that. - The default configure calls this before it returns. - It complains about missing required attrs - Can be overridden in subclasses - """ - cls = type(self) - - for k in cls.declarative_attrs: - # read all declarative attributes. This will trigger errors if they are - # in an invalid state, like beeing required but not set - getattr(self, k) - - - def configure(self, *args, **kwargs): - """ - Ensures that invariant of the class are satisfied. - Override to compute uninitialized state of the class. - """ - self.validate() - - - def summary_info(self): - # type: () -> typing.Dict[str, str] - """ - A more structured __str__ - A 2 column table represented as a dict of str->str - The default __str__ and html representations of this object are derived from - this table. - Override this method and return such a table filled with instance information - that informs the user about your instance - """ - cls = type(self) - ret = {'Type': cls.__name__} - if self.title: - ret['title'] = str(self.title) - - for aname in cls.declarative_attrs: - try: - attr_field = getattr(self, aname) - if isinstance(attr_field, numpy.ndarray): - ret.update(narray_summary_info(attr_field, ar_name=aname)) - elif isinstance(attr_field, HasTraits): - ret[aname] = attr_field.title - else: - ret[aname] = repr(attr_field) - except TraitError: - ret[aname] = 'unavailable' - return ret diff --git a/dsl_cuda/example/tvb/basic/neotraits/_declarative_base.py b/dsl_cuda/example/tvb/basic/neotraits/_declarative_base.py deleted file mode 100644 index d9037bf70b..0000000000 --- a/dsl_cuda/example/tvb/basic/neotraits/_declarative_base.py +++ /dev/null @@ -1,277 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -This private module implements the neotraits declarative machinery. -The basic Attribute Property and their automatic discovery by the Metaclass. -""" -import abc -import inspect -import logging - -from tvb.basic.neotraits.ex import TraitTypeError, TraitAttributeError -from tvb.basic.neotraits.info import auto_docstring - -import sys - -if sys.version_info[0] == 3: - import typing - -# a logger for the whole traits system -log = logging.getLogger('tvb.traits') - - -class _Attr(object): - """ - A private base class of Attributes - Contains the minimum expected functionality by the declarative system. - """ - def __init__(self): - self.field_name = None # type: typing.Optional[str] # to be set by metaclass - self.owner = None # type: typing.Optional[MetaType] # to be set by metaclass - - def _assert_have_field_name(self): - """ check that the fields we expect the be set by metaclass have been set """ - if self.field_name is None: - # this is the case if the descriptor is not in a class of type MetaType - raise AttributeError("Declarative attributes can only be declared in subclasses of HasTraits") - - # subclass api - - def _post_bind_validate(self): - # type: () -> None - """ - Validates this instance of Attr. - This is called just after field_name is set, by MetaType. - We do checks here and not in init in order to give better error messages. - Attr should be considered initialized only after this has run - """ - - # descriptor protocol - - def __get__(self, instance, owner): - # type: (typing.Optional['HasTraits'], 'MetaType') -> typing.Any - self._assert_have_field_name() - if instance is None: - # called from class, not an instance - return self - # data is stored on the instance in a field with the same name - return instance.__dict__[self.field_name] - - - def __set__(self, instance, value): - # type: ('HasTraits', typing.Any) -> None - self._assert_have_field_name() - instance.__dict__[self.field_name] = value - - - def __delete__(self, instance): - raise TraitAttributeError("can't be deleted", attr=self) - - # A modest attempt of making Attr immutable - - def __setattr__(self, key, value): - """ After owner is set disallow any field assignment """ - if getattr(self, 'owner', None) is not None: - raise TraitAttributeError( - "Can't change an Attr after it has been bound to a class." - "Reusing Attr instances for different fields is not supported." - ) - super(_Attr, self).__setattr__(key, value) - - - def __delattr__(self, item): - raise TraitAttributeError("Deleting an Attr field is not supported.") - - - -class _Property(object): - def __init__(self, fget, attr, fset=None): - # type: (typing.Callable, _Attr, typing.Optional[typing.Callable]) -> None - self.fget = fget - self.fset = fset - self.__doc__ = fget.__doc__ - self.attr = attr - - - -class MetaType(abc.ABCMeta): - """ - Metaclass for the declarative traits. - We inherit ABCMeta so that the users may use @abstractmethod without having to - deal with 2 meta-classes. - Even though we do this we don't support the dynamic registration of subtypes to these abc's - """ - - # This is a python metaclass. - # For an introduction see https://docs.python.org/2/reference/datamodel.html - - # here to avoid some hasattr; is None etc checks. And to make pycharm happy - # should be harmless and shadowed by _declarative_attrs on the returned classes - _own_declarative_attrs = () # type: typing.Tuple[str, ...] # name of all declarative fields on this class - _own_declarative_props = () # type: typing.Tuple[str, ...] - - # A record of all the classes we have created. - # note: As this holds references and not weakrefs it will prevent class garbage collection. - # Deleting classes would break get_known_subclasses and this cache - __classes = {} # type: typing.Dict[str, type] - - - def get_known_subclasses(cls, include_abstract=False, include_itself=False): - # type: (bool) -> typing.Dict[str, typing.Type[MetaType]] - """ - Returns all subclasses that exist *now*. - New subclasses can be created after this call, - after importing a new module or dynamically creating subclasses. - Use with care. Use after most relevant modules have been imported. - """ - ret = {} - - for k, c in cls.__classes.items(): - if issubclass(c, cls): - if inspect.isabstract(c) and not include_abstract: - continue - if c == cls and not include_itself: - continue - ret.update({k: c}) - return ret - - def __walk_mro_inherit_declarations(cls, declaration): - ret = [] - for super_cls in cls.mro(): - if isinstance(super_cls, MetaType): - for attr_name in getattr(super_cls, declaration): - if attr_name not in ret: # attr was overridden, don't duplicate - ret.append(attr_name) - return tuple(ret) - - @property - def declarative_attrs(cls): - # type: () -> typing.Tuple[str, ...] - """ - Gathers all the declared attributes, including the ones declared in superclasses. - This is a meta-property common to all classes with this metatype - """ - # We walk the mro here. This is in contrast with _own_declarative_attrs which is - # not computed but cached by the metaclass on the class. - # Caching is faster, but comes with the cost of taking care of the caches validity - return cls.__walk_mro_inherit_declarations('_own_declarative_attrs') - - @property - def declarative_props(cls): - # type: () -> typing.Tuple[str, ...] - """ - Gathers all the declared props, including the ones declared in superclasses. - This is a meta-property common to all classes with this metatype - """ - return cls.__walk_mro_inherit_declarations('_own_declarative_props') - - # here only to have a similar invocation like declarative_attrs - # namely type(traited_instance).own_declarative_attrs - # consider the traited_instance._own_declarative_attrs discouraged - @property - def own_declarative_attrs(cls): - return cls._own_declarative_attrs - - - def __new__(mcs, type_name, bases, namespace): - """ - Gathers the names of all declarative fields. - Tell each Attr of the name of the field it is bound to. - """ - # gather all declarative attributes defined in the class to be constructed. - # validate all declarations before constructing the new type - attrs = [] - props = [] - - for k, v in namespace.items(): - if isinstance(v, _Attr): - attrs.append(k) - elif isinstance(v, _Property): - props.append(k) - - # record the names of the declarative attrs in the _own_declarative_attrs field - if '_own_declarative_attrs' in namespace: - raise TraitTypeError('class attribute _own_declarative_attrs is reserved in traited classes') - if '_own_declarative_props' in namespace: - raise TraitTypeError('class attribute _own_declarative_props is reserved in traited classes') - - namespace['_own_declarative_attrs'] = tuple(attrs) - namespace['_own_declarative_props'] = tuple(props) - # construct the class - cls = super(MetaType, mcs).__new__(mcs, type_name, bases, namespace) - - # inform the Attr instances about the class their are bound to - for attr_name in attrs: - v = namespace[attr_name] - v.field_name = attr_name - v.owner = cls - # noinspection PyProtectedMember - v._post_bind_validate() - - # do the same for props. - for prop_name in props: - v = namespace[prop_name].attr - v.field_name = prop_name - v.owner = cls - # noinspection PyProtectedMember - v._post_bind_validate() - - # update docstring. Note that this is only possible if cls was created by a metatype in python - setattr(cls, '__doc__', auto_docstring(cls)) - - # update the HasTraits class registry - mcs.__classes.update({cls.__name__: cls}) - return cls - - - # note: Any methods defined here are metamethods, visible from all classes with this metatype - # AClass.field if not found on AClass will be looked up on the metaclass - # If you just want a regular private method here name it def __lala(cls) - # double __ not for any privacy but to reduce namespace pollution - # double __ will mangle the names and make such lookups fail - - # warn about dynamic Attributes - - - def __setattr__(self, key, value): - """ - Complain if TraitedClass.a = Attr() - Traits assumes that all attributes are statically declared in the class body - """ - if isinstance(value, _Attr): - log.warning('dynamically assigned Attributes are not supported') - super(MetaType, self).__setattr__(key, value) - - - def __delattr__(self, item): - if isinstance(getattr(self, item, None), _Attr): - log.warning('Dynamically removing Attributes is not supported') - super(MetaType, self).__delattr__(item) diff --git a/dsl_cuda/example/tvb/basic/neotraits/api.py b/dsl_cuda/example/tvb/basic/neotraits/api.py deleted file mode 100644 index de159f1852..0000000000 --- a/dsl_cuda/example/tvb/basic/neotraits/api.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -The public api of the neotraits package. -""" - -from ._core import HasTraits, trait_property, cached_trait_property -from .info import narray_describe, narray_summary_info -from ._attr import Attr, Int, Float, NArray, Final, List, Range, LinspaceRange, Dim diff --git a/dsl_cuda/example/tvb/basic/neotraits/ex.py b/dsl_cuda/example/tvb/basic/neotraits/ex.py deleted file mode 100644 index 81f04577e5..0000000000 --- a/dsl_cuda/example/tvb/basic/neotraits/ex.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - - -class TraitError(Exception): - def __init__(self, msg='', trait=None, attr=None): - self.trait = trait - self.attr = attr - super(TraitError, self).__init__(msg) - - - def __str__(self): - lines = [self.args[0]] - if self.attr: - lines.append(' attribute {}'.format(self.attr)) - if self.trait: - lines.append(' class {}'.format(type(self.trait).__name__)) - - return '\n'.join(lines) - - - -class TraitAttributeError(TraitError, AttributeError): - pass - - -class TraitValueError(TraitError, ValueError): - pass - - -class TraitTypeError(TraitError, TypeError): - pass diff --git a/dsl_cuda/example/tvb/basic/neotraits/info.py b/dsl_cuda/example/tvb/basic/neotraits/info.py deleted file mode 100644 index e8e51d8719..0000000000 --- a/dsl_cuda/example/tvb/basic/neotraits/info.py +++ /dev/null @@ -1,165 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -Functions that inform a user about the state of a traited class or object. - -Some of these functions are here so that they won't clutter the core trait implementation. -""" - -import numpy -import sys - -if sys.version_info[0] == 3: - import typing - - -def auto_docstring(cls): - """ generate a docstring for the new class in which the Attrs are documented """ - header = 'Traited class [{}.{}]'.format(cls.__module__, cls.__name__) - - doc = [ - header, - len(header) * '^', - '', - ] - if cls.__doc__ is not None: - doc.extend([cls.__doc__, '']) - - doc.extend([ - 'Attributes declared', - '"""""""""""""""""""', - '' - ]) - # a rst definition list for all attributes - for attr_name in cls.declarative_attrs: - attr = getattr(cls, attr_name) - # the standard repr of the attribute - doc.append('{} : {}'.format(attr_name, str(attr))) - # and now the doc property - for line in attr.doc.splitlines(): - doc.append(' ' + line.lstrip()) - doc.append('') - - if cls.declarative_props: - doc.extend([ - '', - 'Properties declared', - '"""""""""""""""""""', - '' - ]) - - for prop_name in cls.declarative_props: - prop = getattr(cls, prop_name) - # the standard repr - doc.append(' {} : {}'.format(prop_name, str(prop))) - # now fish the docstrings - for line in prop.attr.doc.splitlines(): - doc.append(' ' + line.lstrip()) - if prop.fget.__doc__ is not None: - for line in prop.fget.__doc__.splitlines(): - doc.append(' ' + line.lstrip()) - - doc = '\n'.join(doc) - - return doc - - -def narray_summary_info(ar, ar_name='', omit_shape=False): - # type: (numpy.ndarray, str, bool) -> typing.Dict[str, str] - """ - A 2 column table represented as a dict of str->str - """ - if ar is None: - return {'is None': 'True'} - - ret = {} - if not omit_shape: - ret.update({'shape': str(ar.shape), 'dtype': str(ar.dtype)}) - - if ar.size == 0: - ret['is empty'] = 'True' - return ret - - if ar.dtype.kind in 'iufc': - has_nan = numpy.isnan(ar).any() - if has_nan: - ret['has NaN'] = 'True' - ret['[min, median, max]'] = '[{:g}, {:g}, {:g}]'.format(ar.min(), numpy.median(ar), ar.max()) - - if ar_name: - return {ar_name + ' ' + k: v for k, v in ret.items()} - else: - return ret - - -def narray_describe(ar): - # type: (numpy.ndarray) -> str - summary = narray_summary_info(ar) - ret = [] - for k in sorted(summary): - ret.append('{:<12}{}'.format(k, summary[k])) - return '\n'.join(ret) - - -# these are here and not on HasTraits just so that that class is not -# complicated by irrelevant string formatting - - -def trait_object_str(self): - cls = type(self) - summary = self.summary_info() - result = ['{} ('.format(cls.__name__)] - maxlenk = max(len(k) for k in summary) - - for k in sorted(summary): - result.append(' {:.<{}} {}'.format(k + ' ', maxlenk, summary[k])) - result.append(')') - return '\n'.join(result) - - -def trait_object_repr_html(self): - cls = type(self) - result = [ - '', - '

{}

'.format(cls.__name__), - '', - '', - ] - - summary = self.summary_info() - - for k in sorted(summary): - row_fmt = '' - result.append(row_fmt.format(k, summary[k])) - - result += ['
value
{}
{}
'] - - return '\n'.join(result) diff --git a/dsl_cuda/example/tvb/basic/profile.py b/dsl_cuda/example/tvb/basic/profile.py deleted file mode 100644 index 3263c5e755..0000000000 --- a/dsl_cuda/example/tvb/basic/profile.py +++ /dev/null @@ -1,165 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -TVB Profile Manager (top level in TVB profile & settings). - -This class is responsible for referring towards application settings, -based on current running environment (e.g. dev vs deployment), or developer profile choice (e.g. web vs console). - -.. moduleauthor:: Lia Domide -.. moduleauthor:: Mihai Andrei -.. moduleauthor:: Bogdan Neacsa - -""" - -import sys -import importlib -from tvb.basic.config.environment import Environment -from tvb.basic.config.profile_settings import BaseSettingsProfile - - -class TvbProfile(object): - """ - ENUM-like class with current TVB profile and accepted values. - """ - - LIBRARY_PROFILE = "LIBRARY_PROFILE" - COMMAND_PROFILE = "COMMAND_PROFILE" - WEB_PROFILE = "WEB_PROFILE" - MATLAB_PROFILE = "MATLAB_PROFILE" - - TEST_LIBRARY_PROFILE = "TEST_LIBRARY_PROFILE" - TEST_POSTGRES_PROFILE = "TEST_POSTGRES_PROFILE" - TEST_SQLITE_PROFILE = "TEST_SQLITE_PROFILE" - - ALL = [LIBRARY_PROFILE, COMMAND_PROFILE, WEB_PROFILE, MATLAB_PROFILE, - TEST_POSTGRES_PROFILE, TEST_SQLITE_PROFILE, TEST_LIBRARY_PROFILE] - - REGISTERED_PROFILES = {} - - CURRENT_PROFILE_NAME = None - - current = BaseSettingsProfile(False) - env = Environment() - - @classmethod - def set_profile(cls, selected_profile, in_operation=False, run_init=True): - """ - Sets TVB profile and do related initializations. - """ - - # Ensure Python is using UTF-8 encoding (otherwise default encoding is ASCII) - # We should make sure UTF-8 gets set before reading from any TVB files - # e.g. TVB_STORAGE will differ if the .tvb.configuration file contains non-ascii bytes - # most of the comments in the simulator are having pieces outside of ascii coverage - if not cls.env.is_distribution() and sys.getdefaultencoding().lower() != 'utf-8': - old_out = sys.stdout - if sys.version_info[0] < 3: - reload(sys) - sys.setdefaultencoding("utf-8") - else: - importlib.reload(sys) - sys.setdefaultencoding('utf-8') - sys.stdout = old_out - - if selected_profile is not None: - cls._load_framework_profiles(selected_profile) - cls._build_profile_class(selected_profile, in_operation, run_init) - - @classmethod - def _build_profile_class(cls, selected_profile, in_operation=False, run_init=True): - """ - :param selected_profile: Profile name to be loaded. - """ - - if selected_profile in cls.REGISTERED_PROFILES: - current_class = cls.REGISTERED_PROFILES[selected_profile] - - cls.current = current_class() - cls.CURRENT_PROFILE_NAME = selected_profile - - if in_operation: - # set flags IN_OPERATION, before initialize** calls, to avoid LoggingBuilder being created there - cls.current.prepare_for_operation_mode() - - if cls.env.is_distribution(): - # initialize deployment first, because in case of a contributor setup this tried to reload - # and initialize_profile loads already too many tvb modules, - # making the reload difficult and prone to more failures - cls.current.initialize_for_deployment() - if run_init: - cls.current.initialize_profile() - - else: - msg = "Invalid profile name %r, expected one of %r" - msg %= (selected_profile, cls.ALL) - raise Exception(msg) - - @classmethod - def _load_framework_profiles(cls, new_profile): - - from tvb.basic.config.profile_settings import LibrarySettingsProfile, TestLibraryProfile, MATLABLibraryProfile - cls.REGISTERED_PROFILES[TvbProfile.LIBRARY_PROFILE] = LibrarySettingsProfile - cls.REGISTERED_PROFILES[TvbProfile.TEST_LIBRARY_PROFILE] = TestLibraryProfile - cls.REGISTERED_PROFILES[TvbProfile.MATLAB_PROFILE] = MATLABLibraryProfile - - if not cls.is_library_mode(new_profile): - try: - from tvb.config.profile_settings import CommandSettingsProfile, WebSettingsProfile - from tvb.config.profile_settings import TestPostgresProfile, TestSQLiteProfile - - cls.REGISTERED_PROFILES[TvbProfile.COMMAND_PROFILE] = CommandSettingsProfile - cls.REGISTERED_PROFILES[TvbProfile.WEB_PROFILE] = WebSettingsProfile - cls.REGISTERED_PROFILES[TvbProfile.TEST_POSTGRES_PROFILE] = TestPostgresProfile - cls.REGISTERED_PROFILES[TvbProfile.TEST_SQLITE_PROFILE] = TestSQLiteProfile - - except ImportError: - pass - - @staticmethod - def is_library_mode(new_profile=None): - - lib_profiles = [TvbProfile.LIBRARY_PROFILE, TvbProfile.TEST_LIBRARY_PROFILE] - result = (new_profile in lib_profiles - or (new_profile is None and TvbProfile.CURRENT_PROFILE_NAME in lib_profiles) - or not TvbProfile.env.is_framework_present()) - - # Make sure default settings are not failing because we are not finding some modules - if (new_profile is None and TvbProfile.CURRENT_PROFILE_NAME is None and - not TvbProfile.env.is_framework_present()): - TvbProfile.set_profile(TvbProfile.LIBRARY_PROFILE) - - return result - - @staticmethod - def is_first_run(): - - return TvbProfile.current.manager.is_first_run() diff --git a/dsl_cuda/example/tvb/basic/readers.py b/dsl_cuda/example/tvb/basic/readers.py deleted file mode 100644 index ff50a79a75..0000000000 --- a/dsl_cuda/example/tvb/basic/readers.py +++ /dev/null @@ -1,260 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# TheVirtualBrain-Scientific Package. This package holds all simulators, and -# analysers necessary to run brain-simulations. You can use it stand alone or -# in conjunction with TheVirtualBrain-Framework Package. See content of the -# documentation-folder for more details. See also http://www.thevirtualbrain.org -# -# (c) 2012-2017, Baycrest Centre for Geriatric Care ("Baycrest") and others -# -# This program is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software Foundation, -# either version 3 of the License, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. See the GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with this -# program. If not, see . -# -# -# CITATION: -# When using The Virtual Brain for scientific publications, please cite it as follows: -# -# Paula Sanz Leon, Stuart A. Knock, M. Marmaduke Woodman, Lia Domide, -# Jochen Mersmann, Anthony R. McIntosh, Viktor Jirsa (2013) -# The Virtual Brain: a simulator of primate brain network dynamics. -# Frontiers in Neuroinformatics (7:10. doi: 10.3389/fninf.2013.00010) -# -# - -""" -This module contains basic reading mechanism for default DataType fields. - -.. moduleauthor:: Lia Domide -""" - -try: - H5PY_SUPPORT = True - import h5py as hdf5 -except ImportError: - H5PY_SUPPORT = False - -import os -import numpy -import zipfile -import uuid -from tempfile import gettempdir -from scipy import io as scipy_io -from tvb.basic.logger.builder import get_logger - - - -class H5Reader(object): - """ - Read one or many numpy arrays from a H5 file. - """ - - def __init__(self, h5_path): - - self.logger = get_logger(__name__) - if H5PY_SUPPORT: - self.hfd5_source = hdf5.File(h5_path, 'r', libver='latest') - else: - self.logger.warning("You need h5py properly installed in order to load from a HDF5 source.") - - - def read_field(self, field, log_exception=True): - - try: - return self.hfd5_source['/' + field][()] - except Exception: - if log_exception: - self.logger.exception("Could not read from %s field" % field) - raise ReaderException("Could not read from %s field" % field) - - - def read_optional_field(self, field): - try: - return self.read_field(field, log_exception=False) - except ReaderException: - return None - - - -class FileReader(object): - """ - Read one or multiple numpy arrays from a text/bz2 file. - """ - - def __init__(self, file_path): - - self.logger = get_logger(__name__) - self.file_path = file_path - self.file_stream = file_path - - - def read_array(self, dtype=numpy.float64, skip_rows=0, use_cols=None, matlab_data_name=None): - - self.logger.debug("Starting to read from: " + str(self.file_path)) - - try: - # Try to read H5: - if self.file_path.endswith('.h5'): - self.logger.error("Not yet implemented read from a ZIP of H5 files!") - return numpy.array([]) - - # Try to read NumPy: - if self.file_path.endswith('.txt') or self.file_path.endswith('.bz2'): - return self._read_text(self.file_stream, dtype, skip_rows, use_cols) - - if self.file_path.endswith('.npz') or self.file_path.endswith(".npy"): - return numpy.load(self.file_stream) - - # Try to read Matlab format: - return self._read_matlab(self.file_stream, matlab_data_name) - - except Exception as e: - msg = "Could not read from %s file \n %s" % (self.file_path, e) - self.logger.exception(msg) - raise ReaderException(msg) - - - def _read_text(self, file_stream, dtype, skip_rows, use_cols): - - array_result = numpy.loadtxt(file_stream, dtype=dtype, skiprows=skip_rows, usecols=use_cols) - return array_result - - - def _read_matlab(self, file_stream, matlab_data_name=None): - - if self.file_path.endswith(".mtx"): - return scipy_io.mmread(file_stream) - - if self.file_path.endswith(".mat"): - matlab_data = scipy_io.matlab.loadmat(file_stream) - return matlab_data[matlab_data_name] - - - def read_gain_from_brainstorm(self): - - if not self.file_path.endswith('.mat'): - raise ReaderException("Brainstorm format is expected in a Matlab file not %s" % self.file_path) - - mat = scipy_io.loadmat(self.file_stream) - expected_fields = ['Gain', 'GridLoc', 'GridOrient'] - - for field in expected_fields: - if field not in mat.keys(): - raise ReaderException("Brainstorm format is expecting field %s" % field) - - gain, loc, ori = (mat[field] for field in expected_fields) - return (gain.reshape((gain.shape[0], -1, 3)) * ori).sum(axis=-1) - - - -class ZipReader(object): - """ - Read one or many numpy arrays from a ZIP archive. - """ - - def __init__(self, zip_path): - - self.logger = get_logger(__name__) - self.zip_archive = zipfile.ZipFile(zip_path) - - def has_file_like(self, file_name): - for actual_name in self.zip_archive.namelist(): - if file_name in actual_name: - return True - return False - - def read_array_from_file(self, file_name, dtype=numpy.float64, skip_rows=0, use_cols=None, matlab_data_name=None): - - matching_file_name = None - for actual_name in self.zip_archive.namelist(): - if file_name in actual_name and not actual_name.startswith("__MACOSX"): - matching_file_name = actual_name - break - - if matching_file_name is None: - # broken by mv - # self.logger.warning("File %r not found in ZIP." % file_name) - raise ReaderException("File %r not found in ZIP." % file_name) - - zip_entry = self.zip_archive.open(matching_file_name, 'r') - - if matching_file_name.endswith(".bz2"): - temp_file = copy_zip_entry_into_temp(zip_entry, matching_file_name) - file_reader = FileReader(temp_file) - result = file_reader.read_array(dtype, skip_rows, use_cols, matlab_data_name) - os.remove(temp_file) - return result - - file_reader = FileReader(matching_file_name) - file_reader.file_stream = zip_entry - return file_reader.read_array(dtype, skip_rows, use_cols, matlab_data_name) - - - def read_optional_array_from_file(self, file_name, dtype=numpy.float64, skip_rows=0, - use_cols=None, matlab_data_name=None): - try: - return self.read_array_from_file(file_name, dtype, skip_rows, use_cols, matlab_data_name) - except ReaderException: - return numpy.array([], dtype=dtype) - - - -class ReaderException(Exception): - pass - - - -def try_get_absolute_path(relative_module, file_suffix): - """ - :param relative_module: python module to be imported. When import of this fails, we will return the file_suffix - :param file_suffix: In case this is already an absolute path, return it immediately, - otherwise append it after the module path - :return: Try to build an absolute path based on a python module and a file-suffix - """ - - result_full_path = file_suffix - - if not os.path.isabs(file_suffix): - - try: - module_import = __import__(relative_module, globals(), locals(), ["__init__"]) - result_full_path = os.path.join(os.path.dirname(module_import.__file__), file_suffix) - - except ImportError: - logger = get_logger(__name__) - logger.exception("Could not import tvb_data Python module for default data-set!") - - return result_full_path - - - -def copy_zip_entry_into_temp(source, file_suffix, buffer_size=1024 * 1024): - """ - Copy a ZIP Entry into a new file created under system temporary folder. - - :param source: ZipEntry - :param file_suffix: String suffix to be added to the temporary file name - :param buffer_size: Buffer size used when copying the file-content - :return: the path towards the new file. - """ - - result_dest_path = os.path.join(gettempdir(), "tvb_" + str(uuid.uuid1()) + file_suffix) - result_dest = open(result_dest_path, 'wb') - - while 1: - copy_buffer = source.read(buffer_size) - if copy_buffer: - result_dest.write(copy_buffer) - else: - break - - source.close() - result_dest.close() - - return result_dest_path From 133775215b93665a4536f4c96f180b087dfdc11f Mon Sep 17 00:00:00 2001 From: DeLaVlag Date: Wed, 15 Jul 2020 18:26:47 +0200 Subject: [PATCH 06/10] minor bugfix regarding coupling --- dsl_cuda/tmpl8_CUDA.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsl_cuda/tmpl8_CUDA.py b/dsl_cuda/tmpl8_CUDA.py index 2ae16095fa..08af086c39 100755 --- a/dsl_cuda/tmpl8_CUDA.py +++ b/dsl_cuda/tmpl8_CUDA.py @@ -183,7 +183,7 @@ // rec_n is used for the scaling over nodes % for m in range(len(coupling)): % for cdp in (coupling[m].derived_parameters): - % if cdp.expression: + % if cdp.expression and (cdp.expression !='None' and cdp.expression !='none'): ${cdp.name} *= ${cdp.expression}; % endif / % endfor From 3e8b0d75f9f465b1c9a38c381dddfec7bb548bb0 Mon Sep 17 00:00:00 2001 From: DeLaVlag Date: Thu, 3 Sep 2020 15:27:47 +0200 Subject: [PATCH 07/10] xml changens, template and example fixes --- dsl_cuda/XMLmodels/epileptor_CUDA.xml | 16 +++--- dsl_cuda/XMLmodels/kuramoto_CUDA.xml | 2 +- dsl_cuda/XMLmodels/oscillator_CUDA.xml | 4 +- dsl_cuda/XMLmodels/rwongwang_CUDA.xml | 5 +- dsl_cuda/example/cuda_run.py | 40 ++++++------- dsl_cuda/example/cuda_setup.py | 79 +++++++++++++++++--------- dsl_cuda/tmpl8_CUDA.py | 10 +++- 7 files changed, 93 insertions(+), 63 deletions(-) diff --git a/dsl_cuda/XMLmodels/epileptor_CUDA.xml b/dsl_cuda/XMLmodels/epileptor_CUDA.xml index 2bb043ffd0..028fb14596 100755 --- a/dsl_cuda/XMLmodels/epileptor_CUDA.xml +++ b/dsl_cuda/XMLmodels/epileptor_CUDA.xml @@ -3,7 +3,7 @@ description="Rate based 2D oscillator for TVB" value=""> - + @@ -43,21 +43,21 @@ - - + + - + - + - + - +
@@ -81,7 +81,7 @@ - +
diff --git a/dsl_cuda/XMLmodels/kuramoto_CUDA.xml b/dsl_cuda/XMLmodels/kuramoto_CUDA.xml index 14fe242fb3..b9aefd1e7c 100755 --- a/dsl_cuda/XMLmodels/kuramoto_CUDA.xml +++ b/dsl_cuda/XMLmodels/kuramoto_CUDA.xml @@ -20,7 +20,7 @@ - +
diff --git a/dsl_cuda/XMLmodels/oscillator_CUDA.xml b/dsl_cuda/XMLmodels/oscillator_CUDA.xml index cf631fb95c..0f3e08d432 100755 --- a/dsl_cuda/XMLmodels/oscillator_CUDA.xml +++ b/dsl_cuda/XMLmodels/oscillator_CUDA.xml @@ -32,8 +32,8 @@ It scales both I and the long range coupling term.."/> - - + +
diff --git a/dsl_cuda/example/cuda_run.py b/dsl_cuda/example/cuda_run.py index f96121cd4a..9b95ee9d01 100755 --- a/dsl_cuda/example/cuda_run.py +++ b/dsl_cuda/example/cuda_run.py @@ -16,7 +16,7 @@ def make_kernel(self, source_file, warp_size, block_dim_x, args, lineinfo=False, with open(source_file, 'r') as fd: source = fd.read() source = source.replace('M_PI_F', '%ff' % (np.pi, )) - opts = ["--ptxas-options=-v"]#, '-maxrregcount=32', '-lineinfo'] + opts = ['--ptxas-options=-v', '-maxrregcount=32', '-lineinfo'] if lineinfo: opts.append('-lineinfo') opts.append('-DWARP_SIZE=%d' % (warp_size, )) @@ -62,6 +62,10 @@ def make_gpu_data(self, data):#{{{ gpu_data[name] = gpuarray.to_gpu(self.cf(array)) return gpu_data#}}} + def gpu_info(self): + cmd = "nvidia-smi -q -d MEMORY,UTILIZATION" + returned_value = os.system(cmd) # returns the exit code in unix + print('returned value:', returned_value) def run_simulation(self, weights, lengths, params_matrix, speeds, logger, args, n_nodes, n_work_items, n_params, nstep, n_inner_steps, buf_len, states, dt, min_speed): @@ -76,7 +80,7 @@ def run_simulation(self, weights, lengths, params_matrix, speeds, logger, args, data[name] = np.zeros(shape + base_shape, 'f') gpu_data = self.make_gpu_data(data)#{{{ - logger.info('history shape %r', data['state'].shape) + # logger.info('history shape %r', data['state'].shape) logger.info('on device mem: %.3f MiB' % (self.nbytes(data) / 1024 / 1024, ))#}}} # setup CUDA stuff#{{{ @@ -94,31 +98,29 @@ def run_simulation(self, weights, lengths, params_matrix, speeds, logger, args, # setup simulation#{{{ tic = time.time() - logger.info('nstep %i', nstep) + # logger.info('nstep %i', nstep) streams = [drv.Stream() for i in range(32)] events = [drv.Event() for i in range(32)] tavg_unpinned = [] tavg = drv.pagelocked_zeros(data['tavg'].shape, dtype=np.float32) - logger.info('data[tavg].shape %s', data['tavg'].shape) + # logger.info('data[tavg].shape %s', data['tavg'].shape) #}}} - # adjust gridDim to keep block size <= 1024 {{{ - block_size_lim = 1024 - n_coupling_per_block = block_size_lim // args.node_threads - n_coupling_blocks = args.n_coupling // n_coupling_per_block - if n_coupling_blocks == 0: - n_coupling_per_block = args.n_coupling - n_coupling_blocks = 1 - final_block_dim = n_coupling_per_block, args.node_threads, 1 - final_grid_dim = speeds.size, n_coupling_blocks - logger.info('final block dim %r', final_block_dim) + gridx = args.n_coupling // args.blockszx + gridy = args.n_speed // args.blockszy + final_block_dim = args.blockszx, args.blockszy, 1 + final_grid_dim = gridx, gridy + + # logger.info('final block dim %r', final_block_dim) logger.info('final grid dim %r', final_grid_dim) - assert n_coupling_per_block * n_coupling_blocks == args.n_coupling #}}} - logger.info('gpu_data[lengts] %s', gpu_data['lengths'].shape) - logger.info('nnodes %r', n_nodes) + # assert n_coupling_per_block * n_coupling_blocks == args.n_coupling #}}} + + # logger.info('gpu_data[lengts] %s', gpu_data['lengths'].shape) + # logger.info('nnodes %r', n_nodes) + # logger.info('gpu_data[lengths] %r', gpu_data['lengths']) # run simulation#{{{ - logger.info('submitting work') + # logger.info('submitting work') for i in range(nstep): # event = events[i % 32] @@ -139,7 +141,7 @@ def run_simulation(self, weights, lengths, params_matrix, speeds, logger, args, tavg, gpu_data['tavg'].ptr) - logger.info('kernel finish..') + # logger.info('kernel finish..') # release pinned memory tavg = np.array(tavg_unpinned) return tavg diff --git a/dsl_cuda/example/cuda_setup.py b/dsl_cuda/example/cuda_setup.py index ea261cebf1..3f6fef6044 100755 --- a/dsl_cuda/example/cuda_setup.py +++ b/dsl_cuda/example/cuda_setup.py @@ -8,6 +8,8 @@ import argparse import os, sys +from numpy import corrcoef + sys.path.insert(0, os.path.join(os.getcwd(), os.pardir)) import LEMS2CUDA @@ -42,7 +44,7 @@ def __init__(self): self.states = 1 def tvb_connectivity(self, speed, global_coupling, dt=0.1): - white_matter = connectivity.Connectivity.from_file(source_file="paupau.zip") + white_matter = connectivity.Connectivity.from_file(source_file="connectivity_68.zip") white_matter.configure() white_matter.speed = np.array([speed]) white_matter_coupling = coupling.Linear(a=global_coupling) @@ -64,7 +66,7 @@ def parse_args(self): # {{{ parser.add_argument('--node_threads', default=1, type=int) parser.add_argument('--model', choices=['Rwongwang', 'Kuramoto', 'Epileptor', 'Oscillator', \ - 'Oscillatorref', 'Kuramotoref', 'Rwongwangref'], + 'Oscillatorref', 'Kuramotoref', 'Rwongwangref', 'Epileptorref'], help="neural mass model to be used during the simulation", default='Oscillator' ) @@ -75,6 +77,11 @@ def parse_args(self): # {{{ parser.add_argument('-b', '--bench', default="regular", type=str, help="What to bench: regular, numba, cuda") + parser.add_argument('-bx', '--blockszx', default="32", type=int, help="Enter block size x") + parser.add_argument('-by', '--blockszy', default="32", type=int, help="Enter block size y") + + parser.add_argument('-val', '--validate', default=False, help="Enable validation to refmodels") + args = parser.parse_args() return args @@ -123,7 +130,7 @@ def check_results(self, n_nodes, n_work_items, tavg, weights, speeds, couplings, logger.info('result OK') def start_cuda(self, logger): - logger.info('start Cuda run') + # logger.info('start Cuda run') from cuda_run import CudaRun cudarun = CudaRun() tavg_data = cudarun.run_simulation(self.weights, self.lengths, self.params, self.speeds, logger, @@ -132,9 +139,11 @@ def start_cuda(self, logger): # Todo: fix this for cuda # self.check_results(self.n_nodes, self.n_work_items, tavg_data, self.weights, self.speeds, self.couplings, logger, self.args) + return tavg_data def set_CUDAmodel_dir(self): - self.args.filename = os.path.join((os.path.dirname(os.path.abspath(__file__))), os.pardir,'CUDAmodels', + # self.args.filename = os.path.join(os.path.join(os.getcwd(), os.pardir), 'CUDAmodels', self.args.model.lower() + '.c') + self.args.filename = os.path.join((os.path.dirname(os.path.abspath(__file__))), os.pardir, 'CUDAmodels', self.args.model.lower() + '.c') def set_states(self): @@ -149,48 +158,62 @@ def set_states(self): elif 'epileptor' in self.args.model.lower(): self.states = 6 + def compare_with_ref(self, logger, tavg0): + self.args.model = self.args.model + 'ref' + self.set_CUDAmodel_dir() + tavg1 = self.start_cuda(logger) + + # compare output to check if same as template + comparison = (tavg0.ravel() == tavg1.ravel()) + logger.info('Templated version is similar to original %d:', comparison.all()) + logger.info('Correlation coefficient: %f', corrcoef(tavg0.ravel(), tavg1.ravel())[0, 1]) + def startsim(self): tic = time.time() logging.basicConfig(level=logging.DEBUG if self.args.verbose else logging.INFO) logger = logging.getLogger('[TVB_CUDA]') - logger.info('dt %f', self.dt) - logger.info('nstep %d', self.nstep) - logger.info('n_inner_steps %f', self.n_inner_steps) - if self.args.test and self.args.n_time % 200: - logger.warning('rerun w/ a multiple of 200 time steps (-n 200, -n 400, etc) for testing') # }}} - - # setup data - logger.info('weights.shape %s', self.weights.shape) - logger.info('lengths.shape %s', self.lengths.shape) - logger.info('n_nodes %d', self.n_nodes) - - # couplings and speeds are not derived from the regular TVB connection setup routine. - # these parameters are swooped every GPU spawn - logger.info('single connectome, %d x %d parameter space', self.ns, self.nc) - logger.info('%d total num threads', self.ns * self.nc) - logger.info('min_speed %f', self.min_speed) - logger.info('real buf_len %d, using power of 2 %d', self.buf_len_, self.buf_len) + # logger.info('dt %f', self.dt) + # logger.info('nstep %d', self.nstep) + # logger.info('n_inner_steps %f', self.n_inner_steps) + # if self.args.test and self.args.n_time % 200: + # logger.warning('rerun w/ a multiple of 200 time steps (-n 200, -n 400, etc) for testing') # }}} + # + # # setup data + # logger.info('weights.shape %s', self.weights.shape) + # logger.info('lengths.shape %s', self.lengths.shape) + # logger.info('n_nodes %d', self.n_nodes) + # + # # couplings and speeds are not derived from the regular TVB connection setup routine. + # # these parameters are swooped every GPU spawn + # logger.info('single connectome, %d x %d parameter space', self.ns, self.nc) + # logger.info('%d total num threads', self.ns * self.nc) + # logger.info('min_speed %f', self.min_speed) + # logger.info('real buf_len %d, using power of 2 %d', self.buf_len_, self.buf_len) self.set_CUDAmodel_dir() self.set_states() - logger.info('number of states %d', self.states) - logger.info('filename %s', self.args.filename) - logger.info('model %s', self.args.model) + # logger.info('number of states %d', self.states) + # logger.info('filename %s', self.args.filename) + # logger.info('model %s', self.args.model) tac = time.time() - logger.info("Setup in: {}".format(tac - tic)) + # logger.info("Setup in: {}".format(tac - tic)) + + tavg0 = self.start_cuda(logger) + toc = time.time() - self.start_cuda(logger) + if (self.args.validate): + self.compare_with_ref(logger, tavg0) toc = time.time() elapsed = toc - tic logger.info('Finished python simulation successfully in: %0.3f', elapsed) - logger.info('%0.3f M step/s', 1e-6 * self.nstep * self.n_inner_steps * self.n_work_items / elapsed) - logger.info('finished') + # logger.info('%0.3f M step/s', 1e-6 * self.nstep * self.n_inner_steps * self.n_work_items / elapsed) + # logger.info('finished') if __name__ == '__main__': diff --git a/dsl_cuda/tmpl8_CUDA.py b/dsl_cuda/tmpl8_CUDA.py index 08af086c39..9acad9b8fa 100755 --- a/dsl_cuda/tmpl8_CUDA.py +++ b/dsl_cuda/tmpl8_CUDA.py @@ -113,6 +113,10 @@ float ${state_var.name} = 0.0; % endfor + % for td in (dynamics.time_derivatives): + float ${td.name} = 0.0; + % endfor / + //***// This is only initialization of the observable for (unsigned int i_node = 0; i_node < n_node; i_node++) { @@ -126,7 +130,7 @@ for (unsigned int t = i_step; t < (i_step + n_step); t++) { //***// This is the loop over nodes, which also should stay the same - for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + for (int i_node = 0; i_node < n_node; i_node++) { % for m in range(len(coupling)): % for cdp in (coupling[m].derived_parameters): @@ -211,13 +215,13 @@ // This is dynamics step and the update in the state of the node % for i, tim_der in enumerate(dynamics.time_derivatives): - ${tim_der.name} += dt * (${tim_der.expression}); + ${tim_der.name} = dt * (${tim_der.expression}); % endfor % if noisepresent: // Add noise (if noise components are present in model), integrate with stochastic forward euler and wrap it up % for ds, td in zip(dynamics.state_variables, dynamics.time_derivatives): - ${ds.name} += nsig * curand_normal2(&crndst).x; + ${ds.name} += nsig * curand_normal(&crndst) + ${td.name}; % endfor / ##% else: ##% for ds, td in zip(dynamics.state_variables, dynamics.time_derivatives): From cea87ab0a3c707044e6d5122dc92fe40cb0c82ba Mon Sep 17 00:00:00 2001 From: Marmaduke Woodman Date: Fri, 11 Sep 2020 10:19:46 +0200 Subject: [PATCH 08/10] reorganize files for reception in TVB --- lems/__init__.py | 0 .../tvb/dsl_cuda}/CUDAmodels/epileptor.c | 0 .../tvb/dsl_cuda}/CUDAmodels/kuramoto.c | 0 .../tvb/dsl_cuda}/CUDAmodels/kuramotoref.c | 0 .../tvb/dsl_cuda}/CUDAmodels/oscillator.c | 0 .../tvb/dsl_cuda}/CUDAmodels/oscillatorref.c | 0 .../tvb/dsl_cuda}/CUDAmodels/refs/balloon.c | 0 .../tvb/dsl_cuda}/CUDAmodels/refs/covar.c | 0 .../dsl_cuda}/CUDAmodels/refs/kuramoto_network.c | 0 .../tvb/dsl_cuda}/CUDAmodels/refs/network.c | 0 .../tvb/dsl_cuda}/CUDAmodels/refs/network_2d.c | 0 .../tvb/dsl_cuda}/CUDAmodels/refs/network_rww.c | 0 .../tvb/dsl_cuda}/CUDAmodels/rwongwang.c | 0 .../tvb/dsl_cuda}/CUDAmodels/rwongwangref.c | 0 .../tvb/dsl_cuda/GPUmemindex.png | Bin .../tvb/dsl_cuda}/LEMS2CUDA.py | 0 .../tvb/dsl_cuda/README.md | 0 .../tvb/dsl_cuda/XMLmodels/__init__.py | 0 .../tvb/dsl_cuda}/XMLmodels/epileptor_CUDA.xml | 0 .../tvb/dsl_cuda}/XMLmodels/kuramoto_CUDA.xml | 0 .../tvb/dsl_cuda}/XMLmodels/oscillator_CUDA.xml | 0 .../tvb/dsl_cuda}/XMLmodels/rwongwang_CUDA.xml | 0 .../tvb/dsl_cuda}/__init__.py | 0 .../tvb/dsl_cuda/example}/__init__.py | 0 .../tvb/dsl_cuda}/example/benchAll.sh | 0 .../tvb/dsl_cuda}/example/cuda_run.py | 0 .../tvb/dsl_cuda}/example/cuda_setup.py | 0 .../tvb/dsl_cuda}/example/runthings | 0 .../tvb/dsl_cuda/lems}/__init__.py | 0 .../tvb/dsl_cuda/lems}/base/__init__.py | 0 .../tvb/dsl_cuda/lems}/base/base.py | 0 .../tvb/dsl_cuda/lems}/base/errors.py | 0 .../tvb/dsl_cuda/lems}/base/map.py | 0 .../tvb/dsl_cuda/lems}/base/stack.py | 0 .../tvb/dsl_cuda/lems}/base/util.py | 0 .../tvb/dsl_cuda/lems}/model/__init__.py | 0 .../tvb/dsl_cuda/lems}/model/component.py | 0 .../tvb/dsl_cuda/lems}/model/dynamics.py | 0 .../tvb/dsl_cuda/lems}/model/fundamental.py | 0 .../tvb/dsl_cuda/lems}/model/model.py | 0 .../tvb/dsl_cuda/lems}/model/simulation.py | 0 .../tvb/dsl_cuda/lems}/model/structure.py | 0 .../tvb/dsl_cuda/lems}/parser/LEMS.py | 0 .../tvb/dsl_cuda/lems}/parser/__init__.py | 0 .../tvb/dsl_cuda/lems}/parser/expr.py | 0 .../tvb/dsl_cuda}/tmpl8_CUDA.py | 0 46 files changed, 0 insertions(+), 0 deletions(-) delete mode 100755 lems/__init__.py rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/epileptor.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/kuramoto.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/kuramotoref.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/oscillator.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/oscillatorref.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/refs/balloon.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/refs/covar.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/refs/kuramoto_network.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/refs/network.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/refs/network_2d.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/refs/network_rww.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/rwongwang.c (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/CUDAmodels/rwongwangref.c (100%) rename GPUmemindex.png => scientific_library/tvb/dsl_cuda/GPUmemindex.png (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/LEMS2CUDA.py (100%) rename README.md => scientific_library/tvb/dsl_cuda/README.md (100%) rename __init__.py => scientific_library/tvb/dsl_cuda/XMLmodels/__init__.py (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/XMLmodels/epileptor_CUDA.xml (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/XMLmodels/kuramoto_CUDA.xml (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/XMLmodels/oscillator_CUDA.xml (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/XMLmodels/rwongwang_CUDA.xml (100%) rename {dsl_cuda/XMLmodels => scientific_library/tvb/dsl_cuda}/__init__.py (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda/example}/__init__.py (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/example/benchAll.sh (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/example/cuda_run.py (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/example/cuda_setup.py (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/example/runthings (100%) rename {dsl_cuda/example => scientific_library/tvb/dsl_cuda/lems}/__init__.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/base/__init__.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/base/base.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/base/errors.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/base/map.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/base/stack.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/base/util.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/model/__init__.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/model/component.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/model/dynamics.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/model/fundamental.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/model/model.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/model/simulation.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/model/structure.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/parser/LEMS.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/parser/__init__.py (100%) rename {lems => scientific_library/tvb/dsl_cuda/lems}/parser/expr.py (100%) rename {dsl_cuda => scientific_library/tvb/dsl_cuda}/tmpl8_CUDA.py (100%) diff --git a/lems/__init__.py b/lems/__init__.py deleted file mode 100755 index e69de29bb2..0000000000 diff --git a/dsl_cuda/CUDAmodels/epileptor.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/epileptor.c similarity index 100% rename from dsl_cuda/CUDAmodels/epileptor.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/epileptor.c diff --git a/dsl_cuda/CUDAmodels/kuramoto.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/kuramoto.c similarity index 100% rename from dsl_cuda/CUDAmodels/kuramoto.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/kuramoto.c diff --git a/dsl_cuda/CUDAmodels/kuramotoref.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/kuramotoref.c similarity index 100% rename from dsl_cuda/CUDAmodels/kuramotoref.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/kuramotoref.c diff --git a/dsl_cuda/CUDAmodels/oscillator.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/oscillator.c similarity index 100% rename from dsl_cuda/CUDAmodels/oscillator.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/oscillator.c diff --git a/dsl_cuda/CUDAmodels/oscillatorref.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/oscillatorref.c similarity index 100% rename from dsl_cuda/CUDAmodels/oscillatorref.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/oscillatorref.c diff --git a/dsl_cuda/CUDAmodels/refs/balloon.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/refs/balloon.c similarity index 100% rename from dsl_cuda/CUDAmodels/refs/balloon.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/refs/balloon.c diff --git a/dsl_cuda/CUDAmodels/refs/covar.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/refs/covar.c similarity index 100% rename from dsl_cuda/CUDAmodels/refs/covar.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/refs/covar.c diff --git a/dsl_cuda/CUDAmodels/refs/kuramoto_network.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/refs/kuramoto_network.c similarity index 100% rename from dsl_cuda/CUDAmodels/refs/kuramoto_network.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/refs/kuramoto_network.c diff --git a/dsl_cuda/CUDAmodels/refs/network.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/refs/network.c similarity index 100% rename from dsl_cuda/CUDAmodels/refs/network.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/refs/network.c diff --git a/dsl_cuda/CUDAmodels/refs/network_2d.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/refs/network_2d.c similarity index 100% rename from dsl_cuda/CUDAmodels/refs/network_2d.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/refs/network_2d.c diff --git a/dsl_cuda/CUDAmodels/refs/network_rww.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/refs/network_rww.c similarity index 100% rename from dsl_cuda/CUDAmodels/refs/network_rww.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/refs/network_rww.c diff --git a/dsl_cuda/CUDAmodels/rwongwang.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/rwongwang.c similarity index 100% rename from dsl_cuda/CUDAmodels/rwongwang.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/rwongwang.c diff --git a/dsl_cuda/CUDAmodels/rwongwangref.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/rwongwangref.c similarity index 100% rename from dsl_cuda/CUDAmodels/rwongwangref.c rename to scientific_library/tvb/dsl_cuda/CUDAmodels/rwongwangref.c diff --git a/GPUmemindex.png b/scientific_library/tvb/dsl_cuda/GPUmemindex.png similarity index 100% rename from GPUmemindex.png rename to scientific_library/tvb/dsl_cuda/GPUmemindex.png diff --git a/dsl_cuda/LEMS2CUDA.py b/scientific_library/tvb/dsl_cuda/LEMS2CUDA.py similarity index 100% rename from dsl_cuda/LEMS2CUDA.py rename to scientific_library/tvb/dsl_cuda/LEMS2CUDA.py diff --git a/README.md b/scientific_library/tvb/dsl_cuda/README.md similarity index 100% rename from README.md rename to scientific_library/tvb/dsl_cuda/README.md diff --git a/__init__.py b/scientific_library/tvb/dsl_cuda/XMLmodels/__init__.py similarity index 100% rename from __init__.py rename to scientific_library/tvb/dsl_cuda/XMLmodels/__init__.py diff --git a/dsl_cuda/XMLmodels/epileptor_CUDA.xml b/scientific_library/tvb/dsl_cuda/XMLmodels/epileptor_CUDA.xml similarity index 100% rename from dsl_cuda/XMLmodels/epileptor_CUDA.xml rename to scientific_library/tvb/dsl_cuda/XMLmodels/epileptor_CUDA.xml diff --git a/dsl_cuda/XMLmodels/kuramoto_CUDA.xml b/scientific_library/tvb/dsl_cuda/XMLmodels/kuramoto_CUDA.xml similarity index 100% rename from dsl_cuda/XMLmodels/kuramoto_CUDA.xml rename to scientific_library/tvb/dsl_cuda/XMLmodels/kuramoto_CUDA.xml diff --git a/dsl_cuda/XMLmodels/oscillator_CUDA.xml b/scientific_library/tvb/dsl_cuda/XMLmodels/oscillator_CUDA.xml similarity index 100% rename from dsl_cuda/XMLmodels/oscillator_CUDA.xml rename to scientific_library/tvb/dsl_cuda/XMLmodels/oscillator_CUDA.xml diff --git a/dsl_cuda/XMLmodels/rwongwang_CUDA.xml b/scientific_library/tvb/dsl_cuda/XMLmodels/rwongwang_CUDA.xml similarity index 100% rename from dsl_cuda/XMLmodels/rwongwang_CUDA.xml rename to scientific_library/tvb/dsl_cuda/XMLmodels/rwongwang_CUDA.xml diff --git a/dsl_cuda/XMLmodels/__init__.py b/scientific_library/tvb/dsl_cuda/__init__.py similarity index 100% rename from dsl_cuda/XMLmodels/__init__.py rename to scientific_library/tvb/dsl_cuda/__init__.py diff --git a/dsl_cuda/__init__.py b/scientific_library/tvb/dsl_cuda/example/__init__.py similarity index 100% rename from dsl_cuda/__init__.py rename to scientific_library/tvb/dsl_cuda/example/__init__.py diff --git a/dsl_cuda/example/benchAll.sh b/scientific_library/tvb/dsl_cuda/example/benchAll.sh similarity index 100% rename from dsl_cuda/example/benchAll.sh rename to scientific_library/tvb/dsl_cuda/example/benchAll.sh diff --git a/dsl_cuda/example/cuda_run.py b/scientific_library/tvb/dsl_cuda/example/cuda_run.py similarity index 100% rename from dsl_cuda/example/cuda_run.py rename to scientific_library/tvb/dsl_cuda/example/cuda_run.py diff --git a/dsl_cuda/example/cuda_setup.py b/scientific_library/tvb/dsl_cuda/example/cuda_setup.py similarity index 100% rename from dsl_cuda/example/cuda_setup.py rename to scientific_library/tvb/dsl_cuda/example/cuda_setup.py diff --git a/dsl_cuda/example/runthings b/scientific_library/tvb/dsl_cuda/example/runthings similarity index 100% rename from dsl_cuda/example/runthings rename to scientific_library/tvb/dsl_cuda/example/runthings diff --git a/dsl_cuda/example/__init__.py b/scientific_library/tvb/dsl_cuda/lems/__init__.py similarity index 100% rename from dsl_cuda/example/__init__.py rename to scientific_library/tvb/dsl_cuda/lems/__init__.py diff --git a/lems/base/__init__.py b/scientific_library/tvb/dsl_cuda/lems/base/__init__.py similarity index 100% rename from lems/base/__init__.py rename to scientific_library/tvb/dsl_cuda/lems/base/__init__.py diff --git a/lems/base/base.py b/scientific_library/tvb/dsl_cuda/lems/base/base.py similarity index 100% rename from lems/base/base.py rename to scientific_library/tvb/dsl_cuda/lems/base/base.py diff --git a/lems/base/errors.py b/scientific_library/tvb/dsl_cuda/lems/base/errors.py similarity index 100% rename from lems/base/errors.py rename to scientific_library/tvb/dsl_cuda/lems/base/errors.py diff --git a/lems/base/map.py b/scientific_library/tvb/dsl_cuda/lems/base/map.py similarity index 100% rename from lems/base/map.py rename to scientific_library/tvb/dsl_cuda/lems/base/map.py diff --git a/lems/base/stack.py b/scientific_library/tvb/dsl_cuda/lems/base/stack.py similarity index 100% rename from lems/base/stack.py rename to scientific_library/tvb/dsl_cuda/lems/base/stack.py diff --git a/lems/base/util.py b/scientific_library/tvb/dsl_cuda/lems/base/util.py similarity index 100% rename from lems/base/util.py rename to scientific_library/tvb/dsl_cuda/lems/base/util.py diff --git a/lems/model/__init__.py b/scientific_library/tvb/dsl_cuda/lems/model/__init__.py similarity index 100% rename from lems/model/__init__.py rename to scientific_library/tvb/dsl_cuda/lems/model/__init__.py diff --git a/lems/model/component.py b/scientific_library/tvb/dsl_cuda/lems/model/component.py similarity index 100% rename from lems/model/component.py rename to scientific_library/tvb/dsl_cuda/lems/model/component.py diff --git a/lems/model/dynamics.py b/scientific_library/tvb/dsl_cuda/lems/model/dynamics.py similarity index 100% rename from lems/model/dynamics.py rename to scientific_library/tvb/dsl_cuda/lems/model/dynamics.py diff --git a/lems/model/fundamental.py b/scientific_library/tvb/dsl_cuda/lems/model/fundamental.py similarity index 100% rename from lems/model/fundamental.py rename to scientific_library/tvb/dsl_cuda/lems/model/fundamental.py diff --git a/lems/model/model.py b/scientific_library/tvb/dsl_cuda/lems/model/model.py similarity index 100% rename from lems/model/model.py rename to scientific_library/tvb/dsl_cuda/lems/model/model.py diff --git a/lems/model/simulation.py b/scientific_library/tvb/dsl_cuda/lems/model/simulation.py similarity index 100% rename from lems/model/simulation.py rename to scientific_library/tvb/dsl_cuda/lems/model/simulation.py diff --git a/lems/model/structure.py b/scientific_library/tvb/dsl_cuda/lems/model/structure.py similarity index 100% rename from lems/model/structure.py rename to scientific_library/tvb/dsl_cuda/lems/model/structure.py diff --git a/lems/parser/LEMS.py b/scientific_library/tvb/dsl_cuda/lems/parser/LEMS.py similarity index 100% rename from lems/parser/LEMS.py rename to scientific_library/tvb/dsl_cuda/lems/parser/LEMS.py diff --git a/lems/parser/__init__.py b/scientific_library/tvb/dsl_cuda/lems/parser/__init__.py similarity index 100% rename from lems/parser/__init__.py rename to scientific_library/tvb/dsl_cuda/lems/parser/__init__.py diff --git a/lems/parser/expr.py b/scientific_library/tvb/dsl_cuda/lems/parser/expr.py similarity index 100% rename from lems/parser/expr.py rename to scientific_library/tvb/dsl_cuda/lems/parser/expr.py diff --git a/dsl_cuda/tmpl8_CUDA.py b/scientific_library/tvb/dsl_cuda/tmpl8_CUDA.py similarity index 100% rename from dsl_cuda/tmpl8_CUDA.py rename to scientific_library/tvb/dsl_cuda/tmpl8_CUDA.py From 2b4cfa6280a43c7825ddbfb13aca6537134f44fd Mon Sep 17 00:00:00 2001 From: Marmaduke Woodman Date: Fri, 11 Sep 2020 13:14:31 +0200 Subject: [PATCH 09/10] adjust paths and module setup --- .../tvb/dsl_cuda/CUDAmodels/kuramoto.c | 8 +++++--- .../tvb/dsl_cuda/CUDAmodels/oscillator.c | 13 +++++++----- scientific_library/tvb/dsl_cuda/LEMS2CUDA.py | 4 ++-- .../tvb/dsl_cuda/{example => run}/__init__.py | 0 .../tvb/dsl_cuda/{example => run}/benchAll.sh | 0 .../tvb/dsl_cuda/{example => run}/cuda_run.py | 3 ++- .../dsl_cuda/{example => run}/cuda_setup.py | 20 +++++++++---------- .../tvb/dsl_cuda/{example => run}/runthings | 0 8 files changed, 27 insertions(+), 21 deletions(-) rename scientific_library/tvb/dsl_cuda/{example => run}/__init__.py (100%) rename scientific_library/tvb/dsl_cuda/{example => run}/benchAll.sh (100%) rename scientific_library/tvb/dsl_cuda/{example => run}/cuda_run.py (99%) rename scientific_library/tvb/dsl_cuda/{example => run}/cuda_setup.py (96%) rename scientific_library/tvb/dsl_cuda/{example => run}/runthings (100%) diff --git a/scientific_library/tvb/dsl_cuda/CUDAmodels/kuramoto.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/kuramoto.c index c6bff4ed6f..019b692147 100755 --- a/scientific_library/tvb/dsl_cuda/CUDAmodels/kuramoto.c +++ b/scientific_library/tvb/dsl_cuda/CUDAmodels/kuramoto.c @@ -72,6 +72,8 @@ __global__ void Kuramoto( float V = 0.0; + float dV = 0.0; + //***// This is only initialization of the observable for (unsigned int i_node = 0; i_node < n_node; i_node++) { @@ -85,7 +87,7 @@ __global__ void Kuramoto( for (unsigned int t = i_step; t < (i_step + n_step); t++) { //***// This is the loop over nodes, which also should stay the same - for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + for (int i_node = 0; i_node < n_node; i_node++) { c_0 = 0.0f; @@ -116,10 +118,10 @@ __global__ void Kuramoto( c_0 *= global_coupling * rec_n; // This is dynamics step and the update in the state of the node - V += dt * (omega + c_0); + dV = dt * (omega + c_0); // Add noise (if noise components are present in model), integrate with stochastic forward euler and wrap it up - V += nsig * curand_normal2(&crndst).x; + V += nsig * curand_normal(&crndst) + dV; // Wrap it within the limits of the model V = wrap_it_PI(V); diff --git a/scientific_library/tvb/dsl_cuda/CUDAmodels/oscillator.c b/scientific_library/tvb/dsl_cuda/CUDAmodels/oscillator.c index ffc1c66f33..d8b218873c 100755 --- a/scientific_library/tvb/dsl_cuda/CUDAmodels/oscillator.c +++ b/scientific_library/tvb/dsl_cuda/CUDAmodels/oscillator.c @@ -101,6 +101,9 @@ __global__ void Oscillator( float V = 0.0; float W = 0.0; + float dV = 0.0; + float dW = 0.0; + //***// This is only initialization of the observable for (unsigned int i_node = 0; i_node < n_node; i_node++) { @@ -114,7 +117,7 @@ __global__ void Oscillator( for (unsigned int t = i_step; t < (i_step + n_step); t++) { //***// This is the loop over nodes, which also should stay the same - for (unsigned int i_node = threadIdx.y; i_node < n_node; i_node+=blockDim.y) + for (int i_node = 0; i_node < n_node; i_node++) { c_0 = 0.0f; @@ -146,12 +149,12 @@ __global__ void Oscillator( c_0 *= global_coupling; // This is dynamics step and the update in the state of the node - V += dt * (d * tau * (alpha * W - f * powf(V, 3) + e * powf(V, 2) + g * V + gamma * I + gamma * c_0 + lc * V)); - W += dt * (d * (a + b * V + c * powf(V, 2) - beta * W) / tau); + dV = dt * (d * tau * (alpha * W - f * powf(V, 3) + e * powf(V, 2) + g * V + gamma * I + gamma * c_0 + lc * V)); + dW = dt * (d * (a + b * V + c * powf(V, 2) - beta * W) / tau); // Add noise (if noise components are present in model), integrate with stochastic forward euler and wrap it up - V += nsig * curand_normal2(&crndst).x; - W += nsig * curand_normal2(&crndst).x; + V += nsig * curand_normal(&crndst) + dV; + W += nsig * curand_normal(&crndst) + dW; // Wrap it within the limits of the model V = wrap_it_V(V); diff --git a/scientific_library/tvb/dsl_cuda/LEMS2CUDA.py b/scientific_library/tvb/dsl_cuda/LEMS2CUDA.py index 7ba5a63d0f..172793d635 100755 --- a/scientific_library/tvb/dsl_cuda/LEMS2CUDA.py +++ b/scientific_library/tvb/dsl_cuda/LEMS2CUDA.py @@ -3,8 +3,8 @@ import os import sys -sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.pardir)) -from lems.model.model import Model +#sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.pardir)) +from tvb.dsl_cuda.lems.model.model import Model def default_lems_folder(): diff --git a/scientific_library/tvb/dsl_cuda/example/__init__.py b/scientific_library/tvb/dsl_cuda/run/__init__.py similarity index 100% rename from scientific_library/tvb/dsl_cuda/example/__init__.py rename to scientific_library/tvb/dsl_cuda/run/__init__.py diff --git a/scientific_library/tvb/dsl_cuda/example/benchAll.sh b/scientific_library/tvb/dsl_cuda/run/benchAll.sh similarity index 100% rename from scientific_library/tvb/dsl_cuda/example/benchAll.sh rename to scientific_library/tvb/dsl_cuda/run/benchAll.sh diff --git a/scientific_library/tvb/dsl_cuda/example/cuda_run.py b/scientific_library/tvb/dsl_cuda/run/cuda_run.py similarity index 99% rename from scientific_library/tvb/dsl_cuda/example/cuda_run.py rename to scientific_library/tvb/dsl_cuda/run/cuda_run.py index 9b95ee9d01..263f6b6ade 100755 --- a/scientific_library/tvb/dsl_cuda/example/cuda_run.py +++ b/scientific_library/tvb/dsl_cuda/run/cuda_run.py @@ -121,7 +121,8 @@ def run_simulation(self, weights, lengths, params_matrix, speeds, logger, args, # run simulation#{{{ # logger.info('submitting work') - for i in range(nstep): + import tqdm + for i in tqdm.trange(nstep): # event = events[i % 32] # stream = streams[i % 32] diff --git a/scientific_library/tvb/dsl_cuda/example/cuda_setup.py b/scientific_library/tvb/dsl_cuda/run/cuda_setup.py similarity index 96% rename from scientific_library/tvb/dsl_cuda/example/cuda_setup.py rename to scientific_library/tvb/dsl_cuda/run/cuda_setup.py index 3f6fef6044..b6a50f93ec 100755 --- a/scientific_library/tvb/dsl_cuda/example/cuda_setup.py +++ b/scientific_library/tvb/dsl_cuda/run/cuda_setup.py @@ -1,6 +1,3 @@ -from tvb.simulator.lab import * -import numpy as np -import numpy.random as rgn import math import time import logging @@ -8,12 +5,11 @@ import argparse import os, sys +import numpy as np from numpy import corrcoef -sys.path.insert(0, os.path.join(os.getcwd(), os.pardir)) -import LEMS2CUDA - -rgn.seed(79) +from tvb.simulator.lab import * +from tvb.dsl_cuda import LEMS2CUDA class TVB_test: @@ -131,7 +127,8 @@ def check_results(self, n_nodes, n_work_items, tavg, weights, speeds, couplings, def start_cuda(self, logger): # logger.info('start Cuda run') - from cuda_run import CudaRun + import pycuda.autoinit + from tvb.dsl_cuda.run.cuda_run import CudaRun cudarun = CudaRun() tavg_data = cudarun.run_simulation(self.weights, self.lengths, self.params, self.speeds, logger, self.args, self.n_nodes, self.n_work_items, self.n_params, self.nstep, @@ -212,16 +209,19 @@ def startsim(self): toc = time.time() elapsed = toc - tic logger.info('Finished python simulation successfully in: %0.3f', elapsed) - # logger.info('%0.3f M step/s', 1e-6 * self.nstep * self.n_inner_steps * self.n_work_items / elapsed) + logger.info('%0.3f M step/s', 1e-6 * self.nstep * self.n_inner_steps * self.n_work_items / elapsed) # logger.info('finished') if __name__ == '__main__': + np.random.seed(79) + example = TVB_test() # start templating the model specified on cli - LEMS2CUDA.cuda_templating(example.args.model, '../../dsl_cuda/XMLmodels/') + here = os.path.abspath(os.path.dirname(__file__)) + LEMS2CUDA.cuda_templating(example.args.model, os.path.join(here, '..', 'XMLmodels')) # start simulation with templated model example.startsim() diff --git a/scientific_library/tvb/dsl_cuda/example/runthings b/scientific_library/tvb/dsl_cuda/run/runthings similarity index 100% rename from scientific_library/tvb/dsl_cuda/example/runthings rename to scientific_library/tvb/dsl_cuda/run/runthings From 0bdb6656a288eddb2fbb250c5bd5d94b29fd230e Mon Sep 17 00:00:00 2001 From: Marmaduke Woodman Date: Fri, 11 Sep 2020 13:26:33 +0200 Subject: [PATCH 10/10] fix paths and add notebook --- scientific_library/tvb/dsl_cuda/LEMS2CUDA.py | 5 +- .../run/{cuda_setup.py => __main__.py} | 0 .../demos/gpu_parallel_parameter_sweeps.ipynb | 160 ++++++++++++++++++ 3 files changed, 163 insertions(+), 2 deletions(-) rename scientific_library/tvb/dsl_cuda/run/{cuda_setup.py => __main__.py} (100%) create mode 100644 tvb_documentation/demos/gpu_parallel_parameter_sweeps.ipynb diff --git a/scientific_library/tvb/dsl_cuda/LEMS2CUDA.py b/scientific_library/tvb/dsl_cuda/LEMS2CUDA.py index 172793d635..0c5cfc3177 100755 --- a/scientific_library/tvb/dsl_cuda/LEMS2CUDA.py +++ b/scientific_library/tvb/dsl_cuda/LEMS2CUDA.py @@ -3,8 +3,9 @@ import os import sys -#sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.pardir)) -from tvb.dsl_cuda.lems.model.model import Model +# not ideal but avoids modifying the vendored LEMS itself +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from lems.model.model import Model def default_lems_folder(): diff --git a/scientific_library/tvb/dsl_cuda/run/cuda_setup.py b/scientific_library/tvb/dsl_cuda/run/__main__.py similarity index 100% rename from scientific_library/tvb/dsl_cuda/run/cuda_setup.py rename to scientific_library/tvb/dsl_cuda/run/__main__.py diff --git a/tvb_documentation/demos/gpu_parallel_parameter_sweeps.ipynb b/tvb_documentation/demos/gpu_parallel_parameter_sweeps.ipynb new file mode 100644 index 0000000000..14d102d25b --- /dev/null +++ b/tvb_documentation/demos/gpu_parallel_parameter_sweeps.ipynb @@ -0,0 +1,160 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# GPU-based parallel parameter sweeps\n", + "\n", + "A frequent bottleneck in TVB-based studies are the parameter sweeps that allow a TVB user to characterize the sensitivity of a TVB model to key parameters such as a conduction velocity and coupling strength. Because the sequence of simulations are parallel and independant, they are amenable to parallelization using general purpose graphics processing units (GPUs) which are now frequently found in HPC centers.\n", + "\n", + "## Quickstart\n", + "\n", + "To assist with a user running on a HPC site via Slurm or similar job scheduler, a helpful command line interface is provided," + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2020-09-11 13:22:53,114 - WARNING - tvb.simulator.common - psutil module not available: no warnings will be issued when a\n", + " simulation may require more memory than available\n", + " INFO log level set to INFO\n", + "/home/maedoc/src/tvb-root/scientific_library/tvb/datatypes/surfaces.py:61: UserWarning: Geodesic distance module is unavailable; some functionality for surfaces will be unavailable.\n", + " warnings.warn(msg)\n", + "usage: __main__.py [-h] [-c N_COUPLING] [-s N_SPEED] [-t] [-n N_TIME] [-v]\n", + " [--node_threads NODE_THREADS]\n", + " [--model {Rwongwang,Kuramoto,Epileptor,Oscillator,Oscillatorref,Kuramotoref,Rwongwangref,Epileptorref}]\n", + " [--lineinfo] [--filename FILENAME] [-b BENCH]\n", + " [-bx BLOCKSZX] [-by BLOCKSZY] [-val VALIDATE]\n", + "\n", + "Run parameter sweep.\n", + "\n", + "optional arguments:\n", + " -h, --help show this help message and exit\n", + " -c N_COUPLING, --n_coupling N_COUPLING\n", + " num grid points for coupling parameter\n", + " -s N_SPEED, --n_speed N_SPEED\n", + " num grid points for speed parameter\n", + " -t, --test check results\n", + " -n N_TIME, --n_time N_TIME\n", + " number of time steps to do (default 400)\n", + " -v, --verbose increase logging verbosity\n", + " --node_threads NODE_THREADS\n", + " --model {Rwongwang,Kuramoto,Epileptor,Oscillator,Oscillatorref,Kuramotoref,Rwongwangref,Epileptorref}\n", + " neural mass model to be used during the simulation\n", + " --lineinfo\n", + " --filename FILENAME Filename to use as GPU kernel definition\n", + " -b BENCH, --bench BENCH\n", + " What to bench: regular, numba, cuda\n", + " -bx BLOCKSZX, --blockszx BLOCKSZX\n", + " Enter block size x\n", + " -by BLOCKSZY, --blockszy BLOCKSZY\n", + " Enter block size y\n", + " -val VALIDATE, --validate VALIDATE\n", + " Enable validation to refmodels\n" + ] + } + ], + "source": [ + "!python -m tvb.dsl_cuda.run --help" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This allows for configuring and running a parameter sweep in single command," + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2020-09-11 13:25:33,377 - WARNING - tvb.simulator.common - psutil module not available: no warnings will be issued when a\n", + " simulation may require more memory than available\n", + " INFO log level set to INFO\n", + "/home/maedoc/src/tvb-root/scientific_library/tvb/datatypes/surfaces.py:61: UserWarning: Geodesic distance module is unavailable; some functionality for surfaces will be unavailable.\n", + " warnings.warn(msg)\n", + "WARNING File 'cortical' not found in ZIP.\n", + "WARNING File 'hemispheres' not found in ZIP.\n", + "WARNING File 'areas' not found in ZIP.\n", + "/home/maedoc/src/tvb-root/scientific_library/tvb/dsl_cuda/run/cuda_run.py:28: UserWarning: The CUDA compiler succeeded, but said the following:\n", + "kernel.cu(67): warning: variable \"global_speed\" was declared but never referenced\n", + "\n", + "kernel.cu(68): warning: variable \"global_coupling\" was declared but never referenced\n", + "\n", + "kernel.cu(91): warning: variable \"J_N\" was declared but never referenced\n", + "\n", + "kernel.cu(92): warning: variable \"J_I\" was declared but never referenced\n", + "\n", + "kernel.cu(94): warning: variable \"lamda\" was declared but never referenced\n", + "\n", + "kernel.cu(107): warning: variable \"rec_n\" was declared but never referenced\n", + "\n", + "kernel.cu(108): warning: variable \"rec_speed_dt\" was declared but never referenced\n", + "\n", + "ptxas info : 64448 bytes gmem, 72 bytes cmem[3]\n", + "ptxas info : Compiling entry function '_Z9RwongwangjjjjjffPfS_S_S_S_' for 'sm_75'\n", + "ptxas info : Function properties for _Z9RwongwangjjjjjffPfS_S_S_S_\n", + " 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads\n", + "ptxas info : Used 32 registers, 424 bytes cmem[0], 8 bytes cmem[2]\n", + "\n", + " network_module = SourceModule(\n", + "100%|███████████████████████████████████████████| 40/40 [00:03<00:00, 12.40it/s]\n" + ] + } + ], + "source": [ + "!python -m tvb.dsl_cuda.run --model Rwongwang -n 40 -c 64 -s 64" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This demonstrates a 64x64 sweep completed in 3s on a Nvidia RTX5000 GPU. \n", + "\n", + "More extensive integration and output analysis in the notebook is possible by making direct use of the API exposed in the dsl_cuda module." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}