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