Construct complex self-assembling models
In this document, we will go over the process of implementing mechanisms that simulate a new model. This new model:
- changes the conductance of one of the channels dynamically
- based on the calcium levels in the cell
- and can be affected by noise
The way we implement the model will be implement three different mechanisms, instead of one monolithic piece of code. This has the following advantages:
- individual mechanisms correspond to conceptually distinct things.
- Mixing and matching mechansisms becomes easier, allowing us to extend this more easily.
- This architecture is particuraly suited to ODEs with mulitple additive terms, where each term is its own mechanism.
An example model¶
We will simulate a regulation model, based on O'Leary et al.
This is the core set of ODEs we are trying to model:
This regulation model applies to each channel i in the neuron, so we would imagine whatever mechanism we create to be added to each channel.
Monolithic architecture¶
Let's imagine what happens if we take these ODEs and stick them all into a single mechanism. We have some problems:
- The (Ca- Ca_T) term really belongs to the compartment, not individual channels, because it is true for all channels
- This means that we have to manually specify that it's the same for all channels, and also gives us the freedom to allow them to be different (which is bad, because this is not in our model)
- If we wanted to use the noise term in a completely different model, we really can't
- If we wanted to replace the first two terms in the first ODE with something else, there's no easy way to do it.
In summary, this monolithic architecture is brittle, and doesn't allow for expansion and limits our flexibility in the model.
Self-assembling architecture¶
An alternate way of writing this model is to use a "self-assembling" architecture, where we break up the model into the smallest conceptual pieces, and make each piece its own mechanism. Here, we have the following mechanisms:
InstCalciumError
, which belongs to a compartment, and measures the instantenous difference between the calcium in that compartment and some target.IntegralController
, which changes implements the ODEs we discussed initially, but allows us to pass whatever we want into the RHS of the first ODEGaussianNoise
, which generates some Gaussian random noise.
We would create a model that looked like this using:
% assuming x is a xolotl model with a compartment named C
x.C.add('InstCalciumError')
x.C.channel1.add('GaussianNoise')
x.C.channel1.add('IntegralController')
% and so on for the other channels
OK, this looks good, but how do these mechanisms know how to connect to each other (the dotted lines in the diagram). For this model to work correctly,
IntegralController
needs to connect toInstCalciumError
so it can read the first term of the RHS of them
ODEIntegralController
needs to connect to the correctGaussianNoise
mechanism (the one that is connected to the same channel it is) to read out the second term of the RHS of them
ODE.
xolotl provides several methods to automatically allow mechanisms to self-assemble. Let's review some of them.
Methods to self-assemble inter-connected mechanisms¶
findMechanismsOfType
¶
How do we make sure the IntegralController
mechanism connects to the InstCalciumError
in the compartment? Because InstCalciumError
is the only one of that type in the compartment, we can simply ask it to find mechanisms of that type and connect to that. In the init
method, we can do something like this:
void IntegralController::init() {
RHS_terms = findMechanismsOfType("calcium_error");
where RHS_terms
is of type vector<mechanism*>
. Now this mechanism has a list of pointers to all mechanisms of this type in the compartment. This allows us to easily extend this model to add another additive term to the ODE -- simply add a new a new mechanism and declare it of type "calcium_error" and it will be automatically wired up.
Where (and how) does one declare mechanism types? Typically this is done in the constructor. For example, this is how we identify InstCalciumError
as a mechanism of type "calcium_error"
// constructor for InstCalciumError
InstCalciumError(...) {
name = "InstCalciumError";
mechanism_type = "calcium_error";
}
findMechanismsControlling
¶
How do we make sure the IntegralController
mechanism connects to the GaussianNoise
that is connected to the same channel as it? This problem is tricky because there are many GaussianNoise
mechanisms in the same compartment, and we need to connect to one and only the one that is also connected to the same channel. That's where this method comes in, which returns a vector of pointers to mechanisms that control the same object as the mechanism we are in.
void IntegralController::init() {
RHS_terms = findMechanismsOfType("calcium_error");
vector <mechanism*> A = findMechanismsControlling(this->controlling_class.c_str())
RHS_terms.insert(RHS_terms.end(), A.begin(), A.end());
You don't have to manually set controlling_class
-- this is done automatically on model creation.