/** * @file Gillespie.cc * * Implements Gillespie simulator * * Copyright (c) 2016 - 2018, Peter Helfer * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include <vector> #include <string> #include <sys/types.h> #include <unistd.h> #include <signal.h> using std::string; #include <unordered_map> #include <libgen.h> #include <format.h> #include <tinyexpr.h> #include "Gillespie.hh" #include "Sched.hh" #include "Trace.hh" /** * Defined symbols */ std::unordered_map<string, string> defines; static inline void dumpDefines() { for (auto d : defines) { fmt::print("{} = {}\n", d.first, d.second); } } /** * Constructor */ Gillespie::Gillespie( const char *gilFileName, void (*preIterFunc)(double time)) : volume(0.0), runIdle(true), idleTick(0.3), preIterFunc(preIterFunc), twidth(9), mwidth(7) { uint numLines = readGilFile(gilFileName); verify(gilFileName, numLines); // dumpDefines(); } static uint factorial(uint n) { uint f = 1; for (uint i = 1; i <= n; i++) { f *= i; } return f; } /** * Print error message and exit * @param file File name * @param line Line number * @param format fmt::print format string * @param args Additional arguments to fmt::print */ template <typename... T> void fail(string file, uint line, const char *format, const T & ... args) { fmt::print(stderr, "File {}, line {}: ", file, line); fmt::print(stderr, format, args...); fmt::print(stderr, "\n"); exit(1); } /** * Substitute defined symbols in a vector of tokens * @param tokens Token vector * @param first First token to process * @param last Last token to process */ static void symSubst( std::vector<string> &tokens, size_t first = 0, size_t last = UINT_MAX) { last = Util::min(last, tokens.size()); for (uint i = first; i < last; i++) { auto sub = defines.find(tokens[i]); if (sub != defines.end()) { tokens[i] = sub->second; } } } /** * If str is a valid arithmetic expression, replace it with * the result of its evaluation. */ static void evalArith(std::string &str, string fname, uint lineNum) { // TRACE_DEBUG("str before = %s", str.c_str()); string errMsg; std::vector<string> tokens = Util::tokenize(str, " ", errMsg); if (!errMsg.empty()) { fail(fname, lineNum, "{}", errMsg); } symSubst(tokens); str = Util::untokenize(tokens); double val = te_interp(str.c_str(), 0); if (val != NAN) { str = std::to_string(val); } // TRACE_DEBUG("str after = %s", str.c_str()); } /** * Greatest reactant cardinality across all the reactions */ static uint maxK; // calculated by verify /** * The number of distinct combinations of k elements that can be * chosen from a set of size n */ inline uint numCombinations(uint n, uint k) { static uint maxN = 0; if (k == 1) { return n; } else { if (n > maxN) { Util::initBinom(maxN = 10 * n, maxK); } return Util::binom(n, k); } } /** * Calculate reaction probabilities (h and a values) for all * reactions. * @return: cumulative probability a0 */ inline double Gillespie::calcReactProbs() { double a0 = 0; for (auto& r : reactions) { if (r.isDirty) { r.h = 1.0; for (uint m = 0; m < molecules.size(); m++) { if (molecules[m].getCount() >= r.left[m]) { for (uint n = 0; n < r.left[m]; n++) { r.h *= numCombinations(molecules[m].getCount(), r.left[m]); } } } if (isPossible(r)) { r.a = r.h * r.c * (1.0 - r.inhibition) ; } else { r.a = 0.0; } r.isDirty = false; r.recalc = true; } else { r.recalc = false; } a0 += r.a; } return a0; } /** * Run the Gillespie algorithm until * (a) stopTime is reached, or * (b) no more reactions are possible and runIdle is false, or * (c) the count of a specified molecule passes through a specified * threshold (from either direction.) * * @param plotInterval Time interval between molecule count outputs * @param stopTime Simulated time at which to stop unconditionally * @param monitorId Id of molecule to be monitored * @param threshold Stop when monitored molecule count reaches this value * @param monitorDelay Continue for this time interval after threshold * reached * @param runIdle Keep running even if no reaction can run */ void Gillespie::run( double plotInterval, double stopTime, const char *monitorId, double threshold, double monitorDelay) { // Initialize the random number generator // Util::initRand(); // Print header line string header = makeHeader(molecules); fmt::print("{}\n", header); // Is monitored molecule initially above or below threshold? // bool monitoring = false; uint monitorIndex = 0; bool monitorInitState = false; bool thresholdReached = false; if (monitorId != NULL) { monitoring = true; monitorIndex = moleculeIndex(monitorId); if (monitorIndex == UINT_MAX) { fmt::print("unknown molecule ({}) specified for monitoring", monitorId); exit(1); } monitorInitState = (molecules[monitorIndex].getCount() > threshold); thresholdReached = false; } double plotTime = 0.0; // When to plot next double t = 0.0; while (t <= stopTime ) { Sched::processEvents(t); // If the monitored molecule reached the threshold, arrange // to stop after the interval specified by monitorDdelay // if (monitoring && !thresholdReached) { if ((molecules[monitorIndex].getCount() == threshold) || ((molecules[monitorIndex].getCount() > threshold) != monitorInitState)) { stopTime = t + monitorDelay; thresholdReached = true; } } // Call the pre-iteration function, if one has been specified // if (preIterFunc != NULL) { (*preIterFunc)(t); } // Calculate reaction probabilities // // a0*dt is the probability that *any* reaction fires in the next // infinitesimal time interval dt // double a0 = calcReactProbs(); // Roll the dice to determine which reaction (r) will happen next // and the time interval (tau) until it happens. // double r1 = 0.0, r2 = 0.0; double tau = 0.0; double sum = 0.0; int r = -1; // next reaction. -1 means none if (a0 != 0.0) { // At least one reaction is possible r1 = Util::randDouble(0.0, 1.0, true); r2 = Util::randDouble(0.0, a0, true); tau = 1.0 / a0 * log(1.0 / r1); if(tau == 0.0) { // should be impossible! fmt::print("a0={}, r1={}\n", a0, r1); TRACE_FATAL("Ouch!"); } sum = 0.0; for (r = 0; r < (int) reactions.size() - 1; r++) { if ((sum += reactions[r].a) >= r2) { break; } } } if (r == -1) { // No reaction was possible if (runIdle) { tau = idleTick; } } // update t // t += tau; // Before updating the molecule counts, output the current counts // for any plot times that occurred during this simulation step. bool reactionPrinted = false; for (; plotTime <= t && plotTime <= stopTime; plotTime += plotInterval) { if (TRACE_DEBUG1_IS_ON && plotTime > 0.0) { fmt::print("{}\n", header); } fmt::print("{:{}.4f}", plotTime, twidth); for (uint m = 0; m < molecules.size(); m++) { fmt::print("{:{}}", molecules[m].getCount(), fwidths[m]); } if (!reactionPrinted) { if (Trace::getTraceLevel() == Trace::TRACE_Debug) { if (r >= 0) { fmt::print(" [{:{}}] {}", reactions[r].id, rwidth, reactions[r].formula); } else { fmt::print(" (no reaction)\n"); } } reactionPrinted = true; } fmt::print("\n"); if (r >= 0) { if (TRACE_DEBUG1_IS_ON) { fmt::print("-----------------------------------\n"); // [r1](10.000, 0, 0.00) fmt::print("{:{}} c h a\n", "", rwidth + 3); for (uint rr = 0; rr < reactions.size(); rr++) { string s; fmt::print("[{:{}}]{}({:7.3f},{:5},{:8.2f}) ", reactions[rr].id, rwidth, reactions[rr].recalc ? '*' : ' ', reactions[rr].c, reactions[rr].h, reactions[rr].a); bool firstReactant = true; for (uint m = 0; m < molecules.size(); m++) { if (reactions[rr].left[m] != 0) { if (firstReactant) { firstReactant = false; } else { s += "+ "; } if (reactions[rr].left[m] > 1) { s += fmt::format("{} ", reactions[rr].left[m]); } s += molecules[m].id + " "; } } fmt::print("{:13} ---> ", s); s = ""; bool firstProduct = true; for (uint m = 0; m < molecules.size(); m++) { if (reactions[rr].right[m] != 0) { if (firstProduct) { firstProduct = false; } else { s += "+ "; } if (reactions[rr].right[m] > 1) { s += fmt::format("{} ", reactions[rr].right[m]); } s += molecules[m].id + " "; } } fmt::print("{}\n", s); } fmt::print("-----------------------------------\n"); fmt::print("R = [{:{}}] {}\n", reactions[r].id.c_str(), rwidth, reactions[r].formula.c_str()); fmt::print("===================================\n"); } } } if (r != -1) { // A reaction happened: update molecule counts // for (uint m = 0; m < molecules.size(); m++) { int delta = -reactions[r].left[m] + reactions[r].right[m]; if (delta != 0) { molecules[m].setCount(molecules[m].getCount() + delta); if (molecules[m].getCount() > 1000000) { TRACE_INFO("Something fishy - about to dump core"); fflush(stdout); kill(getpid(), SIGABRT); } } } } else { // No reaction was possible // if (!runIdle) { // We don't run idle - stop break; } } } if (TRACE_DEBUG1_IS_ON) { fmt::print("t = {}.2f\n", t, twidth); } } void Gillespie::Reaction::parseFormula(string fname, uint lineNum) { left = std::vector<uint>(g.molecules.size(), 0); right = std::vector<uint>(g.molecules.size(), 0); string errMsg; std::vector<string> tokens = Util::tokenize(formula, " ", errMsg); if (!errMsg.empty()) { fail(fname, lineNum, "{}", errMsg); } enum { LEFT = 0, RIGHT = 1 } side = LEFT; enum { COUNT, MOLECULE, OPERATOR } expect = COUNT; uint count = 0; for (auto& token : tokens) { switch (expect) { case COUNT: if (Util::isDigitsOnly(token)) { count = strtoul(token.c_str(), NULL, 10); if (count != 0) { expect = MOLECULE; } else { expect = OPERATOR; // lone 0 means 'nothing' } break; } else { count = 1; // fall thru to MOLECULE } case MOLECULE: { uint m = g.moleculeIndex(token); if (m < g.molecules.size()) { if (side == LEFT) { left[m] = count; } else { right[m] = count; } expect = OPERATOR; } else { fail(fname, lineNum, "Unknown molecule: {}", token); } break; } case OPERATOR: if (side == LEFT && token == "--->") { side = RIGHT; expect = COUNT; break; } else if (token == "+") { expect = COUNT; break; } else { fail(fname, lineNum, "Expected operator, got '{}'", token); } break; } } if (side != RIGHT || expect != OPERATOR) { fail(fname, lineNum, "Incomplete reaction formula"); } // c = prod(mj!)/pow(v, n-1) * k // // Calculate the stochastic reaction constant c by multiplying the // deterministic (concentration-based) reaction rate k by the product of // the factorials of the cardinalities of the reactants mj and dividing // by the volume v raised to the number n of reactant molecules minus 1. // // Why divide by V^(n-1)? // The n is because the concentration-based formulation involves a V for // each reactant concentration (X/V), and the -1 is because the k // number represents reaction rate per unit volume). // // Why multiply by prod(mj!)? // // The number of unique combinations of X and Y is X*Y, but combinations // of X and X is X * (X-1) / 2 = (approx) x^2 / 2), and of 3 xs is // X^3/3!, etc. See Gillespie 1977. // // For details see my see my document gillespie.docx in // C:/neuroscience/memory_reconsolidation/cellular_recons_paper // uint n = 0; // number of reactant molecules uint p = 1; // product of factorials of reactant cardinalities for (auto& m : left) { n += m; p *= factorial(m); } c = p * k / pow(g.volume, n - 1); } /** * Retrieve molecule index by id */ uint Gillespie::moleculeIndex(string id) { for (uint i = 0; i < molecules.size(); i++) { if (Util::strCiEq(id, molecules[i].id)) { return i; } } return UINT_MAX; } /** * Retrieve reaction index by id */ uint Gillespie::reactionIndex(string id) { for (uint i = 0; i < reactions.size(); i++) { if (Util::strCiEq(id, reactions[i].id)) { return i; } } return UINT_MAX; } /** * Expand a wildcard (*) in a reaction ID */ static std::unordered_map<string, uint> rNumbers; void Gillespie::expandReactionWildcard(string &id) { size_t pos = id.find('*'); if (pos != string::npos) { if (rNumbers.find(id) == rNumbers.end()) { rNumbers[id] = 0; } string newId; do { uint n = rNumbers[id]; rNumbers[id] = n + 1; newId = id.substr(0, pos) + std::to_string(n) + id.substr(pos + 1); } while (reactionIndex(newId) < reactions.size()); id = newId; } } /** * Remove # and everything that follows it from line */ static void stripComment(char *line) { char *p; if ((p = strchr(line, '#')) != NULL) { *p = '\0'; } } /** * Divide a string into tokens * @param s The string to tokenize * @param fname File name, for error messages * @param lineNum Line number, for error messages */ static std::vector<std::string> tokenizeString( std::string s, std::string fname, uint lineNum) { string errMsg; std::vector<std::string> tokens = Util::tokenize( s, " \t", errMsg, "'\""); if (!errMsg.empty()) { fail(fname, lineNum, "{}", errMsg); } return tokens; } /** * struct used to schedule setCount event */ struct SetCountData { Gillespie *g; uint m; uint count; string comment; SetCountData(Gillespie *g, uint m, uint count, string comment) : g(g), m(m), count(count), comment(comment) {} }; /** * Set a molecule count to a specified value * This function is called from the event scheduler. */ static void setCount( double stime, double now, SetCountData *data) { data->g->setMoleculeCount(data->m, data->count); delete data; } /** * struct used to schedule setInhib event */ struct SetInhibData { Gillespie *g; uint r; double level; string comment; SetInhibData(Gillespie *g, uint r, double level, string comment) : g(g), r(r), level(level), comment(comment) {} }; /** * Set a reaction's inhibition level to a specified value * This function is called from the event scheduler. */ static void setInhib( double stime, double now, SetInhibData *data) { data->g->setReactionInhibition(data->r, data->level); delete data; } static uint checkParams( string directive, std::vector<string> tokens, uint min, uint max, string fname, uint lineNum) { uint n = tokens.size(); if (n < min || n > max) { fail(fname, lineNum, "\"{}\" requires min {}, max {} parameters, found {}", directive, min, max, tokens.size()); } return n; } /* * Read system from .gil file * @param fname File name * @erturn Number of lines read */ uint Gillespie::readGilFile(const char *fname) { static bool overrideAllowed = false; const char *suffix = ".gil"; FILE *fp = fopen(fname, "r"); // If file named fname doesn't exist, and fname doesn't // end in suffix, then generously append suffix and try again // if (fp == NULL && errno == ENOENT && strstr(fname, suffix) != fname + strlen(fname) - strlen(suffix)) { static char buf[1024]; strcpy(buf, fname); strcat(buf, suffix); fname = buf; fp = fopen(fname, "r"); } if (fp == NULL) { perror(fname); exit(errno); } string errMsg; char line[1000]; uint lineNum; for (lineNum = 1; fgets(line, sizeof(line), fp) != NULL; lineNum++) { if (line[strlen(line) - 1] == '\n') { line[strlen(line) - 1] = '\0'; } stripComment(line); if (Util::isBlank(line)) continue; char *colonPos = strchr(line, ':'); if (colonPos == NULL) { fail(fname, lineNum, "Bad directive (no colon)"); } string directive = Util::wstrip(string(line).substr(0, colonPos - line)); std::vector<string> tokens = tokenizeString(Util::wstrip(colonPos + 1), fname, lineNum); if (Util::strCiEq(directive, "include")) { checkParams("include", tokens, 1, 1, fname, lineNum); string path = tokens[0]; if (path[0] != '/') { // Relative path. Prepend directory path of current file. // char fnameCopy[strlen(fname)+1]; strcpy(fnameCopy, fname); path = string(dirname(fnameCopy)) + '/' + path; } readGilFile(path.c_str()); } else if (Util::strCiEq(directive, "define")) { symSubst(tokens, 1); checkParams("define", tokens, 2, 2, fname, lineNum); if (defines.find(tokens[0]) != defines.end()) { fail(fname, lineNum, "redefinition: {}", tokens[0]); } evalArith(tokens[1], fname, lineNum); defines.insert(std::make_pair(tokens[0], tokens[1])); } else if (Util::strCiEq(directive, "volume")) { symSubst(tokens); checkParams("volume", tokens, 1, 1, fname, lineNum); volume = std::stod(tokens[0]); } else if (Util::strCiEq(directive, "runIdle")) { symSubst(tokens); checkParams("runIdle", tokens, 1, 1, fname, lineNum); runIdle = Util::strToBool(tokens[0], errMsg); if (!errMsg.empty()) { fail(fname, lineNum, "{}: {}", errMsg, line); } } else if (Util::strCiEq(directive, "idleTick")) { symSubst(tokens); checkParams("idleTick", tokens, 1, 1, fname, lineNum); idleTick = std::stod(tokens[0]); } else if (Util::strCiEq(directive, "molecule")) { symSubst(tokens, 1); uint nParams = checkParams("molecule", tokens, 2, 3, fname, lineNum); Molecule m(*this, tokens[0], Util::strToUint(tokens[1], errMsg), nParams == 3 ? tokens[2] : ""); if (!errMsg.empty()) { fail(fname, lineNum, "{}: {}", errMsg, tokens[1]); } uint pos = moleculeIndex(tokens[0]); if (pos < molecules.size()) { if (overrideAllowed) { molecules[pos] = m; } else { fail(fname, lineNum, "Duplicate molecule id: {}", tokens[0]); } } else { #if 1 molecules.push_back(m); #else molecules.push_back(*this, Molecule( tokens[0], Util::strToUint(tokens[1], errMsg), nParams == 3 ? tokens[2] : "")); #endif } } else if (Util::strCiEq(directive, "reaction")) { symSubst(tokens, 1); uint nParams = checkParams("reaction", tokens, 3, 4, fname, lineNum); expandReactionWildcard(tokens[0]); Reaction r = Reaction( *this, tokens[0], tokens[1], Util::strToDouble(tokens[2], errMsg), nParams == 4 ? tokens[3] : ""); if (!errMsg.empty()) { fail(fname, lineNum, "{}: {}", errMsg, tokens[2]); } r.parseFormula(fname, lineNum); uint pos = reactionIndex(tokens[0]); if (pos < reactions.size()) { if (overrideAllowed) { reactions[pos] = r; } else { fail(fname, lineNum, "Duplicate reaction id: {}", tokens[0]); } } else { reactions.push_back(r); } } else if (Util::strCiEq(directive, "setcount")) { symSubst(tokens, 1); uint nParams = checkParams("setcount", tokens, 3, 4, fname, lineNum); string &id = tokens[0]; uint m = moleculeIndex(id); if (m >= molecules.size()) { fail(fname, lineNum, "unknown molecule: {}", id); } double time = Util::strToDouble(tokens[1], errMsg); if (!errMsg.empty()) { fail(fname, lineNum, "{}: {}", errMsg, tokens[1]); } uint count = Util::strToUint(tokens[2], errMsg); if (!errMsg.empty()) { fail(fname, lineNum, "{}: {}", errMsg, tokens[2]); } string comment = nParams == 4 ? tokens[3] : ""; SetCountData *sc = new SetCountData(this, m, count, comment); Sched::scheduleEvent( time, (Sched::VoidPtrCallback) setCount, sc); } else if (Util::strCiEq(directive, "setInhib")) { symSubst(tokens, 1); uint nParams = checkParams("setInhib", tokens, 3, 4, fname, lineNum); string &id = tokens[0]; uint r = reactionIndex(id); if (r >= reactions.size()) { fail(fname, lineNum, "unknown reaction: {}", id); } double time = Util::strToDouble(tokens[1], errMsg); if (!errMsg.empty()) { fail(fname, lineNum, "{}: {}", errMsg, tokens[1]); } double level = Util::strToDouble(tokens[2], errMsg); if (!errMsg.empty()) { fail(fname, lineNum, "{}: {}", errMsg, tokens[2]); } string comment = nParams == 4 ? tokens[3] : ""; if (level < 0.0 || level > 1.0) { fail(fname, lineNum, "Invalid inhibition level ({}), " "must be between 0.0 and 1.0", tokens[2]); } SetInhibData *md = new SetInhibData(this, r, level, comment); Sched::scheduleEvent( time, (Sched::VoidPtrCallback) setInhib, md); } else if (Util::strCiEq(directive, "allowOverride")) { symSubst(tokens); checkParams("allowOverride", tokens, 1, 1, fname, lineNum); overrideAllowed = Util::strToBool(tokens[0], errMsg); if (!errMsg.empty()) { fail(fname, lineNum, "{}: {}", errMsg, line); } } else { fail(fname, lineNum, "Unknown directive: {}", directive); } } return lineNum; } /** * Verify and post-process data read from .gil file(s) * @param fname For error reportage * @param lineNum For error reportage */ void Gillespie::verify(const char *fname, uint lineNum) { if (molecules.size() == 0) { fail(fname, lineNum, "No molecules specified"); } if (reactions.size() == 0) { fail(fname, lineNum, "No reactions specified"); } if (volume == 0) { fail(fname, lineNum, "volume not specified"); } // Find the max cardinality for any reactant in any reaction // for (auto r : reactions) { for (auto m : r.left) { if (m > maxK) { maxK = m; } } } // Determine max reaction ID length // rwidth = 0; for (auto r : reactions) { rwidth = Util::max(rwidth, (uint) r.id.size()); } // For each molecule, create a vector of reactions in // which it is a reactant. // for (uint r = 0; r < reactions.size(); r++) { for (uint m = 0; m < molecules.size(); m++) { if (reactions[r].left[m] != 0) { molecules[m].downstreamReactions.push_back(r); } } } } /** * Check if there are enough molecules for reaction r to happen */ bool Gillespie::isPossible(Reaction &r) { for (uint m = 0; m < r.left.size(); m++) { if (r.left[m] > molecules[m].getCount()) { return false; } } return true; } /** * Make a header line for the output * @param molecules Array of molecules */ string Gillespie::makeHeader( const std::vector<Molecule> &molecules) { fwidths.resize(molecules.size()); string s = fmt::format("{:>{}}", "t", twidth); for (uint m = 0; m < molecules.size(); m++) { fwidths[m] = Util::max(mwidth, (uint) molecules[m].id.size() + 1); s += fmt::format( "{:>{}}", molecules[m].id.c_str(), fwidths[m]); } return s; } void Gillespie::printMolecules() { fmt::print("Molecule Count Description Reactions\n"); for (auto m : molecules) { fmt::print("{:9}{:7} {:40}{:3}: ", m.id, m.getCount(), m.description, m.downstreamReactions.size()); for (uint r : m.downstreamReactions) { fmt::print("{} ", reactions[r].id); } fmt::print("\n"); } } void Gillespie::printReactions() { fmt::print("{:{}.{}} Formula k Description\n", "Reaction", rwidth, rwidth); for (auto r : reactions) { fmt::print("{:{}}{:25} {:8.3f} {}\n", r.id, rwidth + 1, r.formula, r.k, r.description); } }