-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathSFI_bases.py
230 lines (192 loc) · 9.89 KB
/
SFI_bases.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
""" A library of projection bases for Stochastic Force Inference. """
import numpy as np
def basis_selector(basis,data):
is_interacting = False
if basis['type'] == 'polynomial':
funcs = polynomial_basis(data.d,basis['order'])
elif basis['type'] == 'Fourier':
funcs = Fourier_basis(data.d,basis['order'],basis['center'],basis['width'])
elif basis['type'] == 'coarse_graining':
funcs = coarse_graining_basis(data.d,basis['width'],basis['center'],basis['order'])
elif basis['type'] == 'FORMA':
funcs = FORMA_basis(data.d,basis['width'],basis['center'],basis['order'])
else: # Interacting particles basis
is_interacting = True
if basis['type'] == 'particles_pair_interaction':
funcs = particles_pair_interaction(data.d,basis['kernels'])
if basis['type'] == 'particles_pair_interaction_periodic':
funcs = particles_pair_interaction_periodic(data.d,basis['kernels'],basis['box'])
elif basis['type'] == 'particles_pair_interaction_scalar':
funcs = particles_pair_interaction_scalar(basis['kernels'])
elif basis['type'] == 'particles_and_polynomial':
funcs = particles_polynomial_composite_basis(data.d,basis['order'],basis['kernels'])
elif basis['type'] == 'self_propelled_particles':
funcs = self_propelled_particles_basis(basis['order'],basis['kernels'])
elif basis['type'] == 'single_self_propelled_particle':
funcs = single_self_propelled_particle_basis()
elif basis['type'] == 'custom':
funcs = basis['functions']
else:
raise KeyError("Unknown basis type.")
return funcs,is_interacting
def polynomial_basis(dim,order):
# A simple polynomial basis, X -> X_mu X_nu ... up to polynomial
# degree 'order'.
# We first generate the coefficients, ie the indices mu,nu.. of
# the polynomials, in a non-redundant way. We start with the
# constant polynomial (empty list of coefficients) and iteratively
# add new indices.
coeffs = [ np.array([[]],dtype=int) ]
for n in range(order):
# Generate the next coefficients:
new_coeffs = []
for c in coeffs[-1]:
# We generate loosely ordered lists of coefficients
# (c1 >= c2 >= c3 ...) (avoids redundancies):
for i in range( (c[-1]+1) if c.shape[0]>0 else dim ):
new_coeffs.append(list(c)+[i])
coeffs.append(np.array(new_coeffs,dtype=int))
# Group all coefficients together
coeffs = [ c for degree in coeffs for c in degree ]
return lambda X : np.array([[ np.prod(x[c]) for c in coeffs ] for x in X])
def Fourier_basis(dim,order,center,width):
coeffs = [ np.array([[]],dtype=int) ]
for n in range(order):
# Generate the next coefficients:
new_coeffs = []
for c in coeffs[-1]:
# We generate loosely ordered lists of coefficients
# (c1 >= c2 >= c3 ...) (avoids redundancies):
for i in range( (c[-1]+1) if c.shape[0]>0 else dim ):
new_coeffs.append(list(c)+[i])
coeffs.append(np.array(new_coeffs,dtype=int))
coeffs = [ c for degree in coeffs[1:] for c in degree ]
if dim >= order:
# Two paradigms for the computation of the function: sparse if
# dim > order, dense otherwise.
def Fourier(X):
Xc = 2 * np.pi* (X - center) / width
vals = np.ones((len(Xc),2*len(coeffs)+1))
for j,x in enumerate(Xc):
for i,c in enumerate(coeffs):
vals[j,2*i+1] = np.cos(sum(x[c]))
vals[j,2*i+2] = np.sin(sum(x[c]))
return vals
else:
coeffs_lowdim = np.array([ [ list(c).count(i) for i in range(dim) ] for c in coeffs ])
def Fourier(X):
Xc = 2 * np.pi* (X - center) / width
vals = np.ones((len(Xc),2*len(coeffs_lowdim)+1))
for j,x in enumerate(Xc):
for i,c in enumerate(coeffs_lowdim):
vals[j,2*i+1] = np.cos( x.dot(c))
vals[j,2*i+2] = np.sin( x.dot(c))
return vals
return Fourier
### COARSE GRAINING ####
def coarse_graining_basis(dim,width,center,order):
print("""Warning, using a non-differentiable basis (coarse-graining). Do
NOT consider the StochasticForceInference methods "F_projections"
and its dependencies; use instead the phi_ansatz (Ito
estimate). The entropy production and capacity will need to be
manually re-computed. Note that SFI with multiplicative noise
and/or measurement noise will NOT work with this choice of
basis.""")
Nfuncs = order**dim
def index_finder(x):
projection_indices = [ int(np.floor( (x[mu] - (center[mu] - 0.5*width[mu]))/(width[mu]) * order)) for mu in range(dim) ]
return sum([ ( imu * order**mu if 0 <= imu < order else np.inf ) for mu,imu in enumerate(projection_indices) ])
def grid_function(X):
val = np.zeros((X.shape[0],Nfuncs))
for i in range(X.shape[0]):
n = index_finder(X[i])
if 0 <= n < Nfuncs:
val[i,n] = 1.
return val
return grid_function
### FORMA ####
""" Linear-by-parts grid coarse-graining; method used in
High-performance reconstruction of microscopic force fields from Brownian trajectories
Laura Perez Garcia, Jaime Donlucas Perez, Giorgio Volpe, Alejandro V. Arzola and Giovanni Volpe
Nature Communicationsvolume 9, Article number: 5166 (2018)
"""
def FORMA_basis(dim,width,center,order):
print("""Warning, using a non-differentiable basis (linear-by-parts
coarse-graining). Do NOT consider the StochasticForceInference
methods "F_projections" and its dependencies; use instead the
phi_ansatz (Ito estimate). The entropy production and capacity
will need to be manually re-computed. Note that SFI with
multiplicative noise and/or measurement noise will NOT work with
this choice of basis.""")
Ncells = order**dim
Nfuncs = (dim+1) * Ncells
def index_finder(x):
projection_indices = [ int(np.floor( (x[mu] - (center[mu] - 0.5*width[mu]))/(width[mu]) * order)) for mu in range(dim) ]
return sum([ ( imu * order**mu if 0 <= imu < order else np.inf ) for mu,imu in enumerate(projection_indices) ])
def grid_function(X):
val = np.zeros((X.shape[0],Nfuncs))
for i in range(X.shape[0]):
n = index_finder(X[i])
if 0 <= n < Nfuncs:
val[i,n] = 1.
for mu in range(dim):
val[i,n+(mu+1)*Ncells] = X[i,mu]
return val
return grid_function
def particles_pair_interaction(dim,kernels):
# Radially symmetric vector-like pair interactions as a sum of
# kernels. Two-particle functions are chosen to be of the form
# f(R_ij) * (Xj - Xi)/Rij for a given set of functions f
# (kernels).
def pair_function_spherical(X):
# X is a Nparticles x dim - shaped array.
Nparticles = X.shape[0]
Xij = np.array([[ Xj - Xi for j,Xj in enumerate(X) ] for i,Xi in enumerate(X) ])
Rij = np.linalg.norm(Xij,axis=2)
f_Rij = np.nan_to_num(np.array([ f(Rij)/Rij for f in kernels ]))
# Combine the kernel index f and the spatial index m into a
# single function index a:
return np.einsum('fij,ijm->ifm',f_Rij,Xij).reshape((Nparticles,dim * len(kernels)))
return pair_function_spherical
def particles_pair_interaction_periodic(dim,kernels,box):
# Radially symmetric vector-like pair interactions as a sum of
# kernels. Two-particle functions are chosen to be of the form
# f(R_ij) * (Xj - Xi)/Rij for a given set of functions f
# (kernels).
def pair_function_spherical(X):
# X is a Nparticles x dim - shaped array.
Nparticles = X.shape[0]
Xij = np.array([[[ (xij+box[d]/2)%box[d]-box[d]/2 for d,xij in enumerate(Xj - Xi)] for j,Xj in enumerate(X) ] for i,Xi in enumerate(X) ])
Rij = np.linalg.norm(Xij,axis=2)
f_Rij = np.nan_to_num(np.array([ f(Rij)/Rij for f in kernels ]))
# Combine the kernel index f and the spatial index m into a
# single function index a:
return np.einsum('fij,ijm->ifm',f_Rij,Xij).reshape((Nparticles,dim * len(kernels)))
return pair_function_spherical
def particles_pair_interaction_scalar(kernels):
# Radially symmetric scalar-like pair interactions as a sum of
# kernels. Two-particle functions are chosen to be of the form
# f(R_ij) for a given set of functions f (kernels).
def pair_function_spherical(X):
# X is a Nparticles x dim - shaped array.
Nparticles = X.shape[0]
Xij = np.array([[ Xj - Xi for j,Xj in enumerate(X) ] for i,Xi in enumerate(X) ])
Rij = np.linalg.norm(Xij,axis=2)
f_Rij = np.nan_to_num(np.array([ f(Rij) for f in kernels ]))
# Combine the kernel index f and the spatial index m into a
# single function index a:
return np.einsum('fij->if',f_Rij)
return pair_function_spherical
def particles_polynomial_composite_basis(dim,order_single,kernels):
# A composite basis: single-particle forces as polynomials
# (external field), and radially symmetric pair interactions as a
# sum of kernels.
poly = polynomial_basis(dim,order_single)
pair = particles_pair_interaction(dim,kernels)
return lambda X : np.array([ v for v in poly(X).T ]+[ v for v in pair(X).T ]).T
def self_propelled_particles_basis(order_single,kernels):
# A basis adapted to 2D self-propelled particles without alignment
self_propulsion = lambda X : np.array([ np.cos(X[:,2]), np.sin(X[:,2]) ]).T
poly = polynomial_basis(2,order_single)
pair = particles_pair_interaction(2,kernels)
return lambda X : np.array([ v for v in poly(X[:,:2]).T ]+[ v for v in self_propulsion(X).T ]+[ v for v in pair(X[:,:2]).T ]).T