Source code for dipde.internals.utilities

# Copyright 2013 Allen Institute
# This file is part of dipde
# dipde 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.
# 
# dipde 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 dipde.  If not, see <http://www.gnu.org/licenses/>.

import numpy as np
import scipy.linalg as spla
import scipy.stats as sps
import scipy.integrate as spi
import bisect

[docs]def fraction_overlap(a1, a2, b1, b2): '''Calculate the fractional overlap between range (a1,a2) and (b1,b2). Used to compute a reallocation of probability mass from one set of bins to another, assuming linear interpolation. ''' if a1 >= b1: # range of A starts after B starts if a2 <= b2: return 1 # A is within B if a1 >= b2: return 0 # A is after B # overlap is from a1 to b2 return (b2 - a1) / (a2 - a1) else: # start of A is before start of B if a2 <= b1: return 0 # A is completely before B if a2 >= b2: # B is subsumed in A, but fraction relative to |A| return (b2 - b1) / (a2 - a1) # overlap is from b1 to a2 return (a2 - b1) / (a2 - a1)
[docs]def redistribute_probability_mass(A, B): '''Takes two 'edge' vectors and returns a 2D matrix mapping each 'bin' in B to overlapping bins in A. Assumes that A and B contain monotonically increasing edge values. ''' mapping = np.zeros((len(A)-1, len(B)-1)) newL = 0 newR = newL # Matrix is mostly zeros -- concentrate on overlapping sections for L in range(len(A)-1): # Advance to the start of the overlap while newL < len(B) and B[newL] < A[L]: newL = newL + 1 if newL > 0: newL = newL - 1 newR = newL # Find end of overlap while newR < len(B) and B[newR] < A[L+1]: newR = newR + 1 if newR >= len(B): newR = len(B) - 1 # Calculate and store remapping weights for j in range(newL, newR): mapping[L][j] = fraction_overlap(A[L], A[L+1], B[j], B[j+1]) return mapping
[docs]def leak_matrix(v, tau): 'Given a list of edges, construct a leak matrix with time constant tau.' zero_bin_ind_list = get_zero_bin_list(v) # Initialize: A = np.zeros((len(v)-1,len(v)-1)) # Positive leak: delta_w_ind = -1 for source_ind in np.arange(max(zero_bin_ind_list)+1, len(v)-1): target_ind = source_ind + delta_w_ind dv = v[source_ind+1]-v[source_ind] bump_rate = v[source_ind+1]/(tau*dv) A[source_ind, source_ind] -= bump_rate A[target_ind, source_ind] += bump_rate # Negative leak: delta_w_ind = 1 for source_ind in np.arange(0, min(zero_bin_ind_list)): target_ind = source_ind + delta_w_ind dv = v[source_ind]-v[target_ind] bump_rate = v[source_ind]/(tau*dv) A[source_ind, source_ind] -= bump_rate A[target_ind, source_ind] += bump_rate return A
[docs]def flux_matrix(v, w, lam, p=1): 'Compute a flux matrix for voltage bins v, weight w, firing rate lam, and probability p.' zero_bin_ind_list = get_zero_bin_list(v) # Flow back into zero bin: if w > 0: # Outflow: A = -np.eye(len(v)-1)*lam*p # Inflow: A += redistribute_probability_mass(v+w, v).T*lam*p # Threshold: flux_to_zero_vector = -A.sum(axis=0) for curr_zero_ind in zero_bin_ind_list: A[curr_zero_ind,:] += flux_to_zero_vector/len(zero_bin_ind_list) else: # Outflow: A = -np.eye(len(v)-1)*lam*p # Inflow: A += redistribute_probability_mass(v+w, v).T*lam*p missing_flux = -A.sum(axis=0) A[0,:] += missing_flux flux_to_zero_vector = np.zeros_like(A.sum(axis=0)) return flux_to_zero_vector, A
[docs]def get_zero_bin_list(v): 'Return a list of indices that map flux back to the zero bin.' # Cast here to avoid mistakes: v = np.array(v) if len(np.where(v==0)[0]) == 1: zero_edge_ind = np.where(v==0)[0][0] if zero_edge_ind == 0: return [0] else: return [zero_edge_ind-1, zero_edge_ind] else: return [bisect.bisect_right(v, 0) - 1]
[docs]def get_v_edges(v_min, v_max, dv): 'Construct voltage edegs, linear discretization.' edges = np.concatenate(( np.arange(v_min, v_max, dv), [v_max] )) edges[np.abs(edges) < np.finfo(np.float).eps] = 0 return edges
[docs]def assert_probability_mass_conserved(pv): 'Assert that probability mass in control nodes sums to 1.' try: assert np.abs(np.abs(pv).sum() - 1) < 1e-12 except: # pragma: no cover raise Exception('Probability mass below threshold: %s' % (np.abs(pv).sum() - 1)) # pragma: no cover
[docs]def descretize(distribution, N, loc=0, scale=1, shape=[]): 'Compute a discrete approzimation to a scipy.stats continuous distribution.' x_list = np.linspace(0,1,N+1) rv = sps.expon(*shape, loc=loc, scale=scale) y = np.zeros(len(x_list)) for ii, x in enumerate(x_list): y[ii] = rv.ppf(x) mean = np.zeros(len(x_list)-1) height = np.zeros(len(x_list)-1) a = np.zeros(len(x_list)-1) for ii, yl_yr in enumerate(zip(y[:-1],y[1:])): yl, yr = yl_yr height[ii] = rv.cdf(yr) - rv.cdf(yl) def mu(x): return x*rv.pdf(x) mean[ii] = spi.quad(mu, yl, yr)[0] a[ii] = mean[ii]/height[ii] return a, height
[docs]def exact_update_method(J, pv, dt=.0001): 'Given a flux matrix, pdate probabilty vector by computing the matrix exponential.' # Exact computation of propogation method, matrix exponential: pv = np.dot(spla.expm(J*dt), pv) assert_probability_mass_conserved(pv) return pv
[docs]def approx_update_method_tol(J, pv, tol=2.2e-16, dt=.0001, norm='inf'): 'Approximate the effect of a matrix exponential, with residual smaller than tol.' # No order specified: J *= dt curr_err = np.inf counter = 0. curr_del = pv pv_new = pv while curr_err > tol: counter += 1 curr_del = J.dot(curr_del)/counter pv_new += curr_del curr_err = spla.norm(curr_del, norm) try: assert_probability_mass_conserved(pv) except: # pragma: no cover raise Exception("Probabiltiy mass error (p_sum=%s) at tol=%s; consider higher order, decrease dt, or increase dv" % (np.abs(pv).sum(), tol)) # pragma: no cover return pv_new
[docs]def approx_update_method_order(J, pv, dt=.0001, approx_order=2): 'Approximate the effect of a matrix exponential, truncating Taylor series at order \'approx_order\'.' # Iterate to a specific order: coeff = 1. curr_del = pv pv_new = pv for curr_order in range(approx_order): coeff *= curr_order+1 curr_del = J.dot(curr_del*dt) pv_new += (1./coeff)*curr_del try: assert_probability_mass_conserved(pv_new) except: # pragma: no cover raise Exception("Probabiltiy mass error (p_sum=%s) at approx_order=%s; consider higher order, decrease dt, or increase dv" % (np.abs(pv).sum(), approx_order)) # pragma: no cover return pv_new