-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfunction_library.py
More file actions
985 lines (841 loc) · 44.8 KB
/
Copy pathfunction_library.py
File metadata and controls
985 lines (841 loc) · 44.8 KB
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
'''
This module contains the 'master functions' used to generate and analyze
networks in this project.
Python 3.x
Colin Campbell
campbeco@mountunion.edu
'''
import numpy as np
import scipy.special as sp
import math
import networkx as nx
from networkx.algorithms import bipartite as bp # needed for configuration_model
def form_network(plants=10,pollinators=10,\
po_mean=3.5,po_stdev=3.272565,po_skewness=1.43,\
pl_mean=3.5,pl_stdev=2.579218,pl_skewness=.73,\
po_decay=1.273077,po_cutoff=.328301,\
pl_decay=.876667,pl_cutoff=.141464):
'''
OVERVIEW:
Forms a bipartite network of plants and pollinators. Plants and pollinators
are:
1. Assigned characteristic proboscis/stem lengths from a skewed normal
distribution.
2. Assigned edges (inter-type only) via an exponentially cut off power law
distribution.
The edges are then made bidirectional, with weights according to the general
schematic:
plants prefer prob<=stem
pollinators prefer prob>=stem
Edge weights are assigned values of -1, or 1. A plant->pollinator edge is
assigned -1 if prob<stem (bad), 1 if prob>stem (good), or 1 if
prob=stem (preferred). We consider prob=stem if the % difference is <=10%.
The percent difference b/w x,y, is defined as 100*abs(x-y)/avg(x,y).
INPUT PARAMETERS:
* pollinators, plants = # of ea. type of species
* mean, stdev, and skewness = mean, stdev, and skewness of distribution from
which the species' characteristic lengths will be drawn. alpha = 0 for
normal dist, - (+) for left (right) skew.
The decay function used for assigning edges b/w plants and pollinators is:
p(x) = x^(-decay) * exp(-cutoff*x)
* decay = characteristic decay rate for power law
* cutoff = exponential cutoff term
Default parameter values are drawn from:
* Stang et al, Annals of Botany 103: 1459-1469, 2009 (for length
distributions)
* Jordano et al, Ecology Letters 6: 69-81, 2003 (for decay function)
PROCESS:
1. Generate range of values from the specified distributions.
2. Assign proboscis/stem lengths according to skewed normal distribution.
3. Assign edges b/w pollinators and plants according to cut off exponential
distribution.
4. Make edges bidirectional, and assign weights according to a 3-valued
fitting function.
'''
#1. Generate range of values from specified distributions
def cdf_skew(X,sgn):
'''
Returns the cumulative density function of the skew normal distribution.
'''
def dens(X,sgn):
'''
Returns the probability density function of the skew normal distribution.
See http://en.wikipedia.org/wiki/Skew_normal_distribution for details.
sp.ndtr returns the area under the standard Gaussian probability density
function, integrated from minus infinity to (arg).
'''
if sgn=='po':
#Compute scaling parameters from supplied information (see word doc for derivation)
po_skew = min([0.99,po_skewness]) # For lognormal the upper limit is 1; must assume sampling bias if value exceeds
arg1=2*po_skew/(4-math.pi)
delta=math.sqrt(math.pi/2.0)*arg1**(1/3)/((1+arg1**(2/3))**.5)
omega=math.sqrt((math.pi*po_stdev*po_stdev)/(math.pi-2*delta*delta))
alpha=delta/(math.sqrt(1-delta*delta))
epsilon=po_mean-omega*delta*math.sqrt(2/math.pi)
arg2=(X-epsilon)/omega
Y = (2./po_stdev)*np.exp(-(arg2)**2/2)/np.sqrt(2*np.pi)
Y *= sp.ndtr(alpha*arg2) #The max value should be 1, but for some reason it is near 1.41424
return Y #This is a normalization issue we account for below (I haven't tracked down the source)
elif sgn=='pl':
#Compute scaling parameters from supplied information (see word doc for derivation)
pl_skew = min([0.99,pl_skewness]) # For lognormal the upper limit is 1; must assume sampling bias if value exceeds
arg1=2*pl_skew/(4-math.pi)
delta=math.sqrt(math.pi/2.0)*arg1**(1/3)/((1+arg1**(2/3))**.5)
omega=math.sqrt((math.pi*pl_stdev*pl_stdev)/(math.pi-2*delta*delta))
alpha=delta/(math.sqrt(1-delta*delta))
epsilon=pl_mean-omega*delta*math.sqrt(2/math.pi)
arg2=(X-epsilon)/omega
Y = (2./pl_stdev)*np.exp(-(arg2)**2/2)/np.sqrt(2*np.pi)
Y *= sp.ndtr(alpha*arg2) #The max value should be 1, but for some reason it is near 1.41424
return Y #This is a normalization issue we account for below (I haven't tracked down the source)
if sgn=='pl':
Y_pl = dens(X,'pl')
Y_pl = np.cumsum(Y_pl)*(X[1]-X[0])
return Y_pl
elif sgn=='po':
Y_po = dens(X,'po')
Y_po = np.cumsum(Y_po)*(X[1]-X[0])
return Y_po
def cutoff_fn(sgn):
'''For power law/exponential cutoff:
We generate this with a standard exponential distribution, and force
the cutoff behavior by accepting each generated value with probability
p=(x/xmin)^(-cutoff).
Source for approach:
http://arxiv.org/PS_cache/arxiv/pdf/0706/0706.1062v2.pdf (pg.40)
we assume xmin=1. (i.e. the lower cutoff for range where power law
applies is 1)'''
if sgn=='pl':
decay,cutoff=pl_decay,pl_cutoff
elif sgn=='po':
decay,cutoff=po_decay,po_cutoff
#Form exponential values
Y_cutoff=np.random.exponential(scale=1.0/cutoff,size=10000)
def cutoff_filter(x):
p=x**(-decay)
comparison=np.random.ranf()
return p>comparison
#Remove some according to power law test
Y_cutoff=list(filter(cutoff_filter,Y_cutoff))
#Fill in empty values
temp=np.array([])
while len(temp)<(10000-len(Y_cutoff)):
x=np.random.exponential(scale=1.0/cutoff)
p=x**(-decay)
comparison=np.random.ranf()
if p>comparison:
temp=np.append(temp,x)
Y_cutoff=np.append(Y_cutoff,temp)
return Y_cutoff
#Form x range for distributions
X_pl = np.arange(pl_mean-50, pl_mean+100, 0.1) #These ranges characterize an appropriate range for the default values
X_po = np.arange(po_mean-50, po_mean+100, 0.1) #If nondefault values are used, care should be taken that the leftmost...
#...value ~0 and the rightmost ~1
#Form skew normal distribution
'''
Procedure for generating values from a skew normal distribution:
1. Form cdf of skew distribution w/ given params.
- Interpret values as CDF(x)=y; y=cdf value for input x
2. Take a value, x1, from uniform distribution[0,1]
3. Find largest value x2 s.t. CDF(x2)<=x1
4. x2 is the output from the end distribution.
'''
Y_pl,Y_po = cdf_skew(X_pl,'pl'),cdf_skew(X_po,'po')
Y_pl_skew,Y_po_skew=[],[]
Y_pl_max=max(Y_pl) #The normalization factors alluded to in the cdf function defn
Y_po_max=max(Y_po)
for i in Y_pl:
x1=np.random.ranf()*(Y_pl_max) #If cdf gave a max value of 1
x2=X_pl[Y_pl.searchsorted(x1)-1] # we'd just call np.random.ranf() here
if x2>0:
Y_pl_skew+=[x2]
for i in Y_po:
x1=np.random.ranf()*(Y_po_max) #If cdf gave a max value of 1
x2=X_po[Y_po.searchsorted(x1)-1] #we'd just call np.random.ranf() here
if x2>0:
Y_po_skew+=[x2]
#Form cutoff power law distribution
pl_cutoff=cutoff_fn('pl')
po_cutoff=cutoff_fn('po')
#2. Assign proboscis/stem lengths
po_lengths=np.arange(0,pollinators)
pl_lengths=np.arange(0,plants)
def assign_lengths_pl(x):
index=np.random.randint(0,len(Y_pl_skew))
return Y_pl_skew[index]
def assign_lengths_po(x):
index=np.random.randint(0,len(Y_po_skew))
return Y_po_skew[index]
po_lengths=list(map(assign_lengths_po,po_lengths))
pl_lengths=list(map(assign_lengths_pl,pl_lengths))
#3. Assign edges b/w pollinators and plants according to cut off exponential distribution.
def obtain_sequence(n,cutoff):
#Obtains degree distributions from the cutoff powerlaw distribution
out=[]
for i in range(0,n):
loc=np.random.randint(0,np.size(cutoff))
out+=[int(math.ceil(cutoff[loc]))] #ceiling function rounds up values to nearest int (avoids 0 assigned degree)
return out
#Obtain degree sequences from exponential cut off degree distribution
aseq=obtain_sequence(pollinators,po_cutoff)
bseq=obtain_sequence(plants,pl_cutoff)
#Their sums must be equal in order to form graph
while sum(aseq)!=sum(bseq):
aseq=obtain_sequence(pollinators,po_cutoff)
bseq=obtain_sequence(plants,pl_cutoff)
#Form bipartite graph (networkx documentation claims using Graph() may lead to inexact degree sequences)
G=bp.configuration_model(aseq, bseq,create_using=nx.Graph())
#Relabel the nodes...
names={}
for i in range(0,pollinators):
names[i]='po_'+str(i)
for i in range(0,plants):
names[i+pollinators]='pl_'+str(i)
G=nx.relabel_nodes(G,names)
G=G.to_directed() #now every edge a-b is duplicated & directed (a->b and b->a)
#4. Assign edge weights according to fitting function
#First, assign nodes their type & lengths from po_lengths and pl_lengths
#(better storage of values now that the network is actually formed)
counter=0
for i in po_lengths:
G.nodes['po_'+str(counter)]['type'] = 'pollinator'
G.nodes['po_'+str(counter)]['length'] = i
counter+=1
counter=0
for i in pl_lengths:
G.nodes['pl_'+str(counter)]['type'] = 'plant'
G.nodes['pl_'+str(counter)]['length'] = i
counter+=1
#Then assign edge weight according to the lengths
for i in G.edges():
a,b=G.nodes[i[0]]['length'],G.nodes[i[1]]['length'] #prob/stem lengths
diff=100.*abs(a-b)/((a+b)/2.0)
#If they're w/in the specified range, both edges (a->b and b->a) are assigned 2
if diff<=10.0:
G[i[0]][i[1]]['data']=1.
G[i[1]][i[0]]['data']=1.
elif a<b: #length1<length2
G[i[1]][i[0]]['data']=-1.
G[i[0]][i[1]]['data']=1.
else: #length1>length2
G[i[1]][i[0]]['data']=1.
G[i[0]][i[1]]['data']=-1.
#Finally, output the graph
return G
def force_state(Graph,state_string,mod=False):
'''
This function forces the network into the state specified by a binary
string (e.g. '001110101'), where the first position applies to pl_0 and
the last to po_m (m=# of pollinators).
It returns the modified graph with node property 'state' assigned
according to state_string.
It is called internally in other functions (e.g. steady_states,
active_edges).
the "mod" parameter was added to accomodate transient_analysis, when we
have removed nodes in a nonuniform manner. leaving mod=False is faster,
but mod=True allows the function to work appropriately in this case.
'''
if mod==False:
#We iterate first over all plant nodes
sc=0 #state counter
nc=0 #node counter
while 1==1:
node='pl_'+str(nc)
if Graph.has_node(node):
Graph.nodes[node]['state']=state_string[sc]
nc+=1
sc+=1
else:
break
#And then over pollinator nodes
nc=0
while 1==1:
node='po_'+str(nc)
if Graph.has_node(node):
Graph.nodes[node]['state']=state_string[sc]
nc+=1
sc+=1
else:
break
else:
def mapper(x):
try:
return int(x[-2:]) #for e.g. pl_15
except ValueError:
return int(x[-1]) #for e.g. pl_5
#Determine maximum label value
labels=Graph.nodes()
labels=list(map(mapper,labels))
nc=max(labels)+1
sc=0
for i in range(nc):
node='pl_'+str(i)
if Graph.has_node(node):
Graph.nodes[node]['state']=state_string[sc]
sc+=1
#And then over pollinator nodes
for i in range(nc):
node='po_'+str(i)
if Graph.has_node(node):
Graph.nodes[node]['state']=state_string[sc]
sc+=1
#After we've iterated over all nodes, we also should have iterated
#over all entries in the state string
if len(state_string)!=sc:
print(sc, len(state_string),len(Graph.nodes()))
print(state_string)
print('error in assigning node states in function \'force_state\'!')
#We also want a dictionary of the states
sys_state={}
for i in Graph.nodes():
sys_state[i]={}
sys_state[i][0]=int(Graph.nodes[i]['state'])
return Graph, sys_state
def update_network(Graph,sys_state,threshold=1,posweight=1,mode='sync',negweight=-1,negspecial=-2,posspecial=4,negnodes=[],extra_data=False):
'''
This function performs one timestep on the network. For an edge to be active
after the update, the total incoming edge weights from active nodes must be
>= threshold. "posweight" allows for the weight of positive edges to be
effectively increased, which allows for another variation on update rules
(instead of just varying the threshold).
mode is 'sync' by default, which updates all the nodes. The only other mode
currently supported is 'omit1', which randomly selects 1 node to not update.
negweight is the weight of a negative edges (recent addition).
negnodes are nodes which have their weights modified to values indicated
by 'posspecial' and 'negspecial.' (negnodes is a deprecated name; it
originally only handled modifications to negative edges.)
It returns the updated network and an updated dictionary of states.
This function should not normally need to be called by the end user. It is
called internally in other functions, which repeatedly update the network
under some conditions.
if extra_data==True, the fn performs analysis of spectral nestedness,
connectance, and size at every time step.
'''
def edge_filter(x):
#used for filtering list of edges to give only edges ending at target
return x[1]==i
def state_filter(x):
#used for filtering list of edges to give only edges with active source node
return sys_state[x[0]][counter-1]==1 or sys_state[x[0]][counter-1]=='1'
def weight_map(x):
#maps list of edges into list of their weights (-1/1), modified to posweight, negweight, and negspecial
if x[0] in negnodes and x[2]['data']==-1:
return negspecial #negative edges originating from negnode list are changed from negweight to negspecial
if x[0] in negnodes and x[2]['data']==1:
return posspecial #positive edges originated from negnode list are changed from posweight to posspecial
if x[2]['data']==-1:
return negweight #negative edges are changed from -1 to negweight
return posweight #positives are increased from 1 to posweight
counter=list(sys_state.values())
counter=len(counter[0].values()) #determines which round of updating we're on
edgelist=Graph.edges(data=True) #stores all edges
if mode=='omit1':
nodelist=Graph.nodes() #if we are to omit updating 1 node, make a list of the nodes
omit_node=np.random.randint(len(nodelist)) #choose 1 index to omit
for i in Graph.nodes():
if mode=='omit1':
if nodelist.index(i)==omit_node:
sys_state[i][counter]=Graph.nodes[i]['state'] #don't update graph node dictionary, but make sure we update the state dictionary with the prev. state
continue #don't update this node if we're in omit1 mode
edges=list(filter(edge_filter,edgelist)) #edges is list of edges ending at i
edges=list(filter(state_filter,edges)) #edges is list of edges with active source (and ending at i)
edges=list(map(weight_map,edges)) #edges is list of weights of edges with active source nodes that end at i
weight=sum(edges)
if weight<threshold:
Graph.nodes[i]['state']=0 #(all t+1 values are determined from t; NOT running update)
sys_state[i][counter]=0
else:
Graph.nodes[i]['state']=1
sys_state[i][counter]=1
if extra_data:
G_stripped = Graph.copy()
for i in Graph.__iter__():
if Graph.nodes[i]['state']==0:
G_stripped.remove_node(i) #for final analysis, remove absent nodes (analyze only 'existing' graph) if we're doing extra analysis here
if nx.number_of_nodes(G_stripped) == 0:
SN=False
C=False
else:
SN = spectral_nestedness(G_stripped)
C = nx.density(G_stripped)
return Graph, sys_state, [SN,C,nx.number_of_nodes(G_stripped),[Graph.nodes[x]['state'] for x in Graph.nodes()]]
else:
return Graph, sys_state
def spectral_nestedness(G):
A = nx.adjacency_matrix(G) # Must form an adjacency matrix based off of the interactions
A = A.todense()
Evals,Evects = np.linalg.eig(A)
SR = max([abs(x.real) for x in Evals]) # Find the spectral radius of this graph
return SR
def check_steady(Graph,sys_state,mode='sync'):
'''
Takes dictionary of states of form:
{node1:{step0: state, step1: state,...} node2:{...},...}
and sees if the final two states are equivalent (steady state reached). If
not, it checks to see if the last state is equivalent to any previous state
(limit cycle). It returns 'steady','limit', or 'chaotic', depending on the
results.
mode is 'sync' by default for all nodes being updated (and therefore calling
a LC whenever a state is repeated). Currently it also supports 'omit1',
which introduces asynchronous behavior, and the output of this function
is always 'chaotic' or a SS.
This function is called internally in other functions and is not meant for
use by the end user.
'''
def state_mapper(x):
#maps list of nodes to contain states of the nodes at update #count
return sys_state[x][count]
counter=list(sys_state.values())
counter=len(counter[0].values()) #determines which round of updating we're on
#First check for steady state
nodes=Graph.nodes()
nodes=node_sorter(nodes) #nodes is now a properly sorted list of node names, e.g. ['pl_0','pl_1',...]
count=counter-1
ult=list(map(state_mapper,nodes)) #is list of the most recent states
count-=1
p_ult=list(map(state_mapper,nodes)) #is list of the second to most recent states
if ult==p_ult:
out=''
for i in ult:
out+=str(i)
return out #return binary state vector for the steady state (as a string)
#If that isn't true, check for limit cycle, but only if we're in 'sync' mode
if mode=='sync':
lc=False
for i in range(0,counter-1):
count=i
x=list(map(state_mapper,nodes))
if x==ult: #x is the same state as ult. All states after it (included ult) comprise the limit cycle
lc=True
lcstates=[]
if lc: #We store these states
state=''
for i in x:
state+=str(i)
lcstates+=[state]
if lc:
return lcstates #A list of all of the states in the limit cycle. Its length is the # of states in the lc
#Otherwise, return 'chaotic', indicating the system is still evolving
return 'chaotic'
def check_sufficiency(Graph,sys_state,steplimit=100,threshold=1,posweight=1,ind=False,TRANS_TYPE=1, FORCE_EXTINCT=False,mode='sync',negweight=-1,negspecial=-2,posspecial=4,negnodes=[],TRANS_LIMIT=0,extra_data=False):
'''
MODIFIED FROM OTHER PROJECTS:
Here, we allow a limit to how long a TRANS_TYPE forced
introduction/extinction lasts, with variable TRANS_LIMIT set to an int for
the # of steps (0 -> never). Setting equal to 'inf' makes the changes
permanent throughout the simulation.
Note also that FORCE_EXTINCT can refer to forced introductions, as well.
In this project we additionally output summary statistics of every network
configuration during the dynamics.
----
This function takes an initialized graph and its dictionary of states and
updates it 'in stasis' (w/o externally introduced species) until it reaches
a steady state, limit cycle, or 100 iterations have occurred.
threshold is what the total edge weight of incoming edges with active source
nodes must meet or exceed for a node to remain active after that update
round.
posweight is the weight of a single positive edge.
ind is an int that corresponds to the position in the state string that will
be forced to remain ON or OFF (based on TRANS_TYPE; 0 for 0->1 perturbations
and 1 for 1->0 perturbations.) ind is False if no such forcing is to occur.
It returns which condition is met and the sys_state dictionary.
'''
SN_out,C_out,size_out,adj_out=[],[],[],[]
if steplimit>2**20:
steplimit=2**20
for i in range(steplimit): #Counting initialization, yields total of steplimit+1 states
#force a particular species to be ON or OFF prior to first update if we're forcing a particular type of perturbation
if i==0 and (ind!=False or type(ind)==int): #(0==False returns True, so account for that)
if TRANS_TYPE==1: #what species will be perturbed to (opposite of what they are perturbed from, i.e. TRANS_TYPE)
target=0
else:
target=1
nodes=Graph.nodes()
nodes=node_sorter(nodes)
end=sys_state[nodes[ind]].keys()
sys_state[nodes[ind]][end[-1]]=target
#perform update once we've made the perturbation
if extra_data:
Graph,sys_state,stats=update_network(Graph=Graph,sys_state=sys_state,threshold=threshold,posweight=posweight,mode=mode,negweight=negweight,negspecial=negspecial,posspecial=posspecial,negnodes=negnodes,extra_data=True)
SN_out+=[stats[0]]
C_out+=[stats[1]]
size_out+=[stats[2]]
adj_out+=[stats[3]]
else:
Graph,sys_state=update_network(Graph=Graph,sys_state=sys_state,threshold=threshold,posweight=posweight,mode=mode,negweight=negweight,negspecial=negspecial,posspecial=posspecial,negnodes=negnodes)
#force the specified node to remain ON/OFF if FORCE_EXTINCT==True
if (ind!=False or type(ind)==int) and FORCE_EXTINCT==True:
if TRANS_LIMIT=='inf' or i<TRANS_LIMIT: #Here, only perform forcing if we're at a tolerable time step (always if TRANS_LIMIT = 'inf' , otherwise only for specified #)
end=sys_state[nodes[ind]].keys() # Python 3 updated needed; see modification to check_sufficiency_mod below
sys_state[nodes[ind]][end[-1]]=target
cond=check_steady(Graph=Graph,sys_state=sys_state,mode=mode)
if cond!='chaotic': #Stop evolving the system if we've reached a steady state or limit cycle
break
if extra_data:
return cond, sys_state, SN_out, C_out, size_out,adj_out
else:
return cond, sys_state
def check_sufficiency_mod(Graph,sys_state,steplimit=100,threshold=1,posweight=1,ind=False,TRANS_TYPE=1, FORCE_EXTINCT=False,mode='sync',negweight=-1,negspecial=-2,posspecial=4,negnodes=[],TRANS_LIMIT='inf',extra_data=False):
'''
MODIFIED VERSION OF check_sufficiency from netev2.py. Here we modify ind to
take a list of vals.
Also different from other versions of this fn (in other projects), we allow
a limit to how long a TRANS_TYPE forced introduction/extinction lasts, with
variable TRANS_LIMIT set to an int for the # of steps. setting equal to 0
makes the changes permanent throughout the simulation.
Note also that FORCE_EXTINCT can refer to forced introductions, as well.
--
This function takes an initialized graph and its dictionary of states and
updates it 'in stasis' (w/o externally introduced species) until it reaches
a steady state, limit cycle, or 100 iterations have occurred.
threshold is what the total edge weight of incoming edges with active source
nodes must meet or exceed for a node to remain active after that update
round.
posweight is the weight of a single positive edge.
ind is an int that corresponds to the position in the state string that will
be forced to remain ON or OFF (based on TRANS_TYPE; 0 for 0->1 perturbations
and 1 for 1->0 perturbations.) ind is False if no such forcing is to occur.
It returns which condition is met and the sys_state dictionary.
'''
if TRANS_TYPE==1:
target=0
else:
target=1
SN_out,C_out,size_out,adj_out=[],[],[],[]
if steplimit>2**20:
steplimit=2**20
for i in range(steplimit): #Counting initialization, yields total of steplimit+1 states
#force a particular species to be ON or OFF prior to first update if we're forcing a particular type of perturbation
if i==0 and (ind!=False or type(ind)==int): #(0==False returns True, so account for that)
if TRANS_TYPE==1: #what species will be perturbed to (opposite of what they are perturbed from, i.e. TRANS_TYPE)
target=0
else:
target=1
nodes=Graph.nodes()
nodes=node_sorter(nodes)
for j in ind:
end=sorted(sys_state[nodes[j]]) # (edited for Python 3) sorted list of dict keys: 0,1,2,... to current time step counter value
sys_state[nodes[j]][end[-1]]=target
#perform update
if extra_data:
Graph,sys_state,stats=update_network(Graph=Graph,sys_state=sys_state,threshold=threshold,posweight=posweight,mode=mode,negweight=negweight,negspecial=negspecial,posspecial=posspecial,negnodes=negnodes,extra_data=True)
SN_out+=[stats[0]]
C_out+=[stats[1]]
size_out+=[stats[2]]
adj_out+=[stats[3]]
else:
Graph,sys_state=update_network(Graph=Graph,sys_state=sys_state,threshold=threshold,posweight=posweight,mode=mode,negweight=negweight,negspecial=negspecial,posspecial=posspecial,negnodes=negnodes)
#force the specified node to remain ON/OFF if FORCE_EXTINCT==True
if ind!=False and FORCE_EXTINCT==True:
if TRANS_LIMIT=='inf' or i<=TRANS_LIMIT: #Here, only perform forcing if we're at a tolerable time step (always if TRANS_LIMIT = 0 , otherwise only for specified #)
for val in ind:
end=sorted(sys_state[nodes[val]])
sys_state[nodes[val]][end[-1]]=target
cond=check_steady(Graph=Graph,sys_state=sys_state,mode=mode)
if cond!='chaotic': #Stop evolving the system if we've reached a steady state or limit cycle
break
if extra_data:
return cond, sys_state, SN_out, C_out, size_out,adj_out
else:
return cond, sys_state
def node_sorter(x):
'''
Since our nodes are strings, just calling nodes.sort() will put e.g. 'pl_5'
after 'pl_10' - this function sorts them properly and returns them in a
list.
'''
def node_plucker(y):
#filters out nodes not of appropriate length
return len(y)==mn
pl=[]
po=[]
for i in x: #split po and pl into two lists
if i.count('l')!=0:
pl+=[i]
else:
po+=[i]
sorter=[]
while len(pl)>0:
mn=min(list(map(len,pl))) #minimum length (i.e. 1 digit, 2 digit,...)
small=list(filter(node_plucker,pl)) #small now has the nodes of the minimum length
pl=list(set(pl)-set(small)) #take all of 'small' out of 'pl'
small.sort() #order the nodes in small
sorter+=small #add these to sorter
while len(po)>0:
mn=min(list(map(len,po))) #repeat process for po
small=list(filter(node_plucker,po)) #should really rewrite this in more condensed fashion
po=list(set(po)-set(small))
small.sort()
sorter+=small
return sorter
def active_edges(Graph, states):
'''
This function takes as input a graph and list of state strings (e.g. as in
the output of the function steady_states; in form ['00111','11000',...]),
and returns a list of the edges that are active (i.e. source and target
nodes are ON) and their edge weights.
'''
def state_filter(x):
#used for filtering list of edges to give only edges with active source and sink nodes
return Graph.nodes[x[0]]['state']=='1' and Graph.nodes[x[1]]['state']=='1'
edgelist=Graph.edges(data=True)
for state in states:
print('Considering state vector:',state)
print('The active edges are:')
G,gbg=force_state(Graph,state) #here we don't care about the returned state dictionary
edges=list(filter(state_filter,edgelist)) #edges holds only edges with active source and target node
for i in edges:
print(i[0],'->',i[1],'\t weight=',i[2]['data'])
def perturb_state(Graph,states,steplimit=True,threshold=1, posweight=1, report=False,negweight=-1,negspecial=-2,negnodes=[]):
'''
This function takes as input a graph and list of state strings (e.g. as in
the output of the function steady_states; in form ['00111','11000',...]),
and for each node in each state:
1. Turns it from ON to OFF or vice versa
2. Allows the dynamics to evolve until either...
- the state goes to a steady state, OR
- the state reaches a limit cycle, OR
- the state evolves the maximum number of times (i.e. goes to a complex
limit cycle).
A dictionary of the form
{state:[(perturbation1,result,stepcount),(perturbation2,result,stepcount)...],...}
is returned, where pertubations are each of the possible OFF-ON changes to
each string in 'states', and the results are the strings of the steady
states, or an int of the limit cycle length, or 'chaotic' otherwise.
stepcount is the number of steps it takes to reach the limit cycle or
steady state.
NOTE: steplimit defaults to the maximum possible (2^N), but may be
overridden with any positive integer value (as a float or int).
'''
def state_modifier(state,i):
#takes a state string and forces the value at index i to be 1
out=state[0:i]+'1'+state[i+1:]
return out
if report == True:
print('Executing perturb_state...')
out={} #keys will be entries in 'states'. Each will have 1 value; a list of (modified state,condition) tuples
N=len(states[0]) #len of state vector (i.e. # of nodes)
if steplimit==True:
steplimit=2**N #The maximum number of steps we can take on the transition graph for each considered steady state
count=0 #for printing status updates
for state in states: #iterate over all input states
if report==True:
print(100.0*count/len(states),'% complete')
out_list=[] #Will be stored as a value for this state in 'out' dictionary
for i in range(N): #iterate over all nodes in this state
if state[i]=='0': #If this node is OFF...
new_state=state[0:i]+'1'+state[i+1:] #Make a copy where it is ON
else: #Otherwise it is ON...
new_state=state[0:i]+'0'+state[i+1:] #So we make a copy where it is OFF
G, sys_state=force_state(Graph,new_state) #Initialize the graph to this modified state
#Allow the system to advance in time; cond is int of limit
#cycle length, 'chaotic', or the steady state string vector e.g. '01101..'
cond,sys_state=check_sufficiency(G,sys_state,int(steplimit),threshold=threshold, posweight=posweight,negweight=negweight,negspecial=negspecial,negnodes=negnodes)
state_count=len(sys_state['pl_0'].keys()) #The number of updates the network went through
if type(cond)==list: #If we enter a limit cycle we report the # of steps it takes to get TO the limit cycle
state_count-=(1+len(cond)) #(subtract additional 1 b/c #edges=#states-1)
elif type(cond)==str: #If we hit a steady state, we report the # of steps it takes to reach the SS
state_count-=2 #subtract 1 b/c #edges=#states-1; subtract additional 1 b/c the state dict has the steady state 2x
out_list+=[(new_state,cond,state_count)] #Store these results
out[state]=out_list #Store final set of perturbed states and the result of their evolution
count+=1
if report==True:
print('Perturb_state finished.')
if report==True: #If desired, we can output the results in an easy to read format
print('--------------')
for i in out.keys():
print('Reporting perturbations on state %s'%i)
n=len(out[i])
for j in range(n):
#Different formatting for chaotic, limit cycle, or steady state (if steplimit=True, chaotic will never be called)
if type(out[i][j][1])==list:
print('Perturbed state %s \t takes \t %d \t steps to go to a cycle of length: %s'%(out[i][j][0],out[i][j][2],out[i][j][1]))
elif out[i][j][1]=='chaotic':
print('Perturbed state %s \t takes \t %d \t steps and fails to reach a limit cycle or steady state.'%(steplimit,out[i][j][0]))
else:
print('Perturbed state %s \t takes \t %d \t steps to go to a steady state: %s'%(out[i][j][0],out[i][j][2],out[i][j][1]))
print('--------------')
return out
def lc_comparison(LC):
'''
This function compares all the states in a given limit cycle, and determines
how many of the nodes share the same state: x=(# of agreement/# of bits)
The average of x over all pairs of states in the limit cycle is calculated.
LC is a LIST of state STRINGS in a
particular limit cycle. As such, 'x' is calculated for each entry in LC;
the average value is returned (so, "what is the average homogeneity of the
states in a limit cycle in this network?").
'''
vals=[]
n=len(LC)
m=len(LC[0]) #The # of nodes in each state
for i in range(n):
for j in range(i+1,n): #Compare every pair of states (order doesn't matter)
s1=LC[i]
s2=LC[j]
counter=0
for k in range(m): #For every bit position...
if s1[k]==s2[k]:
counter+=1 #...determine if the bits from each state agree.
vals+=[1.0*counter/m] #Store the rating for each pair...
out=np.mean(vals)
return out #...and return the average result.
def lc_lc_comparison(lc1,lc2):
'''
This function takes two limit cycles and determines an average similarity
between them. The process is to determine the average expression for each
species among a given limit cycle's states, and then make a comparison among
the LCs by abs(LC1_exp-LC2_exp).
We normalize the results such that an end value of 0 corresponds to exactly
equal expression levels for all species, and 1 corresponds to exactly
opposite expression levels for all species.
'''
m=len(lc1[0]) #the number of species
n1,n2=len(lc1),len(lc2)
avg1,avg2=[0]*m,[0]*m #holds the average expression (0 vs 1) among the LC states for each input LC
#determine average expression levels in the LCs
for i in range(n1):
for j in range(i+1,n1):
s1,s2=lc1[i],lc1[j]
for k in range(m):
if s1[k]==s2[k]:
avg1[k]+=1.0/(math.factorial(n1)/(2*math.factorial(n1-2))) #each term is one of the possible state comparison sets (n1 choose 2)
for i in range(n2):
for j in range(i+1,n2):
s1,s2=lc2[i],lc2[j]
for k in range(m):
if s1[k]==s2[k]:
avg2[k]+=1.0/(math.factorial(n1)/(2*math.factorial(n1-2)))
#now compare those averages
out=0
for i in range(m):
out+=(1.0/m)*abs(avg1[i]-avg2[i])
return out
def ss_lc_comparison(SS,LC):
'''
This function takes a steady state and limit cycle and returns an average
similarity between them. An average expression level for the lc is
determined (as in lc_lc_comparision(), and compared to the ss expression
levels.
'''
n=len(LC)
m=len(LC[0]) #The # of nodes in each state
avg=[0]*m
for i in range(n):
for j in range(i+1,n): #Compare every pair of states (order doesn't matter)
s1=LC[i]
s2=LC[j]
for k in range(m):
if s1[k]==s2[k]:
avg[k]+=1.0/(math.factorial(n)/(2*math.factorial(n-2)))
#compare this average LC expression level to the SS...
out=0
for i in range(m):
out+=(1.0/m)*abs(avg[i]-int(SS[i]))
return out
def ss_ss_comparison(SS1,SS2):
'''
This function takes two steady states and performs a bitwise comparison
of their expression values. The average difference is returned, so a value
of 0 corresponds to exact agreement (same SS) and a value of 1 correponds
to exact disagreement (e.g. 000 and 111).
'''
n=len(SS1)
out=0
for i in range(n):
out+=(1.0/n)*abs(int(SS1[i])-int(SS2[i]))
return out
def steady_states(Graph, threshold=1,report=False):
'''
This function searches through all the states in a network, and determines
how many are steady states.
It outputs as text the # of steady states, and if report=True, it prints
the states in binary according to the format:
pl_0,pl_1,...pl_n, po_0,...po_m
(where there are n, m plants, pollinators).
NOTE: It generates an error for very large networks. This function is
not currently used in "official" analysis (i.e. in random_graph_analysis.py).
'''
def update():
'''
This function begins updating the states of the network. If any node's
new state is different from its last, the procedure stops and 'False' is
returned.
If each node keeps its old state, 'True' is returned.
'''
def edge_filter(x):
#used for filtering list of edges to give only edges ending at target
return x[1]==i
def state_filter(x):
#used for filtering list of edges to give only edges with active source node
return Graph.nodes[x[0]]['state']=='1'
def weight_map(x):
#maps list of edges into list of their weights
return x[2]['data']
out=True #returned variable. Becomes False if a state is updated to a new value (ON->OFF or OFF->ON)
edgelist=Graph.edges(data=True) #stores all edges
for i in Graph.nodes():
edges=list(filter(edge_filter,edgelist)) #edges is list of edges ending at i
edges=list(filter(state_filter,edges)) #edges is list of edges with active source (and ending at i)
edges=list(map(weight_map,edges)) #edges is list of weights of edges with active source nodes that end at i
weight=sum(edges)
if weight<threshold: #node will be updated to OFF
if Graph.nodes[i]['state']=='1': #if it is ON, the state has changed and we break out of the loop
out=False
break
elif Graph.nodes[i]['state']=='0': #node will be updated to ON; if it is OFF the state has changed and we break out of the loop
out=False
break
return out
#We now force the state to iteratively be every possible state, and
#update it to determine whether or not it is a steady state.
out=[] #Will hold list of state vectors as strings
N=len(Graph.nodes()) #Number of nodes
M=2**N #Number of states
tracker=0 #Counts # of steady states
for i in range(0,M): #Iterate over all states
if report ==True and i in (M/4.0,M/2.0,3.0*M/4.0):
print(*100.0*i/M,'% complete! \t')
state=bin(i) #state is binary string with '0b' on left side
state=state.split('0b',1) #state is now list ['',x] where x is binary string w/o '0b'
state=state[1] #state is now just binary string
Z=N-len(state) #Z stores the number of 0's to add s.t. state is of appropriate length
state='0'*Z+state #state now has necessary padding of 0's
H,gbg=force_state(Graph,state) #Feed this state into the network (here, we don't care about the returned dictionary)
result=update() #Determine if it is a steady state
if result==True:
tracker+=1 #Store successful hit
out+=[state]
if (result and report)==True: #If desired (i.e. report=True), output state vector of each steady state
print('steady state found:\t',state)
if report==True:
print ('%d steady states found')%tracker#Print # of steady states
return out #Return a list of their strings
def superset(strings):
#converts a set of LC states into a superset, e.g. 0110 and 1000 becomes
#1110
#used internally in some below fns
string_length=len(strings[0])
string_count=len(strings)
out=''
for j in range(string_length):
x=False
for k in range(string_count):
if strings[k][j]=='1':
out+='1' #if any state is active at this position, so will the superset
x=True
break #then move on to the next position
if not x: out+='0' #otherwise, this position is off
return out
def Hamming(x,y):
#returns Hamming distance of two lists with entries = 0 or 1
if len(x)!=len(y): raise RuntimeError("inputs to Hamming() must be equal length.")
return len([1 for i in range(len(x)) if x[i]!=y[i]])/float(len(x))
def overlap(x,y):
# returns intersection/union of two steady states (so 1 == identical presence; 0 == no shared present species)
if len(x)!=len(y): raise RuntimeError("inputs to overlap() must be equal length.")
intersect, union = 0,0
for i in range(len(x)):
if x[i] == y[i] == '1': intersect += 1
if x[i] == '1' or y[i] == '1': union += 1
return intersect/union