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 }