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 }