Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forms of propensity calculations #35

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open

Forms of propensity calculations #35

wants to merge 7 commits into from

Conversation

heejochoi
Copy link

This PR aims to solve issue #34 by facilitating alternative propensity calculations:

  • Adds optional input argument called forms, which mediates using different functions to calculate the reaction's propensity. If forms is not provided as an input argument, the propensities of all reactions in the system are computed according to standard Gillespie -- ie. defaults to current behavior.

  • Allows the existing rates input argument to be optionally a list (or array) of nested lists (or arrays) of variable lengths to accommodate for propensity calculations that involve more than one constant.

  • Adds test for forms.

Copy link
Member

@prismofeverything prismofeverything left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @heejochoi, looks good! I couldn't find any issues with this, I ran the code and the tests all pass. I have a couple comments but mostly just planning for the future.

self.rates = rates
if rates.ndim == 1:
rates = [[rate] for rate in rates.astype(np.float64)]
self.rates_flat, self.rates_lengths, self.rates_indexes = flat_indexes(rates)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good! This means our rates are always in the same form, even if there is just a single rate per reaction.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from not being clear why 'rates' would be something other than 1D,

  1. I think there is a more NumPy-flavored solution to converting a 1D array to a 2D array - see np.atleast_2d.
  2. Why are you casting to floats in here? If it's not 1D, is it OK if they are not floats? Do they have to be floats at all, and if so, could that be handled at a different layer?

if forms is not None:
self.forms = forms
else:
self.forms = np.zeros(stoichiometry.shape[0], dtype=np.int64)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, good. Leave all the optional functionality in the python and pass one consistent form to the C. Thanks for this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a bit more elegant to write code like

if forms is None:
    forms = # some sort of default

self.forms = forms

which separates out the input parsing from the attribute assignment. However this is more my preference than a standard, if I'm not mistaken. This is a common enough case that you'd think there would be a standard but I've never seen mention of one.

self.rates_flat,
self.rates_lengths,
self.rates_indexes,
self.forms,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More args! Let's talk about what it looks like to make structs for the nested lists.... may be more trouble than it is worth, but could also clarify some things. It is a tradeoff we should evaluate.


break;

case 1: // Michaelis-Menten
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the form switch. We may want to talk about what it looks like to

  • Add more forms.
  • Use user provided forms.
  • Move the essence of each form into its own function?

There will get to be a point where this switch gets unwieldy, but it serves for now.

@@ -39,7 +39,10 @@ typedef struct {
int reactions_count;
int substrates_count;
long *stoichiometry;
double *rates;
double *rates_flat;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love how many places you have to add this.... Looking forward to evaluating @1fish2's proposal of using cython for the glue (module) code here.

Copy link
Contributor

@jmason42 jmason42 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming back to this after a while is interesting, as it highlights the places where the code isn't very clear. I've added some comments to that effect.

From what I can glean, you've implemented a pseudo-convenience kinetics form as an alternative to the standard stochastic form. I don't want to throw up any roadblocks here but I do want to make it clear that anyone who uses this form is playing with fire, for several reasons:

  • Michaelis-Menten kinetics, and its derivatives, are all based on concentrations, while stochastic simulation algorithms utilize molecule counts. You'll need to be very careful about converting between kinetic rate constants and their stochastic equivalents, including your new saturation constants (i.e. Michaelis-Menten constants).
  • This pseudo-convenience kinetics approach will break in a case where a reactant has a stoichiometry of 2 if the number of molecules goes to 1.
  • Using a saturation function in a stochastic simulation is on shaky grounds compared to the original form. I don't think it's totally out of the question - you're bridging between two modeling approaches and that often requires trying things before worrying about correctness.
  • You're not handling any type of competition, including the competitive effects between the forward and reverse reactants for an enzyme catalyzing a reversible reaction. So, be very apprehensive about introducing any reversible reactions. Otherwise you'll be double-counting enzyme capacity.

@@ -80,12 +80,13 @@ class StochasticSystem(object):
(and zero everywhere else).
'''

def __init__(self, stoichiometry, rates, random_seed=0):
def __init__(self, stoichiometry, rates, forms=None, random_seed=0):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a doc string, as I've forgotten what 'rates' is and don't know where to look. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these stochastic rate constants, perhaps? How do they play with the new 'forms'? Are 'forms' like kinetic rate laws sans the outermost proportionality constant?

self.rates = rates
if rates.ndim == 1:
rates = [[rate] for rate in rates.astype(np.float64)]
self.rates_flat, self.rates_lengths, self.rates_indexes = flat_indexes(rates)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from not being clear why 'rates' would be something other than 1D,

  1. I think there is a more NumPy-flavored solution to converting a 1D array to a 2D array - see np.atleast_2d.
  2. Why are you casting to floats in here? If it's not 1D, is it OK if they are not floats? Do they have to be floats at all, and if so, could that be handled at a different layer?

if forms is not None:
self.forms = forms
else:
self.forms = np.zeros(stoichiometry.shape[0], dtype=np.int64)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a bit more elegant to write code like

if forms is None:
    forms = # some sort of default

self.forms = forms

which separates out the input parsing from the attribute assignment. However this is more my preference than a standard, if I'm not mistaken. This is a common enough case that you'd think there would be a standard but I've never seen mention of one.

@@ -131,7 +142,7 @@ def __init__(self, stoichiometry, rates, random_seed=0):

def evolve(self, duration, state):
steps, time, events, outcome = self.obsidian.evolve(duration, state)
occurrences = np.zeros(len(self.rates))
occurrences = np.zeros(self.stoichiometry.shape[0])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think self.stoichiometry.shape[0] is used in a couple places and should probably be factored out for convenience/consistency.

break;

default:
printf("arrow.obsidian.evolve - unexpected form: %ld", forms[reaction]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs to throw an error rather than just print a message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants