1 /**
2 Copyright: Copyright (c) 2019- Alexander Orlov. All rights reserved.
3 License: $(LINK2 https://opensource.org/licenses/BSL-1.0, BSL-1.0 License).
4 Authors: Alexander Orlov, $(LINK2 mailto:sascha.orlov@gmail.com, sascha.orlov@gmail.com) 
5 */
6 
7 /**
8 The module contains various physical time reaction propagation algorithms based on the Gillespie algorithm from 1977. 
9 In the current version this module contains
10 - the default version of Gillespie
11 - The modification introduced by Sandmann 2008, which makes possible faster sample reaction arrival times,
12     in case all propensities of the reactions are known exactly. I. e. this modification can be applied, in case 
13     propensities were not estimated. 
14 - The modification introduced by Petzold et. al. 2006. At the expense of allocating an ancillary propensities array, 
15     where their cumulative sum is stored, the velocity of sampling the reaction index increases, dependent on the 
16     chosen search policy of assumedSorted ranges.
17 - The module provides also a common testing routine, containing tests for all combinations of modifications. 
18 */
19 module gillespied; 
20 import std.math : fabs, isNaN; 
21 import std.range : enumerate, ElementType, assumeSorted, isForwardRange, isInputRange, empty, front, back, put, iota;
22 import std.meta : AliasSeq;
23 import std.typecons : Flag;
24 import std.traits : isFloatingPoint, isIterable;  
25 import std.algorithm.sorting : isSorted;
26 
27 /**
28 Having mir library imported, this one will prefere it over the standard library random generator, assuming its 
29 performance is better.
30 */
31 version(Have_mir_random)
32 {
33     import mir.random : rand, randIndex; 
34 }
35 else // the last else case will remain as default case for ever. 
36 {
37     import std.random : uniform01, uniform; 
38 }
39 
40 /**
41 The algorithms should work with all kinds of floating numbers as well as with integers.
42 */
43 alias possibleTypes = AliasSeq!(float, double, real, size_t);
44 
45 /**
46 This struct models the time and reaction evolution as stated by Gillespie, see [1] and [2]. After [3], all properly or
47 rather "related to the intended master equation" formulated algorithms, modelling physical time propagation algorithms are equivalent. This concerns algorithms with rejection as well as the ones which are rejection-free, as the current algorithm. Whereas the rejection-free algorithms form a subset of the algorithms with rejection. 
48 
49 The default Gillespie algorithm was enhanced by two features. 
50 - If the reaction propensities are known, i. e. they do not have to be estimated, one of the random numbers needed by 
51 the original algorithm can be saved. As consequence the longer logarithmic operation is cancelled. See [4]. 
52 
53 - The search of next reaction is done over the cumulative sum of provided propensities. This search can be enhanced by 
54 using space. In this case, any of available search algorithms can be applied to the cumulative sum range, which is 
55 naturally ordered. In [5] the binary search algorithm was applied, whereas in the present case, the search policy is 
56 managed by the standard library.
57 
58 [1] D. T. Gillespie, J. Comput. Phys. 434, 403 (1976).
59 [2] D. T. Gillespie, 93555, 2340 (1977).
60 [3] S. A. Serebrinsky, Phys. Rev. E - Stat. Nonlinear, Soft Matter Phys. 83, 2010 (2011).
61 [4] W. Sandmann, Comput. Biol. Chem. J. 32, 292 (2008).
62 [5] H. Li and L. R. Petzold, Tech. Rep. 1 (2006). (logarithmic direct method)
63 */
64 
65 /**
66 Whether the Sandmann enhancement is turned on
67 */ 
68 alias Sandmann = Flag!"Sandmann"; 
69 /**
70 Whether the cumulative sum is stored separately inside the provided algorithm struct. In this case, a constructor is 
71 provided, which recieves the amount of propensietes during the simulation. An array of proper type is expanded to store
72 the cumulative fold resuls, which can be search faster. 
73 */ 
74 alias LDM = Flag!"LDM"; 
75 
76 /**
77 The floating type in use is defined either by the user on algorithm declaration or is chosen to be real, in case 
78 propensities come in form of integer values. 
79 */
80 template FloatingType(T)
81 {
82     static if(isFloatingPoint!T)
83     {
84         alias FloatingType = T; 
85     }
86     else
87     {
88         alias FloatingType = real; 
89     }
90 }
91 
92 private struct GillespieAlgorithm(
93     Sandmann sandmann, // whether to sample another uniform value for timings, see [5]
94     LDM ldm, // whether to use an array for storing cumulative sum, see [3]
95     T // type of propensities
96 )
97 {
98     static if(ldm == LDM.yes)
99     {
100         /**
101         The only action to perform on creation is to blow up the working cache. Available only in case LDM is chosen
102         */
103         this(size_t val)
104         out
105         {
106             assert(propensities.length); 
107         }
108         do
109         {
110             propsArr_.length = val; 
111         }
112 
113         /**
114         In LDM case, it is naturally to make the length of the object available.
115         */
116         size_t length() @nogc
117         {
118             return propensities.length; 
119         }
120     }
121 
122     /**
123 	consumes any range of propensities, where cumulativeFold is applicable at. 
124 	*/
125 	void put(P)(P props) if(isInputRange!P && is(ElementType!P == T))
126     in
127     {
128         assert(!props.empty);
129         static if(isFloatingPoint!T)
130         {
131             import std.algorithm.searching : any; 
132             assert(!props.any!isNaN); 
133         }
134     }
135     do
136 	{
137         import std.algorithm.iteration : cumulativeFold;
138 
139         auto resCumFold = props.cumulativeFold!((a, b) => a + b); 
140  
141         static if(ldm == LDM.yes)
142         {
143             foreach(i, el; resCumFold.enumerate) 
144             {
145                 propensities[i] = el;
146             }
147             
148             assert(propensities.isSorted);
149         }
150         else
151         {
152             static assert(isForwardRange!P);
153             import std.algorithm.iteration : fold;
154             import std.algorithm.comparison : max;
155 
156             a0_ = resCumFold.fold!max; 
157             nextReaction_ = getNextReaction(resCumFold);
158         }
159     }
160 
161     /**
162     This method provides the arrival time to the next reaction. 
163     Returns: the according floating point type value, corresponding to the provided propensities. This value is the 
164     exponential distributed time needed for the next reaction to arrive. 
165     */
166 	FloatingType!T tau() @nogc
167 	{
168         typeof(return) retVal = cast(typeof(return))1.0/cast(typeof(return))a0; 
169 
170 		static if(sandmann == Sandmann.no)
171 		{
172             import std.math : log; 
173             
174             FloatingType!T rndNum = 0.0; 
175 
176             version(Have_mir_random)
177             {
178                 rndNum = rand!(typeof(return)).fabs;
179             }
180             else
181             {
182                 rndNum = uniform01!(typeof(return));
183             }
184             retVal = - retVal * log(rndNum); 
185         }
186 
187 		return retVal; 
188 	}
189 
190 	/**
191 	This method yields the next reaction index, as stated by Gillespie. The sampling is done according to the original 
192     sampling algorithm, taking advantage of stored propensities array if applicable.
193 	*/
194 	size_t index()() //not possible to mark @nogc directly, due to phobos uniform is not @nogc
195 	{
196         static if(ldm == LDM.yes)
197         {
198             return getNextReaction(propensities); 
199         }
200         else
201         {
202             return nextReaction_; 
203         }
204 	}
205 
206     private: 
207 	static if(ldm == LDM.yes)
208     {
209         T[] propsArr_; 
210         auto propensities() @nogc 
211         {
212             return propsArr_;
213         }
214     }
215     else
216     {
217         T a0_; 
218         size_t nextReaction_; 
219     }
220 
221     auto a0() @nogc
222     {
223         static if(ldm == LDM.yes)
224         {
225             return propensities.back; 
226         }
227         else
228         {
229             return a0_;
230         }
231     }
232 
233     size_t getNextReaction(R)(R range) 
234     {
235         if(a0 > 0)
236         {
237             T rndNum;
238 
239             static if(isFloatingPoint!T)
240             {
241                 version(Have_mir_random)
242                 {
243                     rndNum = rand!T.fabs; 
244                 }
245                 else
246                 {
247                     rndNum = uniform01!T; 
248                 }
249                 rndNum *= a0; 
250             }
251             else
252             {
253                 version(Have_mir_random)
254                 {
255                     rndNum = randIndex!size_t(a0); 
256                 }
257                 else
258                 {
259                     rndNum = uniform(T(0), a0); 
260                 }
261             }
262             
263             /**
264             The usage of persistent working cache allows to accelerate the search of next reaction by binary search.
265             See [3] for reference.
266             */
267             static if(ldm == LDM.yes)
268             {
269                 assert(range.isSorted);
270                 return range.assumeSorted.lowerBound(rndNum).length;  // range is propsarr
271             }
272             else
273             {
274                 /*
275                 The simple case. Do not assume anything about the range. Count until the value is equal or exceeds the 
276                 random generated one. 
277                 */
278                 import std.algorithm.searching : countUntil; 
279                 return range.countUntil!"a > b"(rndNum);
280             }
281         }
282         else
283         {
284             return typeof(return).max; 
285         }
286     }
287 }
288 
289 /**
290 The convenience function to release a gillespie algorithm structure with some preset properties. 
291 In this case, the preset handles the memory allocation due using an internal buffer for storing passed propensities. 
292 Other properties do not affect memory allocations. 
293 */
294 auto gillespieAlgorithm(
295     Sandmann sandmann = Sandmann.yes,
296     LDM ldm = LDM.no,
297     T = real
298 )(size_t val = size_t.init)
299 {
300     static if(ldm == LDM.yes)
301     {
302         return GillespieAlgorithm!(sandmann, ldm, T)(val);
303     }
304     else
305     {
306         return GillespieAlgorithm!(sandmann, ldm, T)();
307     }
308 }
309 
310 version(unittest):
311 import std.algorithm.iteration : sum;
312 import std.algorithm.searching : any;
313 import std.math : isInfinity, abs, approxEqual; 
314 
315 enum minTestedSizes = 0UL; 
316 enum maxTestedSizes = 1UL << size_t.sizeof;
317 
318 // Common testing function.
319 void testTemplate(Sandmann sandmann, LDM ldm, T, size_t l)()
320 {
321     // 1. generate the environmental propensities of needed length
322     T[] inputProps = new T[l];
323 
324     // 2. Declare the usage of the algorithm
325     GillespieAlgorithm!(sandmann, ldm, T) s; 
326 
327     // 3. In case the environment does not provide propensities, it is an error to use the algorithm.
328     static if(l)
329     {
330         // 2b. If using Petzold's enhancement, the algorithm has not only to be declared but also to be allocated
331         static if(ldm == LDM.yes)
332         {
333             s = typeof(s)(inputProps.length); 
334         }
335 
336         /*
337         4. The algorithm is intrinsically stochastic. 
338         Choosing a large amount of runs and samples to be able to take an average. 
339         */
340         enum maxAmount = (1UL << 20)/l; 
341 
342         // 5. Declaring and initializing the checked result. 
343         FloatingType!T res = 0.0; 
344         FloatingType!T[] resArray = new FloatingType!T[maxAmount]; 
345         resArray[] = 0.0; 
346         
347         foreach(i; 0 .. maxAmount)
348         {
349             // 7a. Fill the mocking propensities with random values. Sometimes, there will be a zero propensity. The 
350             // index of the zero propensity has to be tracked to be checked later for not being choosen in all 
351             // circumstances. 
352             immutable indexWithZeroPropensity = inputProps.fill; 
353             
354             // 7b. Sometimes, there are cases, when propensities become zero altogether
355             if((indexWithZeroPropensity >= inputProps.length) && !i)
356             {
357                 inputProps[] = 0; 
358             }
359             
360             // 8. Feed the propensities to the algorithm
361             put(s, inputProps); 
362             
363             // 9. Calculate the physical arrival time for the next reaction. This is simulated by exp distribution with 
364             // the single parameter of the intensity taken as the sum of all propensities feeded in the step 8. 
365             immutable tau = s.tau; 
366 
367             if(tau.isInfinity)
368             {
369                 // 10a. In case all propensites were zero, arriving time is infinit. In this case no valid reaction 
370                 // index can be generated. 
371                 immutable ind = s.index; 
372                 assert(ind >= inputProps.length);
373             }
374             else
375             {
376                 // 10b. Otherwise, different assertions on the arrival timing can be done: 
377                 static if(sandmann == Sandmann.yes)
378                 {
379                     // 11a. If the Sandmann modification is taken into account, the timing was not sampled, but taken 
380                     // exactly. Assert on its value directly.
381                     assert(approxEqual(tau, 1.0/inputProps.sum));
382                 }
383                 else
384                 {
385                     // 11b. Otherwise, the mean of the arrival timings have to be inspected. Form a sum of differences 
386                     // of the sampled timings and the expected ones. 
387                     res += tau - 1.0/inputProps.sum; 
388                     resArray[i] = tau - 1.0/inputProps.sum; 
389                 }
390 
391                 // 12. In any case, the sampled reaction index cannot be the excluded one in step 7a, as the propensity
392                 // of the according reaction was set to zero. 
393                 assert(s.index != indexWithZeroPropensity);
394             }
395         }
396         // 11c. The mean of the differences has to be approximately zero, as taking the average on all runs has to obey
397         // the expectation of the exponential distribution.
398         import std.conv : to;  
399         assert(!resArray.any!isNaN);
400         assert(approxEqual(resArray.sum/maxAmount, res/maxAmount));
401         assert(approxEqual(abs(resArray.sum/maxAmount), 0), abs(resArray.sum/maxAmount).to!string);
402         assert(approxEqual(abs(res/maxAmount), 0), abs(res/maxAmount).to!string);
403     }
404     else
405     {
406         // See 3.
407         import std.exception : assertThrown; 
408         static if(ldm == LDM.yes)
409         {
410             assertThrown!Error(typeof(s)(inputProps.length));
411         }
412         else
413         {
414             assertThrown!Error(put(s, inputProps));
415         }
416     }
417 }
418 
419 // Auxillary function for testing. Mocks the environmental propensities. 
420 size_t fill(R)(R inputProps)
421 {
422     alias T = ElementType!R;  
423     
424     foreach(ref el; inputProps)
425     {
426         T rndNum = 0; 
427         
428         auto maxAmount = T.max/inputProps.length; 
429 
430         static if(isFloatingPoint!T)
431         {
432             version(Have_mir_random)
433             {
434                 rndNum = rand!T.fabs * maxAmount;
435             }
436             else
437             {
438                 rndNum = uniform01!T * maxAmount;
439             }
440         }
441         else
442         {
443             version(Have_mir_random)
444             {
445                 rndNum = randIndex!T(maxAmount);
446             }
447             else
448             {
449                 rndNum = uniform(T(0), maxAmount);
450             }
451         }
452         el = rndNum;
453     }
454 
455     size_t retVal = inputProps.length;
456     
457     if(retVal > 1)
458     {
459         bool modify;
460 
461         version(Have_mir_random)
462         {
463             modify = rand!bool;
464         }
465         else
466         {
467             modify = uniform01!real <= 0.5;
468         }
469 
470         if(modify)
471         {
472             version(Have_mir_random)
473             {
474                 retVal = randIndex!size_t(retVal);
475             }
476             else
477             {
478                 retVal = uniform(size_t(0), retVal);
479             }
480             inputProps[retVal] = 0;
481         }
482     }
483     return retVal;
484 }